From 0835dc71b3caf1f24352d19bd597bc56595675af Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Sun, 1 Dec 2024 15:34:29 -0300 Subject: [PATCH 01/89] More devtools stuff (#1637) * devtools: memory map viewer * devtools: batch highlight only for non-group viewer * devtools: fix not showing entire user data * devtools: shader debug viewer * devtools: add more reg naming --- CMakeLists.txt | 4 + src/common/config.cpp | 12 ++ src/common/config.h | 2 + src/core/debug_state.cpp | 11 +- src/core/debug_state.h | 48 ++++++- src/core/devtools/gcn/gcn_context_regs.cpp | 10 ++ src/core/devtools/layer.cpp | 45 +++++- src/core/devtools/options.cpp | 11 +- src/core/devtools/options.h | 3 +- src/core/devtools/widget/cmd_list.cpp | 4 +- src/core/devtools/widget/common.h | 57 ++++++++ src/core/devtools/widget/memory_map.cpp | 134 ++++++++++++++++++ src/core/devtools/widget/memory_map.h | 33 +++++ src/core/devtools/widget/reg_view.cpp | 49 +------ src/core/devtools/widget/shader_list.cpp | 95 +++++++++++++ src/core/devtools/widget/shader_list.h | 28 ++++ src/core/memory.h | 6 + .../renderer_vulkan/vk_pipeline_cache.cpp | 4 + 18 files changed, 491 insertions(+), 65 deletions(-) create mode 100644 src/core/devtools/widget/memory_map.cpp create mode 100644 src/core/devtools/widget/memory_map.h create mode 100644 src/core/devtools/widget/shader_list.cpp create mode 100644 src/core/devtools/widget/shader_list.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ed366a0b5..2f503832c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -435,10 +435,14 @@ set(DEV_TOOLS src/core/devtools/layer.cpp src/core/devtools/widget/frame_graph.cpp src/core/devtools/widget/frame_graph.h src/core/devtools/widget/imgui_memory_editor.h + src/core/devtools/widget/memory_map.cpp + src/core/devtools/widget/memory_map.h src/core/devtools/widget/reg_popup.cpp src/core/devtools/widget/reg_popup.h src/core/devtools/widget/reg_view.cpp src/core/devtools/widget/reg_view.h + src/core/devtools/widget/shader_list.cpp + src/core/devtools/widget/shader_list.h src/core/devtools/widget/text_editor.cpp src/core/devtools/widget/text_editor.h ) diff --git a/src/common/config.cpp b/src/common/config.cpp index 5c2c8cda6..eae8897c8 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -47,6 +47,7 @@ static std::string backButtonBehavior = "left"; static bool useSpecialPad = false; static int specialPadClass = 1; static bool isDebugDump = false; +static bool isShaderDebug = false; static bool isShowSplash = false; static bool isAutoUpdate = false; static bool isNullGpu = false; @@ -159,6 +160,10 @@ bool debugDump() { return isDebugDump; } +bool collectShadersForDebug() { + return isShaderDebug; +} + bool showSplash() { return isShowSplash; } @@ -235,6 +240,10 @@ void setDebugDump(bool enable) { isDebugDump = enable; } +void setCollectShaderForDebug(bool enable) { + isShaderDebug = enable; +} + void setShowSplash(bool enable) { isShowSplash = enable; } @@ -571,6 +580,7 @@ void load(const std::filesystem::path& path) { const toml::value& debug = data.at("Debug"); isDebugDump = toml::find_or(debug, "DebugDump", false); + isShaderDebug = toml::find_or(debug, "CollectShader", false); } if (data.contains("GUI")) { @@ -662,6 +672,7 @@ void save(const std::filesystem::path& path) { data["Vulkan"]["rdocMarkersEnable"] = vkMarkers; data["Vulkan"]["crashDiagnostic"] = vkCrashDiagnostic; data["Debug"]["DebugDump"] = isDebugDump; + data["Debug"]["CollectShader"] = isShaderDebug; data["GUI"]["theme"] = mw_themes; data["GUI"]["iconSize"] = m_icon_size; data["GUI"]["sliderPos"] = m_slider_pos; @@ -717,6 +728,7 @@ void setDefaultValues() { useSpecialPad = false; specialPadClass = 1; isDebugDump = false; + isShaderDebug = false; isShowSplash = false; isAutoUpdate = false; isNullGpu = false; diff --git a/src/common/config.h b/src/common/config.h index 400115745..d98c94480 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -37,6 +37,7 @@ u32 getScreenHeight(); s32 getGpuId(); bool debugDump(); +bool collectShadersForDebug(); bool showSplash(); bool autoUpdate(); bool nullGpu(); @@ -47,6 +48,7 @@ bool isRdocEnabled(); u32 vblankDiv(); void setDebugDump(bool enable); +void setCollectShaderForDebug(bool enable); void setShowSplash(bool enable); void setAutoUpdate(bool enable); void setNullGpu(bool enable); diff --git a/src/core/debug_state.cpp b/src/core/debug_state.cpp index 1dc4297c3..562cb62e8 100644 --- a/src/core/debug_state.cpp +++ b/src/core/debug_state.cpp @@ -157,7 +157,7 @@ void DebugStateImpl::PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, if (is_compute) { dump.is_compute = true; const auto& cs = dump.regs.cs_program; - dump.cs_data = ComputerShaderDump{ + dump.cs_data = PipelineComputerProgramDump{ .cs_program = cs, .code = std::vector{cs.Code().begin(), cs.Code().end()}, }; @@ -167,7 +167,7 @@ void DebugStateImpl::PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, auto stage = regs.ProgramForStage(i); if (stage->address_lo != 0) { auto code = stage->Code(); - dump.stages[i] = ShaderDump{ + dump.stages[i] = PipelineShaderProgramDump{ .user_data = *stage, .code = std::vector{code.begin(), code.end()}, }; @@ -176,3 +176,10 @@ void DebugStateImpl::PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, } } } + +void DebugStateImpl::CollectShader(const std::string& name, std::span spv, + std::span raw_code) { + shader_dump_list.emplace_back(name, std::vector{spv.begin(), spv.end()}, + std::vector{raw_code.begin(), raw_code.end()}); + std::ranges::sort(shader_dump_list, {}, &ShaderDump::name); +} diff --git a/src/core/debug_state.h b/src/core/debug_state.h index cd1c6aa93..759755b52 100644 --- a/src/core/debug_state.h +++ b/src/core/debug_state.h @@ -30,7 +30,8 @@ namespace Core::Devtools { class Layer; namespace Widget { class FrameGraph; -} +class ShaderList; +} // namespace Widget } // namespace Core::Devtools namespace DebugStateType { @@ -49,12 +50,12 @@ struct QueueDump { uintptr_t base_addr; }; -struct ShaderDump { +struct PipelineShaderProgramDump { Vulkan::Liverpool::ShaderProgram user_data{}; std::vector code{}; }; -struct ComputerShaderDump { +struct PipelineComputerProgramDump { Vulkan::Liverpool::ComputeProgram cs_program{}; std::vector code{}; }; @@ -63,8 +64,8 @@ struct RegDump { bool is_compute{false}; static constexpr size_t MaxShaderStages = 5; Vulkan::Liverpool::Regs regs{}; - std::array stages{}; - ComputerShaderDump cs_data{}; + std::array stages{}; + PipelineComputerProgramDump cs_data{}; }; struct FrameDump { @@ -73,9 +74,41 @@ struct FrameDump { std::unordered_map regs; // address -> reg dump }; +struct ShaderDump { + std::string name; + std::vector spv; + std::vector raw_code; + + std::string cache_spv_disasm{}; + std::string cache_raw_disasm{}; + + ShaderDump(std::string name, std::vector spv, std::vector raw_code) + : name(std::move(name)), spv(std::move(spv)), raw_code(std::move(raw_code)) {} + + ShaderDump(const ShaderDump& other) = delete; + ShaderDump(ShaderDump&& other) noexcept + : name{std::move(other.name)}, spv{std::move(other.spv)}, + raw_code{std::move(other.raw_code)}, cache_spv_disasm{std::move(other.cache_spv_disasm)}, + cache_raw_disasm{std::move(other.cache_raw_disasm)} {} + ShaderDump& operator=(const ShaderDump& other) = delete; + ShaderDump& operator=(ShaderDump&& other) noexcept { + if (this == &other) + return *this; + name = std::move(other.name); + spv = std::move(other.spv); + raw_code = std::move(other.raw_code); + cache_spv_disasm = std::move(other.cache_spv_disasm); + cache_raw_disasm = std::move(other.cache_raw_disasm); + return *this; + } +}; + class DebugStateImpl { friend class Core::Devtools::Layer; friend class Core::Devtools::Widget::FrameGraph; + friend class Core::Devtools::Widget::ShaderList; + + std::queue debug_message_popup; std::mutex guest_threads_mutex{}; std::vector guest_threads{}; @@ -94,7 +127,7 @@ class DebugStateImpl { std::shared_mutex frame_dump_list_mutex; std::vector frame_dump_list{}; - std::queue debug_message_popup; + std::vector shader_dump_list{}; public: void ShowDebugMessage(std::string message) { @@ -152,6 +185,9 @@ public: void PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, const AmdGpu::Liverpool::Regs& regs, bool is_compute = false); + + void CollectShader(const std::string& name, std::span spv, + std::span raw_code); }; } // namespace DebugStateType diff --git a/src/core/devtools/gcn/gcn_context_regs.cpp b/src/core/devtools/gcn/gcn_context_regs.cpp index 843ba9e65..5a591111e 100644 --- a/src/core/devtools/gcn/gcn_context_regs.cpp +++ b/src/core/devtools/gcn/gcn_context_regs.cpp @@ -289,6 +289,16 @@ const char* GetContextRegName(u32 reg_offset) { return "mmSPI_PS_INPUT_CNTL_2"; case mmSPI_PS_INPUT_CNTL_3: return "mmSPI_PS_INPUT_CNTL_3"; + case mmPA_SU_POLY_OFFSET_FRONT_SCALE: + return "mmPA_SU_POLY_OFFSET_FRONT_SCALE"; + case mmPA_SU_POLY_OFFSET_FRONT_OFFSET: + return "mmPA_SU_POLY_OFFSET_FRONT_OFFSET"; + case mmPA_SU_POLY_OFFSET_BACK_SCALE: + return "mmPA_SU_POLY_OFFSET_BACK_SCALE"; + case mmPA_SU_POLY_OFFSET_BACK_OFFSET: + return "mmPA_SU_POLY_OFFSET_BACK_OFFSET"; + case mmPA_SU_POLY_OFFSET_CLAMP: + return "mmPA_SU_POLY_OFFSET_CLAMP"; default: break; } diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 2c4ce20b6..2c2099f4d 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "layer.h" + #include #include "common/config.h" @@ -9,11 +11,12 @@ #include "core/debug_state.h" #include "imgui/imgui_std.h" #include "imgui_internal.h" -#include "layer.h" #include "options.h" #include "video_core/renderer_vulkan/vk_presenter.h" #include "widget/frame_dump.h" #include "widget/frame_graph.h" +#include "widget/memory_map.h" +#include "widget/shader_list.h" extern std::unique_ptr presenter; @@ -35,6 +38,9 @@ static float debug_popup_timing = 3.0f; static bool just_opened_options = false; +static Widget::MemoryMapViewer memory_map; +static Widget::ShaderList shader_list; + // clang-format off static std::string help_text = #include "help.txt" @@ -63,6 +69,7 @@ void L::DrawMenuBar() { } if (BeginMenu("GPU Tools")) { MenuItem("Show frame info", nullptr, &frame_graph.is_open); + MenuItem("Show loaded shaders", nullptr, &shader_list.open); if (BeginMenu("Dump frames")) { SliderInt("Count", &dump_frame_count, 1, 5); if (MenuItem("Dump", "Ctrl+Alt+F9", nullptr, !DebugState.DumpingCurrentFrame())) { @@ -81,6 +88,12 @@ void L::DrawMenuBar() { } ImGui::EndMenu(); } + if (BeginMenu("Debug")) { + if (MenuItem("Memory map")) { + memory_map.open = true; + } + ImGui::EndMenu(); + } EndMainMenuBar(); } @@ -175,19 +188,29 @@ void L::DrawAdvanced() { bool close_popup_options = true; if (BeginPopupModal("GPU Tools Options", &close_popup_options, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { - static char disassembly_cli[512]; + static char disassembler_cli_isa[512]; + static char disassembler_cli_spv[512]; static bool frame_dump_render_on_collapse; if (just_opened_options) { just_opened_options = false; - auto s = Options.disassembly_cli.copy(disassembly_cli, sizeof(disassembly_cli) - 1); - disassembly_cli[s] = '\0'; + auto s = Options.disassembler_cli_isa.copy(disassembler_cli_isa, + sizeof(disassembler_cli_isa) - 1); + disassembler_cli_isa[s] = '\0'; + s = Options.disassembler_cli_spv.copy(disassembler_cli_spv, + sizeof(disassembler_cli_spv) - 1); + disassembler_cli_spv[s] = '\0'; frame_dump_render_on_collapse = Options.frame_dump_render_on_collapse; } - InputText("Shader disassembler: ", disassembly_cli, sizeof(disassembly_cli)); + InputText("Shader isa disassembler: ", disassembler_cli_isa, sizeof(disassembler_cli_isa)); if (IsItemHovered()) { - SetTooltip(R"(Command to disassemble shaders. Example "dis.exe" --raw "{src}")"); + SetTooltip(R"(Command to disassemble shaders. Example: dis.exe --raw "{src}")"); + } + InputText("Shader SPIRV disassembler: ", disassembler_cli_spv, + sizeof(disassembler_cli_spv)); + if (IsItemHovered()) { + SetTooltip(R"(Command to disassemble shaders. Example: spirv-cross -V "{src}")"); } Checkbox("Show frame dump popups even when collapsed", &frame_dump_render_on_collapse); if (IsItemHovered()) { @@ -196,7 +219,8 @@ void L::DrawAdvanced() { } if (Button("Save")) { - Options.disassembly_cli = disassembly_cli; + Options.disassembler_cli_isa = disassembler_cli_isa; + Options.disassembler_cli_spv = disassembler_cli_spv; Options.frame_dump_render_on_collapse = frame_dump_render_on_collapse; SaveIniSettingsToDisk(io.IniFilename); CloseCurrentPopup(); @@ -219,6 +243,13 @@ void L::DrawAdvanced() { EndPopup(); } + + if (memory_map.open) { + memory_map.Draw(); + } + if (shader_list.open) { + shader_list.Draw(); + } } void L::DrawSimple() { diff --git a/src/core/devtools/options.cpp b/src/core/devtools/options.cpp index 1b49da76b..2def42071 100644 --- a/src/core/devtools/options.cpp +++ b/src/core/devtools/options.cpp @@ -12,8 +12,12 @@ TOptions Options; void LoadOptionsConfig(const char* line) { char str[512]; int i; - if (sscanf(line, "disassembly_cli=%511[^\n]", str) == 1) { - Options.disassembly_cli = str; + if (sscanf(line, "disassembler_cli_isa=%511[^\n]", str) == 1) { + Options.disassembler_cli_isa = str; + return; + } + if (sscanf(line, "disassembler_cli_spv=%511[^\n]", str) == 1) { + Options.disassembler_cli_spv = str; return; } if (sscanf(line, "frame_dump_render_on_collapse=%d", &i) == 1) { @@ -23,7 +27,8 @@ void LoadOptionsConfig(const char* line) { } void SerializeOptionsConfig(ImGuiTextBuffer* buf) { - buf->appendf("disassembly_cli=%s\n", Options.disassembly_cli.c_str()); + buf->appendf("disassembler_cli_isa=%s\n", Options.disassembler_cli_isa.c_str()); + buf->appendf("disassembler_cli_spv=%s\n", Options.disassembler_cli_spv.c_str()); buf->appendf("frame_dump_render_on_collapse=%d\n", Options.frame_dump_render_on_collapse); } diff --git a/src/core/devtools/options.h b/src/core/devtools/options.h index c3a8aaf31..70e1d137b 100644 --- a/src/core/devtools/options.h +++ b/src/core/devtools/options.h @@ -10,7 +10,8 @@ struct ImGuiTextBuffer; namespace Core::Devtools { struct TOptions { - std::string disassembly_cli{}; + std::string disassembler_cli_isa{"clrxdisasm --raw \"{src}\""}; + std::string disassembler_cli_spv{"spirv-cross -V \"{src}\""}; bool frame_dump_render_on_collapse{false}; }; diff --git a/src/core/devtools/widget/cmd_list.cpp b/src/core/devtools/widget/cmd_list.cpp index 219d25d6a..7c550cf2e 100644 --- a/src/core/devtools/widget/cmd_list.cpp +++ b/src/core/devtools/widget/cmd_list.cpp @@ -1306,7 +1306,7 @@ void CmdListViewer::Draw(bool only_batches_view) { if (batch.id == batch_bp) { // highlight batch at breakpoint PushStyleColor(ImGuiCol_Header, ImVec4{1.0f, 0.5f, 0.5f, 0.5f}); } - if (batch.id == highlight_batch) { + if (batch.id == highlight_batch && !group_batches) { PushStyleColor(ImGuiCol_Text, ImVec4{1.0f, 0.7f, 0.7f, 1.0f}); } @@ -1459,7 +1459,7 @@ void CmdListViewer::Draw(bool only_batches_view) { } } - if (batch.id == highlight_batch) { + if (batch.id == highlight_batch && !group_batches) { PopStyleColor(); } diff --git a/src/core/devtools/widget/common.h b/src/core/devtools/widget/common.h index e650f5fc7..4429f5581 100644 --- a/src/core/devtools/widget/common.h +++ b/src/core/devtools/widget/common.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -10,9 +11,16 @@ #include #include "common/bit_field.h" +#include "common/io_file.h" #include "common/types.h" +#include "core/debug_state.h" #include "video_core/amdgpu/pm4_opcodes.h" +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif + namespace Core::Devtools::Widget { /* * Generic PM4 header @@ -106,4 +114,53 @@ static bool IsDrawCall(AmdGpu::PM4ItOpcode opcode) { } } +inline std::optional exec_cli(const char* cli) { + std::array buffer{}; + std::string output; + const auto f = popen(cli, "r"); + if (!f) { + pclose(f); + return {}; + } + while (fgets(buffer.data(), buffer.size(), f)) { + output += buffer.data(); + } + pclose(f); + return output; +} + +inline std::string RunDisassembler(const std::string& disassembler_cli, + const std::vector& shader_code) { + std::string shader_dis; + + if (disassembler_cli.empty()) { + shader_dis = "No disassembler set"; + } else { + auto bin_path = std::filesystem::temp_directory_path() / "shadps4_tmp_shader.bin"; + + constexpr std::string_view src_arg = "{src}"; + std::string cli = disassembler_cli; + const auto pos = cli.find(src_arg); + if (pos == std::string::npos) { + DebugState.ShowDebugMessage("Disassembler CLI does not contain {src} argument\n" + + disassembler_cli); + } else { + cli.replace(pos, src_arg.size(), "\"" + bin_path.string() + "\""); + Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Write); + file.Write(shader_code); + file.Close(); + + auto result = exec_cli(cli.c_str()); + shader_dis = result.value_or("Could not disassemble shader"); + if (shader_dis.empty()) { + shader_dis = "Disassembly empty or failed"; + } + + std::filesystem::remove(bin_path); + } + } + + return shader_dis; +} + } // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/memory_map.cpp b/src/core/devtools/widget/memory_map.cpp new file mode 100644 index 000000000..afafd2853 --- /dev/null +++ b/src/core/devtools/widget/memory_map.cpp @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "core/debug_state.h" +#include "core/memory.h" +#include "memory_map.h" + +using namespace ImGui; + +namespace Core::Devtools::Widget { + +bool MemoryMapViewer::Iterator::DrawLine() { + if (is_vma) { + if (vma.it == vma.end) { + return false; + } + auto m = vma.it->second; + if (m.type == VMAType::Free) { + ++vma.it; + return DrawLine(); + } + TableNextColumn(); + Text("%zX", m.base); + TableNextColumn(); + Text("%zX", m.size); + TableNextColumn(); + Text("%s", magic_enum::enum_name(m.type).data()); + TableNextColumn(); + Text("%s", magic_enum::enum_name(m.prot).data()); + TableNextColumn(); + if (m.is_exec) { + Text("X"); + } + TableNextColumn(); + Text("%s", m.name.c_str()); + ++vma.it; + return true; + } + if (dmem.it == dmem.end) { + return false; + } + auto m = dmem.it->second; + if (m.is_free) { + ++dmem.it; + return DrawLine(); + } + TableNextColumn(); + Text("%llX", m.base); + TableNextColumn(); + Text("%llX", m.size); + TableNextColumn(); + auto type = static_cast<::Libraries::Kernel::MemoryTypes>(m.memory_type); + Text("%s", magic_enum::enum_name(type).data()); + TableNextColumn(); + Text("%d", m.is_pooled); + ++dmem.it; + return true; +} + +void MemoryMapViewer::Draw() { + SetNextWindowSize({600.0f, 500.0f}, ImGuiCond_FirstUseEver); + if (!Begin("Memory map", &open)) { + End(); + return; + } + + auto mem = Memory::Instance(); + std::scoped_lock lck{mem->mutex}; + + { + bool next_showing_vma = showing_vma; + if (showing_vma) { + PushStyleColor(ImGuiCol_Button, ImVec4{1.0f, 0.7f, 0.7f, 1.0f}); + } + if (Button("VMem")) { + next_showing_vma = true; + } + if (showing_vma) { + PopStyleColor(); + } + SameLine(); + if (!showing_vma) { + PushStyleColor(ImGuiCol_Button, ImVec4{1.0f, 0.7f, 0.7f, 1.0f}); + } + if (Button("DMem")) { + next_showing_vma = false; + } + if (!showing_vma) { + PopStyleColor(); + } + showing_vma = next_showing_vma; + } + + Iterator it{}; + if (showing_vma) { + it.is_vma = true; + it.vma.it = mem->vma_map.begin(); + it.vma.end = mem->vma_map.end(); + } else { + it.is_vma = false; + it.dmem.it = mem->dmem_map.begin(); + it.dmem.end = mem->dmem_map.end(); + } + + if (BeginTable("memory_view_table", showing_vma ? 6 : 4, + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingFixedFit)) { + if (showing_vma) { + TableSetupColumn("Address"); + TableSetupColumn("Size"); + TableSetupColumn("Type"); + TableSetupColumn("Prot"); + TableSetupColumn("Is Exec"); + TableSetupColumn("Name"); + } else { + TableSetupColumn("Address"); + TableSetupColumn("Size"); + TableSetupColumn("Type"); + TableSetupColumn("Pooled"); + } + TableHeadersRow(); + + while (it.DrawLine()) + ; + EndTable(); + } + + End(); +} + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/memory_map.h b/src/core/devtools/widget/memory_map.h new file mode 100644 index 000000000..cc7697c8c --- /dev/null +++ b/src/core/devtools/widget/memory_map.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/memory.h" + +namespace Core::Devtools::Widget { + +class MemoryMapViewer { + struct Iterator { + bool is_vma; + struct { + MemoryManager::DMemMap::iterator it; + MemoryManager::DMemMap::iterator end; + } dmem; + struct { + MemoryManager::VMAMap::iterator it; + MemoryManager::VMAMap::iterator end; + } vma; + + bool DrawLine(); + }; + + bool showing_vma = true; + +public: + bool open = false; + + void Draw(); +}; + +} // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/reg_view.cpp b/src/core/devtools/widget/reg_view.cpp index 10cc88085..a60090a8c 100644 --- a/src/core/devtools/widget/reg_view.cpp +++ b/src/core/devtools/widget/reg_view.cpp @@ -25,21 +25,6 @@ using magic_enum::enum_name; constexpr auto depth_id = 0xF3; -static std::optional exec_cli(const char* cli) { - std::array buffer{}; - std::string output; - const auto f = popen(cli, "r"); - if (!f) { - pclose(f); - return {}; - } - while (fgets(buffer.data(), buffer.size(), f)) { - output += buffer.data(); - } - pclose(f); - return output; -} - namespace Core::Devtools::Widget { void RegView::ProcessShader(int shader_id) { @@ -54,38 +39,12 @@ void RegView::ProcessShader(int shader_id) { user_data = s.user_data.user_data; } - std::string shader_dis; - - if (Options.disassembly_cli.empty()) { - shader_dis = "No disassembler set"; - } else { - auto bin_path = std::filesystem::temp_directory_path() / "shadps4_tmp_shader.bin"; - - constexpr std::string_view src_arg = "{src}"; - std::string cli = Options.disassembly_cli; - const auto pos = cli.find(src_arg); - if (pos == std::string::npos) { - DebugState.ShowDebugMessage("Disassembler CLI does not contain {src} argument"); - } else { - cli.replace(pos, src_arg.size(), "\"" + bin_path.string() + "\""); - Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Write); - file.Write(shader_code); - file.Close(); - - auto result = exec_cli(cli.c_str()); - shader_dis = result.value_or("Could not disassemble shader"); - if (shader_dis.empty()) { - shader_dis = "Disassembly empty or failed"; - } - - std::filesystem::remove(bin_path); - } - } + std::string shader_dis = RunDisassembler(Options.disassembler_cli_isa, shader_code); MemoryEditor hex_view; hex_view.Open = true; hex_view.ReadOnly = true; - hex_view.Cols = 8; + hex_view.Cols = 16; hex_view.OptShowAscii = false; hex_view.OptShowOptions = false; @@ -376,7 +335,9 @@ void RegView::Draw() { if (!shader) { Text("Stage not selected"); } else { - shader->hex_view.DrawContents(shader->user_data.data(), shader->user_data.size()); + shader->hex_view.DrawContents(shader->user_data.data(), + shader->user_data.size() * + sizeof(Vulkan::Liverpool::UserData::value_type)); } } End(); diff --git a/src/core/devtools/widget/shader_list.cpp b/src/core/devtools/widget/shader_list.cpp new file mode 100644 index 000000000..b056880dd --- /dev/null +++ b/src/core/devtools/widget/shader_list.cpp @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "shader_list.h" + +#include + +#include "common.h" +#include "common/config.h" +#include "core/debug_state.h" +#include "core/devtools/options.h" +#include "imgui/imgui_std.h" + +using namespace ImGui; + +namespace Core::Devtools::Widget { + +void ShaderList::DrawShader(DebugStateType::ShaderDump& value) { + if (!loaded_data) { + loaded_data = true; + if (value.cache_raw_disasm.empty()) { + value.cache_raw_disasm = RunDisassembler(Options.disassembler_cli_isa, value.raw_code); + } + isa_editor.SetText(value.cache_raw_disasm); + + if (value.cache_spv_disasm.empty()) { + value.cache_spv_disasm = RunDisassembler(Options.disassembler_cli_spv, value.spv); + } + spv_editor.SetText(value.cache_spv_disasm); + } + + if (SmallButton("<-")) { + selected_shader = -1; + } + SameLine(); + Text("%s", value.name.c_str()); + SameLine(0.0f, 7.0f); + if (BeginCombo("Shader type", showing_isa ? "ISA" : "SPIRV", ImGuiComboFlags_WidthFitPreview)) { + if (Selectable("SPIRV")) { + showing_isa = false; + } + if (Selectable("ISA")) { + showing_isa = true; + } + EndCombo(); + } + + if (showing_isa) { + isa_editor.Render("ISA", GetContentRegionAvail()); + } else { + spv_editor.Render("SPIRV", GetContentRegionAvail()); + } +} + +ShaderList::ShaderList() { + isa_editor.SetPalette(TextEditor::GetDarkPalette()); + isa_editor.SetReadOnly(true); + spv_editor.SetPalette(TextEditor::GetDarkPalette()); + spv_editor.SetReadOnly(true); + spv_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::GLSL()); +} + +void ShaderList::Draw() { + SetNextWindowSize({500.0f, 600.0f}, ImGuiCond_FirstUseEver); + if (!Begin("Shader list", &open)) { + End(); + return; + } + + if (!Config::collectShadersForDebug()) { + DrawCenteredText("Enable 'CollectShader' in config to see shaders"); + End(); + return; + } + + if (selected_shader >= 0) { + DrawShader(DebugState.shader_dump_list[selected_shader]); + End(); + return; + } + + auto width = GetContentRegionAvail().x; + int i = 0; + for (const auto& shader : DebugState.shader_dump_list) { + if (ButtonEx(shader.name.c_str(), {width, 20.0f}, ImGuiButtonFlags_NoHoveredOnFocus)) { + selected_shader = i; + loaded_data = false; + } + i++; + } + + End(); +} + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/shader_list.h b/src/core/devtools/widget/shader_list.h new file mode 100644 index 000000000..5a47f656d --- /dev/null +++ b/src/core/devtools/widget/shader_list.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "core/debug_state.h" +#include "text_editor.h" + +namespace Core::Devtools::Widget { + +class ShaderList { + int selected_shader = -1; + TextEditor isa_editor{}; + TextEditor spv_editor{}; + bool loaded_data = false; + bool showing_isa = false; + + void DrawShader(DebugStateType::ShaderDump& value); + +public: + ShaderList(); + + bool open = false; + + void Draw(); +}; + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/memory.h b/src/core/memory.h index a9a42e1c2..2efa02763 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -20,6 +20,10 @@ namespace Libraries::Kernel { struct OrbisQueryInfo; } +namespace Core::Devtools::Widget { +class MemoryMapViewer; +} + namespace Core { enum class MemoryProt : u32 { @@ -257,6 +261,8 @@ private: size_t total_flexible_size{}; size_t flexible_usage{}; Vulkan::Rasterizer* rasterizer{}; + + friend class ::Core::Devtools::Widget::MemoryMapViewer; }; using Memory = Common::Singleton; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index a4c213ca8..612e950bb 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -7,6 +7,7 @@ #include "common/hash.h" #include "common/io_file.h" #include "common/path_util.h" +#include "core/debug_state.h" #include "shader_recompiler/backend/spirv/emit_spirv.h" #include "shader_recompiler/info.h" #include "shader_recompiler/recompiler.h" @@ -416,6 +417,9 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, const auto module = CompileSPV(spv, instance.GetDevice()); const auto name = fmt::format("{}_{:#x}_{}", info.stage, info.pgm_hash, perm_idx); Vulkan::SetObjectName(instance.GetDevice(), module, name); + if (Config::collectShadersForDebug()) { + DebugState.CollectShader(name, spv, code); + } return module; } From 3dc0f6d831093b931ad5fac38ba714ed413db7f1 Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Mon, 2 Dec 2024 03:04:44 +0000 Subject: [PATCH 02/89] cmake: fix build (#1645) --- externals/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 50eda0288..bc2d41bda 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -69,7 +69,7 @@ if (NOT TARGET ZLIB::ZLIB) FetchContent_MakeAvailable(ZLIB) add_library(ZLIB::ZLIB ALIAS zlib) # libpng expects this variable to exist after its find_package(ZLIB) - get_target_property(ZLIB_INCLUDE_DIRS zlib INTERFACE_INCLUDE_DIRECTORIES) + set(ZLIB_INCLUDE_DIRS "${FETCHCONTENT_BASE_DIR}/zlib-build") endif() # SDL3 From fda4f06518de074c8e8d1fbc99caec890b55875f Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:46:51 -0800 Subject: [PATCH 03/89] devtools: More warning fixes (#1652) --- src/core/devtools/widget/memory_map.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/devtools/widget/memory_map.cpp b/src/core/devtools/widget/memory_map.cpp index afafd2853..dc8f5c2e9 100644 --- a/src/core/devtools/widget/memory_map.cpp +++ b/src/core/devtools/widget/memory_map.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include @@ -23,7 +24,7 @@ bool MemoryMapViewer::Iterator::DrawLine() { return DrawLine(); } TableNextColumn(); - Text("%zX", m.base); + Text("%" PRIXPTR, m.base); TableNextColumn(); Text("%zX", m.size); TableNextColumn(); @@ -48,9 +49,9 @@ bool MemoryMapViewer::Iterator::DrawLine() { return DrawLine(); } TableNextColumn(); - Text("%llX", m.base); + Text("%" PRIXPTR, m.base); TableNextColumn(); - Text("%llX", m.size); + Text("%zX", m.size); TableNextColumn(); auto type = static_cast<::Libraries::Kernel::MemoryTypes>(m.memory_type); Text("%s", magic_enum::enum_name(type).data()); From eb844b9b63bfddc70ef40665d5bffacd01f31a10 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 2 Dec 2024 23:20:54 +0200 Subject: [PATCH 04/89] shader_recompiler: Implement manual barycentric interpolation path (#1644) * shader_recompiler: Implement manual barycentric interpolation path * clang format * emit_spirv: Fix typo * emit_spirv: Simplify variable definition * spirv_emit: clang format --- .../backend/spirv/emit_spirv.cpp | 8 +- .../spirv/emit_spirv_context_get_set.cpp | 85 ++++++++++--------- .../backend/spirv/emit_spirv_special.cpp | 3 + .../backend/spirv/spirv_emit_context.cpp | 69 ++++++++++++--- .../backend/spirv/spirv_emit_context.h | 7 +- src/shader_recompiler/profile.h | 1 + .../renderer_vulkan/vk_instance.cpp | 7 ++ src/video_core/renderer_vulkan/vk_instance.h | 5 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 2 + 9 files changed, 129 insertions(+), 58 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index e84908a57..1e7032f10 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -206,7 +206,7 @@ Id DefineMain(EmitContext& ctx, const IR::Program& program) { return main; } -void SetupCapabilities(const Info& info, EmitContext& ctx) { +void SetupCapabilities(const Info& info, const Profile& profile, EmitContext& ctx) { ctx.AddCapability(spv::Capability::Image1D); ctx.AddCapability(spv::Capability::Sampled1D); ctx.AddCapability(spv::Capability::ImageQuery); @@ -251,6 +251,10 @@ void SetupCapabilities(const Info& info, EmitContext& ctx) { if (info.stage == Stage::Geometry) { ctx.AddCapability(spv::Capability::Geometry); } + if (info.stage == Stage::Fragment && profile.needs_manual_interpolation) { + ctx.AddExtension("SPV_KHR_fragment_shader_barycentric"); + ctx.AddCapability(spv::Capability::FragmentBarycentricKHR); + } } void DefineEntryPoint(const IR::Program& program, EmitContext& ctx, Id main) { @@ -342,7 +346,7 @@ std::vector EmitSPIRV(const Profile& profile, const RuntimeInfo& runtime_in EmitContext ctx{profile, runtime_info, program.info, binding}; const Id main{DefineMain(ctx, program)}; DefineEntryPoint(program, ctx, main); - SetupCapabilities(program.info, ctx); + SetupCapabilities(program.info, profile, ctx); SetupFloatMode(ctx, profile, runtime_info, main); PatchPhiNodes(program, ctx); binding.user_data += program.info.ud_mask.NumRegs(); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 064200d99..d8c0a17bd 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -171,54 +171,38 @@ Id EmitReadStepRate(EmitContext& ctx, int rate_idx) { rate_idx == 0 ? ctx.u32_zero_value : ctx.u32_one_value)); } +Id EmitGetAttributeForGeometry(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index) { + if (IR::IsPosition(attr)) { + ASSERT(attr == IR::Attribute::Position0); + const auto position_arr_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[4]); + const auto pointer{ + ctx.OpAccessChain(position_arr_ptr, ctx.gl_in, ctx.ConstU32(index), ctx.ConstU32(0u))}; + const auto position_comp_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[1]); + return ctx.OpLoad(ctx.F32[1], + ctx.OpAccessChain(position_comp_ptr, pointer, ctx.ConstU32(comp))); + } + + if (IR::IsParam(attr)) { + const u32 param_id{u32(attr) - u32(IR::Attribute::Param0)}; + const auto param = ctx.input_params.at(param_id).id; + const auto param_arr_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[4]); + const auto pointer{ctx.OpAccessChain(param_arr_ptr, param, ctx.ConstU32(index))}; + const auto position_comp_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[1]); + return ctx.OpLoad(ctx.F32[1], + ctx.OpAccessChain(position_comp_ptr, pointer, ctx.ConstU32(comp))); + } + UNREACHABLE(); +} + Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index) { if (ctx.info.stage == Stage::Geometry) { - if (IR::IsPosition(attr)) { - ASSERT(attr == IR::Attribute::Position0); - const auto position_arr_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[4]); - const auto pointer{ctx.OpAccessChain(position_arr_ptr, ctx.gl_in, ctx.ConstU32(index), - ctx.ConstU32(0u))}; - const auto position_comp_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[1]); - return ctx.OpLoad(ctx.F32[1], - ctx.OpAccessChain(position_comp_ptr, pointer, ctx.ConstU32(comp))); - } - - if (IR::IsParam(attr)) { - const u32 param_id{u32(attr) - u32(IR::Attribute::Param0)}; - const auto param = ctx.input_params.at(param_id).id; - const auto param_arr_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[4]); - const auto pointer{ctx.OpAccessChain(param_arr_ptr, param, ctx.ConstU32(index))}; - const auto position_comp_ptr = ctx.TypePointer(spv::StorageClass::Input, ctx.F32[1]); - return ctx.OpLoad(ctx.F32[1], - ctx.OpAccessChain(position_comp_ptr, pointer, ctx.ConstU32(comp))); - } - UNREACHABLE(); + return EmitGetAttributeForGeometry(ctx, attr, comp, index); } if (IR::IsParam(attr)) { const u32 index{u32(attr) - u32(IR::Attribute::Param0)}; const auto& param{ctx.input_params.at(index)}; - if (param.buffer_handle < 0) { - if (!ValidId(param.id)) { - // Attribute is disabled or varying component is not written - return ctx.ConstF32(comp == 3 ? 1.0f : 0.0f); - } - - Id result; - if (param.is_default) { - result = ctx.OpCompositeExtract(param.component_type, param.id, comp); - } else if (param.num_components > 1) { - const Id pointer{ - ctx.OpAccessChain(param.pointer_type, param.id, ctx.ConstU32(comp))}; - result = ctx.OpLoad(param.component_type, pointer); - } else { - result = ctx.OpLoad(param.component_type, param.id); - } - if (param.is_integer) { - result = ctx.OpBitcast(ctx.F32[1], result); - } - return result; - } else { + if (param.buffer_handle >= 0) { const auto step_rate = EmitReadStepRate(ctx, param.id.value); const auto offset = ctx.OpIAdd( ctx.U32[1], @@ -229,7 +213,26 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index) { ctx.ConstU32(comp)); return EmitReadConstBuffer(ctx, param.buffer_handle, offset); } + + Id result; + if (param.is_loaded) { + // Attribute is either default or manually interpolated. The id points to an already + // loaded vector. + result = ctx.OpCompositeExtract(param.component_type, param.id, comp); + } else if (param.num_components > 1) { + // Attribute is a vector and we need to access a specific component. + const Id pointer{ctx.OpAccessChain(param.pointer_type, param.id, ctx.ConstU32(comp))}; + result = ctx.OpLoad(param.component_type, pointer); + } else { + // Attribute is a single float or interger, simply load it. + result = ctx.OpLoad(param.component_type, param.id); + } + if (param.is_integer) { + result = ctx.OpBitcast(ctx.F32[1], result); + } + return result; } + switch (attr) { case IR::Attribute::FragCoord: { const Id coord = ctx.OpLoad( diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp index e9ffdcce8..4a22ba09f 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp @@ -8,6 +8,9 @@ namespace Shader::Backend::SPIRV { void EmitPrologue(EmitContext& ctx) { + if (ctx.stage == Stage::Fragment) { + ctx.DefineInterpolatedAttribs(); + } ctx.DefineBufferOffsets(); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index dc404b121..6c8eb1236 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -222,6 +222,36 @@ void EmitContext::DefineBufferOffsets() { } } +void EmitContext::DefineInterpolatedAttribs() { + if (!profile.needs_manual_interpolation) { + return; + } + // Iterate all input attributes, load them and manually interpolate with barycentric + // coordinates. + for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { + const auto& input = runtime_info.fs_info.inputs[i]; + const u32 semantic = input.param_index; + auto& params = input_params[semantic]; + if (input.is_flat || params.is_loaded) { + continue; + } + const Id p_array{OpLoad(TypeArray(F32[4], ConstU32(3U)), params.id)}; + const Id p0{OpCompositeExtract(F32[4], p_array, 0U)}; + const Id p1{OpCompositeExtract(F32[4], p_array, 1U)}; + const Id p2{OpCompositeExtract(F32[4], p_array, 2U)}; + const Id p10{OpFSub(F32[4], p1, p0)}; + const Id p20{OpFSub(F32[4], p2, p0)}; + const Id bary_coord{OpLoad(F32[3], gl_bary_coord_id)}; + const Id bary_coord_y{OpCompositeExtract(F32[1], bary_coord, 1)}; + const Id bary_coord_z{OpCompositeExtract(F32[1], bary_coord, 2)}; + const Id p10_y{OpVectorTimesScalar(F32[4], p10, bary_coord_y)}; + const Id p20_z{OpVectorTimesScalar(F32[4], p20, bary_coord_z)}; + params.id = OpFAdd(F32[4], p0, OpFAdd(F32[4], p10_y, p20_z)); + Name(params.id, fmt::format("fs_in_attr{}", semantic)); + params.is_loaded = true; + } +} + Id MakeDefaultValue(EmitContext& ctx, u32 default_value) { switch (default_value) { case 0: @@ -260,14 +290,14 @@ void EmitContext::DefineInputs() { input.instance_step_rate == Info::VsInput::InstanceIdType::OverStepRate0 ? 0 : 1; // Note that we pass index rather than Id - input_params[input.binding] = { - rate_idx, - input_u32, - U32[1], - input.num_components, - true, - false, - input.instance_data_buf, + input_params[input.binding] = SpirvAttribute{ + .id = rate_idx, + .pointer_type = input_u32, + .component_type = U32[1], + .num_components = input.num_components, + .is_integer = true, + .is_loaded = false, + .buffer_handle = input.instance_data_buf, }; } else { Id id{DefineInput(type, input.binding)}; @@ -286,6 +316,10 @@ void EmitContext::DefineInputs() { frag_coord = DefineVariable(F32[4], spv::BuiltIn::FragCoord, spv::StorageClass::Input); frag_depth = DefineVariable(F32[1], spv::BuiltIn::FragDepth, spv::StorageClass::Output); front_facing = DefineVariable(U1[1], spv::BuiltIn::FrontFacing, spv::StorageClass::Input); + if (profile.needs_manual_interpolation) { + gl_bary_coord_id = + DefineVariable(F32[3], spv::BuiltIn::BaryCoordKHR, spv::StorageClass::Input); + } for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { const auto& input = runtime_info.fs_info.inputs[i]; const u32 semantic = input.param_index; @@ -299,14 +333,21 @@ void EmitContext::DefineInputs() { const IR::Attribute param{IR::Attribute::Param0 + input.param_index}; const u32 num_components = info.loads.NumComponents(param); const Id type{F32[num_components]}; - const Id id{DefineInput(type, semantic)}; - if (input.is_flat) { - Decorate(id, spv::Decoration::Flat); + Id attr_id{}; + if (profile.needs_manual_interpolation && !input.is_flat) { + attr_id = DefineInput(TypeArray(type, ConstU32(3U)), semantic); + Decorate(attr_id, spv::Decoration::PerVertexKHR); + Name(attr_id, fmt::format("fs_in_attr{}_p", semantic)); + } else { + attr_id = DefineInput(type, semantic); + Name(attr_id, fmt::format("fs_in_attr{}", semantic)); + } + if (input.is_flat) { + Decorate(attr_id, spv::Decoration::Flat); } - Name(id, fmt::format("fs_in_attr{}", semantic)); input_params[semantic] = - GetAttributeInfo(AmdGpu::NumberFormat::Float, id, num_components, false); - interfaces.push_back(id); + GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, num_components, false); + interfaces.push_back(attr_id); } break; case Stage::Compute: diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index fb30a5dd6..1c5da946d 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -42,7 +42,9 @@ public: ~EmitContext(); Id Def(const IR::Value& value); + void DefineBufferOffsets(); + void DefineInterpolatedAttribs(); [[nodiscard]] Id DefineInput(Id type, u32 location) { const Id input_id{DefineVar(type, spv::StorageClass::Input)}; @@ -197,6 +199,9 @@ public: Id shared_memory_u32_type{}; + Id interpolate_func{}; + Id gl_bary_coord_id{}; + struct TextureDefinition { const VectorIds* data_types; Id id; @@ -241,7 +246,7 @@ public: Id component_type; u32 num_components; bool is_integer{}; - bool is_default{}; + bool is_loaded{}; s32 buffer_handle{-1}; }; std::array input_params{}; diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h index bbda731e0..a868ab76c 100644 --- a/src/shader_recompiler/profile.h +++ b/src/shader_recompiler/profile.h @@ -24,6 +24,7 @@ struct Profile { bool support_explicit_workgroup_layout{}; bool has_broken_spirv_clamp{}; bool lower_left_origin_mode{}; + bool needs_manual_interpolation{}; u64 min_ssbo_alignment{}; }; diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 580458e7e..1c150ce28 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -256,6 +256,7 @@ bool Instance::CreateDevice() { workgroup_memory_explicit_layout = add_extension(VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME); vertex_input_dynamic_state = add_extension(VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME); + fragment_shader_barycentric = add_extension(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME); // The next two extensions are required to be available together in order to support write masks color_write_en = add_extension(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME); @@ -399,6 +400,9 @@ bool Instance::CreateDevice() { vk::PhysicalDevicePrimitiveTopologyListRestartFeaturesEXT{ .primitiveTopologyListRestart = true, }, + vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR{ + .fragmentShaderBarycentric = true, + }, #ifdef __APPLE__ feature_chain.get(), #endif @@ -438,6 +442,9 @@ bool Instance::CreateDevice() { if (!vertex_input_dynamic_state) { device_chain.unlink(); } + if (!fragment_shader_barycentric) { + device_chain.unlink(); + } auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get()); if (device_result != vk::Result::eSuccess) { diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 51c2c57c5..5a46ef6fe 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -143,6 +143,11 @@ public: return maintenance5; } + /// Returns true when VK_KHR_fragment_shader_barycentric is supported. + bool IsFragmentShaderBarycentricSupported() const { + return fragment_shader_barycentric; + } + bool IsListRestartSupported() const { return list_restart; } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 612e950bb..a1ed7edac 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -169,6 +169,8 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_preserve = bool(vk12_props.shaderDenormPreserveFloat32), .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_explicit_workgroup_layout = true, + .needs_manual_interpolation = instance.IsFragmentShaderBarycentricSupported() && + instance.GetDriverID() == vk::DriverId::eNvidiaProprietary, }; auto [cache_result, cache] = instance.GetDevice().createPipelineCacheUnique({}); ASSERT_MSG(cache_result == vk::Result::eSuccess, "Failed to create pipeline cache: {}", From f0b75289c8c810a53294c975453c7faba4f49ef2 Mon Sep 17 00:00:00 2001 From: psucien Date: Mon, 2 Dec 2024 22:24:54 +0100 Subject: [PATCH 05/89] video_core: few detiler formats added --- src/video_core/texture_cache/tile_manager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index c4f24420d..7430168d0 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -182,12 +182,15 @@ vk::Format DemoteImageFormatForDetiling(vk::Format format) { case vk::Format::eB8G8R8A8Srgb: case vk::Format::eB8G8R8A8Unorm: case vk::Format::eR8G8B8A8Unorm: + case vk::Format::eR8G8B8A8Snorm: case vk::Format::eR8G8B8A8Uint: case vk::Format::eR32Sfloat: case vk::Format::eR32Uint: case vk::Format::eR16G16Sfloat: case vk::Format::eR16G16Unorm: + case vk::Format::eR16G16Snorm: case vk::Format::eB10G11R11UfloatPack32: + case vk::Format::eA2B10G10R10UnormPack32: return vk::Format::eR32Uint; case vk::Format::eBc1RgbaSrgbBlock: case vk::Format::eBc1RgbaUnormBlock: From c66db95378fb7ae9b68ea3980eae248da55d2250 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 3 Dec 2024 10:05:51 +0200 Subject: [PATCH 06/89] Misc Ime fixes continue (#1655) * core/libraries: Misc. Ime fixes * fixed issues --------- Co-authored-by: Daniel R <47796739+polybiusproxy@users.noreply.github.com> --- src/core/libraries/ime/ime.cpp | 95 +++++++++++++++++++---------- src/core/libraries/ime/ime.h | 26 ++++++-- src/core/libraries/ime/ime_common.h | 2 +- src/core/libraries/ime/ime_ui.cpp | 81 ++++++++++++------------ src/core/libraries/ime/ime_ui.h | 6 +- 5 files changed, 131 insertions(+), 79 deletions(-) diff --git a/src/core/libraries/ime/ime.cpp b/src/core/libraries/ime/ime.cpp index 700585ff3..dfd659db8 100644 --- a/src/core/libraries/ime/ime.cpp +++ b/src/core/libraries/ime/ime.cpp @@ -44,10 +44,14 @@ public: openEvent.param.rect.y = m_param.ime.posy; } else { openEvent.param.resource_id_array.userId = 1; - openEvent.param.resource_id_array.resource_id[0] = 1; + openEvent.param.resource_id_array.resourceId[0] = 1; } - Execute(nullptr, &openEvent, true); + // Are we supposed to call the event handler on init with + // ADD_OSK? + if (!ime_mode && False(m_param.key.option & OrbisImeKeyboardOption::AddOsk)) { + Execute(nullptr, &openEvent, true); + } if (ime_mode) { g_ime_state = ImeState(&m_param.ime); @@ -56,6 +60,11 @@ public: } s32 Update(OrbisImeEventHandler handler) { + if (!m_ime_mode) { + /* We don't handle any events for ImeKeyboard */ + return ORBIS_OK; + } + std::unique_lock lock{g_ime_state.queue_mutex}; while (!g_ime_state.event_queue.empty()) { @@ -85,6 +94,16 @@ public: } } + s32 SetText(const char16_t* text, u32 length) { + g_ime_state.SetText(text, length); + return ORBIS_OK; + } + + s32 SetCaret(const OrbisImeCaret* caret) { + g_ime_state.SetCaret(caret->index); + return ORBIS_OK; + } + bool IsIme() { return m_ime_mode; } @@ -98,6 +117,7 @@ private: }; static std::unique_ptr g_ime_handler; +static std::unique_ptr g_keyboard_handler; int PS4_SYSV_ABI FinalizeImeModule() { LOG_ERROR(Lib_Ime, "(STUBBED) called"); @@ -130,9 +150,6 @@ s32 PS4_SYSV_ABI sceImeClose() { if (!g_ime_handler) { return ORBIS_IME_ERROR_NOT_OPENED; } - if (!g_ime_handler->IsIme()) { - return ORBIS_IME_ERROR_NOT_OPENED; - } g_ime_handler.release(); g_ime_ui = ImeUi(); @@ -233,14 +250,11 @@ s32 PS4_SYSV_ABI sceImeGetPanelSize(const OrbisImeParam* param, u32* width, u32* s32 PS4_SYSV_ABI sceImeKeyboardClose(s32 userId) { LOG_INFO(Lib_Ime, "(STUBBED) called"); - if (!g_ime_handler) { - return ORBIS_IME_ERROR_NOT_OPENED; - } - if (g_ime_handler->IsIme()) { + if (!g_keyboard_handler) { return ORBIS_IME_ERROR_NOT_OPENED; } - g_ime_handler.release(); + g_keyboard_handler.release(); return ORBIS_OK; } @@ -255,18 +269,17 @@ int PS4_SYSV_ABI sceImeKeyboardGetResourceId() { } s32 PS4_SYSV_ABI sceImeKeyboardOpen(s32 userId, const OrbisImeKeyboardParam* param) { - LOG_ERROR(Lib_Ime, "(STUBBED) called"); + LOG_INFO(Lib_Ime, "called"); if (!param) { return ORBIS_IME_ERROR_INVALID_ADDRESS; } - if (g_ime_handler) { + if (g_keyboard_handler) { return ORBIS_IME_ERROR_BUSY; } - // g_ime_handler = std::make_unique(param); - // return ORBIS_OK; - return ORBIS_IME_ERROR_CONNECTION_FAILED; // Fixup + g_keyboard_handler = std::make_unique(param); + return ORBIS_OK; } int PS4_SYSV_ABI sceImeKeyboardOpenInternal() { @@ -287,16 +300,14 @@ int PS4_SYSV_ABI sceImeKeyboardUpdate() { s32 PS4_SYSV_ABI sceImeOpen(const OrbisImeParam* param, const void* extended) { LOG_INFO(Lib_Ime, "called"); - if (!g_ime_handler) { - g_ime_handler = std::make_unique(param); - } else { - if (g_ime_handler->IsIme()) { - return ORBIS_IME_ERROR_BUSY; - } - - g_ime_handler->Init((void*)param, true); + if (!param) { + return ORBIS_IME_ERROR_INVALID_ADDRESS; + } + if (g_ime_handler) { + return ORBIS_IME_ERROR_BUSY; } + g_ime_handler = std::make_unique(param); return ORBIS_OK; } @@ -322,13 +333,29 @@ int PS4_SYSV_ABI sceImeSetCandidateIndex() { } int PS4_SYSV_ABI sceImeSetCaret(const OrbisImeCaret* caret) { - LOG_ERROR(Lib_Ime, "(STUBBED) called"); - return ORBIS_OK; + LOG_TRACE(Lib_Ime, "called"); + + if (!g_ime_handler) { + return ORBIS_IME_ERROR_NOT_OPENED; + } + if (!caret) { + return ORBIS_IME_ERROR_INVALID_ADDRESS; + } + + return g_ime_handler->SetCaret(caret); } -int PS4_SYSV_ABI sceImeSetText() { - LOG_ERROR(Lib_Ime, "(STUBBED) called"); - return ORBIS_OK; +s32 PS4_SYSV_ABI sceImeSetText(const char16_t* text, u32 length) { + LOG_TRACE(Lib_Ime, "called"); + + if (!g_ime_handler) { + return ORBIS_IME_ERROR_NOT_OPENED; + } + if (!text) { + return ORBIS_IME_ERROR_INVALID_ADDRESS; + } + + return g_ime_handler->SetText(text, length); } int PS4_SYSV_ABI sceImeSetTextGeometry() { @@ -337,13 +364,19 @@ int PS4_SYSV_ABI sceImeSetTextGeometry() { } s32 PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler) { - LOG_TRACE(Lib_Ime, "called"); + if (g_ime_handler) { + g_ime_handler->Update(handler); + } - if (!g_ime_handler) { + if (g_keyboard_handler) { + g_keyboard_handler->Update(handler); + } + + if (!g_ime_handler || !g_keyboard_handler) { return ORBIS_IME_ERROR_NOT_OPENED; } - return g_ime_handler->Update(handler); + return ORBIS_OK; } int PS4_SYSV_ABI sceImeVshClearPreedit() { diff --git a/src/core/libraries/ime/ime.h b/src/core/libraries/ime/ime.h index 2915b70da..448ee6896 100644 --- a/src/core/libraries/ime/ime.h +++ b/src/core/libraries/ime/ime.h @@ -26,6 +26,24 @@ enum class OrbisImeKeyboardOption : u32 { }; DECLARE_ENUM_FLAG_OPERATORS(OrbisImeKeyboardOption) +enum class OrbisImeOption : u32 { + DEFAULT = 0, + MULTILINE = 1, + NO_AUTO_CAPITALIZATION = 2, + PASSWORD = 4, + LANGUAGES_FORCED = 8, + EXT_KEYBOARD = 16, + NO_LEARNING = 32, + FIXED_POSITION = 64, + DISABLE_RESUME = 256, + DISABLE_AUTO_SPACE = 512, + DISABLE_POSITION_ADJUSTMENT = 2048, + EXPANDED_PREEDIT_BUFFER = 4096, + USE_JAPANESE_EISUU_KEY_AS_CAPSLOCK = 8192, + USE_2K_COORDINATES = 16384, +}; +DECLARE_ENUM_FLAG_OPERATORS(OrbisImeOption) + struct OrbisImeKeyboardParam { OrbisImeKeyboardOption option; s8 reserved1[4]; @@ -41,9 +59,9 @@ struct OrbisImeParam { OrbisImeEnterLabel enter_label; OrbisImeInputMethod input_method; OrbisImeTextFilter filter; - u32 option; - u32 max_text_length; - char16_t* input_text_buffer; + OrbisImeOption option; + u32 maxTextLength; + char16_t* inputTextBuffer; float posx; float posy; OrbisImeHorizontalAlignment horizontal_alignment; @@ -93,7 +111,7 @@ int PS4_SYSV_ABI sceImeOpenInternal(); void PS4_SYSV_ABI sceImeParamInit(OrbisImeParam* param); int PS4_SYSV_ABI sceImeSetCandidateIndex(); s32 PS4_SYSV_ABI sceImeSetCaret(const OrbisImeCaret* caret); -int PS4_SYSV_ABI sceImeSetText(); +s32 PS4_SYSV_ABI sceImeSetText(const char16_t* text, u32 length); int PS4_SYSV_ABI sceImeSetTextGeometry(); s32 PS4_SYSV_ABI sceImeUpdate(OrbisImeEventHandler handler); int PS4_SYSV_ABI sceImeVshClearPreedit(); diff --git a/src/core/libraries/ime/ime_common.h b/src/core/libraries/ime/ime_common.h index 77f23d91d..6d4afd81d 100644 --- a/src/core/libraries/ime/ime_common.h +++ b/src/core/libraries/ime/ime_common.h @@ -142,7 +142,7 @@ struct OrbisImeKeycode { struct OrbisImeKeyboardResourceIdArray { s32 userId; - u32 resource_id[6]; + u32 resourceId[5]; }; enum class OrbisImeCaretMovementDirection : u32 { diff --git a/src/core/libraries/ime/ime_ui.cpp b/src/core/libraries/ime/ime_ui.cpp index c5f41c5e8..8eaa48178 100644 --- a/src/core/libraries/ime/ime_ui.cpp +++ b/src/core/libraries/ime/ime_ui.cpp @@ -16,7 +16,7 @@ ImeState::ImeState(const OrbisImeParam* param) { } work_buffer = param->work; - text_buffer = param->input_text_buffer; + text_buffer = param->inputTextBuffer; std::size_t text_len = std::char_traits::length(text_buffer); if (!ConvertOrbisToUTF8(text_buffer, text_len, current_text.begin(), @@ -26,15 +26,13 @@ ImeState::ImeState(const OrbisImeParam* param) { } ImeState::ImeState(ImeState&& other) noexcept - : input_changed(other.input_changed), work_buffer(other.work_buffer), - text_buffer(other.text_buffer), current_text(std::move(other.current_text)), - event_queue(std::move(other.event_queue)) { + : work_buffer(other.work_buffer), text_buffer(other.text_buffer), + current_text(std::move(other.current_text)), event_queue(std::move(other.event_queue)) { other.text_buffer = nullptr; } ImeState& ImeState::operator=(ImeState&& other) noexcept { if (this != &other) { - input_changed = other.input_changed; work_buffer = other.work_buffer; text_buffer = other.text_buffer; current_text = std::move(other.current_text); @@ -63,6 +61,10 @@ void ImeState::SendCloseEvent() { SendEvent(&closeEvent); } +void ImeState::SetText(const char16_t* text, u32 length) {} + +void ImeState::SetCaret(u32 position) {} + bool ImeState::ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len, char* utf8_text, std::size_t utf8_text_len) { std::fill(utf8_text, utf8_text + utf8_text_len, '\0'); @@ -180,9 +182,8 @@ void ImeUi::DrawInputText() { if (first_render) { SetKeyboardFocusHere(); } - if (InputTextEx("##ImeInput", nullptr, state->current_text.begin(), ime_param->max_text_length, + if (InputTextEx("##ImeInput", nullptr, state->current_text.begin(), ime_param->maxTextLength, input_size, ImGuiInputTextFlags_CallbackAlways, InputTextCallback, this)) { - state->input_changed = true; } } @@ -190,6 +191,39 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ImeUi* ui = static_cast(data->UserData); ASSERT(ui); + static std::string lastText; + std::string currentText(data->Buf, data->BufTextLen); + if (currentText != lastText) { + OrbisImeEditText eventParam{}; + eventParam.str = reinterpret_cast(ui->ime_param->work); + eventParam.caret_index = data->CursorPos; + eventParam.area_num = 1; + + eventParam.text_area[0].mode = 1; // Edit mode + eventParam.text_area[0].index = data->CursorPos; + eventParam.text_area[0].length = data->BufTextLen; + + if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str, + ui->ime_param->maxTextLength)) { + LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); + return 0; + } + + if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, + ui->ime_param->inputTextBuffer, + ui->ime_param->maxTextLength)) { + LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); + return 0; + } + + OrbisImeEvent event{}; + event.id = OrbisImeEventId::UpdateText; + event.param.text = eventParam; + + lastText = currentText; + ui->state->SendEvent(&event); + } + static int lastCaretPos = -1; if (lastCaretPos == -1) { lastCaretPos = data->CursorPos; @@ -209,39 +243,6 @@ int ImeUi::InputTextCallback(ImGuiInputTextCallbackData* data) { ui->state->SendEvent(&event); } - static std::string lastText; - std::string currentText(data->Buf, data->BufTextLen); - if (currentText != lastText) { - OrbisImeEditText eventParam{}; - eventParam.str = reinterpret_cast(ui->ime_param->work); - eventParam.caret_index = data->CursorPos; - eventParam.area_num = 1; - - eventParam.text_area[0].mode = 1; // Edit mode - eventParam.text_area[0].index = data->CursorPos; - eventParam.text_area[0].length = data->BufTextLen; - - if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, eventParam.str, - ui->ime_param->max_text_length)) { - LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); - return 0; - } - - if (!ui->state->ConvertUTF8ToOrbis(data->Buf, data->BufTextLen, - ui->ime_param->input_text_buffer, - ui->ime_param->max_text_length)) { - LOG_ERROR(Lib_ImeDialog, "Failed to convert Orbis char to UTF-8"); - return 0; - } - - OrbisImeEvent event{}; - event.id = OrbisImeEventId::UpdateText; - event.param.text = eventParam; - - lastText = currentText; - ui->state->SendEvent(&event); - } - return 0; } diff --git a/src/core/libraries/ime/ime_ui.h b/src/core/libraries/ime/ime_ui.h index ebd70a7c8..a2a806bb9 100644 --- a/src/core/libraries/ime/ime_ui.h +++ b/src/core/libraries/ime/ime_ui.h @@ -22,10 +22,7 @@ class ImeState { friend class ImeHandler; friend class ImeUi; - bool input_changed = false; - void* work_buffer{}; - char16_t* text_buffer{}; // A character can hold up to 4 bytes in UTF-8 @@ -43,6 +40,9 @@ public: void SendEnterEvent(); void SendCloseEvent(); + void SetText(const char16_t* text, u32 length); + void SetCaret(u32 position); + private: bool ConvertOrbisToUTF8(const char16_t* orbis_text, std::size_t orbis_text_len, char* utf8_text, std::size_t native_text_len); From 6bf93071cf43f19b428fb1f4f34039a71cb315ee Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Tue, 3 Dec 2024 14:15:08 +0200 Subject: [PATCH 07/89] hot-fix: Correct getpagesize Tested on my PS4 pro, returns 16KB instead of 4KB --- src/core/libraries/kernel/kernel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index b310c7be9..4028116ef 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -203,7 +203,7 @@ int PS4_SYSV_ABI _sigprocmask() { } int PS4_SYSV_ABI posix_getpagesize() { - return 4096; + return 16_KB; } void RegisterKernel(Core::Loader::SymbolsResolver* sym) { From 74b091fd0818894b19949a1a12d183e152f712ea Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:15:58 -0800 Subject: [PATCH 08/89] renderer_vulkan: Add support for indexed QuadList draw. (#1661) --- src/video_core/buffer_cache/buffer_cache.cpp | 28 ++++++++++++++++++- .../renderer_vulkan/liverpool_to_vk.cpp | 13 --------- .../renderer_vulkan/liverpool_to_vk.h | 28 ++++++++++++++++++- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 77b353c2f..1f05b16c8 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -236,7 +236,7 @@ bool BufferCache::BindVertexBuffers(const Shader::Info& vs_info) { u32 BufferCache::BindIndexBuffer(bool& is_indexed, u32 index_offset) { // Emulate QuadList primitive type with CPU made index buffer. const auto& regs = liverpool->regs; - if (regs.primitive_type == AmdGpu::PrimitiveType::QuadList) { + if (regs.primitive_type == AmdGpu::PrimitiveType::QuadList && !is_indexed) { is_indexed = true; // Emit indices. @@ -262,6 +262,32 @@ u32 BufferCache::BindIndexBuffer(bool& is_indexed, u32 index_offset) { VAddr index_address = regs.index_base_address.Address(); index_address += index_offset * index_size; + if (regs.primitive_type == AmdGpu::PrimitiveType::QuadList) { + // Convert indices. + const u32 new_index_size = regs.num_indices * index_size * 6 / 4; + const auto [data, offset] = stream_buffer.Map(new_index_size); + const auto index_ptr = reinterpret_cast(index_address); + switch (index_type) { + case vk::IndexType::eUint16: + Vulkan::LiverpoolToVK::ConvertQuadToTriangleListIndices(data, index_ptr, + regs.num_indices); + break; + case vk::IndexType::eUint32: + Vulkan::LiverpoolToVK::ConvertQuadToTriangleListIndices(data, index_ptr, + regs.num_indices); + break; + default: + UNREACHABLE_MSG("Unsupported QuadList index type {}", vk::to_string(index_type)); + break; + } + stream_buffer.Commit(); + + // Bind index buffer. + const auto cmdbuf = scheduler.CommandBuffer(); + cmdbuf.bindIndexBuffer(stream_buffer.Handle(), offset, index_type); + return new_index_size / index_size; + } + // Bind index buffer. const u32 index_buffer_size = regs.num_indices * index_size; const auto [vk_buffer, offset] = ObtainBuffer(index_address, index_buffer_size, false); diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index 258e7f391..2262a429a 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -726,19 +726,6 @@ vk::Format DepthFormat(DepthBuffer::ZFormat z_format, DepthBuffer::StencilFormat return format->vk_format; } -void EmitQuadToTriangleListIndices(u8* out_ptr, u32 num_vertices) { - static constexpr u16 NumVerticesPerQuad = 4; - u16* out_data = reinterpret_cast(out_ptr); - for (u16 i = 0; i < num_vertices; i += NumVerticesPerQuad) { - *out_data++ = i; - *out_data++ = i + 1; - *out_data++ = i + 2; - *out_data++ = i; - *out_data++ = i + 2; - *out_data++ = i + 3; - } -} - vk::ClearValue ColorBufferClearValue(const AmdGpu::Liverpool::ColorBuffer& color_buffer) { const auto comp_swap = color_buffer.info.comp_swap.Value(); const auto format = color_buffer.info.format.Value(); diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.h b/src/video_core/renderer_vulkan/liverpool_to_vk.h index 70e707fad..287ba691e 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.h +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.h @@ -68,7 +68,33 @@ vk::ClearValue ColorBufferClearValue(const AmdGpu::Liverpool::ColorBuffer& color vk::SampleCountFlagBits NumSamples(u32 num_samples, vk::SampleCountFlags supported_flags); -void EmitQuadToTriangleListIndices(u8* out_indices, u32 num_vertices); +static constexpr u16 NumVerticesPerQuad = 4; + +inline void EmitQuadToTriangleListIndices(u8* out_ptr, u32 num_vertices) { + u16* out_data = reinterpret_cast(out_ptr); + for (u16 i = 0; i < num_vertices; i += NumVerticesPerQuad) { + *out_data++ = i; + *out_data++ = i + 1; + *out_data++ = i + 2; + *out_data++ = i; + *out_data++ = i + 2; + *out_data++ = i + 3; + } +} + +template +void ConvertQuadToTriangleListIndices(u8* out_ptr, const u8* in_ptr, u32 num_vertices) { + T* out_data = reinterpret_cast(out_ptr); + const T* in_data = reinterpret_cast(in_ptr); + for (u16 i = 0; i < num_vertices; i += NumVerticesPerQuad) { + *out_data++ = in_data[i]; + *out_data++ = in_data[i + 1]; + *out_data++ = in_data[i + 2]; + *out_data++ = in_data[i]; + *out_data++ = in_data[i + 2]; + *out_data++ = in_data[i + 3]; + } +} static inline vk::Format PromoteFormatToDepth(vk::Format fmt) { if (fmt == vk::Format::eR32Sfloat) { From 920acb8d8b7524f32eafefccd5396f7c07f8c158 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 4 Dec 2024 03:03:47 -0800 Subject: [PATCH 09/89] renderer_vulkan: Parse fetch shader per-pipeline (#1656) * shader_recompiler: Read image format info directly from sharps instead of storing in shader info. * renderer_vulkan: Parse fetch shader per-pipeline * Few minor fixes. * shader_recompiler: Specialize on vertex attribute number types. * shader_recompiler: Move GetDrawOffsets to fetch shader --- .../backend/spirv/emit_spirv_image.cpp | 3 +- .../backend/spirv/spirv_emit_context.cpp | 91 ++++++++++--------- .../frontend/fetch_shader.cpp | 12 ++- src/shader_recompiler/frontend/fetch_shader.h | 56 +++++++++++- .../frontend/translate/translate.cpp | 36 ++------ src/shader_recompiler/info.h | 47 ++-------- .../ir/passes/resource_tracking_pass.cpp | 4 - src/shader_recompiler/profile.h | 1 + src/shader_recompiler/specialization.h | 45 ++++++++- src/video_core/amdgpu/pixel_format.h | 19 +++- src/video_core/amdgpu/resource.h | 4 + src/video_core/buffer_cache/buffer_cache.cpp | 21 +++-- src/video_core/buffer_cache/buffer_cache.h | 8 +- .../renderer_vulkan/vk_graphics_pipeline.cpp | 32 ++++--- .../renderer_vulkan/vk_graphics_pipeline.h | 7 ++ .../renderer_vulkan/vk_instance.cpp | 7 ++ src/video_core/renderer_vulkan/vk_instance.h | 7 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 40 ++++---- .../renderer_vulkan/vk_pipeline_cache.h | 7 +- .../renderer_vulkan/vk_rasterizer.cpp | 14 +-- src/video_core/texture_cache/image_view.cpp | 7 +- 21 files changed, 286 insertions(+), 182 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index 40e5ea8b9..fe2660705 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -187,7 +187,8 @@ Id EmitImageFetch(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, const Id EmitImageQueryDimensions(EmitContext& ctx, IR::Inst* inst, u32 handle, Id lod, bool has_mips) { const auto& texture = ctx.images[handle & 0xFFFF]; const Id image = ctx.OpLoad(texture.image_type, texture.id); - const auto type = ctx.info.images[handle & 0xFFFF].type; + const auto sharp = ctx.info.images[handle & 0xFFFF].GetSharp(ctx.info); + const auto type = sharp.GetBoundType(); const Id zero = ctx.u32_zero_value; const auto mips{[&] { return has_mips ? ctx.OpImageQueryLevels(ctx.U32[1], image) : zero; }}; const bool uses_lod{type != AmdGpu::ImageType::Color2DMsaa && !texture.is_storage}; diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 6c8eb1236..4ce9f4221 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -4,6 +4,7 @@ #include "common/assert.h" #include "common/div_ceil.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" +#include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/ir/passes/srt.h" #include "video_core/amdgpu/types.h" @@ -155,18 +156,12 @@ void EmitContext::DefineInterfaces() { } const VectorIds& GetAttributeType(EmitContext& ctx, AmdGpu::NumberFormat fmt) { - switch (fmt) { - case AmdGpu::NumberFormat::Float: - case AmdGpu::NumberFormat::Unorm: - case AmdGpu::NumberFormat::Snorm: - case AmdGpu::NumberFormat::SnormNz: - case AmdGpu::NumberFormat::Sscaled: - case AmdGpu::NumberFormat::Uscaled: - case AmdGpu::NumberFormat::Srgb: + switch (GetNumberClass(fmt)) { + case AmdGpu::NumberClass::Float: return ctx.F32; - case AmdGpu::NumberFormat::Sint: + case AmdGpu::NumberClass::Sint: return ctx.S32; - case AmdGpu::NumberFormat::Uint: + case AmdGpu::NumberClass::Uint: return ctx.U32; default: break; @@ -176,18 +171,12 @@ const VectorIds& GetAttributeType(EmitContext& ctx, AmdGpu::NumberFormat fmt) { EmitContext::SpirvAttribute EmitContext::GetAttributeInfo(AmdGpu::NumberFormat fmt, Id id, u32 num_components, bool output) { - switch (fmt) { - case AmdGpu::NumberFormat::Float: - case AmdGpu::NumberFormat::Unorm: - case AmdGpu::NumberFormat::Snorm: - case AmdGpu::NumberFormat::SnormNz: - case AmdGpu::NumberFormat::Sscaled: - case AmdGpu::NumberFormat::Uscaled: - case AmdGpu::NumberFormat::Srgb: + switch (GetNumberClass(fmt)) { + case AmdGpu::NumberClass::Float: return {id, output ? output_f32 : input_f32, F32[1], num_components, false}; - case AmdGpu::NumberFormat::Uint: + case AmdGpu::NumberClass::Uint: return {id, output ? output_u32 : input_u32, U32[1], num_components, true}; - case AmdGpu::NumberFormat::Sint: + case AmdGpu::NumberClass::Sint: return {id, output ? output_s32 : input_s32, S32[1], num_components, true}; default: break; @@ -280,33 +269,42 @@ void EmitContext::DefineInputs() { base_vertex = DefineVariable(U32[1], spv::BuiltIn::BaseVertex, spv::StorageClass::Input); instance_id = DefineVariable(U32[1], spv::BuiltIn::InstanceIndex, spv::StorageClass::Input); - for (const auto& input : info.vs_inputs) { - ASSERT(input.binding < IR::NumParams); - const Id type{GetAttributeType(*this, input.fmt)[4]}; - if (input.instance_step_rate == Info::VsInput::InstanceIdType::OverStepRate0 || - input.instance_step_rate == Info::VsInput::InstanceIdType::OverStepRate1) { - + const auto fetch_shader = Gcn::ParseFetchShader(info); + if (!fetch_shader) { + break; + } + for (const auto& attrib : fetch_shader->attributes) { + ASSERT(attrib.semantic < IR::NumParams); + const auto sharp = attrib.GetSharp(info); + const Id type{GetAttributeType(*this, sharp.GetNumberFmt())[4]}; + if (attrib.UsesStepRates()) { const u32 rate_idx = - input.instance_step_rate == Info::VsInput::InstanceIdType::OverStepRate0 ? 0 - : 1; + attrib.GetStepRate() == Gcn::VertexAttribute::InstanceIdType::OverStepRate0 ? 0 + : 1; + const u32 num_components = AmdGpu::NumComponents(sharp.GetDataFmt()); + const auto buffer = + std::ranges::find_if(info.buffers, [&attrib](const auto& buffer) { + return buffer.instance_attrib == attrib.semantic; + }); // Note that we pass index rather than Id - input_params[input.binding] = SpirvAttribute{ + input_params[attrib.semantic] = SpirvAttribute{ .id = rate_idx, .pointer_type = input_u32, .component_type = U32[1], - .num_components = input.num_components, + .num_components = std::min(attrib.num_elements, num_components), .is_integer = true, .is_loaded = false, - .buffer_handle = input.instance_data_buf, + .buffer_handle = int(buffer - info.buffers.begin()), }; } else { - Id id{DefineInput(type, input.binding)}; - if (input.instance_step_rate == Info::VsInput::InstanceIdType::Plain) { - Name(id, fmt::format("vs_instance_attr{}", input.binding)); + Id id{DefineInput(type, attrib.semantic)}; + if (attrib.GetStepRate() == Gcn::VertexAttribute::InstanceIdType::Plain) { + Name(id, fmt::format("vs_instance_attr{}", attrib.semantic)); } else { - Name(id, fmt::format("vs_in_attr{}", input.binding)); + Name(id, fmt::format("vs_in_attr{}", attrib.semantic)); } - input_params[input.binding] = GetAttributeInfo(input.fmt, id, 4, false); + input_params[attrib.semantic] = + GetAttributeInfo(sharp.GetNumberFmt(), id, 4, false); interfaces.push_back(id); } } @@ -553,9 +551,10 @@ void EmitContext::DefineBuffers() { void EmitContext::DefineTextureBuffers() { for (const auto& desc : info.texture_buffers) { - const bool is_integer = - desc.nfmt == AmdGpu::NumberFormat::Uint || desc.nfmt == AmdGpu::NumberFormat::Sint; - const VectorIds& sampled_type{GetAttributeType(*this, desc.nfmt)}; + const auto sharp = desc.GetSharp(info); + const auto nfmt = sharp.GetNumberFmt(); + const bool is_integer = AmdGpu::IsInteger(nfmt); + const VectorIds& sampled_type{GetAttributeType(*this, nfmt)}; const u32 sampled = desc.is_written ? 2 : 1; const Id image_type{TypeImage(sampled_type[1], spv::Dim::Buffer, false, false, false, sampled, spv::ImageFormat::Unknown)}; @@ -650,10 +649,11 @@ spv::ImageFormat GetFormat(const AmdGpu::Image& image) { } Id ImageType(EmitContext& ctx, const ImageResource& desc, Id sampled_type) { - const auto image = ctx.info.ReadUdSharp(desc.sharp_idx); + const auto image = desc.GetSharp(ctx.info); const auto format = desc.is_atomic ? GetFormat(image) : spv::ImageFormat::Unknown; + const auto type = image.GetBoundType(); const u32 sampled = desc.is_storage ? 2 : 1; - switch (desc.type) { + switch (type) { case AmdGpu::ImageType::Color1D: return ctx.TypeImage(sampled_type, spv::Dim::Dim1D, false, false, false, sampled, format); case AmdGpu::ImageType::Color1DArray: @@ -672,14 +672,15 @@ Id ImageType(EmitContext& ctx, const ImageResource& desc, Id sampled_type) { default: break; } - throw InvalidArgument("Invalid texture type {}", desc.type); + throw InvalidArgument("Invalid texture type {}", type); } void EmitContext::DefineImagesAndSamplers() { for (const auto& image_desc : info.images) { - const bool is_integer = image_desc.nfmt == AmdGpu::NumberFormat::Uint || - image_desc.nfmt == AmdGpu::NumberFormat::Sint; - const VectorIds& data_types = GetAttributeType(*this, image_desc.nfmt); + const auto sharp = image_desc.GetSharp(info); + const auto nfmt = sharp.GetNumberFmt(); + const bool is_integer = AmdGpu::IsInteger(nfmt); + const VectorIds& data_types = GetAttributeType(*this, nfmt); const Id sampled_type = data_types[1]; const Id image_type{ImageType(*this, image_desc, sampled_type)}; const Id pointer_type{TypePointer(spv::StorageClass::UniformConstant, image_type)}; diff --git a/src/shader_recompiler/frontend/fetch_shader.cpp b/src/shader_recompiler/frontend/fetch_shader.cpp index 16938410c..8ae664d79 100644 --- a/src/shader_recompiler/frontend/fetch_shader.cpp +++ b/src/shader_recompiler/frontend/fetch_shader.cpp @@ -34,8 +34,14 @@ namespace Shader::Gcn { * We take the reverse way, extract the original input semantics from these instructions. **/ -FetchShaderData ParseFetchShader(const u32* code, u32* out_size) { - FetchShaderData data{}; +std::optional ParseFetchShader(const Shader::Info& info) { + if (!info.has_fetch_shader) { + return std::nullopt; + } + const u32* code; + std::memcpy(&code, &info.user_data[info.fetch_shader_sgpr_base], sizeof(code)); + + FetchShaderData data{.code = code}; GcnCodeSlice code_slice(code, code + std::numeric_limits::max()); GcnDecodeContext decoder; @@ -49,7 +55,7 @@ FetchShaderData ParseFetchShader(const u32* code, u32* out_size) { u32 semantic_index = 0; while (!code_slice.atEnd()) { const auto inst = decoder.decodeInstruction(code_slice); - *out_size += inst.length; + data.size += inst.length; if (inst.opcode == Opcode::S_SETPC_B64) { break; diff --git a/src/shader_recompiler/frontend/fetch_shader.h b/src/shader_recompiler/frontend/fetch_shader.h index 0e5d15419..ee9f5c805 100644 --- a/src/shader_recompiler/frontend/fetch_shader.h +++ b/src/shader_recompiler/frontend/fetch_shader.h @@ -3,26 +3,80 @@ #pragma once +#include #include #include "common/types.h" +#include "shader_recompiler/info.h" namespace Shader::Gcn { struct VertexAttribute { + enum InstanceIdType : u8 { + None = 0, + OverStepRate0 = 1, + OverStepRate1 = 2, + Plain = 3, + }; + u8 semantic; ///< Semantic index of the attribute u8 dest_vgpr; ///< Destination VGPR to load first component. u8 num_elements; ///< Number of components to load u8 sgpr_base; ///< SGPR that contains the pointer to the list of vertex V# u8 dword_offset; ///< The dword offset of the V# that describes this attribute. u8 instance_data; ///< Indicates that the buffer will be accessed in instance rate + + [[nodiscard]] InstanceIdType GetStepRate() const { + return static_cast(instance_data); + } + + [[nodiscard]] bool UsesStepRates() const { + const auto step_rate = GetStepRate(); + return step_rate == OverStepRate0 || step_rate == OverStepRate1; + } + + [[nodiscard]] constexpr AmdGpu::Buffer GetSharp(const Shader::Info& info) const noexcept { + return info.ReadUdReg(sgpr_base, dword_offset); + } + + bool operator==(const VertexAttribute& other) const { + return semantic == other.semantic && dest_vgpr == other.dest_vgpr && + num_elements == other.num_elements && sgpr_base == other.sgpr_base && + dword_offset == other.dword_offset && instance_data == other.instance_data; + } }; struct FetchShaderData { + const u32* code; + u32 size = 0; std::vector attributes; s8 vertex_offset_sgpr = -1; ///< SGPR of vertex offset from VADDR s8 instance_offset_sgpr = -1; ///< SGPR of instance offset from VADDR + + [[nodiscard]] bool UsesStepRates() const { + return std::ranges::find_if(attributes, [](const VertexAttribute& attribute) { + return attribute.UsesStepRates(); + }) != attributes.end(); + } + + [[nodiscard]] std::pair GetDrawOffsets(const AmdGpu::Liverpool::Regs& regs, + const Info& info) const { + u32 vertex_offset = regs.index_offset; + u32 instance_offset = 0; + if (vertex_offset == 0 && vertex_offset_sgpr != -1) { + vertex_offset = info.user_data[vertex_offset_sgpr]; + } + if (instance_offset_sgpr != -1) { + instance_offset = info.user_data[instance_offset_sgpr]; + } + return {vertex_offset, instance_offset}; + } + + bool operator==(const FetchShaderData& other) const { + return attributes == other.attributes && vertex_offset_sgpr == other.vertex_offset_sgpr && + instance_offset_sgpr == other.instance_offset_sgpr; + } }; -FetchShaderData ParseFetchShader(const u32* code, u32* out_size); +std::optional ParseFetchShader(const Shader::Info& info); } // namespace Shader::Gcn diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 005c4a7ff..68625a12b 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -368,13 +368,11 @@ void Translator::SetDst64(const InstOperand& operand, const IR::U64F64& value_ra void Translator::EmitFetch(const GcnInst& inst) { // Read the pointer to the fetch shader assembly. - const u32 sgpr_base = inst.src[0].code; - const u32* code; - std::memcpy(&code, &info.user_data[sgpr_base], sizeof(code)); + info.has_fetch_shader = true; + info.fetch_shader_sgpr_base = inst.src[0].code; - // Parse the assembly to generate a list of attributes. - u32 fetch_size{}; - const auto fetch_data = ParseFetchShader(code, &fetch_size); + const auto fetch_data = ParseFetchShader(info); + ASSERT(fetch_data.has_value()); if (Config::dumpShaders()) { using namespace Common::FS; @@ -384,13 +382,10 @@ void Translator::EmitFetch(const GcnInst& inst) { } const auto filename = fmt::format("vs_{:#018x}.fetch.bin", info.pgm_hash); const auto file = IOFile{dump_dir / filename, FileAccessMode::Write}; - file.WriteRaw(code, fetch_size); + file.WriteRaw(fetch_data->code, fetch_data->size); } - info.vertex_offset_sgpr = fetch_data.vertex_offset_sgpr; - info.instance_offset_sgpr = fetch_data.instance_offset_sgpr; - - for (const auto& attrib : fetch_data.attributes) { + for (const auto& attrib : fetch_data->attributes) { const IR::Attribute attr{IR::Attribute::Param0 + attrib.semantic}; IR::VectorReg dst_reg{attrib.dest_vgpr}; @@ -420,29 +415,14 @@ void Translator::EmitFetch(const GcnInst& inst) { // In case of programmable step rates we need to fallback to instance data pulling in // shader, so VBs should be bound as regular data buffers - s32 instance_buf_handle = -1; - const auto step_rate = static_cast(attrib.instance_data); - if (step_rate == Info::VsInput::OverStepRate0 || - step_rate == Info::VsInput::OverStepRate1) { + if (attrib.UsesStepRates()) { info.buffers.push_back({ .sharp_idx = info.srt_info.ReserveSharp(attrib.sgpr_base, attrib.dword_offset, 4), .used_types = IR::Type::F32, .is_instance_data = true, + .instance_attrib = attrib.semantic, }); - instance_buf_handle = s32(info.buffers.size() - 1); - info.uses_step_rates = true; } - - const u32 num_components = AmdGpu::NumComponents(buffer.GetDataFmt()); - info.vs_inputs.push_back({ - .fmt = buffer.GetNumberFmt(), - .binding = attrib.semantic, - .num_components = std::min(attrib.num_elements, num_components), - .sgpr_base = attrib.sgpr_base, - .dword_offset = attrib.dword_offset, - .instance_step_rate = step_rate, - .instance_data_buf = instance_buf_handle, - }); } } diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index c7ae2a1e5..d382d0e7c 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -45,6 +45,7 @@ struct BufferResource { AmdGpu::Buffer inline_cbuf; bool is_gds_buffer{}; bool is_instance_data{}; + u8 instance_attrib{}; bool is_written{}; bool IsStorage(AmdGpu::Buffer buffer) const noexcept { @@ -57,7 +58,6 @@ using BufferResourceList = boost::container::small_vector; struct TextureBufferResource { u32 sharp_idx; - AmdGpu::NumberFormat nfmt; bool is_written{}; constexpr AmdGpu::Buffer GetSharp(const Info& info) const noexcept; @@ -66,8 +66,6 @@ using TextureBufferResourceList = boost::container::small_vector vs_inputs{}; - struct AttributeFlags { bool Get(IR::Attribute attrib, u32 comp = 0) const { return flags[Index(attrib)] & (1 << comp); @@ -179,9 +159,6 @@ struct Info { CopyShaderData gs_copy_data; - s8 vertex_offset_sgpr = -1; - s8 instance_offset_sgpr = -1; - BufferResourceList buffers; TextureBufferResourceList texture_buffers; ImageResourceList images; @@ -208,10 +185,11 @@ struct Info { bool uses_shared{}; bool uses_fp16{}; bool uses_fp64{}; - bool uses_step_rates{}; bool translation_failed{}; // indicates that shader has unsupported instructions bool has_readconst{}; u8 mrt_mask{0u}; + bool has_fetch_shader{false}; + u32 fetch_shader_sgpr_base{0u}; explicit Info(Stage stage_, ShaderParams params) : stage{stage_}, pgm_hash{params.hash}, pgm_base{params.Base()}, @@ -252,18 +230,6 @@ struct Info { bnd.user_data += ud_mask.NumRegs(); } - [[nodiscard]] std::pair GetDrawOffsets(const AmdGpu::Liverpool::Regs& regs) const { - u32 vertex_offset = regs.index_offset; - u32 instance_offset = 0; - if (vertex_offset == 0 && vertex_offset_sgpr != -1) { - vertex_offset = user_data[vertex_offset_sgpr]; - } - if (instance_offset_sgpr != -1) { - instance_offset = user_data[instance_offset_sgpr]; - } - return {vertex_offset, instance_offset}; - } - void RefreshFlatBuf() { flattened_ud_buf.resize(srt_info.flattened_bufsize_dw); ASSERT(user_data.size() <= NumUserDataRegs); @@ -284,7 +250,12 @@ constexpr AmdGpu::Buffer TextureBufferResource::GetSharp(const Info& info) const } constexpr AmdGpu::Image ImageResource::GetSharp(const Info& info) const noexcept { - return info.ReadUdSharp(sharp_idx); + const auto image = info.ReadUdSharp(sharp_idx); + if (!image.Valid()) { + // Fall back to null image if unbound. + return AmdGpu::Image::Null(); + } + return image; } constexpr AmdGpu::Sampler SamplerResource::GetSharp(const Info& info) const noexcept { diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 7d29c845d..c1ff3d2f2 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -381,7 +381,6 @@ void PatchTextureBufferInstruction(IR::Block& block, IR::Inst& inst, Info& info, const auto buffer = info.ReadUdSharp(sharp); const s32 binding = descriptors.Add(TextureBufferResource{ .sharp_idx = sharp, - .nfmt = buffer.GetNumberFmt(), .is_written = inst.GetOpcode() == IR::Opcode::StoreBufferFormatF32, }); @@ -660,11 +659,8 @@ void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descrip } } - const auto type = image.IsPartialCubemap() ? AmdGpu::ImageType::Color2DArray : image.GetType(); u32 image_binding = descriptors.Add(ImageResource{ .sharp_idx = tsharp, - .type = type, - .nfmt = image.GetNumberFmt(), .is_storage = is_storage, .is_depth = bool(inst_info.is_depth), .is_atomic = IsImageAtomicInstruction(inst), diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h index a868ab76c..96c458d44 100644 --- a/src/shader_recompiler/profile.h +++ b/src/shader_recompiler/profile.h @@ -22,6 +22,7 @@ struct Profile { bool support_fp32_denorm_preserve{}; bool support_fp32_denorm_flush{}; bool support_explicit_workgroup_layout{}; + bool support_legacy_vertex_attributes{}; bool has_broken_spirv_clamp{}; bool lower_left_origin_mode{}; bool needs_manual_interpolation{}; diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 225b164b5..740b89dda 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -6,12 +6,19 @@ #include #include "common/types.h" +#include "frontend/fetch_shader.h" #include "shader_recompiler/backend/bindings.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/passes/srt.h" namespace Shader { +struct VsAttribSpecialization { + AmdGpu::NumberClass num_class{}; + + auto operator<=>(const VsAttribSpecialization&) const = default; +}; + struct BufferSpecialization { u16 stride : 14; u16 is_storage : 1; @@ -50,6 +57,8 @@ struct StageSpecialization { const Shader::Info* info; RuntimeInfo runtime_info; + Gcn::FetchShaderData fetch_shader_data{}; + boost::container::small_vector vs_attribs; std::bitset bitset{}; boost::container::small_vector buffers; boost::container::small_vector tex_buffers; @@ -57,9 +66,19 @@ struct StageSpecialization { boost::container::small_vector fmasks; Backend::Bindings start{}; - explicit StageSpecialization(const Shader::Info& info_, RuntimeInfo runtime_info_, - Backend::Bindings start_) + explicit StageSpecialization(const Info& info_, RuntimeInfo runtime_info_, + const Profile& profile_, Backend::Bindings start_) : info{&info_}, runtime_info{runtime_info_}, start{start_} { + if (const auto fetch_shader = Gcn::ParseFetchShader(info_)) { + fetch_shader_data = *fetch_shader; + if (info_.stage == Stage::Vertex && !profile_.support_legacy_vertex_attributes) { + // Specialize shader on VS input number types to follow spec. + ForEachSharp(vs_attribs, fetch_shader_data.attributes, + [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { + spec.num_class = AmdGpu::GetNumberClass(sharp.GetNumberFmt()); + }); + } + } u32 binding{}; if (info->has_readconst) { binding++; @@ -75,8 +94,7 @@ struct StageSpecialization { }); ForEachSharp(binding, images, info->images, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { - spec.type = sharp.IsPartialCubemap() ? AmdGpu::ImageType::Color2DArray - : sharp.GetType(); + spec.type = sharp.GetBoundType(); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); }); ForEachSharp(binding, fmasks, info->fmasks, @@ -86,6 +104,17 @@ struct StageSpecialization { }); } + void ForEachSharp(auto& spec_list, auto& desc_list, auto&& func) { + for (const auto& desc : desc_list) { + auto& spec = spec_list.emplace_back(); + const auto sharp = desc.GetSharp(*info); + if (!sharp) { + continue; + } + func(spec, desc, sharp); + } + } + void ForEachSharp(u32& binding, auto& spec_list, auto& desc_list, auto&& func) { for (const auto& desc : desc_list) { auto& spec = spec_list.emplace_back(); @@ -106,6 +135,14 @@ struct StageSpecialization { if (runtime_info != other.runtime_info) { return false; } + if (fetch_shader_data != other.fetch_shader_data) { + return false; + } + for (u32 i = 0; i < vs_attribs.size(); i++) { + if (vs_attribs[i] != other.vs_attribs[i]) { + return false; + } + } u32 binding{}; if (info->has_readconst != other.info->has_readconst) { return false; diff --git a/src/video_core/amdgpu/pixel_format.h b/src/video_core/amdgpu/pixel_format.h index e83313ea4..38c81ba5f 100644 --- a/src/video_core/amdgpu/pixel_format.h +++ b/src/video_core/amdgpu/pixel_format.h @@ -10,7 +10,24 @@ namespace AmdGpu { -[[nodiscard]] constexpr bool IsInteger(NumberFormat nfmt) { +enum NumberClass { + Float, + Sint, + Uint, +}; + +[[nodiscard]] constexpr NumberClass GetNumberClass(const NumberFormat nfmt) { + switch (nfmt) { + case NumberFormat::Sint: + return Sint; + case NumberFormat::Uint: + return Uint; + default: + return Float; + } +} + +[[nodiscard]] constexpr bool IsInteger(const NumberFormat nfmt) { return nfmt == AmdGpu::NumberFormat::Sint || nfmt == AmdGpu::NumberFormat::Uint; } diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index f43fc9800..a78a68391 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -304,6 +304,10 @@ struct Image { const auto viewed_slice = last_array - base_array + 1; return GetType() == ImageType::Cube && viewed_slice < 6; } + + ImageType GetBoundType() const noexcept { + return IsPartialCubemap() ? ImageType::Color2DArray : GetType(); + } }; static_assert(sizeof(Image) == 32); // 256bits diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 1f05b16c8..1abdb230b 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -5,6 +5,7 @@ #include "common/alignment.h" #include "common/scope_exit.h" #include "common/types.h" +#include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/info.h" #include "video_core/amdgpu/liverpool.h" #include "video_core/buffer_cache/buffer_cache.h" @@ -107,7 +108,8 @@ void BufferCache::DownloadBufferMemory(Buffer& buffer, VAddr device_addr, u64 si } } -bool BufferCache::BindVertexBuffers(const Shader::Info& vs_info) { +bool BufferCache::BindVertexBuffers( + const Shader::Info& vs_info, const std::optional& fetch_shader) { boost::container::small_vector attributes; boost::container::small_vector bindings; SCOPE_EXIT { @@ -126,7 +128,7 @@ bool BufferCache::BindVertexBuffers(const Shader::Info& vs_info) { } }; - if (vs_info.vs_inputs.empty()) { + if (!fetch_shader || fetch_shader->attributes.empty()) { return false; } @@ -150,30 +152,29 @@ bool BufferCache::BindVertexBuffers(const Shader::Info& vs_info) { // Calculate buffers memory overlaps bool has_step_rate = false; boost::container::static_vector ranges{}; - for (const auto& input : vs_info.vs_inputs) { - if (input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate0 || - input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate1) { + for (const auto& attrib : fetch_shader->attributes) { + if (attrib.UsesStepRates()) { has_step_rate = true; continue; } - const auto& buffer = vs_info.ReadUdReg(input.sgpr_base, input.dword_offset); + const auto& buffer = attrib.GetSharp(vs_info); if (buffer.GetSize() == 0) { continue; } guest_buffers.emplace_back(buffer); ranges.emplace_back(buffer.base_address, buffer.base_address + buffer.GetSize()); attributes.push_back({ - .location = input.binding, - .binding = input.binding, + .location = attrib.semantic, + .binding = attrib.semantic, .format = Vulkan::LiverpoolToVK::SurfaceFormat(buffer.GetDataFmt(), buffer.GetNumberFmt()), .offset = 0, }); bindings.push_back({ - .binding = input.binding, + .binding = attrib.semantic, .stride = buffer.GetStride(), - .inputRate = input.instance_step_rate == Shader::Info::VsInput::None + .inputRate = attrib.GetStepRate() == Shader::Gcn::VertexAttribute::InstanceIdType::None ? vk::VertexInputRate::eVertex : vk::VertexInputRate::eInstance, .divisor = 1, diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index e2519e942..b1bf77f8a 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -20,8 +20,11 @@ struct Liverpool; } namespace Shader { -struct Info; +namespace Gcn { +struct FetchShaderData; } +struct Info; +} // namespace Shader namespace VideoCore { @@ -76,7 +79,8 @@ public: void InvalidateMemory(VAddr device_addr, u64 size); /// Binds host vertex buffers for the current draw. - bool BindVertexBuffers(const Shader::Info& vs_info); + bool BindVertexBuffers(const Shader::Info& vs_info, + const std::optional& fetch_shader); /// Bind host index buffer for the current draw. u32 BindIndexBuffer(bool& is_indexed, u32 index_offset); diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index d0d16ac75..d53204c77 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include @@ -10,6 +11,8 @@ #include "video_core/amdgpu/resource.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/renderer_vulkan/vk_graphics_pipeline.h" + +#include "shader_recompiler/frontend/fetch_shader.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/texture_cache/texture_cache.h" @@ -20,8 +23,10 @@ GraphicsPipeline::GraphicsPipeline(const Instance& instance_, Scheduler& schedul DescriptorHeap& desc_heap_, const GraphicsPipelineKey& key_, vk::PipelineCache pipeline_cache, std::span infos, + std::optional fetch_shader_, std::span modules) - : Pipeline{instance_, scheduler_, desc_heap_, pipeline_cache}, key{key_} { + : Pipeline{instance_, scheduler_, desc_heap_, pipeline_cache}, key{key_}, + fetch_shader{std::move(fetch_shader_)} { const vk::Device device = instance.GetDevice(); std::ranges::copy(infos, stages.begin()); BuildDescSetLayout(); @@ -46,32 +51,31 @@ GraphicsPipeline::GraphicsPipeline(const Instance& instance_, Scheduler& schedul boost::container::static_vector vertex_bindings; boost::container::static_vector vertex_attributes; - if (!instance.IsVertexInputDynamicState()) { - const auto& vs_info = stages[u32(Shader::Stage::Vertex)]; - for (const auto& input : vs_info->vs_inputs) { - if (input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate0 || - input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate1) { + if (fetch_shader && !instance.IsVertexInputDynamicState()) { + const auto& vs_info = GetStage(Shader::Stage::Vertex); + for (const auto& attrib : fetch_shader->attributes) { + if (attrib.UsesStepRates()) { // Skip attribute binding as the data will be pulled by shader continue; } - const auto buffer = - vs_info->ReadUdReg(input.sgpr_base, input.dword_offset); + const auto buffer = attrib.GetSharp(vs_info); if (buffer.GetSize() == 0) { continue; } vertex_attributes.push_back({ - .location = input.binding, - .binding = input.binding, + .location = attrib.semantic, + .binding = attrib.semantic, .format = LiverpoolToVK::SurfaceFormat(buffer.GetDataFmt(), buffer.GetNumberFmt()), .offset = 0, }); vertex_bindings.push_back({ - .binding = input.binding, + .binding = attrib.semantic, .stride = buffer.GetStride(), - .inputRate = input.instance_step_rate == Shader::Info::VsInput::None - ? vk::VertexInputRate::eVertex - : vk::VertexInputRate::eInstance, + .inputRate = + attrib.GetStepRate() == Shader::Gcn::VertexAttribute::InstanceIdType::None + ? vk::VertexInputRate::eVertex + : vk::VertexInputRate::eInstance, }); } } diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index 4f4abfd16..91ffe4ea4 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -4,6 +4,7 @@ #include #include "common/types.h" +#include "shader_recompiler/frontend/fetch_shader.h" #include "video_core/renderer_vulkan/liverpool_to_vk.h" #include "video_core/renderer_vulkan/vk_common.h" #include "video_core/renderer_vulkan/vk_pipeline_common.h" @@ -59,9 +60,14 @@ public: GraphicsPipeline(const Instance& instance, Scheduler& scheduler, DescriptorHeap& desc_heap, const GraphicsPipelineKey& key, vk::PipelineCache pipeline_cache, std::span stages, + std::optional fetch_shader, std::span modules); ~GraphicsPipeline(); + const std::optional& GetFetchShader() const noexcept { + return fetch_shader; + } + bool IsEmbeddedVs() const noexcept { static constexpr size_t EmbeddedVsHash = 0x9b2da5cf47f8c29f; return key.stage_hashes[u32(Shader::Stage::Vertex)] == EmbeddedVsHash; @@ -94,6 +100,7 @@ private: private: GraphicsPipelineKey key; + std::optional fetch_shader{}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 1c150ce28..49e4987db 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -265,6 +265,7 @@ bool Instance::CreateDevice() { const bool robustness = add_extension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); list_restart = add_extension(VK_EXT_PRIMITIVE_TOPOLOGY_LIST_RESTART_EXTENSION_NAME); maintenance5 = add_extension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME); + legacy_vertex_attributes = add_extension(VK_EXT_LEGACY_VERTEX_ATTRIBUTES_EXTENSION_NAME); // These extensions are promoted by Vulkan 1.3, but for greater compatibility we use Vulkan 1.2 // with extensions. @@ -403,6 +404,9 @@ bool Instance::CreateDevice() { vk::PhysicalDeviceFragmentShaderBarycentricFeaturesKHR{ .fragmentShaderBarycentric = true, }, + vk::PhysicalDeviceLegacyVertexAttributesFeaturesEXT{ + .legacyVertexAttributes = true, + }, #ifdef __APPLE__ feature_chain.get(), #endif @@ -445,6 +449,9 @@ bool Instance::CreateDevice() { if (!fragment_shader_barycentric) { device_chain.unlink(); } + if (!legacy_vertex_attributes) { + device_chain.unlink(); + } auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get()); if (device_result != vk::Result::eSuccess) { diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 5a46ef6fe..81303c9cc 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -148,10 +148,16 @@ public: return fragment_shader_barycentric; } + /// Returns true when VK_EXT_primitive_topology_list_restart is supported. bool IsListRestartSupported() const { return list_restart; } + /// Returns true when VK_EXT_legacy_vertex_attributes is supported. + bool IsLegacyVertexAttributesSupported() const { + return legacy_vertex_attributes; + } + /// Returns true when geometry shaders are supported by the device bool IsGeometryStageSupported() const { return features.geometryShader; @@ -320,6 +326,7 @@ private: bool null_descriptor{}; bool maintenance5{}; bool list_restart{}; + bool legacy_vertex_attributes{}; u64 min_imported_host_pointer_alignment{}; u32 subgroup_size{}; bool tooling_info{}; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index a1ed7edac..47713f0ff 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -169,6 +169,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_preserve = bool(vk12_props.shaderDenormPreserveFloat32), .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_explicit_workgroup_layout = true, + .support_legacy_vertex_attributes = instance_.IsLegacyVertexAttributesSupported(), .needs_manual_interpolation = instance.IsFragmentShaderBarycentricSupported() && instance.GetDriverID() == vk::DriverId::eNvidiaProprietary, }; @@ -187,7 +188,7 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline() { const auto [it, is_new] = graphics_pipelines.try_emplace(graphics_key); if (is_new) { it.value() = graphics_pipeline_pool.Create(instance, scheduler, desc_heap, graphics_key, - *pipeline_cache, infos, modules); + *pipeline_cache, infos, fetch_shader, modules); } return it->second; } @@ -304,8 +305,12 @@ bool PipelineCache::RefreshGraphicsKey() { } auto params = Liverpool::GetParams(*pgm); - std::tie(infos[stage_out_idx], modules[stage_out_idx], key.stage_hashes[stage_out_idx]) = - GetProgram(stage_in, params, binding); + std::optional fetch_shader_; + std::tie(infos[stage_out_idx], modules[stage_out_idx], fetch_shader_, + key.stage_hashes[stage_out_idx]) = GetProgram(stage_in, params, binding); + if (fetch_shader_) { + fetch_shader = fetch_shader_; + } return true; }; @@ -341,16 +346,14 @@ bool PipelineCache::RefreshGraphicsKey() { } } - const auto* vs_info = infos[static_cast(Shader::Stage::Vertex)]; - if (vs_info && !instance.IsVertexInputDynamicState()) { + const auto vs_info = infos[static_cast(Shader::Stage::Vertex)]; + if (vs_info && fetch_shader && !instance.IsVertexInputDynamicState()) { u32 vertex_binding = 0; - for (const auto& input : vs_info->vs_inputs) { - if (input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate0 || - input.instance_step_rate == Shader::Info::VsInput::InstanceIdType::OverStepRate1) { + for (const auto& attrib : fetch_shader->attributes) { + if (attrib.UsesStepRates()) { continue; } - const auto& buffer = - vs_info->ReadUdReg(input.sgpr_base, input.dword_offset); + const auto& buffer = attrib.GetSharp(*vs_info); if (buffer.GetSize() == 0) { continue; } @@ -394,7 +397,7 @@ bool PipelineCache::RefreshComputeKey() { Shader::Backend::Bindings binding{}; const auto* cs_pgm = &liverpool->regs.cs_program; const auto cs_params = Liverpool::GetParams(*cs_pgm); - std::tie(infos[0], modules[0], compute_key) = + std::tie(infos[0], modules[0], fetch_shader, compute_key) = GetProgram(Shader::Stage::Compute, cs_params, binding); return true; } @@ -425,24 +428,26 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, return module; } -std::tuple PipelineCache::GetProgram( - Shader::Stage stage, Shader::ShaderParams params, Shader::Backend::Bindings& binding) { +std::tuple, u64> +PipelineCache::GetProgram(Shader::Stage stage, Shader::ShaderParams params, + Shader::Backend::Bindings& binding) { const auto runtime_info = BuildRuntimeInfo(stage); auto [it_pgm, new_program] = program_cache.try_emplace(params.hash); if (new_program) { Program* program = program_pool.Create(stage, params); auto start = binding; const auto module = CompileModule(program->info, runtime_info, params.code, 0, binding); - const auto spec = Shader::StageSpecialization(program->info, runtime_info, start); + const auto spec = Shader::StageSpecialization(program->info, runtime_info, profile, start); program->AddPermut(module, std::move(spec)); it_pgm.value() = program; - return std::make_tuple(&program->info, module, HashCombine(params.hash, 0)); + return std::make_tuple(&program->info, module, spec.fetch_shader_data, + HashCombine(params.hash, 0)); } Program* program = it_pgm->second; auto& info = program->info; info.RefreshFlatBuf(); - const auto spec = Shader::StageSpecialization(info, runtime_info, binding); + const auto spec = Shader::StageSpecialization(info, runtime_info, profile, binding); size_t perm_idx = program->modules.size(); vk::ShaderModule module{}; @@ -456,7 +461,8 @@ std::tuple PipelineCache::GetProgram module = it->module; perm_idx = std::distance(program->modules.begin(), it); } - return std::make_tuple(&info, module, HashCombine(params.hash, perm_idx)); + return std::make_tuple(&info, module, spec.fetch_shader_data, + HashCombine(params.hash, perm_idx)); } void PipelineCache::DumpShader(std::span code, u64 hash, Shader::Stage stage, diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 662bcbd80..e4a8abd4f 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -47,8 +47,10 @@ public: const ComputePipeline* GetComputePipeline(); - std::tuple GetProgram( - Shader::Stage stage, Shader::ShaderParams params, Shader::Backend::Bindings& binding); + std::tuple, + u64> + GetProgram(Shader::Stage stage, Shader::ShaderParams params, + Shader::Backend::Bindings& binding); private: bool RefreshGraphicsKey(); @@ -80,6 +82,7 @@ private: tsl::robin_map graphics_pipelines; std::array infos{}; std::array modules{}; + std::optional fetch_shader{}; GraphicsPipelineKey graphics_key{}; u64 compute_key{}; }; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index ff5e88141..084b7c345 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -187,13 +187,14 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset) { } const auto& vs_info = pipeline->GetStage(Shader::Stage::Vertex); - buffer_cache.BindVertexBuffers(vs_info); + const auto& fetch_shader = pipeline->GetFetchShader(); + buffer_cache.BindVertexBuffers(vs_info, fetch_shader); const u32 num_indices = buffer_cache.BindIndexBuffer(is_indexed, index_offset); BeginRendering(*pipeline, state); UpdateDynamicState(*pipeline); - const auto [vertex_offset, instance_offset] = vs_info.GetDrawOffsets(regs); + const auto [vertex_offset, instance_offset] = fetch_shader->GetDrawOffsets(regs, vs_info); const auto cmdbuf = scheduler.CommandBuffer(); cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline->Handle()); @@ -243,7 +244,8 @@ void Rasterizer::DrawIndirect(bool is_indexed, VAddr arg_address, u32 offset, u3 } const auto& vs_info = pipeline->GetStage(Shader::Stage::Vertex); - buffer_cache.BindVertexBuffers(vs_info); + const auto& fetch_shader = pipeline->GetFetchShader(); + buffer_cache.BindVertexBuffers(vs_info, fetch_shader); buffer_cache.BindIndexBuffer(is_indexed, 0); const auto& [buffer, base] = @@ -397,10 +399,8 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) { if (!stage) { continue; } - if (stage->uses_step_rates) { - push_data.step0 = regs.vgt_instance_step_rate_0; - push_data.step1 = regs.vgt_instance_step_rate_1; - } + push_data.step0 = regs.vgt_instance_step_rate_0; + push_data.step1 = regs.vgt_instance_step_rate_1; stage->PushUd(binding, push_data); BindBuffers(*stage, binding, push_data, set_writes, buffer_barriers); diff --git a/src/video_core/texture_cache/image_view.cpp b/src/video_core/texture_cache/image_view.cpp index 488d44a7f..61cabdf11 100644 --- a/src/video_core/texture_cache/image_view.cpp +++ b/src/video_core/texture_cache/image_view.cpp @@ -87,12 +87,9 @@ ImageViewInfo::ImageViewInfo(const AmdGpu::Image& image, const Shader::ImageReso range.extent.levels = image.last_level - image.base_level + 1; } range.extent.layers = image.last_array - image.base_array + 1; - type = ConvertImageViewType(image.GetType()); + type = ConvertImageViewType(image.GetBoundType()); - // Adjust view type for partial cubemaps and arrays - if (image.IsPartialCubemap()) { - type = vk::ImageViewType::e2DArray; - } + // Adjust view type for arrays if (type == vk::ImageViewType::eCube) { if (desc.is_array) { type = vk::ImageViewType::eCubeArray; From c019b54fecff141bade82ef6efbbd1fdbc7d231a Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:21:03 -0800 Subject: [PATCH 10/89] thread: Configure stack and guard on POSIX hosts. (#1664) --- src/core/libraries/kernel/threads/pthread.cpp | 2 +- src/core/thread.cpp | 4 +++- src/core/thread.h | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index 4629980c9..793ddd1fe 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -281,7 +281,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt /* Create thread */ new_thread->native_thr = Core::Thread(); - int ret = new_thread->native_thr.Create(RunThread, new_thread); + int ret = new_thread->native_thr.Create(RunThread, new_thread, &new_thread->attr); ASSERT_MSG(ret == 0, "Failed to create thread with error {}", ret); if (ret) { *thread = nullptr; diff --git a/src/core/thread.cpp b/src/core/thread.cpp index e9c46b522..a93f16c8d 100644 --- a/src/core/thread.cpp +++ b/src/core/thread.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "libraries/kernel/threads/pthread.h" #include "thread.h" #ifdef _WIN64 @@ -15,7 +16,7 @@ Thread::Thread() : native_handle{0} {} Thread::~Thread() {} -int Thread::Create(ThreadFunc func, void* arg) { +int Thread::Create(ThreadFunc func, void* arg, const ::Libraries::Kernel::PthreadAttr* attr) { #ifdef _WIN64 native_handle = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)func, arg, 0, nullptr); return native_handle ? 0 : -1; @@ -23,6 +24,7 @@ int Thread::Create(ThreadFunc func, void* arg) { pthread_t* pthr = reinterpret_cast(&native_handle); pthread_attr_t pattr; pthread_attr_init(&pattr); + pthread_attr_setstack(&pattr, attr->stackaddr_attr, attr->stacksize_attr); return pthread_create(pthr, &pattr, (PthreadFunc)func, arg); #endif } diff --git a/src/core/thread.h b/src/core/thread.h index 8665100af..cfb8b8309 100644 --- a/src/core/thread.h +++ b/src/core/thread.h @@ -5,6 +5,10 @@ #include "common/types.h" +namespace Libraries::Kernel { +struct PthreadAttr; +} // namespace Libraries::Kernel + namespace Core { class Thread { @@ -15,7 +19,7 @@ public: Thread(); ~Thread(); - int Create(ThreadFunc func, void* arg); + int Create(ThreadFunc func, void* arg, const ::Libraries::Kernel::PthreadAttr* attr); void Exit(); uintptr_t GetHandle() { From 2380f2f9c9340fcf3b35cf9ff9ff24e3726e4d89 Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Thu, 5 Dec 2024 13:00:17 -0300 Subject: [PATCH 11/89] Virtual device abstraction (#1577) * IOFile: removes seek limit checks when file is writable * add virtual devices scaffold * add stdin/out/err as virtual devices * fixed some merging issues * clang-fix --------- Co-authored-by: georgemoralis --- CMakeLists.txt | 7 + src/common/io_file.cpp | 22 +-- src/common/io_file.h | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/common/va_ctx.h | 111 +++++++++++++ src/core/devices/base_device.cpp | 12 ++ src/core/devices/base_device.h | 72 +++++++++ src/core/devices/ioccom.h | 67 ++++++++ src/core/devices/logger.cpp | 64 ++++++++ src/core/devices/logger.h | 37 +++++ src/core/devices/nop_device.h | 55 +++++++ src/core/file_sys/fs.cpp | 31 +++- src/core/file_sys/fs.h | 12 +- src/core/libraries/kernel/file_system.cpp | 186 ++++++++++++++++++---- src/core/libraries/kernel/kernel.cpp | 79 +++++---- src/core/libraries/kernel/process.cpp | 2 +- src/emulator.cpp | 3 + 18 files changed, 687 insertions(+), 77 deletions(-) create mode 100644 src/common/va_ctx.h create mode 100644 src/core/devices/base_device.cpp create mode 100644 src/core/devices/base_device.h create mode 100644 src/core/devices/ioccom.h create mode 100644 src/core/devices/logger.cpp create mode 100644 src/core/devices/logger.h create mode 100644 src/core/devices/nop_device.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f503832c..56760be37 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -502,6 +502,7 @@ set(COMMON src/common/logging/backend.cpp src/common/types.h src/common/uint128.h src/common/unique_function.h + src/common/va_ctx.h src/common/version.h src/common/ntapi.h src/common/ntapi.cpp @@ -526,6 +527,12 @@ set(CORE src/core/aerolib/stubs.cpp src/core/crypto/crypto.cpp src/core/crypto/crypto.h src/core/crypto/keys.h + src/core/devices/base_device.cpp + src/core/devices/base_device.h + src/core/devices/ioccom.h + src/core/devices/logger.cpp + src/core/devices/logger.h + src/core/devices/nop_device.h src/core/file_format/pfs.h src/core/file_format/pkg.cpp src/core/file_format/pkg.h diff --git a/src/common/io_file.cpp b/src/common/io_file.cpp index dd3a40cae..067010a26 100644 --- a/src/common/io_file.cpp +++ b/src/common/io_file.cpp @@ -377,16 +377,18 @@ bool IOFile::Seek(s64 offset, SeekOrigin origin) const { return false; } - u64 size = GetSize(); - if (origin == SeekOrigin::CurrentPosition && Tell() + offset > size) { - LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); - return false; - } else if (origin == SeekOrigin::SetOrigin && (u64)offset > size) { - LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); - return false; - } else if (origin == SeekOrigin::End && offset > 0) { - LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); - return false; + if (False(file_access_mode & (FileAccessMode::Write | FileAccessMode::Append))) { + u64 size = GetSize(); + if (origin == SeekOrigin::CurrentPosition && Tell() + offset > size) { + LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); + return false; + } else if (origin == SeekOrigin::SetOrigin && (u64)offset > size) { + LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); + return false; + } else if (origin == SeekOrigin::End && offset > 0) { + LOG_ERROR(Common_Filesystem, "Seeking past the end of the file"); + return false; + } } errno = 0; diff --git a/src/common/io_file.h b/src/common/io_file.h index 8fed4981f..feb2110ac 100644 --- a/src/common/io_file.h +++ b/src/common/io_file.h @@ -10,6 +10,7 @@ #include "common/concepts.h" #include "common/types.h" +#include "enum.h" namespace Common::FS { @@ -42,6 +43,7 @@ enum class FileAccessMode { */ ReadAppend = Read | Append, }; +DECLARE_ENUM_FLAG_OPERATORS(FileAccessMode); enum class FileType { BinaryFile, diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 632b2b329..75c61a188 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -69,6 +69,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Common, Memory) \ CLS(Core) \ SUB(Core, Linker) \ + SUB(Core, Devices) \ CLS(Config) \ CLS(Debug) \ CLS(Kernel) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index e7e91882a..a0e7d021f 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -35,6 +35,7 @@ enum class Class : u8 { Common_Memory, ///< Memory mapping and management functions Core, ///< LLE emulation core Core_Linker, ///< The module linker + Core_Devices, ///< Devices emulation Config, ///< Emulator configuration (including commandline) Debug, ///< Debugging tools Kernel, ///< The HLE implementation of the PS4 kernel. diff --git a/src/common/va_ctx.h b/src/common/va_ctx.h new file mode 100644 index 000000000..e0b8c0bab --- /dev/null +++ b/src/common/va_ctx.h @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "common/types.h" + +#define VA_ARGS \ + uint64_t rdi, uint64_t rsi, uint64_t rdx, uint64_t rcx, uint64_t r8, uint64_t r9, \ + uint64_t overflow_arg_area, __m128 xmm0, __m128 xmm1, __m128 xmm2, __m128 xmm3, \ + __m128 xmm4, __m128 xmm5, __m128 xmm6, __m128 xmm7, ... + +#define VA_CTX(ctx) \ + alignas(16)::Common::VaCtx ctx{}; \ + (ctx).reg_save_area.gp[0] = rdi; \ + (ctx).reg_save_area.gp[1] = rsi; \ + (ctx).reg_save_area.gp[2] = rdx; \ + (ctx).reg_save_area.gp[3] = rcx; \ + (ctx).reg_save_area.gp[4] = r8; \ + (ctx).reg_save_area.gp[5] = r9; \ + (ctx).reg_save_area.fp[0] = xmm0; \ + (ctx).reg_save_area.fp[1] = xmm1; \ + (ctx).reg_save_area.fp[2] = xmm2; \ + (ctx).reg_save_area.fp[3] = xmm3; \ + (ctx).reg_save_area.fp[4] = xmm4; \ + (ctx).reg_save_area.fp[5] = xmm5; \ + (ctx).reg_save_area.fp[6] = xmm6; \ + (ctx).reg_save_area.fp[7] = xmm7; \ + (ctx).va_list.reg_save_area = &(ctx).reg_save_area; \ + (ctx).va_list.gp_offset = offsetof(::Common::VaRegSave, gp); \ + (ctx).va_list.fp_offset = offsetof(::Common::VaRegSave, fp); \ + (ctx).va_list.overflow_arg_area = &overflow_arg_area; + +namespace Common { + +// https://stackoverflow.com/questions/4958384/what-is-the-format-of-the-x86-64-va-list-structure + +struct VaList { + u32 gp_offset; + u32 fp_offset; + void* overflow_arg_area; + void* reg_save_area; +}; + +struct VaRegSave { + u64 gp[6]; + __m128 fp[8]; +}; + +struct VaCtx { + VaRegSave reg_save_area; + VaList va_list; +}; + +template +T vaArgRegSaveAreaGp(VaList* l) { + auto* addr = reinterpret_cast(static_cast(l->reg_save_area) + l->gp_offset); + l->gp_offset += Size; + return *addr; +} +template +T vaArgOverflowArgArea(VaList* l) { + auto ptr = ((reinterpret_cast(l->overflow_arg_area) + (Align - 1)) & ~(Align - 1)); + auto* addr = reinterpret_cast(ptr); + l->overflow_arg_area = reinterpret_cast(ptr + Size); + return *addr; +} + +template +T vaArgRegSaveAreaFp(VaList* l) { + auto* addr = reinterpret_cast(static_cast(l->reg_save_area) + l->fp_offset); + l->fp_offset += Size; + return *addr; +} + +inline int vaArgInteger(VaList* l) { + if (l->gp_offset <= 40) { + return vaArgRegSaveAreaGp(l); + } + return vaArgOverflowArgArea(l); +} + +inline long long vaArgLongLong(VaList* l) { + if (l->gp_offset <= 40) { + return vaArgRegSaveAreaGp(l); + } + return vaArgOverflowArgArea(l); +} +inline long vaArgLong(VaList* l) { + if (l->gp_offset <= 40) { + return vaArgRegSaveAreaGp(l); + } + return vaArgOverflowArgArea(l); +} + +inline double vaArgDouble(VaList* l) { + if (l->fp_offset <= 160) { + return vaArgRegSaveAreaFp(l); + } + return vaArgOverflowArgArea(l); +} + +template +T* vaArgPtr(VaList* l) { + if (l->gp_offset <= 40) { + return vaArgRegSaveAreaGp(l); + } + return vaArgOverflowArgArea(l); +} + +} // namespace Common diff --git a/src/core/devices/base_device.cpp b/src/core/devices/base_device.cpp new file mode 100644 index 000000000..4f91c81c7 --- /dev/null +++ b/src/core/devices/base_device.cpp @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "base_device.h" + +namespace Core::Devices { + +BaseDevice::BaseDevice() = default; + +BaseDevice::~BaseDevice() = default; + +} // namespace Core::Devices \ No newline at end of file diff --git a/src/core/devices/base_device.h b/src/core/devices/base_device.h new file mode 100644 index 000000000..351af82b4 --- /dev/null +++ b/src/core/devices/base_device.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" +#include "common/va_ctx.h" + +namespace Libraries::Kernel { +struct OrbisKernelStat; +struct SceKernelIovec; +} // namespace Libraries::Kernel + +namespace Core::Devices { + +class BaseDevice { +public: + explicit BaseDevice(); + + virtual ~BaseDevice() = 0; + + virtual int ioctl(u64 cmd, Common::VaCtx* args) { + return ORBIS_KERNEL_ERROR_ENOTTY; + } + + virtual s64 write(const void* buf, size_t nbytes) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual size_t readv(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual size_t writev(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual s64 preadv(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt, u64 offset) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual s64 lseek(s64 offset, int whence) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual s64 read(void* buf, size_t nbytes) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual int fstat(Libraries::Kernel::OrbisKernelStat* sb) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual s32 fsync() { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual int ftruncate(s64 length) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual int getdents(void* buf, u32 nbytes, s64* basep) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + virtual s64 pwrite(const void* buf, size_t nbytes, u64 offset) { + return ORBIS_KERNEL_ERROR_EBADF; + } +}; + +} // namespace Core::Devices diff --git a/src/core/devices/ioccom.h b/src/core/devices/ioccom.h new file mode 100644 index 000000000..671ee33d4 --- /dev/null +++ b/src/core/devices/ioccom.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +/*- + * Copyright (c) 1982, 1986, 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * @(#)ioccom.h 8.2 (Berkeley) 3/28/94 + * $FreeBSD$ + */ + +#define IOCPARM_SHIFT 13 /* number of bits for ioctl size */ +#define IOCPARM_MASK ((1 << IOCPARM_SHIFT) - 1) /* parameter length mask */ +#define IOCPARM_LEN(x) (((x) >> 16) & IOCPARM_MASK) +#define IOCBASECMD(x) ((x) & ~(IOCPARM_MASK << 16)) +#define IOCGROUP(x) (((x) >> 8) & 0xff) + +#define IOCPARM_MAX (1 << IOCPARM_SHIFT) /* max size of ioctl */ +#define IOC_VOID 0x20000000 /* no parameters */ +#define IOC_OUT 0x40000000 /* copy out parameters */ +#define IOC_IN 0x80000000 /* copy in parameters */ +#define IOC_INOUT (IOC_IN | IOC_OUT) +#define IOC_DIRMASK (IOC_VOID | IOC_OUT | IOC_IN) + +#define _IOC(inout, group, num, len) \ + ((unsigned long)((inout) | (((len) & IOCPARM_MASK) << 16) | ((group) << 8) | (num))) +#define _IO(g, n) _IOC(IOC_VOID, (g), (n), 0) +#define _IOWINT(g, n) _IOC(IOC_VOID, (g), (n), sizeof(int)) +#define _IOR(g, n, t) _IOC(IOC_OUT, (g), (n), sizeof(t)) +#define _IOW(g, n, t) _IOC(IOC_IN, (g), (n), sizeof(t)) +/* this should be _IORW, but stdio got there first */ +#define _IOWR(g, n, t) _IOC(IOC_INOUT, (g), (n), sizeof(t)) + +/* +# Simple parse of ioctl cmd +def parse(v): + print('inout', (v >> 24 & 0xFF)) + print('len', hex(v >> 16 & 0xFF)) + print('group', chr(v >> 8 & 0xFF)) + print('num', hex(v & 0xFF)) +*/ diff --git a/src/core/devices/logger.cpp b/src/core/devices/logger.cpp new file mode 100644 index 000000000..bf5a28382 --- /dev/null +++ b/src/core/devices/logger.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/kernel/file_system.h" +#include "logger.h" + +namespace Core::Devices { + +Logger::Logger(std::string prefix, bool is_err) : prefix(std::move(prefix)), is_err(is_err) {} + +Logger::~Logger() = default; + +s64 Logger::write(const void* buf, size_t nbytes) { + log(static_cast(buf), nbytes); + return nbytes; +} +size_t Logger::writev(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) { + for (int i = 0; i < iovcnt; i++) { + log(static_cast(iov[i].iov_base), iov[i].iov_len); + } + return iovcnt; +} + +s64 Logger::pwrite(const void* buf, size_t nbytes, u64 offset) { + log(static_cast(buf), nbytes); + return nbytes; +} + +s32 Logger::fsync() { + log_flush(); + return 0; +} + +void Logger::log(const char* buf, size_t nbytes) { + std::scoped_lock lock{mtx}; + const char* end = buf + nbytes; + for (const char* it = buf; it < end; ++it) { + char c = *it; + if (c == '\r') { + continue; + } + if (c == '\n') { + log_flush(); + continue; + } + buffer.push_back(c); + } +} + +void Logger::log_flush() { + std::scoped_lock lock{mtx}; + if (buffer.empty()) { + return; + } + if (is_err) { + LOG_ERROR(Tty, "[{}] {}", prefix, std::string_view{buffer}); + } else { + LOG_INFO(Tty, "[{}] {}", prefix, std::string_view{buffer}); + } + buffer.clear(); +} + +} // namespace Core::Devices \ No newline at end of file diff --git a/src/core/devices/logger.h b/src/core/devices/logger.h new file mode 100644 index 000000000..bfb07f337 --- /dev/null +++ b/src/core/devices/logger.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "base_device.h" + +#include +#include +#include + +namespace Core::Devices { + +class Logger final : BaseDevice { + std::string prefix; + bool is_err; + + std::recursive_mutex mtx; + std::vector buffer; + +public: + explicit Logger(std::string prefix, bool is_err); + + ~Logger() override; + + s64 write(const void* buf, size_t nbytes) override; + size_t writev(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) override; + s64 pwrite(const void* buf, size_t nbytes, u64 offset) override; + + s32 fsync() override; + +private: + void log(const char* buf, size_t nbytes); + void log_flush(); +}; + +} // namespace Core::Devices diff --git a/src/core/devices/nop_device.h b/src/core/devices/nop_device.h new file mode 100644 index 000000000..a75b92f1b --- /dev/null +++ b/src/core/devices/nop_device.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include "base_device.h" + +namespace Core::Devices { + +class NopDevice final : BaseDevice { + u32 handle; + +public: + explicit NopDevice(u32 handle) : handle(handle) {} + + ~NopDevice() override = default; + + int ioctl(u64 cmd, Common::VaCtx* args) override { + return 0; + } + s64 write(const void* buf, size_t nbytes) override { + return 0; + } + size_t readv(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) override { + return 0; + } + size_t writev(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) override { + return 0; + } + s64 preadv(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt, u64 offset) override { + return 0; + } + s64 lseek(s64 offset, int whence) override { + return 0; + } + s64 read(void* buf, size_t nbytes) override { + return 0; + } + int fstat(Libraries::Kernel::OrbisKernelStat* sb) override { + return 0; + } + s32 fsync() override { + return 0; + } + int ftruncate(s64 length) override { + return 0; + } + int getdents(void* buf, u32 nbytes, s64* basep) override { + return 0; + } + s64 pwrite(const void* buf, size_t nbytes, u64 offset) override { + return 0; + } +}; + +} // namespace Core::Devices diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 769940cf0..0fdbb2783 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -4,12 +4,12 @@ #include #include "common/config.h" #include "common/string_util.h" +#include "core/devices/logger.h" +#include "core/devices/nop_device.h" #include "core/file_sys/fs.h" namespace Core::FileSys { -constexpr int RESERVED_HANDLES = 3; // First 3 handles are stdin,stdout,stderr - void MntPoints::Mount(const std::filesystem::path& host_folder, const std::string& guest_folder, bool read_only) { std::scoped_lock lock{m_mutex}; @@ -135,7 +135,6 @@ int HandleTable::CreateHandle() { std::scoped_lock lock{m_mutex}; auto* file = new File{}; - file->is_directory = false; file->is_opened = false; int existingFilesNum = m_files.size(); @@ -143,23 +142,23 @@ int HandleTable::CreateHandle() { for (int index = 0; index < existingFilesNum; index++) { if (m_files.at(index) == nullptr) { m_files[index] = file; - return index + RESERVED_HANDLES; + return index; } } m_files.push_back(file); - return m_files.size() + RESERVED_HANDLES - 1; + return m_files.size() - 1; } void HandleTable::DeleteHandle(int d) { std::scoped_lock lock{m_mutex}; - delete m_files.at(d - RESERVED_HANDLES); - m_files[d - RESERVED_HANDLES] = nullptr; + delete m_files.at(d); + m_files[d] = nullptr; } File* HandleTable::GetFile(int d) { std::scoped_lock lock{m_mutex}; - return m_files.at(d - RESERVED_HANDLES); + return m_files.at(d); } File* HandleTable::GetFile(const std::filesystem::path& host_name) { @@ -171,4 +170,20 @@ File* HandleTable::GetFile(const std::filesystem::path& host_name) { return nullptr; } +void HandleTable::CreateStdHandles() { + auto setup = [this](const char* path, auto* device) { + int fd = CreateHandle(); + auto* file = GetFile(fd); + file->is_opened = true; + file->type = FileType::Device; + file->m_guest_name = path; + file->device = + std::shared_ptr{reinterpret_cast(device)}; + }; + // order matters + setup("/dev/stdin", new Devices::NopDevice(0)); // stdin + setup("/dev/stdout", new Devices::Logger("stdout", false)); // stdout + setup("/dev/stderr", new Devices::Logger("stderr", true)); // stderr +} + } // namespace Core::FileSys diff --git a/src/core/file_sys/fs.h b/src/core/file_sys/fs.h index eeaeaf781..b0153c162 100644 --- a/src/core/file_sys/fs.h +++ b/src/core/file_sys/fs.h @@ -9,6 +9,7 @@ #include #include #include "common/io_file.h" +#include "core/devices/base_device.h" namespace Core::FileSys { @@ -55,15 +56,22 @@ struct DirEntry { bool isFile; }; +enum class FileType { + Regular, // standard file + Directory, + Device, +}; + struct File { std::atomic_bool is_opened{}; - std::atomic_bool is_directory{}; + std::atomic type{FileType::Regular}; std::filesystem::path m_host_name; std::string m_guest_name; Common::FS::IOFile f; std::vector dirents; u32 dirents_index; std::mutex m_mutex; + std::shared_ptr device; // only valid for type == Device }; class HandleTable { @@ -76,6 +84,8 @@ public: File* GetFile(int d); File* GetFile(const std::filesystem::path& host_name); + void CreateStdHandles(); + private: std::vector m_files; std::mutex m_mutex; diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 1b95e5270..447467cb7 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -11,6 +11,39 @@ #include "core/libraries/libs.h" #include "kernel.h" +#include +#include + +#include "core/devices/logger.h" +#include "core/devices/nop_device.h" + +namespace D = Core::Devices; +using FactoryDevice = std::function(u32, const char*, int, u16)>; + +#define GET_DEVICE_FD(fd) \ + [](u32, const char*, int, u16) { \ + return Common::Singleton::Instance()->GetFile(fd)->device; \ + } + +// prefix path, only dev devices +static std::map available_device = { + // clang-format off + {"/dev/stdin", GET_DEVICE_FD(0)}, + {"/dev/stdout", GET_DEVICE_FD(1)}, + {"/dev/stderr", GET_DEVICE_FD(2)}, + + {"/dev/fd/0", GET_DEVICE_FD(0)}, + {"/dev/fd/1", GET_DEVICE_FD(1)}, + {"/dev/fd/2", GET_DEVICE_FD(2)}, + + {"/dev/deci_stdin", GET_DEVICE_FD(0)}, + {"/dev/deci_stdout", GET_DEVICE_FD(1)}, + {"/dev/deci_stderr", GET_DEVICE_FD(2)}, + + {"/dev/null", GET_DEVICE_FD(0)}, // fd0 (stdin) is a nop device + // clang-format on +}; + namespace Libraries::Kernel { auto GetDirectoryEntries(const std::filesystem::path& path) { @@ -24,8 +57,8 @@ auto GetDirectoryEntries(const std::filesystem::path& path) { return files; } -int PS4_SYSV_ABI sceKernelOpen(const char* path, int flags, u16 mode) { - LOG_INFO(Kernel_Fs, "path = {} flags = {:#x} mode = {}", path, flags, mode); +int PS4_SYSV_ABI sceKernelOpen(const char* raw_path, int flags, u16 mode) { + LOG_INFO(Kernel_Fs, "path = {} flags = {:#x} mode = {}", raw_path, flags, mode); auto* h = Common::Singleton::Instance(); auto* mnt = Common::Singleton::Instance(); @@ -44,22 +77,35 @@ int PS4_SYSV_ABI sceKernelOpen(const char* path, int flags, u16 mode) { bool direct = (flags & ORBIS_KERNEL_O_DIRECT) != 0; bool directory = (flags & ORBIS_KERNEL_O_DIRECTORY) != 0; - if (std::string_view{path} == "/dev/console") { + std::string_view path{raw_path}; + + if (path == "/dev/console") { return 2000; } - if (std::string_view{path} == "/dev/deci_tty6") { + if (path == "/dev/deci_tty6") { return 2001; } - if (std::string_view{path} == "/dev/stdout") { - return 2002; - } - if (std::string_view{path} == "/dev/urandom") { + if (path == "/dev/urandom") { return 2003; } + u32 handle = h->CreateHandle(); auto* file = h->GetFile(handle); + + if (path.starts_with("/dev/")) { + for (const auto& [prefix, factory] : available_device) { + if (path.starts_with(prefix)) { + file->is_opened = true; + file->type = Core::FileSys::FileType::Device; + file->m_guest_name = path; + file->device = factory(handle, path.data(), flags, mode); + return handle; + } + } + } + if (directory) { - file->is_directory = true; + file->type = Core::FileSys::FileType::Directory; file->m_guest_name = path; file->m_host_name = mnt->GetHostPath(file->m_guest_name); if (!std::filesystem::is_directory(file->m_host_name)) { // directory doesn't exist @@ -135,11 +181,12 @@ int PS4_SYSV_ABI sceKernelClose(int d) { if (file == nullptr) { return ORBIS_KERNEL_ERROR_EBADF; } - if (!file->is_directory) { + if (file->type == Core::FileSys::FileType::Regular) { file->f.Close(); } file->is_opened = false; LOG_INFO(Kernel_Fs, "Closing {}", file->m_guest_name); + // FIXME: Lock file mutex before deleting it? h->DeleteHandle(d); return ORBIS_OK; } @@ -155,14 +202,6 @@ int PS4_SYSV_ABI posix_close(int d) { } size_t PS4_SYSV_ABI sceKernelWrite(int d, const void* buf, size_t nbytes) { - if (d <= 2) { // stdin,stdout,stderr - char* str = strdup((const char*)buf); - if (str[nbytes - 1] == '\n') - str[nbytes - 1] = 0; - LOG_INFO(Tty, "{}", str); - free(str); - return nbytes; - } auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(d); if (file == nullptr) { @@ -170,6 +209,9 @@ size_t PS4_SYSV_ABI sceKernelWrite(int d, const void* buf, size_t nbytes) { } std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->write(buf, nbytes); + } return file->f.WriteRaw(buf, nbytes); } @@ -207,17 +249,63 @@ int PS4_SYSV_ABI sceKernelUnlink(const char* path) { size_t PS4_SYSV_ABI _readv(int d, const SceKernelIovec* iov, int iovcnt) { auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(d); - size_t total_read = 0; + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + int r = file->device->readv(iov, iovcnt); + if (r < 0) { + ErrSceToPosix(r); + return -1; + } + return r; + } + size_t total_read = 0; for (int i = 0; i < iovcnt; i++) { total_read += file->f.ReadRaw(iov[i].iov_base, iov[i].iov_len); } return total_read; } +size_t PS4_SYSV_ABI _writev(int fd, const SceKernelIovec* iov, int iovcn) { + if (fd == 1) { + size_t total_written = 0; + for (int i = 0; i < iovcn; i++) { + total_written += ::fwrite(iov[i].iov_base, 1, iov[i].iov_len, stdout); + } + return total_written; + } + auto* h = Common::Singleton::Instance(); + auto* file = h->GetFile(fd); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + std::scoped_lock lk{file->m_mutex}; + + if (file->type == Core::FileSys::FileType::Device) { + return file->device->writev(iov, iovcn); + } + size_t total_written = 0; + for (int i = 0; i < iovcn; i++) { + total_written += file->f.WriteRaw(iov[i].iov_base, iov[i].iov_len); + } + return total_written; +} + s64 PS4_SYSV_ABI sceKernelLseek(int d, s64 offset, int whence) { auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(d); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->lseek(offset, whence); + } Common::FS::SeekOrigin origin{}; if (whence == 0) { @@ -228,7 +316,6 @@ s64 PS4_SYSV_ABI sceKernelLseek(int d, s64 offset, int whence) { origin = Common::FS::SeekOrigin::End; } - std::scoped_lock lk{file->m_mutex}; if (!file->f.Seek(offset, origin)) { LOG_CRITICAL(Kernel_Fs, "sceKernelLseek: failed to seek"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -261,6 +348,9 @@ s64 PS4_SYSV_ABI sceKernelRead(int d, void* buf, size_t nbytes) { } std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->read(buf, nbytes); + } return file->f.ReadRaw(buf, nbytes); } @@ -409,7 +499,13 @@ int PS4_SYSV_ABI posix_stat(const char* path, OrbisKernelStat* sb) { int PS4_SYSV_ABI sceKernelCheckReachability(const char* path) { auto* mnt = Common::Singleton::Instance(); - const auto path_name = mnt->GetHostPath(path); + std::string_view guest_path{path}; + for (const auto& prefix : available_device | std::views::keys) { + if (guest_path.starts_with(prefix)) { + return ORBIS_OK; + } + } + const auto path_name = mnt->GetHostPath(guest_path); if (!std::filesystem::exists(path_name)) { return ORBIS_KERNEL_ERROR_ENOENT; } @@ -431,6 +527,10 @@ s64 PS4_SYSV_ABI sceKernelPreadv(int d, SceKernelIovec* iov, int iovcnt, s64 off } std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->preadv(iov, iovcnt, offset); + } + const s64 pos = file->f.Tell(); SCOPE_EXIT { file->f.Seek(pos); @@ -466,18 +566,25 @@ int PS4_SYSV_ABI sceKernelFStat(int fd, OrbisKernelStat* sb) { } std::memset(sb, 0, sizeof(OrbisKernelStat)); - if (file->is_directory) { - sb->st_mode = 0000777u | 0040000u; - sb->st_size = 0; - sb->st_blksize = 512; - sb->st_blocks = 0; - // TODO incomplete - } else { + switch (file->type) { + case Core::FileSys::FileType::Device: + return file->device->fstat(sb); + case Core::FileSys::FileType::Regular: sb->st_mode = 0000777u | 0100000u; sb->st_size = file->f.GetSize(); sb->st_blksize = 512; sb->st_blocks = (sb->st_size + 511) / 512; // TODO incomplete + break; + case Core::FileSys::FileType::Directory: + sb->st_mode = 0000777u | 0040000u; + sb->st_size = 0; + sb->st_blksize = 512; + sb->st_blocks = 0; + // TODO incomplete + break; + default: + UNREACHABLE(); } return ORBIS_OK; } @@ -495,6 +602,13 @@ int PS4_SYSV_ABI posix_fstat(int fd, OrbisKernelStat* sb) { s32 PS4_SYSV_ABI sceKernelFsync(int fd) { auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(fd); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + if (file->type == Core::FileSys::FileType::Device) { + return file->device->fsync(); + } file->f.Flush(); return ORBIS_OK; } @@ -517,6 +631,10 @@ int PS4_SYSV_ABI sceKernelFtruncate(int fd, s64 length) { return ORBIS_KERNEL_ERROR_EBADF; } + if (file->type == Core::FileSys::FileType::Device) { + return file->device->ftruncate(length); + } + if (file->m_host_name.empty()) { return ORBIS_KERNEL_ERROR_EACCES; } @@ -538,10 +656,15 @@ static int GetDents(int fd, char* buf, int nbytes, s64* basep) { if (file == nullptr) { return ORBIS_KERNEL_ERROR_EBADF; } + if (file->type != Core::FileSys::FileType::Device) { + return file->device->getdents(buf, nbytes, basep); + } + if (file->dirents_index == file->dirents.size()) { return ORBIS_OK; } - if (!file->is_directory || nbytes < 512 || file->dirents_index > file->dirents.size()) { + if (file->type != Core::FileSys::FileType::Directory || nbytes < 512 || + file->dirents_index > file->dirents.size()) { return ORBIS_KERNEL_ERROR_EINVAL; } const auto& entry = file->dirents.at(file->dirents_index++); @@ -586,6 +709,10 @@ s64 PS4_SYSV_ABI sceKernelPwrite(int d, void* buf, size_t nbytes, s64 offset) { } std::scoped_lock lk{file->m_mutex}; + + if (file->type == Core::FileSys::FileType::Device) { + return file->device->pwrite(buf, nbytes, offset); + } const s64 pos = file->f.Tell(); SCOPE_EXIT { file->f.Seek(pos); @@ -637,6 +764,7 @@ void RegisterFileSystem(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("4wSze92BhLI", "libkernel", 1, "libkernel", 1, 1, sceKernelWrite); LIB_FUNCTION("+WRlkKjZvag", "libkernel", 1, "libkernel", 1, 1, _readv); + LIB_FUNCTION("YSHRBRLn2pI", "libkernel", 1, "libkernel", 1, 1, _writev); LIB_FUNCTION("Oy6IpwgtYOk", "libkernel", 1, "libkernel", 1, 1, posix_lseek); LIB_FUNCTION("Oy6IpwgtYOk", "libScePosix", 1, "libkernel", 1, 1, posix_lseek); LIB_FUNCTION("oib76F-12fk", "libkernel", 1, "libkernel", 1, 1, sceKernelLseek); diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 4028116ef..0b4e89fc7 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -9,6 +9,9 @@ #include "common/logging/log.h" #include "common/polyfill_thread.h" #include "common/thread.h" +#include "common/va_ctx.h" +#include "core/file_sys/fs.h" +#include "core/libraries/error_codes.h" #include "core/libraries/kernel/equeue.h" #include "core/libraries/kernel/file_system.h" #include "core/libraries/kernel/kernel.h" @@ -24,6 +27,7 @@ #ifdef _WIN64 #include #endif +#include namespace Libraries::Kernel { @@ -65,19 +69,6 @@ static PS4_SYSV_ABI void stack_chk_fail() { UNREACHABLE(); } -struct iovec { - void* iov_base; /* Base address. */ - size_t iov_len; /* Length. */ -}; - -size_t PS4_SYSV_ABI _writev(int fd, const struct iovec* iov, int iovcn) { - size_t total_written = 0; - for (int i = 0; i < iovcn; i++) { - total_written += ::fwrite(iov[i].iov_base, 1, iov[i].iov_len, stdout); - } - return total_written; -} - static thread_local int g_posix_errno = 0; int* PS4_SYSV_ABI __Error() { @@ -142,24 +133,33 @@ void PS4_SYSV_ABI sceLibcHeapGetTraceInfo(HeapInfoInfo* info) { } s64 PS4_SYSV_ABI ps4__write(int d, const char* buf, std::size_t nbytes) { - if (d <= 2) { // stdin,stdout,stderr - std::string_view str{buf}; - if (str[nbytes - 1] == '\n') { - str = str.substr(0, nbytes - 1); - } - LOG_INFO(Tty, "{}", str); - return nbytes; + auto* h = Common::Singleton::Instance(); + auto* file = h->GetFile(d); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; } - LOG_ERROR(Kernel, "(STUBBED) called d = {} nbytes = {} ", d, nbytes); - UNREACHABLE(); - return ORBIS_OK; + std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->write(buf, nbytes); + } + return file->f.WriteRaw(buf, nbytes); } s64 PS4_SYSV_ABI ps4__read(int d, void* buf, u64 nbytes) { - ASSERT_MSG(d == 0, "d is not 0!"); - - return static_cast( - strlen(std::fgets(static_cast(buf), static_cast(nbytes), stdin))); + if (d == 0) { + return static_cast( + strlen(std::fgets(static_cast(buf), static_cast(nbytes), stdin))); + } + auto* h = Common::Singleton::Instance(); + auto* file = h->GetFile(d); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + std::scoped_lock lk{file->m_mutex}; + if (file->type == Core::FileSys::FileType::Device) { + return file->device->read(buf, nbytes); + } + return file->f.ReadRaw(buf, nbytes); } struct OrbisKernelUuid { @@ -189,6 +189,29 @@ int PS4_SYSV_ABI sceKernelUuidCreate(OrbisKernelUuid* orbisUuid) { return 0; } +int PS4_SYSV_ABI kernel_ioctl(int fd, u64 cmd, VA_ARGS) { + auto* h = Common::Singleton::Instance(); + auto* file = h->GetFile(fd); + if (file == nullptr) { + LOG_INFO(Lib_Kernel, "ioctl: fd = {:X} cmd = {:X} file == nullptr", fd, cmd); + g_posix_errno = POSIX_EBADF; + return -1; + } + if (file->type != Core::FileSys::FileType::Device) { + LOG_WARNING(Lib_Kernel, "ioctl: fd = {:X} cmd = {:X} file->type != Device", fd, cmd); + g_posix_errno = ENOTTY; + return -1; + } + VA_CTX(ctx); + int result = file->device->ioctl(cmd, &ctx); + LOG_TRACE(Lib_Kernel, "ioctl: fd = {:X} cmd = {:X} result = {}", fd, cmd, result); + if (result < 0) { + ErrSceToPosix(result); + return -1; + } + return result; +} + const char* PS4_SYSV_ABI sceKernelGetFsSandboxRandomWord() { const char* path = "sys"; return path; @@ -219,13 +242,13 @@ void RegisterKernel(Core::Loader::SymbolsResolver* sym) { Libraries::Kernel::RegisterException(sym); LIB_OBJ("f7uOxY9mM1U", "libkernel", 1, "libkernel", 1, 1, &g_stack_chk_guard); + LIB_FUNCTION("PfccT7qURYE", "libkernel", 1, "libkernel", 1, 1, kernel_ioctl); LIB_FUNCTION("JGfTMBOdUJo", "libkernel", 1, "libkernel", 1, 1, sceKernelGetFsSandboxRandomWord); LIB_FUNCTION("XVL8So3QJUk", "libkernel", 1, "libkernel", 1, 1, posix_connect); LIB_FUNCTION("6xVpy0Fdq+I", "libkernel", 1, "libkernel", 1, 1, _sigprocmask); LIB_FUNCTION("Xjoosiw+XPI", "libkernel", 1, "libkernel", 1, 1, sceKernelUuidCreate); LIB_FUNCTION("Ou3iL1abvng", "libkernel", 1, "libkernel", 1, 1, stack_chk_fail); LIB_FUNCTION("9BcDykPmo1I", "libkernel", 1, "libkernel", 1, 1, __Error); - LIB_FUNCTION("YSHRBRLn2pI", "libkernel", 1, "libkernel", 1, 1, _writev); LIB_FUNCTION("DRuBt2pvICk", "libkernel", 1, "libkernel", 1, 1, ps4__read); LIB_FUNCTION("k+AXqu2-eBc", "libkernel", 1, "libkernel", 1, 1, posix_getpagesize); LIB_FUNCTION("k+AXqu2-eBc", "libScePosix", 1, "libkernel", 1, 1, posix_getpagesize); diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index 15e4ff820..a657ddf98 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -20,7 +20,7 @@ int PS4_SYSV_ABI sceKernelIsNeoMode() { int PS4_SYSV_ABI sceKernelGetCompiledSdkVersion(int* ver) { int version = Common::ElfInfo::Instance().RawFirmwareVer(); *ver = version; - return (version > 0) ? ORBIS_OK : ORBIS_KERNEL_ERROR_EINVAL; + return (version >= 0) ? ORBIS_OK : ORBIS_KERNEL_ERROR_EINVAL; } int PS4_SYSV_ABI sceKernelGetCpumode() { diff --git a/src/emulator.cpp b/src/emulator.cpp index 1d2542d2b..60d6e18d7 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -75,6 +75,9 @@ Emulator::Emulator() { LOG_INFO(Config, "Vulkan rdocMarkersEnable: {}", Config::vkMarkersEnabled()); LOG_INFO(Config, "Vulkan crashDiagnostics: {}", Config::vkCrashDiagnosticEnabled()); + // Create stdin/stdout/stderr + Common::Singleton::Instance()->CreateStdHandles(); + // Defer until after logging is initialized. memory = Core::Memory::Instance(); controller = Common::Singleton::Instance(); From 98f0cb65d757a6fb5877ec66573a48ed0bc3b994 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:21:35 +0100 Subject: [PATCH 12/89] The way to Unity, pt.1 (#1659) --- src/common/ntapi.cpp | 6 + src/common/ntapi.h | 417 +++++++++++++++++- src/core/libraries/kernel/threads/pthread.cpp | 31 +- src/core/libraries/kernel/threads/pthread.h | 2 +- src/core/memory.cpp | 10 +- src/core/thread.cpp | 104 ++++- src/core/thread.h | 21 +- src/video_core/amdgpu/liverpool.cpp | 2 +- 8 files changed, 564 insertions(+), 29 deletions(-) diff --git a/src/common/ntapi.cpp b/src/common/ntapi.cpp index 0fe797e09..ffdedb17f 100644 --- a/src/common/ntapi.cpp +++ b/src/common/ntapi.cpp @@ -5,8 +5,11 @@ #include "ntapi.h" +NtClose_t NtClose = nullptr; NtDelayExecution_t NtDelayExecution = nullptr; NtSetInformationFile_t NtSetInformationFile = nullptr; +NtCreateThread_t NtCreateThread = nullptr; +NtTerminateThread_t NtTerminateThread = nullptr; namespace Common::NtApi { @@ -14,9 +17,12 @@ void Initialize() { HMODULE nt_handle = GetModuleHandleA("ntdll.dll"); // http://stackoverflow.com/a/31411628/4725495 + NtClose = (NtClose_t)GetProcAddress(nt_handle, "NtClose"); NtDelayExecution = (NtDelayExecution_t)GetProcAddress(nt_handle, "NtDelayExecution"); NtSetInformationFile = (NtSetInformationFile_t)GetProcAddress(nt_handle, "NtSetInformationFile"); + NtCreateThread = (NtCreateThread_t)GetProcAddress(nt_handle, "NtCreateThread"); + NtTerminateThread = (NtTerminateThread_t)GetProcAddress(nt_handle, "NtTerminateThread"); } } // namespace Common::NtApi diff --git a/src/common/ntapi.h b/src/common/ntapi.h index 17d353403..743174061 100644 --- a/src/common/ntapi.h +++ b/src/common/ntapi.h @@ -108,14 +108,427 @@ typedef struct _FILE_DISPOSITION_INFORMATION { BOOLEAN DeleteFile; } FILE_DISPOSITION_INFORMATION, *PFILE_DISPOSITION_INFORMATION; -typedef u32(__stdcall* NtDelayExecution_t)(BOOL Alertable, PLARGE_INTEGER DelayInterval); +typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWCH Buffer; +} UNICODE_STRING, *PUNICODE_STRING; -typedef u32(__stdcall* NtSetInformationFile_t)(HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, +typedef const UNICODE_STRING* PCUNICODE_STRING; + +typedef struct _OBJECT_ATTRIBUTES { + ULONG Length; + HANDLE RootDirectory; + PCUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; // PSECURITY_DESCRIPTOR; + PVOID SecurityQualityOfService; // PSECURITY_QUALITY_OF_SERVICE +} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES; + +typedef const OBJECT_ATTRIBUTES* PCOBJECT_ATTRIBUTES; + +typedef struct _CLIENT_ID { + HANDLE UniqueProcess; + HANDLE UniqueThread; +} CLIENT_ID, *PCLIENT_ID; + +typedef struct _INITIAL_TEB { + struct { + PVOID OldStackBase; + PVOID OldStackLimit; + } OldInitialTeb; + PVOID StackBase; + PVOID StackLimit; + PVOID StackAllocationBase; +} INITIAL_TEB, *PINITIAL_TEB; + +typedef struct _PEB_LDR_DATA { + ULONG Length; + BOOLEAN Initialized; + PVOID SsHandle; + LIST_ENTRY InLoadOrderModuleList; + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; + PVOID EntryInProgress; + BOOLEAN ShutdownInProgress; + HANDLE ShutdownThreadId; +} PEB_LDR_DATA, *PPEB_LDR_DATA; + +typedef struct _CURDIR { + UNICODE_STRING DosPath; + PVOID Handle; +} CURDIR, *PCURDIR; + +typedef struct RTL_DRIVE_LETTER_CURDIR { + USHORT Flags; + USHORT Length; + ULONG TimeStamp; + UNICODE_STRING DosPath; +} RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR; + +typedef struct _RTL_USER_PROCESS_PARAMETERS { + ULONG AllocationSize; + ULONG Size; + ULONG Flags; + ULONG DebugFlags; + HANDLE ConsoleHandle; + ULONG ConsoleFlags; + HANDLE hStdInput; + HANDLE hStdOutput; + HANDLE hStdError; + CURDIR CurrentDirectory; + UNICODE_STRING DllPath; + UNICODE_STRING ImagePathName; + UNICODE_STRING CommandLine; + PWSTR Environment; + ULONG dwX; + ULONG dwY; + ULONG dwXSize; + ULONG dwYSize; + ULONG dwXCountChars; + ULONG dwYCountChars; + ULONG dwFillAttribute; + ULONG dwFlags; + ULONG wShowWindow; + UNICODE_STRING WindowTitle; + UNICODE_STRING Desktop; + UNICODE_STRING ShellInfo; + UNICODE_STRING RuntimeInfo; + RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[0x20]; + ULONG_PTR EnvironmentSize; + ULONG_PTR EnvironmentVersion; + PVOID PackageDependencyData; + ULONG ProcessGroupId; + ULONG LoaderThreads; +} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; + +typedef struct tagRTL_BITMAP { + ULONG SizeOfBitMap; + PULONG Buffer; +} RTL_BITMAP, *PRTL_BITMAP; + +typedef struct { + UINT next; + UINT id; + ULONGLONG addr; + ULONGLONG size; + UINT args[4]; +} CROSS_PROCESS_WORK_ENTRY; + +typedef union { + struct { + UINT first; + UINT counter; + }; + volatile LONGLONG hdr; +} CROSS_PROCESS_WORK_HDR; + +typedef struct { + CROSS_PROCESS_WORK_HDR free_list; + CROSS_PROCESS_WORK_HDR work_list; + ULONGLONG unknown[4]; + CROSS_PROCESS_WORK_ENTRY entries[1]; +} CROSS_PROCESS_WORK_LIST; + +typedef struct _CHPEV2_PROCESS_INFO { + ULONG Wow64ExecuteFlags; /* 000 */ + USHORT NativeMachineType; /* 004 */ + USHORT EmulatedMachineType; /* 006 */ + HANDLE SectionHandle; /* 008 */ + CROSS_PROCESS_WORK_LIST* CrossProcessWorkList; /* 010 */ + void* unknown; /* 018 */ +} CHPEV2_PROCESS_INFO, *PCHPEV2_PROCESS_INFO; + +typedef u64(__stdcall* KERNEL_CALLBACK_PROC)(void*, ULONG); + +typedef struct _PEB { /* win32/win64 */ + BOOLEAN InheritedAddressSpace; /* 000/000 */ + BOOLEAN ReadImageFileExecOptions; /* 001/001 */ + BOOLEAN BeingDebugged; /* 002/002 */ + UCHAR ImageUsedLargePages : 1; /* 003/003 */ + UCHAR IsProtectedProcess : 1; + UCHAR IsImageDynamicallyRelocated : 1; + UCHAR SkipPatchingUser32Forwarders : 1; + UCHAR IsPackagedProcess : 1; + UCHAR IsAppContainer : 1; + UCHAR IsProtectedProcessLight : 1; + UCHAR IsLongPathAwareProcess : 1; + HANDLE Mutant; /* 004/008 */ + HMODULE ImageBaseAddress; /* 008/010 */ + PPEB_LDR_DATA LdrData; /* 00c/018 */ + RTL_USER_PROCESS_PARAMETERS* ProcessParameters; /* 010/020 */ + PVOID SubSystemData; /* 014/028 */ + HANDLE ProcessHeap; /* 018/030 */ + PRTL_CRITICAL_SECTION FastPebLock; /* 01c/038 */ + PVOID AtlThunkSListPtr; /* 020/040 */ + PVOID IFEOKey; /* 024/048 */ + ULONG ProcessInJob : 1; /* 028/050 */ + ULONG ProcessInitializing : 1; + ULONG ProcessUsingVEH : 1; + ULONG ProcessUsingVCH : 1; + ULONG ProcessUsingFTH : 1; + ULONG ProcessPreviouslyThrottled : 1; + ULONG ProcessCurrentlyThrottled : 1; + ULONG ProcessImagesHotPatched : 1; + ULONG ReservedBits0 : 24; + KERNEL_CALLBACK_PROC* KernelCallbackTable; /* 02c/058 */ + ULONG Reserved; /* 030/060 */ + ULONG AtlThunkSListPtr32; /* 034/064 */ + PVOID ApiSetMap; /* 038/068 */ + ULONG TlsExpansionCounter; /* 03c/070 */ + PRTL_BITMAP TlsBitmap; /* 040/078 */ + ULONG TlsBitmapBits[2]; /* 044/080 */ + PVOID ReadOnlySharedMemoryBase; /* 04c/088 */ + PVOID SharedData; /* 050/090 */ + PVOID* ReadOnlyStaticServerData; /* 054/098 */ + PVOID AnsiCodePageData; /* 058/0a0 */ + PVOID OemCodePageData; /* 05c/0a8 */ + PVOID UnicodeCaseTableData; /* 060/0b0 */ + ULONG NumberOfProcessors; /* 064/0b8 */ + ULONG NtGlobalFlag; /* 068/0bc */ + LARGE_INTEGER CriticalSectionTimeout; /* 070/0c0 */ + SIZE_T HeapSegmentReserve; /* 078/0c8 */ + SIZE_T HeapSegmentCommit; /* 07c/0d0 */ + SIZE_T HeapDeCommitTotalFreeThreshold; /* 080/0d8 */ + SIZE_T HeapDeCommitFreeBlockThreshold; /* 084/0e0 */ + ULONG NumberOfHeaps; /* 088/0e8 */ + ULONG MaximumNumberOfHeaps; /* 08c/0ec */ + PVOID* ProcessHeaps; /* 090/0f0 */ + PVOID GdiSharedHandleTable; /* 094/0f8 */ + PVOID ProcessStarterHelper; /* 098/100 */ + PVOID GdiDCAttributeList; /* 09c/108 */ + PVOID LoaderLock; /* 0a0/110 */ + ULONG OSMajorVersion; /* 0a4/118 */ + ULONG OSMinorVersion; /* 0a8/11c */ + ULONG OSBuildNumber; /* 0ac/120 */ + ULONG OSPlatformId; /* 0b0/124 */ + ULONG ImageSubSystem; /* 0b4/128 */ + ULONG ImageSubSystemMajorVersion; /* 0b8/12c */ + ULONG ImageSubSystemMinorVersion; /* 0bc/130 */ + KAFFINITY ActiveProcessAffinityMask; /* 0c0/138 */ +#ifdef _WIN64 + ULONG GdiHandleBuffer[60]; /* /140 */ +#else + ULONG GdiHandleBuffer[34]; /* 0c4/ */ +#endif + PVOID PostProcessInitRoutine; /* 14c/230 */ + PRTL_BITMAP TlsExpansionBitmap; /* 150/238 */ + ULONG TlsExpansionBitmapBits[32]; /* 154/240 */ + ULONG SessionId; /* 1d4/2c0 */ + ULARGE_INTEGER AppCompatFlags; /* 1d8/2c8 */ + ULARGE_INTEGER AppCompatFlagsUser; /* 1e0/2d0 */ + PVOID ShimData; /* 1e8/2d8 */ + PVOID AppCompatInfo; /* 1ec/2e0 */ + UNICODE_STRING CSDVersion; /* 1f0/2e8 */ + PVOID ActivationContextData; /* 1f8/2f8 */ + PVOID ProcessAssemblyStorageMap; /* 1fc/300 */ + PVOID SystemDefaultActivationData; /* 200/308 */ + PVOID SystemAssemblyStorageMap; /* 204/310 */ + SIZE_T MinimumStackCommit; /* 208/318 */ + PVOID* FlsCallback; /* 20c/320 */ + LIST_ENTRY FlsListHead; /* 210/328 */ + union { + PRTL_BITMAP FlsBitmap; /* 218/338 */ +#ifdef _WIN64 + CHPEV2_PROCESS_INFO* ChpeV2ProcessInfo; /* /338 */ +#endif + }; + ULONG FlsBitmapBits[4]; /* 21c/340 */ + ULONG FlsHighIndex; /* 22c/350 */ + PVOID WerRegistrationData; /* 230/358 */ + PVOID WerShipAssertPtr; /* 234/360 */ + PVOID EcCodeBitMap; /* 238/368 */ + PVOID pImageHeaderHash; /* 23c/370 */ + ULONG HeapTracingEnabled : 1; /* 240/378 */ + ULONG CritSecTracingEnabled : 1; + ULONG LibLoaderTracingEnabled : 1; + ULONG SpareTracingBits : 29; + ULONGLONG CsrServerReadOnlySharedMemoryBase; /* 248/380 */ + ULONG TppWorkerpListLock; /* 250/388 */ + LIST_ENTRY TppWorkerpList; /* 254/390 */ + PVOID WaitOnAddressHashTable[0x80]; /* 25c/3a0 */ + PVOID TelemetryCoverageHeader; /* 45c/7a0 */ + ULONG CloudFileFlags; /* 460/7a8 */ + ULONG CloudFileDiagFlags; /* 464/7ac */ + CHAR PlaceholderCompatibilityMode; /* 468/7b0 */ + CHAR PlaceholderCompatibilityModeReserved[7]; /* 469/7b1 */ + PVOID LeapSecondData; /* 470/7b8 */ + ULONG LeapSecondFlags; /* 474/7c0 */ + ULONG NtGlobalFlag2; /* 478/7c4 */ +} PEB, *PPEB; + +typedef struct _RTL_ACTIVATION_CONTEXT_STACK_FRAME { + struct _RTL_ACTIVATION_CONTEXT_STACK_FRAME* Previous; + struct _ACTIVATION_CONTEXT* ActivationContext; + ULONG Flags; +} RTL_ACTIVATION_CONTEXT_STACK_FRAME, *PRTL_ACTIVATION_CONTEXT_STACK_FRAME; + +typedef struct _ACTIVATION_CONTEXT_STACK { + RTL_ACTIVATION_CONTEXT_STACK_FRAME* ActiveFrame; + LIST_ENTRY FrameListCache; + ULONG Flags; + ULONG NextCookieSequenceNumber; + ULONG_PTR StackId; +} ACTIVATION_CONTEXT_STACK, *PACTIVATION_CONTEXT_STACK; + +typedef struct _GDI_TEB_BATCH { + ULONG Offset; + HANDLE HDC; + ULONG Buffer[0x136]; +} GDI_TEB_BATCH; + +typedef struct _TEB_ACTIVE_FRAME_CONTEXT { + ULONG Flags; + const char* FrameName; +} TEB_ACTIVE_FRAME_CONTEXT, *PTEB_ACTIVE_FRAME_CONTEXT; + +typedef struct _TEB_ACTIVE_FRAME { + ULONG Flags; + struct _TEB_ACTIVE_FRAME* Previous; + TEB_ACTIVE_FRAME_CONTEXT* Context; +} TEB_ACTIVE_FRAME, *PTEB_ACTIVE_FRAME; + +typedef struct _TEB { /* win32/win64 */ + NT_TIB Tib; /* 000/0000 */ + PVOID EnvironmentPointer; /* 01c/0038 */ + CLIENT_ID ClientId; /* 020/0040 */ + PVOID ActiveRpcHandle; /* 028/0050 */ + PVOID ThreadLocalStoragePointer; /* 02c/0058 */ + PPEB Peb; /* 030/0060 */ + ULONG LastErrorValue; /* 034/0068 */ + ULONG CountOfOwnedCriticalSections; /* 038/006c */ + PVOID CsrClientThread; /* 03c/0070 */ + PVOID Win32ThreadInfo; /* 040/0078 */ + ULONG User32Reserved[26]; /* 044/0080 */ + ULONG UserReserved[5]; /* 0ac/00e8 */ + PVOID WOW32Reserved; /* 0c0/0100 */ + ULONG CurrentLocale; /* 0c4/0108 */ + ULONG FpSoftwareStatusRegister; /* 0c8/010c */ + PVOID ReservedForDebuggerInstrumentation[16]; /* 0cc/0110 */ +#ifdef _WIN64 + PVOID SystemReserved1[30]; /* /0190 */ +#else + PVOID SystemReserved1[26]; /* 10c/ used for krnl386 private data in Wine */ +#endif + char PlaceholderCompatibilityMode; /* 174/0280 */ + BOOLEAN PlaceholderHydrationAlwaysExplicit; /* 175/0281 */ + char PlaceholderReserved[10]; /* 176/0282 */ + DWORD ProxiedProcessId; /* 180/028c */ + ACTIVATION_CONTEXT_STACK ActivationContextStack; /* 184/0290 */ + UCHAR WorkingOnBehalfOfTicket[8]; /* 19c/02b8 */ + LONG ExceptionCode; /* 1a4/02c0 */ + ACTIVATION_CONTEXT_STACK* ActivationContextStackPointer; /* 1a8/02c8 */ + ULONG_PTR InstrumentationCallbackSp; /* 1ac/02d0 */ + ULONG_PTR InstrumentationCallbackPreviousPc; /* 1b0/02d8 */ + ULONG_PTR InstrumentationCallbackPreviousSp; /* 1b4/02e0 */ +#ifdef _WIN64 + ULONG TxFsContext; /* /02e8 */ + BOOLEAN InstrumentationCallbackDisabled; /* /02ec */ + BOOLEAN UnalignedLoadStoreExceptions; /* /02ed */ +#else + BOOLEAN InstrumentationCallbackDisabled; /* 1b8/ */ + BYTE SpareBytes1[23]; /* 1b9/ */ + ULONG TxFsContext; /* 1d0/ */ +#endif + GDI_TEB_BATCH GdiTebBatch; /* 1d4/02f0 used for ntdll private data in Wine */ + CLIENT_ID RealClientId; /* 6b4/07d8 */ + HANDLE GdiCachedProcessHandle; /* 6bc/07e8 */ + ULONG GdiClientPID; /* 6c0/07f0 */ + ULONG GdiClientTID; /* 6c4/07f4 */ + PVOID GdiThreadLocaleInfo; /* 6c8/07f8 */ + ULONG_PTR Win32ClientInfo[62]; /* 6cc/0800 used for user32 private data in Wine */ + PVOID glDispatchTable[233]; /* 7c4/09f0 */ + PVOID glReserved1[29]; /* b68/1138 */ + PVOID glReserved2; /* bdc/1220 */ + PVOID glSectionInfo; /* be0/1228 */ + PVOID glSection; /* be4/1230 */ + PVOID glTable; /* be8/1238 */ + PVOID glCurrentRC; /* bec/1240 */ + PVOID glContext; /* bf0/1248 */ + ULONG LastStatusValue; /* bf4/1250 */ + UNICODE_STRING StaticUnicodeString; /* bf8/1258 */ + WCHAR StaticUnicodeBuffer[261]; /* c00/1268 */ + PVOID DeallocationStack; /* e0c/1478 */ + PVOID TlsSlots[64]; /* e10/1480 */ + LIST_ENTRY TlsLinks; /* f10/1680 */ + PVOID Vdm; /* f18/1690 */ + PVOID ReservedForNtRpc; /* f1c/1698 */ + PVOID DbgSsReserved[2]; /* f20/16a0 */ + ULONG HardErrorMode; /* f28/16b0 */ +#ifdef _WIN64 + PVOID Instrumentation[11]; /* /16b8 */ +#else + PVOID Instrumentation[9]; /* f2c/ */ +#endif + GUID ActivityId; /* f50/1710 */ + PVOID SubProcessTag; /* f60/1720 */ + PVOID PerflibData; /* f64/1728 */ + PVOID EtwTraceData; /* f68/1730 */ + PVOID WinSockData; /* f6c/1738 */ + ULONG GdiBatchCount; /* f70/1740 */ + ULONG IdealProcessorValue; /* f74/1744 */ + ULONG GuaranteedStackBytes; /* f78/1748 */ + PVOID ReservedForPerf; /* f7c/1750 */ + PVOID ReservedForOle; /* f80/1758 */ + ULONG WaitingOnLoaderLock; /* f84/1760 */ + PVOID SavedPriorityState; /* f88/1768 */ + ULONG_PTR ReservedForCodeCoverage; /* f8c/1770 */ + PVOID ThreadPoolData; /* f90/1778 */ + PVOID* TlsExpansionSlots; /* f94/1780 */ +#ifdef _WIN64 + union { + PVOID DeallocationBStore; /* /1788 */ + PVOID* ChpeV2CpuAreaInfo; /* /1788 */ + } DUMMYUNIONNAME; + PVOID BStoreLimit; /* /1790 */ +#endif + ULONG MuiGeneration; /* f98/1798 */ + ULONG IsImpersonating; /* f9c/179c */ + PVOID NlsCache; /* fa0/17a0 */ + PVOID ShimData; /* fa4/17a8 */ + ULONG HeapVirtualAffinity; /* fa8/17b0 */ + PVOID CurrentTransactionHandle; /* fac/17b8 */ + TEB_ACTIVE_FRAME* ActiveFrame; /* fb0/17c0 */ + PVOID* FlsSlots; /* fb4/17c8 */ + PVOID PreferredLanguages; /* fb8/17d0 */ + PVOID UserPrefLanguages; /* fbc/17d8 */ + PVOID MergedPrefLanguages; /* fc0/17e0 */ + ULONG MuiImpersonation; /* fc4/17e8 */ + USHORT CrossTebFlags; /* fc8/17ec */ + USHORT SameTebFlags; /* fca/17ee */ + PVOID TxnScopeEnterCallback; /* fcc/17f0 */ + PVOID TxnScopeExitCallback; /* fd0/17f8 */ + PVOID TxnScopeContext; /* fd4/1800 */ + ULONG LockCount; /* fd8/1808 */ + LONG WowTebOffset; /* fdc/180c */ + PVOID ResourceRetValue; /* fe0/1810 */ + PVOID ReservedForWdf; /* fe4/1818 */ + ULONGLONG ReservedForCrt; /* fe8/1820 */ + GUID EffectiveContainerId; /* ff0/1828 */ +} TEB, *PTEB; +static_assert(offsetof(TEB, DeallocationStack) == + 0x1478); /* The only member we care about at the moment */ + +typedef u64(__stdcall* NtClose_t)(HANDLE Handle); + +typedef u64(__stdcall* NtDelayExecution_t)(BOOL Alertable, PLARGE_INTEGER DelayInterval); + +typedef u64(__stdcall* NtSetInformationFile_t)(HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); +typedef u64(__stdcall* NtCreateThread_t)(PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, + PCOBJECT_ATTRIBUTES ObjectAttributes, HANDLE ProcessHandle, + PCLIENT_ID ClientId, PCONTEXT ThreadContext, + PINITIAL_TEB InitialTeb, BOOLEAN CreateSuspended); + +typedef u64(__stdcall* NtTerminateThread_t)(HANDLE ThreadHandle, u64 ExitStatus); + +extern NtClose_t NtClose; extern NtDelayExecution_t NtDelayExecution; extern NtSetInformationFile_t NtSetInformationFile; +extern NtCreateThread_t NtCreateThread; +extern NtTerminateThread_t NtTerminateThread; namespace Common::NtApi { void Initialize(); diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index 793ddd1fe..a562c51b2 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -206,6 +206,7 @@ static void RunThread(void* arg) { DebugState.AddCurrentThreadToGuestList(); /* Run the current thread's start routine with argument: */ + curthread->native_thr.Initialize(); void* ret = Core::ExecuteGuest(curthread->start_routine, curthread->arg); /* Remove thread from tracking */ @@ -280,7 +281,7 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt (*thread) = new_thread; /* Create thread */ - new_thread->native_thr = Core::Thread(); + new_thread->native_thr = Core::NativeThread(); int ret = new_thread->native_thr.Create(RunThread, new_thread, &new_thread->attr); ASSERT_MSG(ret == 0, "Failed to create thread with error {}", ret); if (ret) { @@ -412,6 +413,33 @@ int PS4_SYSV_ABI posix_pthread_getschedparam(PthreadT pthread, SchedPolicy* poli return 0; } +int PS4_SYSV_ABI posix_pthread_setschedparam(PthreadT pthread, SchedPolicy policy, + const SchedParam* param) { + if (pthread == nullptr || param == nullptr) { + return POSIX_EINVAL; + } + + auto* thread_state = ThrState::Instance(); + if (pthread == g_curthread) { + g_curthread->lock.lock(); + } else if (int ret = thread_state->FindThread(pthread, /*include dead*/ 0); ret != 0) { + return ret; + } + + if (pthread->attr.sched_policy == policy && + (policy == SchedPolicy::Other || pthread->attr.prio == param->sched_priority)) { + pthread->attr.prio = param->sched_priority; + pthread->lock.unlock(); + return 0; + } + + // TODO: _thr_setscheduler + pthread->attr.sched_policy = policy; + pthread->attr.prio = param->sched_priority; + pthread->lock.unlock(); + return 0; +} + int PS4_SYSV_ABI scePthreadGetprio(PthreadT thread, int* priority) { SchedParam param; SchedPolicy policy; @@ -495,6 +523,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("lZzFeSxPl08", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_setcancelstate); LIB_FUNCTION("a2P9wYGeZvc", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_setprio); LIB_FUNCTION("FIs3-UQT9sg", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_getschedparam); + LIB_FUNCTION("Xs9hdiD7sAA", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_setschedparam); LIB_FUNCTION("6XG4B33N09g", "libScePosix", 1, "libkernel", 1, 1, sched_yield); // Posix-Kernel diff --git a/src/core/libraries/kernel/threads/pthread.h b/src/core/libraries/kernel/threads/pthread.h index b41ca2abd..9d71c75e8 100644 --- a/src/core/libraries/kernel/threads/pthread.h +++ b/src/core/libraries/kernel/threads/pthread.h @@ -259,7 +259,7 @@ struct Pthread { int refcount; PthreadEntryFunc start_routine; void* arg; - Core::Thread native_thr; + Core::NativeThread native_thr; PthreadAttr attr; bool cancel_enable; bool cancel_pending; diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 15fde2a57..3e1cd441f 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -328,7 +328,7 @@ int MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, size_t size, Mem } // Map the file. - impl.MapFile(mapped_addr, size, offset, std::bit_cast(prot), fd); + impl.MapFile(mapped_addr, size_aligned, offset, std::bit_cast(prot), fd); // Add virtual memory area auto& new_vma = CarveVMA(mapped_addr, size_aligned)->second; @@ -512,9 +512,8 @@ int MemoryManager::VirtualQuery(VAddr addr, int flags, info->is_flexible.Assign(vma.type == VMAType::Flexible); info->is_direct.Assign(vma.type == VMAType::Direct); info->is_stack.Assign(vma.type == VMAType::Stack); - info->is_pooled.Assign(vma.type == VMAType::Pooled); - info->is_committed.Assign(vma.type != VMAType::Free && vma.type != VMAType::Reserved && - vma.type != VMAType::PoolReserved); + info->is_pooled.Assign(vma.type == VMAType::PoolReserved); + info->is_committed.Assign(vma.type == VMAType::Pooled); vma.name.copy(info->name.data(), std::min(info->name.size(), vma.name.size())); if (vma.type == VMAType::Direct) { const auto dmem_it = FindDmemArea(vma.phys_base); @@ -585,6 +584,7 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, size_t size, std::strin "Range provided is not fully contained in vma"); it->second.name = name; } + VAddr MemoryManager::SearchFree(VAddr virtual_addr, size_t size, u32 alignment) { // If the requested address is below the mapped range, start search from the lowest address auto min_search_address = impl.SystemManagedVirtualBase(); @@ -691,7 +691,7 @@ MemoryManager::DMemHandle MemoryManager::Split(DMemHandle dmem_handle, size_t of new_area.size -= offset_in_area; return dmem_map.emplace_hint(std::next(dmem_handle), new_area.base, new_area); -}; +} int MemoryManager::GetDirectMemoryType(PAddr addr, int* directMemoryTypeOut, void** directMemoryStartOut, void** directMemoryEndOut) { diff --git a/src/core/thread.cpp b/src/core/thread.cpp index a93f16c8d..f87e3c8dc 100644 --- a/src/core/thread.cpp +++ b/src/core/thread.cpp @@ -4,45 +4,125 @@ #include "libraries/kernel/threads/pthread.h" #include "thread.h" +#include "core/libraries/kernel/threads/pthread.h" + #ifdef _WIN64 #include +#include "common/ntapi.h" #else #include #endif namespace Core { -Thread::Thread() : native_handle{0} {} - -Thread::~Thread() {} - -int Thread::Create(ThreadFunc func, void* arg, const ::Libraries::Kernel::PthreadAttr* attr) { #ifdef _WIN64 - native_handle = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)func, arg, 0, nullptr); - return native_handle ? 0 : -1; -#else +#define KGDT64_R3_DATA (0x28) +#define KGDT64_R3_CODE (0x30) +#define KGDT64_R3_CMTEB (0x50) +#define RPL_MASK (0x03) + +#define INITIAL_FPUCW (0x037f) +#define INITIAL_MXCSR_MASK (0xffbf) +#define EFLAGS_INTERRUPT_MASK (0x200) + +void InitializeTeb(INITIAL_TEB* teb, const ::Libraries::Kernel::PthreadAttr* attr) { + teb->StackBase = (void*)((u64)attr->stackaddr_attr + attr->stacksize_attr); + teb->StackLimit = nullptr; + teb->StackAllocationBase = attr->stackaddr_attr; +} + +void InitializeContext(CONTEXT* ctx, ThreadFunc func, void* arg, + const ::Libraries::Kernel::PthreadAttr* attr) { + /* Note: The stack has to be reversed */ + ctx->Rsp = (u64)attr->stackaddr_attr + attr->stacksize_attr; + ctx->Rbp = (u64)attr->stackaddr_attr + attr->stacksize_attr; + ctx->Rcx = (u64)arg; + ctx->Rip = (u64)func; + + ctx->SegGs = KGDT64_R3_DATA | RPL_MASK; + ctx->SegEs = KGDT64_R3_DATA | RPL_MASK; + ctx->SegDs = KGDT64_R3_DATA | RPL_MASK; + ctx->SegCs = KGDT64_R3_CODE | RPL_MASK; + ctx->SegSs = KGDT64_R3_DATA | RPL_MASK; + ctx->SegFs = KGDT64_R3_CMTEB | RPL_MASK; + + ctx->EFlags = 0x3000 | EFLAGS_INTERRUPT_MASK; + ctx->MxCsr = INITIAL_MXCSR; + + ctx->FltSave.ControlWord = INITIAL_FPUCW; + ctx->FltSave.MxCsr = INITIAL_MXCSR; + ctx->FltSave.MxCsr_Mask = INITIAL_MXCSR_MASK; + + ctx->ContextFlags = + CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT; +} +#endif + +NativeThread::NativeThread() : native_handle{0} {} + +NativeThread::~NativeThread() {} + +int NativeThread::Create(ThreadFunc func, void* arg, const ::Libraries::Kernel::PthreadAttr* attr) { +#ifndef _WIN64 pthread_t* pthr = reinterpret_cast(&native_handle); pthread_attr_t pattr; pthread_attr_init(&pattr); pthread_attr_setstack(&pattr, attr->stackaddr_attr, attr->stacksize_attr); return pthread_create(pthr, &pattr, (PthreadFunc)func, arg); +#else + CLIENT_ID clientId{}; + INITIAL_TEB teb{}; + CONTEXT ctx{}; + + clientId.UniqueProcess = GetCurrentProcess(); + clientId.UniqueThread = GetCurrentThread(); + + InitializeTeb(&teb, attr); + InitializeContext(&ctx, func, arg, attr); + + return NtCreateThread(&native_handle, THREAD_ALL_ACCESS, nullptr, GetCurrentProcess(), + &clientId, &ctx, &teb, false); #endif } -void Thread::Exit() { +void NativeThread::Exit() { if (!native_handle) { return; } + tid = 0; + #ifdef _WIN64 - CloseHandle(native_handle); + NtClose(native_handle); native_handle = nullptr; - // We call this assuming the thread has finished execution. - ExitThread(0); + /* The Windows kernel will free the stack + given at thread creation via INITIAL_TEB + (StackAllocationBase) upon thread termination. + + In earlier Windows versions (NT4 to Windows Server 2003), + you could get around this via disabling FreeStackOnTermination + on the TEB. This has been removed since then. + + To avoid this, we must forcefully set the TEB + deallocation stack pointer to NULL so ZwFreeVirtualMemory fails + in the kernel and our stack is not freed. + */ + auto* teb = reinterpret_cast(NtCurrentTeb()); + teb->DeallocationStack = nullptr; + + NtTerminateThread(nullptr, 0); #else pthread_exit(nullptr); #endif } +void NativeThread::Initialize() { +#if _WIN64 + tid = GetCurrentThreadId(); +#else + tid = (u64)pthread_self(); +#endif +} + } // namespace Core \ No newline at end of file diff --git a/src/core/thread.h b/src/core/thread.h index cfb8b8309..3bac0e699 100644 --- a/src/core/thread.h +++ b/src/core/thread.h @@ -11,27 +11,34 @@ struct PthreadAttr; namespace Core { -class Thread { -public: - using ThreadFunc = void (*)(void*); - using PthreadFunc = void* (*)(void*); +using ThreadFunc = void (*)(void*); +using PthreadFunc = void* (*)(void*); - Thread(); - ~Thread(); +class NativeThread { +public: + NativeThread(); + ~NativeThread(); int Create(ThreadFunc func, void* arg, const ::Libraries::Kernel::PthreadAttr* attr); void Exit(); + void Initialize(); + uintptr_t GetHandle() { return reinterpret_cast(native_handle); } + u64 GetTid() { + return tid; + } + private: -#if _WIN64 +#ifdef _WIN64 void* native_handle; #else uintptr_t native_handle; #endif + u64 tid; }; } // namespace Core \ No newline at end of file diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index f7b710edd..b81806d10 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -46,7 +46,7 @@ Liverpool::~Liverpool() { } void Liverpool::Process(std::stop_token stoken) { - Common::SetCurrentThreadName("shadPS4:GPU_CommandProcessor"); + Common::SetCurrentThreadName("shadPS4:GpuCommandProcessor"); while (!stoken.stop_requested()) { { From 15ae7a094d3a4430ac2e7358c3a85af1409de404 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:45:55 +0100 Subject: [PATCH 13/89] hotfix: fix inverted operator on GetDents --- src/core/libraries/kernel/file_system.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 447467cb7..2a65255fb 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -656,7 +656,7 @@ static int GetDents(int fd, char* buf, int nbytes, s64* basep) { if (file == nullptr) { return ORBIS_KERNEL_ERROR_EBADF; } - if (file->type != Core::FileSys::FileType::Device) { + if (file->type == Core::FileSys::FileType::Device) { return file->device->getdents(buf, nbytes, basep); } From 37f4bad2b7efd74643f2b7d579011aa942e251f2 Mon Sep 17 00:00:00 2001 From: psucien <168137814+psucien@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:09:22 +0100 Subject: [PATCH 14/89] video_core: fix for targets clears and copies (#1670) --- .../renderer_vulkan/vk_rasterizer.cpp | 49 ++++++++++++++----- .../texture_cache/texture_cache.cpp | 15 +++--- src/video_core/texture_cache/texture_cache.h | 23 +++++++-- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 084b7c345..620e5f103 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -102,9 +102,6 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { continue; } - const bool is_clear = texture_cache.IsMetaCleared(col_buf.CmaskAddress()); - texture_cache.TouchMeta(col_buf.CmaskAddress(), false); - const auto& hint = liverpool->last_cb_extent[col_buf_id]; auto& [image_id, desc] = cb_descs.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{col_buf, hint}); @@ -113,6 +110,10 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { auto& image = texture_cache.GetImage(image_id); image.binding.is_target = 1u; + const auto slice = image_view.info.range.base.layer; + const bool is_clear = texture_cache.IsMetaCleared(col_buf.CmaskAddress(), slice); + texture_cache.TouchMeta(col_buf.CmaskAddress(), slice, false); + const auto mip = image_view.info.range.base.level; state.width = std::min(state.width, std::max(image.info.size.width >> mip, 1u)); state.height = std::min(state.height, std::max(image.info.size.height >> mip, 1u)); @@ -134,8 +135,6 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { (regs.depth_control.stencil_enable && regs.depth_buffer.stencil_info.format != StencilFormat::Invalid))) { const auto htile_address = regs.depth_htile_data_base.GetAddress(); - const bool is_clear = regs.depth_render_control.depth_clear_enable || - texture_cache.IsMetaCleared(htile_address); const auto& hint = liverpool->last_db_extent; auto& [image_id, desc] = db_desc.emplace(std::piecewise_construct, std::tuple{}, @@ -146,6 +145,11 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { auto& image = texture_cache.GetImage(image_id); image.binding.is_target = 1u; + const auto slice = image_view.info.range.base.layer; + const bool is_clear = regs.depth_render_control.depth_clear_enable || + texture_cache.IsMetaCleared(htile_address, slice); + ASSERT(desc.view_info.range.extent.layers == 1); + state.width = std::min(state.width, image.info.size.width); state.height = std::min(state.height, image.info.size.height); state.depth_image = image.image; @@ -157,7 +161,7 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { .clearValue = vk::ClearValue{.depthStencil = {.depth = regs.depth_clear, .stencil = regs.stencil_clear}}, }; - texture_cache.TouchMeta(htile_address, false); + texture_cache.TouchMeta(htile_address, slice, false); state.has_depth = regs.depth_buffer.z_info.format != AmdGpu::Liverpool::DepthBuffer::ZFormat::Invalid; state.has_stencil = regs.depth_buffer.stencil_info.format != @@ -359,9 +363,11 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) { // will need its full emulation anyways. For cases of metadata read a warning will be // logged. const auto IsMetaUpdate = [&](const auto& desc) { - const VAddr address = desc.GetSharp(info).base_address; + const auto sharp = desc.GetSharp(info); + const VAddr address = sharp.base_address; if (desc.is_written) { - if (texture_cache.TouchMeta(address, true)) { + // Assume all slices were updates + if (texture_cache.ClearMeta(address)) { LOG_TRACE(Render_Vulkan, "Metadata update skipped"); return true; } @@ -373,17 +379,36 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) { return false; }; + // Assume if a shader reads and writes metas at the same time, it is a copy shader. + bool meta_read = false; for (const auto& desc : info.buffers) { if (desc.is_gds_buffer) { continue; } - if (IsMetaUpdate(desc)) { - return false; + if (!desc.is_written) { + const VAddr address = desc.GetSharp(info).base_address; + meta_read = texture_cache.IsMeta(address); } } + for (const auto& desc : info.texture_buffers) { - if (IsMetaUpdate(desc)) { - return false; + if (!desc.is_written) { + const VAddr address = desc.GetSharp(info).base_address; + meta_read = texture_cache.IsMeta(address); + } + } + + if (!meta_read) { + for (const auto& desc : info.buffers) { + if (IsMetaUpdate(desc)) { + return false; + } + } + + for (const auto& desc : info.texture_buffers) { + if (IsMetaUpdate(desc)) { + return false; + } } } } diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 4373fdc52..1670648b3 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -398,17 +398,15 @@ ImageView& TextureCache::FindRenderTarget(BaseDesc& desc) { // Register meta data for this color buffer if (!(image.flags & ImageFlagBits::MetaRegistered)) { if (desc.info.meta_info.cmask_addr) { - surface_metas.emplace( - desc.info.meta_info.cmask_addr, - MetaDataInfo{.type = MetaDataInfo::Type::CMask, .is_cleared = true}); + surface_metas.emplace(desc.info.meta_info.cmask_addr, + MetaDataInfo{.type = MetaDataInfo::Type::CMask}); image.info.meta_info.cmask_addr = desc.info.meta_info.cmask_addr; image.flags |= ImageFlagBits::MetaRegistered; } if (desc.info.meta_info.fmask_addr) { - surface_metas.emplace( - desc.info.meta_info.fmask_addr, - MetaDataInfo{.type = MetaDataInfo::Type::FMask, .is_cleared = true}); + surface_metas.emplace(desc.info.meta_info.fmask_addr, + MetaDataInfo{.type = MetaDataInfo::Type::FMask}); image.info.meta_info.fmask_addr = desc.info.meta_info.fmask_addr; image.flags |= ImageFlagBits::MetaRegistered; } @@ -428,9 +426,8 @@ ImageView& TextureCache::FindDepthTarget(BaseDesc& desc) { // Register meta data for this depth buffer if (!(image.flags & ImageFlagBits::MetaRegistered)) { if (desc.info.meta_info.htile_addr) { - surface_metas.emplace( - desc.info.meta_info.htile_addr, - MetaDataInfo{.type = MetaDataInfo::Type::HTile, .is_cleared = true}); + surface_metas.emplace(desc.info.meta_info.htile_addr, + MetaDataInfo{.type = MetaDataInfo::Type::HTile}); image.info.meta_info.htile_addr = desc.info.meta_info.htile_addr; image.flags |= ImageFlagBits::MetaRegistered; } diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index fab4c832f..676ede777 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -156,18 +156,31 @@ public: return surface_metas.contains(address); } - bool IsMetaCleared(VAddr address) const { + bool IsMetaCleared(VAddr address, u32 slice) const { const auto& it = surface_metas.find(address); if (it != surface_metas.end()) { - return it.value().is_cleared; + return it.value().clear_mask & (1u << slice); } return false; } - bool TouchMeta(VAddr address, bool is_clear) { + bool ClearMeta(VAddr address) { auto it = surface_metas.find(address); if (it != surface_metas.end()) { - it.value().is_cleared = is_clear; + it.value().clear_mask = u32(-1); + return true; + } + return false; + } + + bool TouchMeta(VAddr address, u32 slice, bool is_clear) { + auto it = surface_metas.find(address); + if (it != surface_metas.end()) { + if (is_clear) { + it.value().clear_mask |= 1u << slice; + } else { + it.value().clear_mask &= ~(1u << slice); + } return true; } return false; @@ -280,7 +293,7 @@ private: HTile, }; Type type; - bool is_cleared; + u32 clear_mask{u32(-1)}; }; tsl::robin_map surface_metas; }; From 7fbe15de284c923187a1e06f0267ccb1cd84aea0 Mon Sep 17 00:00:00 2001 From: Richard Habitzreuter Date: Thu, 5 Dec 2024 18:09:43 -0300 Subject: [PATCH 15/89] Missing dependency on building-windows.md (#1658) * Missing dependency on building-windows.md * Update building-windows.md --- documents/building-windows.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documents/building-windows.md b/documents/building-windows.md index 48fd09c41..0da630f0b 100644 --- a/documents/building-windows.md +++ b/documents/building-windows.md @@ -79,7 +79,7 @@ Normal x86-based computers, follow: 1. Open "MSYS2 MINGW64" from your new applications 2. Run `pacman -Syu`, let it complete; -3. Run `pacman -S --needed git mingw-w64-x86_64-binutils mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-ffmpeg` +3. Run `pacman -S --needed git mingw-w64-x86_64-binutils mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-rapidjson mingw-w64-x86_64-ninja mingw-w64-x86_64-ffmpeg` 1. Optional (Qt only): run `pacman -S --needed mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-qt6-multimedia` 4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4` 5. Run `cd shadPS4` @@ -93,7 +93,7 @@ ARM64-based computers, follow: 1. Open "MSYS2 CLANGARM64" from your new applications 2. Run `pacman -Syu`, let it complete; -3. Run `pacman -S --needed git mingw-w64-clang-aarch64-binutils mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja mingw-w64-clang-aarch64-ffmpeg` +3. Run `pacman -S --needed git mingw-w64-clang-aarch64-binutils mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-rapidjson mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja mingw-w64-clang-aarch64-ffmpeg` 1. Optional (Qt only): run `pacman -S --needed mingw-w64-clang-aarch64-qt6-base mingw-w64-clang-aarch64-qt6-tools mingw-w64-clang-aarch64-qt6-multimedia` 4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4` 5. Run `cd shadPS4` From 642dedea8c9099b0b84ff4cd653d0c79fc9bb4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 5 Dec 2024 22:09:59 +0100 Subject: [PATCH 16/89] Handle INDIRECT_BUFFER_CONST in ProcessCeUpdate (#1613) --- src/video_core/amdgpu/liverpool.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index b81806d10..38fca6bc0 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -161,6 +161,19 @@ Liverpool::Task Liverpool::ProcessCeUpdate(std::span ccb) { } break; } + case PM4ItOpcode::IndirectBufferConst: { + const auto* indirect_buffer = reinterpret_cast(header); + auto task = ProcessCeUpdate( + {indirect_buffer->Address(), indirect_buffer->ib_size}); + while (!task.handle.done()) { + task.handle.resume(); + + TracyFiberLeave; + co_yield {}; + TracyFiberEnter(ccb_task_name); + }; + break; + } default: const u32 count = header->type3.NumWords(); UNREACHABLE_MSG("Unknown PM4 type 3 opcode {:#x} with count {}", From 874508f8c2bd721e1f97a0a877dd4686bf43ef83 Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Thu, 5 Dec 2024 21:10:27 +0000 Subject: [PATCH 17/89] cmake: unbundle stb (#1601) --- CMakeLists.txt | 5 ++++- REUSE.toml | 2 +- cmake/Findstb.cmake | 19 +++++++++++++++++++ externals/CMakeLists.txt | 7 +++++++ externals/{ => stb}/stb_image.h | 0 src/common/stb.cpp | 7 +++++++ src/common/stb.h | 6 ++++++ src/core/file_format/splash.cpp | 6 +----- src/imgui/renderer/texture_manager.cpp | 3 +-- 9 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 cmake/Findstb.cmake rename externals/{ => stb}/stb_image.h (100%) create mode 100644 src/common/stb.cpp create mode 100644 src/common/stb.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 56760be37..378b8f78d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,7 @@ find_package(magic_enum 0.9.6 CONFIG) find_package(PNG 1.6 MODULE) find_package(RenderDoc 1.6.0 MODULE) find_package(SDL3 3.1.2 CONFIG) +find_package(stb MODULE) find_package(toml11 4.2.0 CONFIG) find_package(tsl-robin-map 1.3.0 CONFIG) find_package(VulkanHeaders 1.3.289 CONFIG) @@ -495,6 +496,8 @@ set(COMMON src/common/logging/backend.cpp src/common/slot_vector.h src/common/spin_lock.cpp src/common/spin_lock.h + src/common/stb.cpp + src/common/stb.h src/common/string_util.cpp src/common/string_util.h src/common/thread.cpp @@ -867,7 +870,7 @@ endif() create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG) -target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml) +target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml stb::headers) target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") diff --git a/REUSE.toml b/REUSE.toml index 2d94c9292..5bd21bead 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -63,7 +63,7 @@ SPDX-FileCopyrightText = "2019-2024 Baldur Karlsson" SPDX-License-Identifier = "MIT" [[annotations]] -path = "externals/stb_image.h" +path = "externals/stb/**" precedence = "aggregate" SPDX-FileCopyrightText = "2017 Sean Barrett" SPDX-License-Identifier = "MIT" diff --git a/cmake/Findstb.cmake b/cmake/Findstb.cmake new file mode 100644 index 000000000..667911e1d --- /dev/null +++ b/cmake/Findstb.cmake @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +find_path(stb_image_INCLUDE_DIR stb_image.h PATH_SUFFIXES stb) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(stb + REQUIRED_VARS stb_image_INCLUDE_DIR +) + +if (stb_FOUND AND NOT TARGET stb::headers) + add_library(stb::headers INTERFACE IMPORTED) + set_property(TARGET stb::headers PROPERTY + INTERFACE_INCLUDE_DIRECTORIES + "${stb_image_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(stb_image_INCLUDE_DIR) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index bc2d41bda..8ccae8070 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -222,3 +222,10 @@ endif() # GCN Headers add_subdirectory(gcn) + +# stb +if (NOT TARGET stb::headers) + add_library(stb INTERFACE) + target_include_directories(stb INTERFACE stb) + add_library(stb::headers ALIAS stb) +endif() diff --git a/externals/stb_image.h b/externals/stb/stb_image.h similarity index 100% rename from externals/stb_image.h rename to externals/stb/stb_image.h diff --git a/src/common/stb.cpp b/src/common/stb.cpp new file mode 100644 index 000000000..0cd916185 --- /dev/null +++ b/src/common/stb.cpp @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#define STB_IMAGE_IMPLEMENTATION +#define STBI_ONLY_PNG +#define STBI_NO_STDIO +#include "common/stb.h" diff --git a/src/common/stb.h b/src/common/stb.h new file mode 100644 index 000000000..6f4d34483 --- /dev/null +++ b/src/common/stb.h @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include diff --git a/src/core/file_format/splash.cpp b/src/core/file_format/splash.cpp index 5e06c912d..b68702157 100644 --- a/src/core/file_format/splash.cpp +++ b/src/core/file_format/splash.cpp @@ -5,13 +5,9 @@ #include "common/assert.h" #include "common/io_file.h" +#include "common/stb.h" #include "splash.h" -#define STB_IMAGE_IMPLEMENTATION -#define STBI_ONLY_PNG -#define STBI_NO_STDIO -#include "externals/stb_image.h" - bool Splash::Open(const std::filesystem::path& filepath) { ASSERT_MSG(filepath.stem().string() != "png", "Unexpected file format passed"); diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp index 7f9c69d49..dd233ee60 100644 --- a/src/imgui/renderer/texture_manager.cpp +++ b/src/imgui/renderer/texture_manager.cpp @@ -4,12 +4,11 @@ #include #include -#include - #include "common/assert.h" #include "common/config.h" #include "common/io_file.h" #include "common/polyfill_thread.h" +#include "common/stb.h" #include "imgui_impl_vulkan.h" #include "texture_manager.h" From 22a2741ea01dcd10ad207da9bb87211cda179099 Mon Sep 17 00:00:00 2001 From: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> Date: Thu, 5 Dec 2024 23:14:16 +0200 Subject: [PATCH 18/89] shader_recompilers: Improvements to SSA phi generation and lane instruction elimination (#1667) * shader_recompiler: Add use tracking for Insts * ssa_rewrite: Recursively remove phis * ssa_rewrite: Correct recursive trivial phi elimination * ir: Improve read lane folding pass * control_flow: Avoid adding unnecessary divergant blocks * clang format * externals: Update ext-boost --------- Co-authored-by: Frodo Baggins --- externals/ext-boost | 2 +- .../frontend/control_flow_graph.cpp | 43 ++++++---- src/shader_recompiler/ir/basic_block.cpp | 2 + src/shader_recompiler/ir/microinstruction.cpp | 50 +++++++----- .../ir/passes/constant_propagation_pass.cpp | 81 ++++++++++++++----- .../passes/lower_shared_mem_to_registers.cpp | 2 +- .../ir/passes/resource_tracking_pass.cpp | 2 +- .../ir/passes/ssa_rewrite_pass.cpp | 19 ++--- src/shader_recompiler/ir/value.h | 48 +++++++++-- src/video_core/amdgpu/liverpool.cpp | 2 +- 10 files changed, 175 insertions(+), 76 deletions(-) diff --git a/externals/ext-boost b/externals/ext-boost index f2474e1b5..ca6f230e6 160000 --- a/externals/ext-boost +++ b/externals/ext-boost @@ -1 +1 @@ -Subproject commit f2474e1b584fb7a3ed6f85ba875e6eacd742ec8a +Subproject commit ca6f230e67be7cc45fc919057f07b2aee64dadc1 diff --git a/src/shader_recompiler/frontend/control_flow_graph.cpp b/src/shader_recompiler/frontend/control_flow_graph.cpp index 354196d31..8c3122b28 100644 --- a/src/shader_recompiler/frontend/control_flow_graph.cpp +++ b/src/shader_recompiler/frontend/control_flow_graph.cpp @@ -47,6 +47,15 @@ static IR::Condition MakeCondition(const GcnInst& inst) { } } +static bool IgnoresExecMask(Opcode opcode) { + switch (opcode) { + case Opcode::V_WRITELANE_B32: + return true; + default: + return false; + } +} + static constexpr size_t LabelReserveSize = 32; CFG::CFG(Common::ObjectPool& block_pool_, std::span inst_list_) @@ -133,20 +142,26 @@ void CFG::EmitDivergenceLabels() { curr_begin = -1; continue; } - // Add a label to the instruction right after the open scope call. - // It is the start of a new basic block. - const auto& save_inst = inst_list[curr_begin]; - const Label label = index_to_pc[curr_begin] + save_inst.length; - AddLabel(label); - // Add a label to the close scope instruction. - // There are 3 cases where we need to close a scope. - // * Close scope instruction inside the block - // * Close scope instruction at the end of the block (cbranch or endpgm) - // * Normal instruction at the end of the block - // For the last case we must NOT add a label as that would cause - // the instruction to be separated into its own basic block. - if (is_close) { - AddLabel(index_to_pc[index]); + // If all instructions in the scope ignore exec masking, we shouldn't insert a + // scope. + const auto start = inst_list.begin() + curr_begin + 1; + if (!std::ranges::all_of(start, inst_list.begin() + index, IgnoresExecMask, + &GcnInst::opcode)) { + // Add a label to the instruction right after the open scope call. + // It is the start of a new basic block. + const auto& save_inst = inst_list[curr_begin]; + const Label label = index_to_pc[curr_begin] + save_inst.length; + AddLabel(label); + // Add a label to the close scope instruction. + // There are 3 cases where we need to close a scope. + // * Close scope instruction inside the block + // * Close scope instruction at the end of the block (cbranch or endpgm) + // * Normal instruction at the end of the block + // For the last case we must NOT add a label as that would cause + // the instruction to be separated into its own basic block. + if (is_close) { + AddLabel(index_to_pc[index]); + } } // Reset scope begin. curr_begin = -1; diff --git a/src/shader_recompiler/ir/basic_block.cpp b/src/shader_recompiler/ir/basic_block.cpp index 426acb2b8..b4d1a78c7 100644 --- a/src/shader_recompiler/ir/basic_block.cpp +++ b/src/shader_recompiler/ir/basic_block.cpp @@ -19,12 +19,14 @@ void Block::AppendNewInst(Opcode op, std::initializer_list args) { Block::iterator Block::PrependNewInst(iterator insertion_point, const Inst& base_inst) { Inst* const inst{inst_pool->Create(base_inst)}; + inst->SetParent(this); return instructions.insert(insertion_point, *inst); } Block::iterator Block::PrependNewInst(iterator insertion_point, Opcode op, std::initializer_list args, u32 flags) { Inst* const inst{inst_pool->Create(op, flags)}; + inst->SetParent(this); const auto result_it{instructions.insert(insertion_point, *inst)}; if (inst->NumArgs() != args.size()) { diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index abd31a728..9b4ad63d2 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include "shader_recompiler/exception.h" @@ -119,10 +120,10 @@ void Inst::SetArg(size_t index, Value value) { } const IR::Value arg{Arg(index)}; if (!arg.IsImmediate()) { - UndoUse(arg); + UndoUse(arg.Inst(), index); } if (!value.IsImmediate()) { - Use(value); + Use(value.Inst(), index); } if (op == Opcode::Phi) { phi_args[index].second = value; @@ -143,7 +144,7 @@ Block* Inst::PhiBlock(size_t index) const { void Inst::AddPhiOperand(Block* predecessor, const Value& value) { if (!value.IsImmediate()) { - Use(value); + Use(value.Inst(), phi_args.size()); } phi_args.emplace_back(predecessor, value); } @@ -155,17 +156,19 @@ void Inst::Invalidate() { void Inst::ClearArgs() { if (op == Opcode::Phi) { - for (auto& pair : phi_args) { + for (auto i = 0; i < phi_args.size(); i++) { + auto& pair = phi_args[i]; IR::Value& value{pair.second}; if (!value.IsImmediate()) { - UndoUse(value); + UndoUse(value.Inst(), i); } } phi_args.clear(); } else { - for (auto& value : args) { + for (auto i = 0; i < args.size(); i++) { + auto& value = args[i]; if (!value.IsImmediate()) { - UndoUse(value); + UndoUse(value.Inst(), i); } } // Reset arguments to null @@ -174,13 +177,21 @@ void Inst::ClearArgs() { } } -void Inst::ReplaceUsesWith(Value replacement) { - Invalidate(); - ReplaceOpcode(Opcode::Identity); - if (!replacement.IsImmediate()) { - Use(replacement); +void Inst::ReplaceUsesWith(Value replacement, bool preserve) { + // Copy since user->SetArg will mutate this->uses + // Could also do temp_uses = std::move(uses) but more readable + const auto temp_uses = uses; + for (const auto& [user, operand] : temp_uses) { + DEBUG_ASSERT(user->Arg(operand).Inst() == this); + user->SetArg(operand, replacement); + } + Invalidate(); + if (preserve) { + // Still useful to have Identity for indirection. + // SSA pass would be more complicated without it + ReplaceOpcode(Opcode::Identity); + SetArg(0, replacement); } - args[0] = replacement; } void Inst::ReplaceOpcode(IR::Opcode opcode) { @@ -195,14 +206,15 @@ void Inst::ReplaceOpcode(IR::Opcode opcode) { op = opcode; } -void Inst::Use(const Value& value) { - Inst* const inst{value.Inst()}; - ++inst->use_count; +void Inst::Use(Inst* used, u32 operand) { + DEBUG_ASSERT(0 == std::count(used->uses.begin(), used->uses.end(), IR::Use(this, operand))); + used->uses.emplace_front(this, operand); } -void Inst::UndoUse(const Value& value) { - Inst* const inst{value.Inst()}; - --inst->use_count; +void Inst::UndoUse(Inst* used, u32 operand) { + IR::Use use(this, operand); + DEBUG_ASSERT(1 == std::count(used->uses.begin(), used->uses.end(), use)); + used->uses.remove(use); } } // namespace Shader::IR diff --git a/src/shader_recompiler/ir/passes/constant_propagation_pass.cpp b/src/shader_recompiler/ir/passes/constant_propagation_pass.cpp index a03fe051c..9624ce6a5 100644 --- a/src/shader_recompiler/ir/passes/constant_propagation_pass.cpp +++ b/src/shader_recompiler/ir/passes/constant_propagation_pass.cpp @@ -43,7 +43,7 @@ bool FoldCommutative(IR::Inst& inst, ImmFn&& imm_fn) { if (is_lhs_immediate && is_rhs_immediate) { const auto result{imm_fn(Arg(lhs), Arg(rhs))}; - inst.ReplaceUsesWith(IR::Value{result}); + inst.ReplaceUsesWithAndRemove(IR::Value{result}); return false; } if (is_lhs_immediate && !is_rhs_immediate) { @@ -75,7 +75,7 @@ bool FoldWhenAllImmediates(IR::Inst& inst, Func&& func) { return false; } using Indices = std::make_index_sequence::NUM_ARGS>; - inst.ReplaceUsesWith(EvalImmediates(inst, func, Indices{})); + inst.ReplaceUsesWithAndRemove(EvalImmediates(inst, func, Indices{})); return true; } @@ -83,12 +83,12 @@ template void FoldBitCast(IR::Inst& inst, IR::Opcode reverse) { const IR::Value value{inst.Arg(0)}; if (value.IsImmediate()) { - inst.ReplaceUsesWith(IR::Value{std::bit_cast(Arg(value))}); + inst.ReplaceUsesWithAndRemove(IR::Value{std::bit_cast(Arg(value))}); return; } IR::Inst* const arg_inst{value.InstRecursive()}; if (arg_inst->GetOpcode() == reverse) { - inst.ReplaceUsesWith(arg_inst->Arg(0)); + inst.ReplaceUsesWithAndRemove(arg_inst->Arg(0)); return; } } @@ -131,7 +131,7 @@ void FoldCompositeExtract(IR::Inst& inst, IR::Opcode construct, IR::Opcode inser if (!result) { return; } - inst.ReplaceUsesWith(*result); + inst.ReplaceUsesWithAndRemove(*result); } void FoldConvert(IR::Inst& inst, IR::Opcode opposite) { @@ -141,7 +141,7 @@ void FoldConvert(IR::Inst& inst, IR::Opcode opposite) { } IR::Inst* const producer{value.InstRecursive()}; if (producer->GetOpcode() == opposite) { - inst.ReplaceUsesWith(producer->Arg(0)); + inst.ReplaceUsesWithAndRemove(producer->Arg(0)); } } @@ -152,9 +152,9 @@ void FoldLogicalAnd(IR::Inst& inst) { const IR::Value rhs{inst.Arg(1)}; if (rhs.IsImmediate()) { if (rhs.U1()) { - inst.ReplaceUsesWith(inst.Arg(0)); + inst.ReplaceUsesWithAndRemove(inst.Arg(0)); } else { - inst.ReplaceUsesWith(IR::Value{false}); + inst.ReplaceUsesWithAndRemove(IR::Value{false}); } } } @@ -162,7 +162,7 @@ void FoldLogicalAnd(IR::Inst& inst) { void FoldSelect(IR::Inst& inst) { const IR::Value cond{inst.Arg(0)}; if (cond.IsImmediate()) { - inst.ReplaceUsesWith(cond.U1() ? inst.Arg(1) : inst.Arg(2)); + inst.ReplaceUsesWithAndRemove(cond.U1() ? inst.Arg(1) : inst.Arg(2)); } } @@ -173,9 +173,9 @@ void FoldLogicalOr(IR::Inst& inst) { const IR::Value rhs{inst.Arg(1)}; if (rhs.IsImmediate()) { if (rhs.U1()) { - inst.ReplaceUsesWith(IR::Value{true}); + inst.ReplaceUsesWithAndRemove(IR::Value{true}); } else { - inst.ReplaceUsesWith(inst.Arg(0)); + inst.ReplaceUsesWithAndRemove(inst.Arg(0)); } } } @@ -183,12 +183,12 @@ void FoldLogicalOr(IR::Inst& inst) { void FoldLogicalNot(IR::Inst& inst) { const IR::U1 value{inst.Arg(0)}; if (value.IsImmediate()) { - inst.ReplaceUsesWith(IR::Value{!value.U1()}); + inst.ReplaceUsesWithAndRemove(IR::Value{!value.U1()}); return; } IR::Inst* const arg{value.InstRecursive()}; if (arg->GetOpcode() == IR::Opcode::LogicalNot) { - inst.ReplaceUsesWith(arg->Arg(0)); + inst.ReplaceUsesWithAndRemove(arg->Arg(0)); } } @@ -199,7 +199,7 @@ void FoldInverseFunc(IR::Inst& inst, IR::Opcode reverse) { } IR::Inst* const arg_inst{value.InstRecursive()}; if (arg_inst->GetOpcode() == reverse) { - inst.ReplaceUsesWith(arg_inst->Arg(0)); + inst.ReplaceUsesWithAndRemove(arg_inst->Arg(0)); return; } } @@ -211,7 +211,7 @@ void FoldAdd(IR::Block& block, IR::Inst& inst) { } const IR::Value rhs{inst.Arg(1)}; if (rhs.IsImmediate() && Arg(rhs) == 0) { - inst.ReplaceUsesWith(inst.Arg(0)); + inst.ReplaceUsesWithAndRemove(inst.Arg(0)); return; } } @@ -226,21 +226,58 @@ void FoldCmpClass(IR::Block& block, IR::Inst& inst) { } else if ((class_mask & IR::FloatClassFunc::Finite) == IR::FloatClassFunc::Finite) { IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; const IR::F32 value = IR::F32{inst.Arg(0)}; - inst.ReplaceUsesWith(ir.LogicalNot(ir.LogicalOr(ir.FPIsInf(value), ir.FPIsInf(value)))); + inst.ReplaceUsesWithAndRemove( + ir.LogicalNot(ir.LogicalOr(ir.FPIsInf(value), ir.FPIsInf(value)))); } else { UNREACHABLE(); } } -void FoldReadLane(IR::Inst& inst) { +void FoldReadLane(IR::Block& block, IR::Inst& inst) { const u32 lane = inst.Arg(1).U32(); IR::Inst* prod = inst.Arg(0).InstRecursive(); - while (prod->GetOpcode() == IR::Opcode::WriteLane) { - if (prod->Arg(2).U32() == lane) { - inst.ReplaceUsesWith(prod->Arg(1)); + + const auto search_chain = [lane](const IR::Inst* prod) -> IR::Value { + while (prod->GetOpcode() == IR::Opcode::WriteLane) { + if (prod->Arg(2).U32() == lane) { + return prod->Arg(1); + } + prod = prod->Arg(0).InstRecursive(); + } + return {}; + }; + + if (prod->GetOpcode() == IR::Opcode::WriteLane) { + if (const IR::Value value = search_chain(prod); !value.IsEmpty()) { + inst.ReplaceUsesWith(value); + } + return; + } + + if (prod->GetOpcode() == IR::Opcode::Phi) { + boost::container::small_vector phi_args; + for (size_t arg_index = 0; arg_index < prod->NumArgs(); ++arg_index) { + const IR::Inst* arg{prod->Arg(arg_index).InstRecursive()}; + if (arg->GetOpcode() != IR::Opcode::WriteLane) { + return; + } + const IR::Value value = search_chain(arg); + if (value.IsEmpty()) { + continue; + } + phi_args.emplace_back(value); + } + if (std::ranges::all_of(phi_args, [&](IR::Value value) { return value == phi_args[0]; })) { + inst.ReplaceUsesWith(phi_args[0]); return; } - prod = prod->Arg(0).InstRecursive(); + const auto insert_point = IR::Block::InstructionList::s_iterator_to(*prod); + IR::Inst* const new_phi{&*block.PrependNewInst(insert_point, IR::Opcode::Phi)}; + new_phi->SetFlags(IR::Type::U32); + for (size_t arg_index = 0; arg_index < phi_args.size(); arg_index++) { + new_phi->AddPhiOperand(prod->PhiBlock(arg_index), phi_args[arg_index]); + } + inst.ReplaceUsesWith(IR::Value{new_phi}); } } @@ -290,7 +327,7 @@ void ConstantPropagation(IR::Block& block, IR::Inst& inst) { case IR::Opcode::SelectF64: return FoldSelect(inst); case IR::Opcode::ReadLane: - return FoldReadLane(inst); + return FoldReadLane(block, inst); case IR::Opcode::FPNeg32: FoldWhenAllImmediates(inst, [](f32 a) { return -a; }); return; diff --git a/src/shader_recompiler/ir/passes/lower_shared_mem_to_registers.cpp b/src/shader_recompiler/ir/passes/lower_shared_mem_to_registers.cpp index 76bfcf911..c109f3595 100644 --- a/src/shader_recompiler/ir/passes/lower_shared_mem_to_registers.cpp +++ b/src/shader_recompiler/ir/passes/lower_shared_mem_to_registers.cpp @@ -25,7 +25,7 @@ void LowerSharedMemToRegisters(IR::Program& program) { }); ASSERT(it != ds_writes.end()); // Replace data read with value written. - inst.ReplaceUsesWith((*it)->Arg(1)); + inst.ReplaceUsesWithAndRemove((*it)->Arg(1)); } } } diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index c1ff3d2f2..89c5c78a0 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -596,7 +596,7 @@ void PatchImageSampleInstruction(IR::Block& block, IR::Inst& inst, Info& info, } return ir.ImageSampleImplicitLod(handle, coords, bias, offset, inst_info); }(); - inst.ReplaceUsesWith(new_inst); + inst.ReplaceUsesWithAndRemove(new_inst); } void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors) { diff --git a/src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp b/src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp index df73c1bc8..1d252bee1 100644 --- a/src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp +++ b/src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp @@ -164,7 +164,6 @@ IR::Opcode UndefOpcode(const FlagTag) noexcept { enum class Status { Start, SetValue, - PreparePhiArgument, PushPhiArgument, }; @@ -253,12 +252,10 @@ public: IR::Inst* const phi{stack.back().phi}; phi->AddPhiOperand(*stack.back().pred_it, stack.back().result); ++stack.back().pred_it; - } - [[fallthrough]]; - case Status::PreparePhiArgument: prepare_phi_operand(); break; } + } } while (stack.size() > 1); return stack.back().result; } @@ -266,9 +263,7 @@ public: void SealBlock(IR::Block* block) { const auto it{incomplete_phis.find(block)}; if (it != incomplete_phis.end()) { - for (auto& pair : it->second) { - auto& variant{pair.first}; - auto& phi{pair.second}; + for (auto& [variant, phi] : it->second) { std::visit([&](auto& variable) { AddPhiOperands(variable, *phi, block); }, variant); } } @@ -289,7 +284,7 @@ private: const size_t num_args{phi.NumArgs()}; for (size_t arg_index = 0; arg_index < num_args; ++arg_index) { const IR::Value& op{phi.Arg(arg_index)}; - if (op.Resolve() == same.Resolve() || op == IR::Value{&phi}) { + if (op.Resolve() == same.Resolve() || op.Resolve() == IR::Value{&phi}) { // Unique value or self-reference continue; } @@ -314,9 +309,15 @@ private: ++reinsert_point; } // Reinsert the phi node and reroute all its uses to the "same" value + const auto users = phi.Uses(); list.insert(reinsert_point, phi); phi.ReplaceUsesWith(same); - // TODO: Try to recursively remove all phi users, which might have become trivial + // Try to recursively remove all phi users, which might have become trivial + for (const auto& [user, arg_index] : users) { + if (user->GetOpcode() == IR::Opcode::Phi) { + TryRemoveTrivialPhi(*user, user->GetParent(), undef_opcode); + } + } return same; } diff --git a/src/shader_recompiler/ir/value.h b/src/shader_recompiler/ir/value.h index 7e46747b9..dbe8b5cc4 100644 --- a/src/shader_recompiler/ir/value.h +++ b/src/shader_recompiler/ir/value.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -107,6 +108,16 @@ public: explicit TypedValue(IR::Inst* inst_) : TypedValue(Value(inst_)) {} }; +struct Use { + Inst* user; + u32 operand; + + Use() = default; + Use(Inst* user_, u32 operand_) : user(user_), operand(operand_) {} + Use(const Use&) = default; + bool operator==(const Use&) const noexcept = default; +}; + class Inst : public boost::intrusive::list_base_hook<> { public: explicit Inst(IR::Opcode op_, u32 flags_) noexcept; @@ -118,14 +129,22 @@ public: Inst& operator=(Inst&&) = delete; Inst(Inst&&) = delete; + IR::Block* GetParent() const { + ASSERT(parent); + return parent; + } + void SetParent(IR::Block* block) { + parent = block; + } + /// Get the number of uses this instruction has. [[nodiscard]] int UseCount() const noexcept { - return use_count; + return uses.size(); } /// Determines whether this instruction has uses or not. [[nodiscard]] bool HasUses() const noexcept { - return use_count > 0; + return uses.size() > 0; } /// Get the opcode this microinstruction represents. @@ -167,7 +186,13 @@ public: void Invalidate(); void ClearArgs(); - void ReplaceUsesWith(Value replacement); + void ReplaceUsesWithAndRemove(Value replacement) { + ReplaceUsesWith(replacement, false); + } + + void ReplaceUsesWith(Value replacement) { + ReplaceUsesWith(replacement, true); + } void ReplaceOpcode(IR::Opcode opcode); @@ -197,25 +222,32 @@ public: return std::bit_cast(definition); } + const auto Uses() const { + return uses; + } + private: struct NonTriviallyDummy { NonTriviallyDummy() noexcept {} }; - void Use(const Value& value); - void UndoUse(const Value& value); + void Use(Inst* used, u32 operand); + void UndoUse(Inst* used, u32 operand); + void ReplaceUsesWith(Value replacement, bool preserve); IR::Opcode op{}; - int use_count{}; u32 flags{}; u32 definition{}; + IR::Block* parent{}; union { NonTriviallyDummy dummy{}; boost::container::small_vector, 2> phi_args; std::array args; }; + + boost::container::list uses; }; -static_assert(sizeof(Inst) <= 128, "Inst size unintentionally increased"); +static_assert(sizeof(Inst) <= 160, "Inst size unintentionally increased"); using U1 = TypedValue; using U8 = TypedValue; @@ -373,4 +405,4 @@ template <> struct hash { std::size_t operator()(const Shader::IR::Value& v) const; }; -} // namespace std \ No newline at end of file +} // namespace std diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 38fca6bc0..1bbd77f82 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -715,7 +715,7 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, int vqid) { false); } else if (dma_data->src_sel == DmaDataSrc::Gds && dma_data->dst_sel == DmaDataDst::Memory) { - LOG_WARNING(Render_Vulkan, "GDS memory read"); + // LOG_WARNING(Render_Vulkan, "GDS memory read"); } else if (dma_data->src_sel == DmaDataSrc::Memory && dma_data->dst_sel == DmaDataDst::Memory) { rasterizer->InlineData(dma_data->DstAddress(), From 77da8bac00ef22b8b79abd3cde284abdf3f18514 Mon Sep 17 00:00:00 2001 From: IndecisiveTurtle <47210458+raphaelthegreat@users.noreply.github.com> Date: Fri, 6 Dec 2024 00:46:34 +0200 Subject: [PATCH 19/89] core: Return proper address of eh frame/add more opcodes --- src/core/module.cpp | 4 ++-- src/emulator.cpp | 2 +- .../frontend/translate/scalar_alu.cpp | 21 +++++++++++++++++++ .../frontend/translate/translate.h | 2 ++ src/video_core/amdgpu/liverpool.cpp | 4 ++-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/core/module.cpp b/src/core/module.cpp index ef34f25c1..70afb932c 100644 --- a/src/core/module.cpp +++ b/src/core/module.cpp @@ -470,8 +470,8 @@ OrbisKernelModuleInfoEx Module::GetModuleInfoEx() const { .tls_align = tls.align, .init_proc_addr = base_virtual_addr + dynamic_info.init_virtual_addr, .fini_proc_addr = base_virtual_addr + dynamic_info.fini_virtual_addr, - .eh_frame_hdr_addr = eh_frame_hdr_addr, - .eh_frame_addr = eh_frame_addr, + .eh_frame_hdr_addr = base_virtual_addr + eh_frame_hdr_addr, + .eh_frame_addr = base_virtual_addr + eh_frame_addr, .eh_frame_hdr_size = eh_frame_hdr_size, .eh_frame_size = eh_frame_size, .segments = info.segments, diff --git a/src/emulator.cpp b/src/emulator.cpp index 60d6e18d7..8a7c04cf4 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -266,7 +266,7 @@ void Emulator::Run(const std::filesystem::path& file) { } void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string game_serial) { - constexpr std::array ModulesToLoad{ + constexpr std::array ModulesToLoad{ {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterlibSceNgs2}, {"libSceFiber.sprx", &Libraries::Fiber::RegisterlibSceFiber}, {"libSceUlt.sprx", nullptr}, diff --git a/src/shader_recompiler/frontend/translate/scalar_alu.cpp b/src/shader_recompiler/frontend/translate/scalar_alu.cpp index de8b9da87..75ad957b3 100644 --- a/src/shader_recompiler/frontend/translate/scalar_alu.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_alu.cpp @@ -50,6 +50,8 @@ void Translator::EmitScalarAlu(const GcnInst& inst) { return S_OR_B64(NegateMode::None, false, inst); case Opcode::S_XOR_B32: return S_XOR_B32(inst); + case Opcode::S_NOT_B32: + return S_NOT_B32(inst); case Opcode::S_XOR_B64: return S_OR_B64(NegateMode::None, true, inst); case Opcode::S_ANDN2_B32: @@ -94,6 +96,8 @@ void Translator::EmitScalarAlu(const GcnInst& inst) { return S_BREV_B32(inst); case Opcode::S_BCNT1_I32_B64: return S_BCNT1_I32_B64(inst); + case Opcode::S_FF1_I32_B64: + return S_FF1_I32_B64(inst); case Opcode::S_AND_SAVEEXEC_B64: return S_SAVEEXEC_B64(NegateMode::None, false, inst); case Opcode::S_ORN2_SAVEEXEC_B64: @@ -301,6 +305,10 @@ void Translator::S_AND_B64(NegateMode negate, const GcnInst& inst) { ASSERT_MSG(-s32(operand.code) + SignedConstIntNegMin - 1 == -1, "SignedConstIntNeg must be -1"); return ir.Imm1(true); + case OperandField::LiteralConst: + ASSERT_MSG(operand.code == 0 || operand.code == std::numeric_limits::max(), + "Unsupported literal {:#x}", operand.code); + return ir.Imm1(operand.code & 1); default: UNREACHABLE(); } @@ -382,6 +390,13 @@ void Translator::S_XOR_B32(const GcnInst& inst) { ir.SetScc(ir.INotEqual(result, ir.Imm32(0))); } +void Translator::S_NOT_B32(const GcnInst& inst) { + const IR::U32 src0{GetSrc(inst.src[0])}; + const IR::U32 result{ir.BitwiseNot(src0)}; + SetDst(inst.dst[0], result); + ir.SetScc(ir.INotEqual(result, ir.Imm32(0))); +} + void Translator::S_LSHL_B32(const GcnInst& inst) { const IR::U32 src0{GetSrc(inst.src[0])}; const IR::U32 src1{GetSrc(inst.src[1])}; @@ -560,6 +575,12 @@ void Translator::S_BCNT1_I32_B64(const GcnInst& inst) { ir.SetScc(ir.INotEqual(result, ir.Imm32(0))); } +void Translator::S_FF1_I32_B64(const GcnInst& inst) { + const IR::U32 src0{GetSrc(inst.src[0])}; + const IR::U32 result{ir.Select(ir.IEqual(src0, ir.Imm32(0U)), ir.Imm32(-1), ir.FindILsb(src0))}; + SetDst(inst.dst[0], result); +} + void Translator::S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& inst) { // This instruction normally operates on 64-bit data (EXEC, VCC, SGPRs) // However here we flatten it to 1-bit EXEC and 1-bit VCC. For the destination diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 3b89372bd..dd379d8ea 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -96,6 +96,7 @@ public: void S_MUL_I32(const GcnInst& inst); void S_BFE_U32(const GcnInst& inst); void S_ABSDIFF_I32(const GcnInst& inst); + void S_NOT_B32(const GcnInst& inst); // SOPK void S_MOVK(const GcnInst& inst); @@ -109,6 +110,7 @@ public: void S_NOT_B64(const GcnInst& inst); void S_BREV_B32(const GcnInst& inst); void S_BCNT1_I32_B64(const GcnInst& inst); + void S_FF1_I32_B64(const GcnInst& inst); void S_GETPC_B64(u32 pc, const GcnInst& inst); void S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& inst); diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 1bbd77f82..c0c5f1b2f 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -565,7 +565,7 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); - if (dma_data->dst_addr_lo == 0x3022C) { + if (dma_data->dst_addr_lo == 0x3022C || !rasterizer) { break; } if (dma_data->src_sel == DmaDataSrc::Data && dma_data->dst_sel == DmaDataDst::Gds) { @@ -700,7 +700,7 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, int vqid) { } case PM4ItOpcode::DmaData: { const auto* dma_data = reinterpret_cast(header); - if (dma_data->dst_addr_lo == 0x3022C) { + if (dma_data->dst_addr_lo == 0x3022C || !rasterizer) { break; } if (dma_data->src_sel == DmaDataSrc::Data && dma_data->dst_sel == DmaDataDst::Gds) { From 17abbcd74d5c21badda079a8e71fa9fa4c20ea30 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:21:35 -0800 Subject: [PATCH 20/89] misc: Fix clang format (#1673) --- src/video_core/amdgpu/liverpool.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index c0c5f1b2f..a4eae8e7a 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -163,8 +163,8 @@ Liverpool::Task Liverpool::ProcessCeUpdate(std::span ccb) { } case PM4ItOpcode::IndirectBufferConst: { const auto* indirect_buffer = reinterpret_cast(header); - auto task = ProcessCeUpdate( - {indirect_buffer->Address(), indirect_buffer->ib_size}); + auto task = + ProcessCeUpdate({indirect_buffer->Address(), indirect_buffer->ib_size}); while (!task.handle.done()) { task.handle.resume(); From d05846a327e609ba514c981c0f28777c77914271 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Fri, 6 Dec 2024 02:59:55 -0800 Subject: [PATCH 21/89] specialization: Fix fetch shader field type (#1675) --- src/shader_recompiler/frontend/fetch_shader.h | 13 ------------- src/shader_recompiler/specialization.h | 19 +++++++++---------- .../renderer_vulkan/vk_pipeline_cache.cpp | 2 ++ .../renderer_vulkan/vk_rasterizer.cpp | 18 +++++++++++++++++- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/shader_recompiler/frontend/fetch_shader.h b/src/shader_recompiler/frontend/fetch_shader.h index ee9f5c805..080b0eb22 100644 --- a/src/shader_recompiler/frontend/fetch_shader.h +++ b/src/shader_recompiler/frontend/fetch_shader.h @@ -58,19 +58,6 @@ struct FetchShaderData { }) != attributes.end(); } - [[nodiscard]] std::pair GetDrawOffsets(const AmdGpu::Liverpool::Regs& regs, - const Info& info) const { - u32 vertex_offset = regs.index_offset; - u32 instance_offset = 0; - if (vertex_offset == 0 && vertex_offset_sgpr != -1) { - vertex_offset = info.user_data[vertex_offset_sgpr]; - } - if (instance_offset_sgpr != -1) { - instance_offset = info.user_data[instance_offset_sgpr]; - } - return {vertex_offset, instance_offset}; - } - bool operator==(const FetchShaderData& other) const { return attributes == other.attributes && vertex_offset_sgpr == other.vertex_offset_sgpr && instance_offset_sgpr == other.instance_offset_sgpr; diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 740b89dda..82c064640 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -57,7 +57,7 @@ struct StageSpecialization { const Shader::Info* info; RuntimeInfo runtime_info; - Gcn::FetchShaderData fetch_shader_data{}; + std::optional fetch_shader_data{}; boost::container::small_vector vs_attribs; std::bitset bitset{}; boost::container::small_vector buffers; @@ -69,15 +69,14 @@ struct StageSpecialization { explicit StageSpecialization(const Info& info_, RuntimeInfo runtime_info_, const Profile& profile_, Backend::Bindings start_) : info{&info_}, runtime_info{runtime_info_}, start{start_} { - if (const auto fetch_shader = Gcn::ParseFetchShader(info_)) { - fetch_shader_data = *fetch_shader; - if (info_.stage == Stage::Vertex && !profile_.support_legacy_vertex_attributes) { - // Specialize shader on VS input number types to follow spec. - ForEachSharp(vs_attribs, fetch_shader_data.attributes, - [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { - spec.num_class = AmdGpu::GetNumberClass(sharp.GetNumberFmt()); - }); - } + fetch_shader_data = Gcn::ParseFetchShader(info_); + if (info_.stage == Stage::Vertex && fetch_shader_data && + !profile_.support_legacy_vertex_attributes) { + // Specialize shader on VS input number types to follow spec. + ForEachSharp(vs_attribs, fetch_shader_data->attributes, + [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { + spec.num_class = AmdGpu::GetNumberClass(sharp.GetNumberFmt()); + }); } u32 binding{}; if (info->has_readconst) { diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 47713f0ff..82a029b95 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -279,6 +279,8 @@ bool PipelineCache::RefreshGraphicsKey() { ++remapped_cb; } + fetch_shader = std::nullopt; + Shader::Backend::Bindings binding{}; const auto& TryBindStageRemap = [&](Shader::Stage stage_in, Shader::Stage stage_out) -> bool { const auto stage_in_idx = static_cast(stage_in); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 620e5f103..e2b6d9749 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -171,6 +171,22 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { return state; } +[[nodiscard]] std::pair GetDrawOffsets( + const AmdGpu::Liverpool::Regs& regs, const Shader::Info& info, + const std::optional& fetch_shader) { + u32 vertex_offset = regs.index_offset; + u32 instance_offset = 0; + if (fetch_shader) { + if (vertex_offset == 0 && fetch_shader->vertex_offset_sgpr != -1) { + vertex_offset = info.user_data[fetch_shader->vertex_offset_sgpr]; + } + if (fetch_shader->instance_offset_sgpr != -1) { + instance_offset = info.user_data[fetch_shader->instance_offset_sgpr]; + } + } + return {vertex_offset, instance_offset}; +} + void Rasterizer::Draw(bool is_indexed, u32 index_offset) { RENDERER_TRACE; @@ -198,7 +214,7 @@ void Rasterizer::Draw(bool is_indexed, u32 index_offset) { BeginRendering(*pipeline, state); UpdateDynamicState(*pipeline); - const auto [vertex_offset, instance_offset] = fetch_shader->GetDrawOffsets(regs, vs_info); + const auto [vertex_offset, instance_offset] = GetDrawOffsets(regs, vs_info, fetch_shader); const auto cmdbuf = scheduler.CommandBuffer(); cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline->Handle()); From 9e618c0e0c14d9fd7daf040509d71392356054be Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Fri, 6 Dec 2024 19:54:59 +0200 Subject: [PATCH 22/89] video_core: Add multipler to handle special cases of texture buffer stride mismatch (#1640) * page_manager: Enable userfaultfd by default * Much faster than page faults and causes less problems * shader_recompiler: Add texel buffer multiplier * Fixes format mismatch assert when vsharp stride is multiple of format stride * shader_recompiler: Specialize UBOs on size * Some games can perform manual vertex pulling and thus bind read only buffers of varying size. We only recompile when the vsharp size is larger than size in shader, in opposite case its not needed * clang format --- CMakeLists.txt | 4 ++++ .../backend/spirv/emit_spirv_context_get_set.cpp | 8 ++++++-- .../backend/spirv/spirv_emit_context.cpp | 2 ++ .../backend/spirv/spirv_emit_context.h | 1 + src/shader_recompiler/info.h | 5 +++++ src/shader_recompiler/specialization.h | 10 ++++++++-- src/video_core/page_manager.cpp | 2 +- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 5 +++-- 8 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 378b8f78d..ae6d1d74e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -875,6 +875,10 @@ target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAlloca target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + target_compile_definitions(shadps4 PRIVATE ENABLE_USERFAULTFD) +endif() + if (APPLE) option(USE_SYSTEM_VULKAN_LOADER "Enables using the system Vulkan loader instead of directly linking with MoltenVK. Useful for loading validation layers." OFF) if (USE_SYSTEM_VULKAN_LOADER) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index d8c0a17bd..b578f0c52 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -326,7 +326,9 @@ Id EmitLoadBufferU32x4(EmitContext& ctx, IR::Inst*, u32 handle, Id address) { Id EmitLoadBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { const auto& buffer = ctx.texture_buffers[handle]; const Id tex_buffer = ctx.OpLoad(buffer.image_type, buffer.id); - const Id coord = ctx.OpIAdd(ctx.U32[1], address, buffer.coord_offset); + const Id coord = + ctx.OpIAdd(ctx.U32[1], ctx.OpShiftLeftLogical(ctx.U32[1], address, buffer.coord_shift), + buffer.coord_offset); Id texel = buffer.is_storage ? ctx.OpImageRead(buffer.result_type, tex_buffer, coord) : ctx.OpImageFetch(buffer.result_type, tex_buffer, coord); if (buffer.is_integer) { @@ -372,7 +374,9 @@ void EmitStoreBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre void EmitStoreBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { const auto& buffer = ctx.texture_buffers[handle]; const Id tex_buffer = ctx.OpLoad(buffer.image_type, buffer.id); - const Id coord = ctx.OpIAdd(ctx.U32[1], address, buffer.coord_offset); + const Id coord = + ctx.OpIAdd(ctx.U32[1], ctx.OpShiftLeftLogical(ctx.U32[1], address, buffer.coord_shift), + buffer.coord_offset); if (buffer.is_integer) { value = ctx.OpBitcast(buffer.result_type, value); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 4ce9f4221..5c7278c6b 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -207,6 +207,8 @@ void EmitContext::DefineBufferOffsets() { push_data_block, ConstU32(half), ConstU32(comp))}; const Id value{OpLoad(U32[1], ptr)}; tex_buffer.coord_offset = OpBitFieldUExtract(U32[1], value, ConstU32(offset), ConstU32(6U)); + tex_buffer.coord_shift = + OpBitFieldUExtract(U32[1], value, ConstU32(offset + 6U), ConstU32(2U)); Name(tex_buffer.coord_offset, fmt::format("texbuf{}_off", binding)); } } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 1c5da946d..4e5e7dd3b 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -223,6 +223,7 @@ public: struct TextureBufferDefinition { Id id; Id coord_offset; + Id coord_shift; u32 binding; Id image_type; Id result_type; diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index d382d0e7c..494bbb4bb 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -105,6 +105,11 @@ struct PushData { ASSERT(offset < 256 && binding < buf_offsets.size()); buf_offsets[binding] = offset; } + + void AddTexelOffset(u32 binding, u32 multiplier, u32 texel_offset) { + ASSERT(texel_offset < 64 && multiplier < 16); + buf_offsets[binding] = texel_offset | ((std::bit_width(multiplier) - 1) << 6); + } }; static_assert(sizeof(PushData) <= 128, "PushData size is greater than minimum size guaranteed by Vulkan spec"); diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 82c064640..2a3bd62f4 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -9,7 +9,6 @@ #include "frontend/fetch_shader.h" #include "shader_recompiler/backend/bindings.h" #include "shader_recompiler/info.h" -#include "shader_recompiler/ir/passes/srt.h" namespace Shader { @@ -22,8 +21,12 @@ struct VsAttribSpecialization { struct BufferSpecialization { u16 stride : 14; u16 is_storage : 1; + u32 size = 0; - auto operator<=>(const BufferSpecialization&) const = default; + bool operator==(const BufferSpecialization& other) const { + return stride == other.stride && is_storage == other.is_storage && + (size >= other.is_storage || is_storage); + } }; struct TextureBufferSpecialization { @@ -86,6 +89,9 @@ struct StageSpecialization { [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { spec.stride = sharp.GetStride(); spec.is_storage = desc.IsStorage(sharp); + if (!spec.is_storage) { + spec.size = sharp.GetSize(); + } }); ForEachSharp(binding, tex_buffers, info->texture_buffers, [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index d26a7067a..80b91b825 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -29,7 +29,7 @@ namespace VideoCore { constexpr size_t PAGESIZE = 4_KB; constexpr size_t PAGEBITS = 12; -#if ENABLE_USERFAULTFD +#ifdef ENABLE_USERFAULTFD struct PageManager::Impl { Impl(Vulkan::Rasterizer* rasterizer_) : rasterizer{rasterizer_} { uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index e2b6d9749..4e858c0d3 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -548,12 +548,13 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding const auto [vk_buffer, offset] = buffer_cache.ObtainBuffer( vsharp.base_address, vsharp.GetSize(), desc.is_written, true, buffer_id); const u32 fmt_stride = AmdGpu::NumBits(vsharp.GetDataFmt()) >> 3; - ASSERT_MSG(fmt_stride == vsharp.GetStride(), + const u32 buf_stride = vsharp.GetStride(); + ASSERT_MSG(buf_stride % fmt_stride == 0, "Texel buffer stride must match format stride"); const u32 offset_aligned = Common::AlignDown(offset, alignment); const u32 adjust = offset - offset_aligned; ASSERT(adjust % fmt_stride == 0); - push_data.AddOffset(binding.buffer, adjust / fmt_stride); + push_data.AddTexelOffset(binding.buffer, buf_stride / fmt_stride, adjust / fmt_stride); buffer_view = vk_buffer->View(offset_aligned, vsharp.GetSize() + adjust, desc.is_written, vsharp.GetDataFmt(), vsharp.GetNumberFmt()); From 6acfdd5e33cdb4b2484e162916242f40eaa9dcc6 Mon Sep 17 00:00:00 2001 From: IndecisiveTurtle Date: Fri, 6 Dec 2024 20:00:21 +0200 Subject: [PATCH 23/89] buffer_cache: Bump usable address space to 40bits * Fixes crashes in games that use the upper region of user area --- src/video_core/buffer_cache/buffer_cache.h | 2 +- src/video_core/buffer_cache/memory_tracker_base.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index b1bf77f8a..3dab95db7 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -42,7 +42,7 @@ public: struct Traits { using Entry = BufferId; - static constexpr size_t AddressSpaceBits = 39; + static constexpr size_t AddressSpaceBits = 40; static constexpr size_t FirstLevelBits = 14; static constexpr size_t PageBits = CACHING_PAGEBITS; }; diff --git a/src/video_core/buffer_cache/memory_tracker_base.h b/src/video_core/buffer_cache/memory_tracker_base.h index 375701c4c..a59bcfff5 100644 --- a/src/video_core/buffer_cache/memory_tracker_base.h +++ b/src/video_core/buffer_cache/memory_tracker_base.h @@ -14,7 +14,7 @@ namespace VideoCore { class MemoryTracker { public: - static constexpr size_t MAX_CPU_PAGE_BITS = 39; + static constexpr size_t MAX_CPU_PAGE_BITS = 40; static constexpr size_t HIGHER_PAGE_BITS = 22; static constexpr size_t HIGHER_PAGE_SIZE = 1ULL << HIGHER_PAGE_BITS; static constexpr size_t HIGHER_PAGE_MASK = HIGHER_PAGE_SIZE - 1ULL; From 357b7829c3eee85d59b620fcfde8562195f50ce2 Mon Sep 17 00:00:00 2001 From: IndecisiveTurtle Date: Fri, 6 Dec 2024 21:50:25 +0200 Subject: [PATCH 24/89] hot-fix: Silence depth macrotiled warning --- src/video_core/texture_cache/tile_manager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index 7430168d0..9823cb4dc 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -392,7 +392,8 @@ std::pair TileManager::TryDetile(vk::Buffer in_buffer, u32 in_o const auto* detiler = GetDetiler(image); if (!detiler) { if (image.info.tiling_mode != AmdGpu::TilingMode::Texture_MacroTiled && - image.info.tiling_mode != AmdGpu::TilingMode::Display_MacroTiled) { + image.info.tiling_mode != AmdGpu::TilingMode::Display_MacroTiled && + image.info.tiling_mode != AmdGpu::TilingMode::Depth_MacroTiled) { LOG_ERROR(Render_Vulkan, "Unsupported tiled image: {} ({})", vk::to_string(image.info.pixel_format), NameOf(image.info.tiling_mode)); } From 7ffa581d4b1aea106485ab3e6957836b5ad22f02 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Fri, 6 Dec 2024 22:04:36 +0100 Subject: [PATCH 25/89] The way to Unity, pt.2 (#1671) --- CMakeLists.txt | 5 +- src/common/ntapi.cpp | 2 - src/common/ntapi.h | 9 +- src/common/thread.cpp | 14 ++- src/common/thread.h | 2 + src/core/devices/logger.cpp | 1 + src/core/libraries/kernel/sync/mutex.cpp | 52 ++++++++ src/core/libraries/kernel/sync/mutex.h | 80 ++++++++++++ src/core/libraries/kernel/sync/semaphore.h | 117 ++++++++++++++++++ src/core/libraries/kernel/threads/condvar.cpp | 4 +- .../libraries/kernel/threads/event_flag.cpp | 1 - src/core/libraries/kernel/threads/pthread.cpp | 1 + src/core/libraries/kernel/threads/pthread.h | 8 +- .../libraries/kernel/threads/semaphore.cpp | 26 ++-- src/core/libraries/kernel/time.cpp | 17 ++- 15 files changed, 311 insertions(+), 28 deletions(-) create mode 100644 src/core/libraries/kernel/sync/mutex.cpp create mode 100644 src/core/libraries/kernel/sync/mutex.h create mode 100644 src/core/libraries/kernel/sync/semaphore.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ae6d1d74e..84146bb01 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,7 +210,10 @@ set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp src/core/libraries/gnmdriver/gnm_error.h ) -set(KERNEL_LIB src/core/libraries/kernel/threads/condvar.cpp +set(KERNEL_LIB src/core/libraries/kernel/sync/mutex.cpp + src/core/libraries/kernel/sync/mutex.h + src/core/libraries/kernel/sync/semaphore.h + src/core/libraries/kernel/threads/condvar.cpp src/core/libraries/kernel/threads/event_flag.cpp src/core/libraries/kernel/threads/exception.cpp src/core/libraries/kernel/threads/exception.h diff --git a/src/common/ntapi.cpp b/src/common/ntapi.cpp index ffdedb17f..e0ff1cef0 100644 --- a/src/common/ntapi.cpp +++ b/src/common/ntapi.cpp @@ -6,7 +6,6 @@ #include "ntapi.h" NtClose_t NtClose = nullptr; -NtDelayExecution_t NtDelayExecution = nullptr; NtSetInformationFile_t NtSetInformationFile = nullptr; NtCreateThread_t NtCreateThread = nullptr; NtTerminateThread_t NtTerminateThread = nullptr; @@ -18,7 +17,6 @@ void Initialize() { // http://stackoverflow.com/a/31411628/4725495 NtClose = (NtClose_t)GetProcAddress(nt_handle, "NtClose"); - NtDelayExecution = (NtDelayExecution_t)GetProcAddress(nt_handle, "NtDelayExecution"); NtSetInformationFile = (NtSetInformationFile_t)GetProcAddress(nt_handle, "NtSetInformationFile"); NtCreateThread = (NtCreateThread_t)GetProcAddress(nt_handle, "NtCreateThread"); diff --git a/src/common/ntapi.h b/src/common/ntapi.h index 743174061..cb1ba7f1c 100644 --- a/src/common/ntapi.h +++ b/src/common/ntapi.h @@ -408,7 +408,7 @@ typedef struct _TEB { /* win32/win64 */ #ifdef _WIN64 PVOID SystemReserved1[30]; /* /0190 */ #else - PVOID SystemReserved1[26]; /* 10c/ used for krnl386 private data in Wine */ + PVOID SystemReserved1[26]; /* 10c/ */ #endif char PlaceholderCompatibilityMode; /* 174/0280 */ BOOLEAN PlaceholderHydrationAlwaysExplicit; /* 175/0281 */ @@ -430,13 +430,13 @@ typedef struct _TEB { /* win32/win64 */ BYTE SpareBytes1[23]; /* 1b9/ */ ULONG TxFsContext; /* 1d0/ */ #endif - GDI_TEB_BATCH GdiTebBatch; /* 1d4/02f0 used for ntdll private data in Wine */ + GDI_TEB_BATCH GdiTebBatch; /* 1d4/02f0 */ CLIENT_ID RealClientId; /* 6b4/07d8 */ HANDLE GdiCachedProcessHandle; /* 6bc/07e8 */ ULONG GdiClientPID; /* 6c0/07f0 */ ULONG GdiClientTID; /* 6c4/07f4 */ PVOID GdiThreadLocaleInfo; /* 6c8/07f8 */ - ULONG_PTR Win32ClientInfo[62]; /* 6cc/0800 used for user32 private data in Wine */ + ULONG_PTR Win32ClientInfo[62]; /* 6cc/0800 */ PVOID glDispatchTable[233]; /* 7c4/09f0 */ PVOID glReserved1[29]; /* b68/1138 */ PVOID glReserved2; /* bdc/1220 */ @@ -511,8 +511,6 @@ static_assert(offsetof(TEB, DeallocationStack) == typedef u64(__stdcall* NtClose_t)(HANDLE Handle); -typedef u64(__stdcall* NtDelayExecution_t)(BOOL Alertable, PLARGE_INTEGER DelayInterval); - typedef u64(__stdcall* NtSetInformationFile_t)(HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass); @@ -525,7 +523,6 @@ typedef u64(__stdcall* NtCreateThread_t)(PHANDLE ThreadHandle, ACCESS_MASK Desir typedef u64(__stdcall* NtTerminateThread_t)(HANDLE ThreadHandle, u64 ExitStatus); extern NtClose_t NtClose; -extern NtDelayExecution_t NtDelayExecution; extern NtSetInformationFile_t NtSetInformationFile; extern NtCreateThread_t NtCreateThread; extern NtTerminateThread_t NtTerminateThread; diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 46df68c38..c87aea6ef 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -147,6 +147,10 @@ void SetCurrentThreadName(const char* name) { SetThreadDescription(GetCurrentThread(), UTF8ToUTF16W(name).data()); } +void SetThreadName(void* thread, const char* name) { + SetThreadDescription(thread, UTF8ToUTF16W(name).data()); +} + #else // !MSVC_VER, so must be POSIX threads // MinGW with the POSIX threading model does not support pthread_setname_np @@ -170,11 +174,19 @@ void SetCurrentThreadName(const char* name) { pthread_setname_np(pthread_self(), name); #endif } + +void SetThreadName(void* thread, const char* name) { + // TODO +} #endif #if defined(_WIN32) void SetCurrentThreadName(const char*) { - // Do Nothing on MingW + // Do Nothing on MinGW +} + +void SetThreadName(void* thread, const char* name) { + // Do Nothing on MinGW } #endif diff --git a/src/common/thread.h b/src/common/thread.h index fd962f8e5..175ba9445 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -23,6 +23,8 @@ void SetCurrentThreadPriority(ThreadPriority new_priority); void SetCurrentThreadName(const char* name); +void SetThreadName(void* thread, const char* name); + class AccurateTimer { std::chrono::nanoseconds target_interval{}; std::chrono::nanoseconds total_wait{}; diff --git a/src/core/devices/logger.cpp b/src/core/devices/logger.cpp index bf5a28382..6f104509c 100644 --- a/src/core/devices/logger.cpp +++ b/src/core/devices/logger.cpp @@ -15,6 +15,7 @@ s64 Logger::write(const void* buf, size_t nbytes) { log(static_cast(buf), nbytes); return nbytes; } + size_t Logger::writev(const Libraries::Kernel::SceKernelIovec* iov, int iovcnt) { for (int i = 0; i < iovcnt; i++) { log(static_cast(iov[i].iov_base), iov[i].iov_len); diff --git a/src/core/libraries/kernel/sync/mutex.cpp b/src/core/libraries/kernel/sync/mutex.cpp new file mode 100644 index 000000000..c5e3eba1d --- /dev/null +++ b/src/core/libraries/kernel/sync/mutex.cpp @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "mutex.h" + +#include "common/assert.h" + +namespace Libraries::Kernel { + +TimedMutex::TimedMutex() { +#ifdef _WIN64 + mtx = CreateMutex(nullptr, false, nullptr); + ASSERT(mtx); +#endif +} + +TimedMutex::~TimedMutex() { +#ifdef _WIN64 + CloseHandle(mtx); +#endif +} + +void TimedMutex::lock() { +#ifdef _WIN64 + for (;;) { + u64 res = WaitForSingleObjectEx(mtx, INFINITE, true); + if (res == WAIT_OBJECT_0) { + return; + } + } +#else + mtx.lock(); +#endif +} + +bool TimedMutex::try_lock() { +#ifdef _WIN64 + return WaitForSingleObjectEx(mtx, 0, true) == WAIT_OBJECT_0; +#else + return mtx.try_lock(); +#endif +} + +void TimedMutex::unlock() { +#ifdef _WIN64 + ReleaseMutex(mtx); +#else + mtx.unlock(); +#endif +} + +} // namespace Libraries::Kernel \ No newline at end of file diff --git a/src/core/libraries/kernel/sync/mutex.h b/src/core/libraries/kernel/sync/mutex.h new file mode 100644 index 000000000..f14a920b4 --- /dev/null +++ b/src/core/libraries/kernel/sync/mutex.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "common/types.h" + +#ifdef _WIN64 +#include +#else +#include +#endif + +namespace Libraries::Kernel { + +class TimedMutex { +public: + TimedMutex(); + ~TimedMutex(); + + void lock(); + bool try_lock(); + + void unlock(); + + template + bool try_lock_for(const std::chrono::duration& rel_time) { +#ifdef _WIN64 + constexpr auto zero = std::chrono::duration::zero(); + const auto now = std::chrono::steady_clock::now(); + + std::chrono::steady_clock::time_point abs_time = now; + if (rel_time > zero) { + constexpr auto max = (std::chrono::steady_clock::time_point::max)(); + if (abs_time < max - rel_time) { + abs_time += rel_time; + } else { + abs_time = max; + } + } + + return try_lock_until(abs_time); +#else + return mtx.try_lock_for(rel_time); +#endif + } + + template + bool try_lock_until(const std::chrono::time_point& abs_time) { +#ifdef _WIN64 + for (;;) { + const auto now = Clock::now(); + if (abs_time <= now) { + return false; + } + + const auto rel_ms = std::chrono::ceil(abs_time - now); + u64 res = WaitForSingleObjectEx(mtx, static_cast(rel_ms.count()), true); + if (res == WAIT_OBJECT_0) { + return true; + } else if (res == WAIT_TIMEOUT) { + return false; + } + } +#else + return mtx.try_lock_until(abs_time); +#endif + } + +private: +#ifdef _WIN64 + HANDLE mtx; +#else + std::timed_mutex mtx; +#endif +}; + +} // namespace Libraries::Kernel \ No newline at end of file diff --git a/src/core/libraries/kernel/sync/semaphore.h b/src/core/libraries/kernel/sync/semaphore.h new file mode 100644 index 000000000..a103472c8 --- /dev/null +++ b/src/core/libraries/kernel/sync/semaphore.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "common/assert.h" +#include "common/types.h" + +#ifdef _WIN64 +#include +#else +#include +#endif + +namespace Libraries::Kernel { + +template +class Semaphore { +public: + Semaphore(s32 initialCount) +#ifndef _WIN64 + : sem{initialCount} +#endif + { +#ifdef _WIN64 + sem = CreateSemaphore(nullptr, initialCount, max, nullptr); + ASSERT(sem); +#endif + } + + ~Semaphore() { +#ifdef _WIN64 + CloseHandle(sem); +#endif + } + + void release() { +#ifdef _WIN64 + ReleaseSemaphore(sem, 1, nullptr); +#else + sem.release(); +#endif + } + + void acquire() { +#ifdef _WIN64 + for (;;) { + u64 res = WaitForSingleObjectEx(sem, INFINITE, true); + if (res == WAIT_OBJECT_0) { + return; + } + } +#else + sem.acquire(); +#endif + } + + bool try_acquire() { +#ifdef _WIN64 + return WaitForSingleObjectEx(sem, 0, true) == WAIT_OBJECT_0; +#else + return sem.try_acquire(); +#endif + } + + template + bool try_acquire_for(const std::chrono::duration& rel_time) { +#ifdef _WIN64 + const auto rel_time_ms = std::chrono::ceil(rel_time); + const u64 timeout_ms = static_cast(rel_time_ms.count()); + + if (timeout_ms == 0) { + return false; + } + + return WaitForSingleObjectEx(sem, timeout_ms, true) == WAIT_OBJECT_0; +#else + return sem.try_acquire_for(rel_time); +#endif + } + + template + bool try_acquire_until(const std::chrono::time_point& abs_time) { +#ifdef _WIN64 + const auto now = Clock::now(); + if (now >= abs_time) { + return false; + } + + const auto rel_time = std::chrono::ceil(abs_time - now); + const u64 timeout_ms = static_cast(rel_time.count()); + if (timeout_ms == 0) { + return false; + } + + u64 res = WaitForSingleObjectEx(sem, static_cast(timeout_ms), true); + return res == WAIT_OBJECT_0; +#else + return sem.try_acquire_until(abs_time); +#endif + } + +private: +#ifdef _WIN64 + HANDLE sem; +#else + std::counting_semaphore sem; +#endif +}; + +using BinarySemaphore = Semaphore<1>; +using CountingSemaphore = Semaphore<0x7FFFFFFF /*ORBIS_KERNEL_SEM_VALUE_MAX*/>; + +} // namespace Libraries::Kernel \ No newline at end of file diff --git a/src/core/libraries/kernel/threads/condvar.cpp b/src/core/libraries/kernel/threads/condvar.cpp index cbe8f6ca7..2927899d9 100644 --- a/src/core/libraries/kernel/threads/condvar.cpp +++ b/src/core/libraries/kernel/threads/condvar.cpp @@ -191,7 +191,7 @@ int PthreadCond::Signal() { PthreadMutex* mp = td->mutex_obj; has_user_waiters = SleepqRemove(sq, td); - std::binary_semaphore* waddr = nullptr; + BinarySemaphore* waddr = nullptr; if (mp->m_owner == curthread) { if (curthread->nwaiter_defer >= Pthread::MaxDeferWaiters) { curthread->WakeAll(); @@ -211,7 +211,7 @@ int PthreadCond::Signal() { struct BroadcastArg { Pthread* curthread; - std::binary_semaphore* waddrs[Pthread::MaxDeferWaiters]; + BinarySemaphore* waddrs[Pthread::MaxDeferWaiters]; int count; }; diff --git a/src/core/libraries/kernel/threads/event_flag.cpp b/src/core/libraries/kernel/threads/event_flag.cpp index 39925153c..ce75bed9e 100644 --- a/src/core/libraries/kernel/threads/event_flag.cpp +++ b/src/core/libraries/kernel/threads/event_flag.cpp @@ -118,7 +118,6 @@ public: } m_bits |= bits; - m_cond_var.notify_all(); } diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index a562c51b2..b2fe09934 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -380,6 +380,7 @@ int PS4_SYSV_ABI posix_sched_get_priority_min() { int PS4_SYSV_ABI posix_pthread_rename_np(PthreadT thread, const char* name) { LOG_INFO(Kernel_Pthread, "name = {}", name); + Common::SetThreadName(reinterpret_cast(thread->native_thr.GetHandle()), name); thread->name = name; return ORBIS_OK; } diff --git a/src/core/libraries/kernel/threads/pthread.h b/src/core/libraries/kernel/threads/pthread.h index 9d71c75e8..456c2ef37 100644 --- a/src/core/libraries/kernel/threads/pthread.h +++ b/src/core/libraries/kernel/threads/pthread.h @@ -11,6 +11,8 @@ #include #include "common/enum.h" +#include "core/libraries/kernel/sync/mutex.h" +#include "core/libraries/kernel/sync/semaphore.h" #include "core/libraries/kernel/time.h" #include "core/thread.h" #include "core/tls.h" @@ -44,7 +46,7 @@ enum class PthreadMutexProt : u32 { }; struct PthreadMutex { - std::timed_mutex m_lock; + TimedMutex m_lock; PthreadMutexFlags m_flags; Pthread* m_owner; int m_count; @@ -288,14 +290,14 @@ struct Pthread { int report_events; int event_mask; std::string name; - std::binary_semaphore wake_sema{0}; + BinarySemaphore wake_sema{0}; SleepQueue* sleepqueue; void* wchan; PthreadMutex* mutex_obj; bool will_sleep; bool has_user_waiters; int nwaiter_defer; - std::binary_semaphore* defer_waiters[MaxDeferWaiters]; + BinarySemaphore* defer_waiters[MaxDeferWaiters]; bool InCritical() const noexcept { return locklevel > 0 || critical_count > 0; diff --git a/src/core/libraries/kernel/threads/semaphore.cpp b/src/core/libraries/kernel/threads/semaphore.cpp index e3c7e9092..5aa04f251 100644 --- a/src/core/libraries/kernel/threads/semaphore.cpp +++ b/src/core/libraries/kernel/threads/semaphore.cpp @@ -6,6 +6,8 @@ #include #include +#include "core/libraries/kernel/sync/semaphore.h" + #include "common/logging/log.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/orbis_error.h" @@ -21,7 +23,7 @@ constexpr int ORBIS_KERNEL_SEM_VALUE_MAX = 0x7FFFFFFF; struct PthreadSem { explicit PthreadSem(s32 value_) : semaphore{value_}, value{value_} {} - std::counting_semaphore semaphore; + CountingSemaphore semaphore; std::atomic value; }; @@ -75,7 +77,7 @@ public: it = wait_list.erase(it); token_count -= waiter->need_count; waiter->was_signaled = true; - waiter->cv.notify_one(); + waiter->sem.release(); } return true; @@ -88,7 +90,7 @@ public: } for (auto* waiter : wait_list) { waiter->was_cancled = true; - waiter->cv.notify_one(); + waiter->sem.release(); } wait_list.clear(); token_count = set_count < 0 ? init_count : set_count; @@ -99,21 +101,21 @@ public: std::scoped_lock lk{mutex}; for (auto* waiter : wait_list) { waiter->was_deleted = true; - waiter->cv.notify_one(); + waiter->sem.release(); } wait_list.clear(); } public: struct WaitingThread { - std::condition_variable cv; + BinarySemaphore sem; u32 priority; s32 need_count; bool was_signaled{}; bool was_deleted{}; bool was_cancled{}; - explicit WaitingThread(s32 need_count, bool is_fifo) : need_count{need_count} { + explicit WaitingThread(s32 need_count, bool is_fifo) : sem{0}, need_count{need_count} { // Retrieve calling thread priority for sorting into waiting threads list. if (!is_fifo) { priority = g_curthread->attr.prio; @@ -134,24 +136,26 @@ public: } int Wait(std::unique_lock& lk, u32* timeout) { + lk.unlock(); if (!timeout) { // Wait indefinitely until we are woken up. - cv.wait(lk); + sem.acquire(); + lk.lock(); return GetResult(false); } // Wait until timeout runs out, recording how much remaining time there was. const auto start = std::chrono::high_resolution_clock::now(); - const auto signaled = cv.wait_for(lk, std::chrono::microseconds(*timeout), - [this] { return was_signaled; }); + sem.try_acquire_for(std::chrono::microseconds(*timeout)); const auto end = std::chrono::high_resolution_clock::now(); const auto time = std::chrono::duration_cast(end - start).count(); - if (signaled) { + lk.lock(); + if (was_signaled) { *timeout -= time; } else { *timeout = 0; } - return GetResult(!signaled); + return GetResult(!was_signaled); } }; diff --git a/src/core/libraries/kernel/time.cpp b/src/core/libraries/kernel/time.cpp index b586431ab..2565b8078 100644 --- a/src/core/libraries/kernel/time.cpp +++ b/src/core/libraries/kernel/time.cpp @@ -52,7 +52,22 @@ u64 PS4_SYSV_ABI sceKernelReadTsc() { int PS4_SYSV_ABI sceKernelUsleep(u32 microseconds) { #ifdef _WIN64 - std::this_thread::sleep_for(std::chrono::microseconds(microseconds)); + const auto start_time = std::chrono::high_resolution_clock::now(); + auto total_wait_time = std::chrono::microseconds(microseconds); + + while (total_wait_time.count() > 0) { + auto wait_time = std::chrono::ceil(total_wait_time).count(); + u64 res = SleepEx(static_cast(wait_time), true); + if (res == WAIT_IO_COMPLETION) { + auto elapsedTime = std::chrono::high_resolution_clock::now() - start_time; + auto elapsedMicroseconds = + std::chrono::duration_cast(elapsedTime).count(); + total_wait_time = std::chrono::microseconds(microseconds - elapsedMicroseconds); + } else { + break; + } + } + return 0; #else timespec start; From e1ecfb8dd1062d3081821aa25ba619f17c887497 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:46:44 -0800 Subject: [PATCH 26/89] semaphore: Add GCD semaphore implementation. (#1677) --- src/core/libraries/kernel/sync/semaphore.h | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/sync/semaphore.h b/src/core/libraries/kernel/sync/semaphore.h index a103472c8..884b08968 100644 --- a/src/core/libraries/kernel/sync/semaphore.h +++ b/src/core/libraries/kernel/sync/semaphore.h @@ -11,6 +11,8 @@ #ifdef _WIN64 #include +#elif defined(__APPLE__) +#include #else #include #endif @@ -21,25 +23,32 @@ template class Semaphore { public: Semaphore(s32 initialCount) -#ifndef _WIN64 +#if !defined(_WIN64) && !defined(__APPLE__) : sem{initialCount} #endif { #ifdef _WIN64 sem = CreateSemaphore(nullptr, initialCount, max, nullptr); ASSERT(sem); +#elif defined(__APPLE__) + sem = dispatch_semaphore_create(initialCount); + ASSERT(sem); #endif } ~Semaphore() { #ifdef _WIN64 CloseHandle(sem); +#elif defined(__APPLE__) + dispatch_release(sem); #endif } void release() { #ifdef _WIN64 ReleaseSemaphore(sem, 1, nullptr); +#elif defined(__APPLE__) + dispatch_semaphore_signal(sem); #else sem.release(); #endif @@ -53,6 +62,13 @@ public: return; } } +#elif defined(__APPLE__) + for (;;) { + const auto res = dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + if (res == 0) { + return; + } + } #else sem.acquire(); #endif @@ -61,6 +77,8 @@ public: bool try_acquire() { #ifdef _WIN64 return WaitForSingleObjectEx(sem, 0, true) == WAIT_OBJECT_0; +#elif defined(__APPLE__) + return dispatch_semaphore_wait(sem, DISPATCH_TIME_NOW) == 0; #else return sem.try_acquire(); #endif @@ -77,6 +95,10 @@ public: } return WaitForSingleObjectEx(sem, timeout_ms, true) == WAIT_OBJECT_0; +#elif defined(__APPLE__) + const auto rel_time_ns = std::chrono::ceil(rel_time).count(); + const auto timeout = dispatch_time(DISPATCH_TIME_NOW, rel_time_ns); + return dispatch_semaphore_wait(sem, timeout) == 0; #else return sem.try_acquire_for(rel_time); #endif @@ -98,6 +120,16 @@ public: u64 res = WaitForSingleObjectEx(sem, static_cast(timeout_ms), true); return res == WAIT_OBJECT_0; +#elif defined(__APPLE__) + auto abs_s = std::chrono::time_point_cast(abs_time); + auto abs_ns = std::chrono::time_point_cast(abs_time) - + std::chrono::time_point_cast(abs_s); + const timespec abs_timespec = { + .tv_sec = abs_s.time_since_epoch().count(), + .tv_nsec = abs_ns.count(), + }; + const auto timeout = dispatch_walltime(&abs_timespec, 0); + return dispatch_semaphore_wait(sem, timeout) == 0; #else return sem.try_acquire_until(abs_time); #endif @@ -106,6 +138,8 @@ public: private: #ifdef _WIN64 HANDLE sem; +#elif defined(__APPLE__) + dispatch_semaphore_t sem; #else std::counting_semaphore sem; #endif From 8eacb88a865fd38d87ce1f2510ecd5e6b108ae27 Mon Sep 17 00:00:00 2001 From: Vladislav Mikhalin Date: Sat, 7 Dec 2024 02:20:09 +0300 Subject: [PATCH 27/89] recompiler: fixed fragment shader built-in attribute access (#1676) * recompiler: fixed fragment shader built-in attribute access * handle en/addr separately * handle other registers as well --- .../frontend/translate/translate.cpp | 75 +++++++++++++++++-- src/shader_recompiler/runtime_info.h | 4 + src/video_core/amdgpu/liverpool.h | 29 ++++++- .../renderer_vulkan/vk_pipeline_cache.cpp | 2 + 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 68625a12b..b3a47fde8 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -53,15 +53,74 @@ void Translator::EmitPrologue() { } break; case Stage::Fragment: - // https://github.com/chaotic-cx/mesa-mirror/blob/72326e15/src/amd/vulkan/radv_shader_args.c#L258 - // The first two VGPRs are used for i/j barycentric coordinates. In the vast majority of - // cases it will be only those two, but if shader is using both e.g linear and perspective - // inputs it can be more For now assume that this isn't the case. - dst_vreg = IR::VectorReg::V2; - for (u32 i = 0; i < 4; i++) { - ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, i)); + dst_vreg = IR::VectorReg::V0; + if (runtime_info.fs_info.addr_flags.persp_sample_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.persp_center_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.persp_centroid_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.persp_pull_model_ena) { + ++dst_vreg; // I/W + ++dst_vreg; // J/W + ++dst_vreg; // 1/W + } + if (runtime_info.fs_info.addr_flags.linear_sample_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.linear_center_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.linear_centroid_ena) { + ++dst_vreg; // I + ++dst_vreg; // J + } + if (runtime_info.fs_info.addr_flags.line_stipple_tex_ena) { + ++dst_vreg; + } + if (runtime_info.fs_info.addr_flags.pos_x_float_ena) { + if (runtime_info.fs_info.en_flags.pos_x_float_ena) { + ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, 0)); + } else { + ir.SetVectorReg(dst_vreg++, ir.Imm32(0.0f)); + } + } + if (runtime_info.fs_info.addr_flags.pos_y_float_ena) { + if (runtime_info.fs_info.en_flags.pos_y_float_ena) { + ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, 1)); + } else { + ir.SetVectorReg(dst_vreg++, ir.Imm32(0.0f)); + } + } + if (runtime_info.fs_info.addr_flags.pos_z_float_ena) { + if (runtime_info.fs_info.en_flags.pos_z_float_ena) { + ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, 2)); + } else { + ir.SetVectorReg(dst_vreg++, ir.Imm32(0.0f)); + } + } + if (runtime_info.fs_info.addr_flags.pos_w_float_ena) { + if (runtime_info.fs_info.en_flags.pos_w_float_ena) { + ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, 3)); + } else { + ir.SetVectorReg(dst_vreg++, ir.Imm32(0.0f)); + } + } + if (runtime_info.fs_info.addr_flags.front_face_ena) { + if (runtime_info.fs_info.en_flags.front_face_ena) { + ir.SetVectorReg(dst_vreg++, ir.GetAttributeU32(IR::Attribute::IsFrontFace)); + } else { + ir.SetVectorReg(dst_vreg++, ir.Imm32(0)); + } } - ir.SetVectorReg(dst_vreg++, ir.GetAttributeU32(IR::Attribute::IsFrontFace)); break; case Stage::Compute: ir.SetVectorReg(dst_vreg++, ir.GetAttributeU32(IR::Attribute::LocalInvocationId, 0)); diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index 4662def93..4c779a368 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -7,6 +7,7 @@ #include #include #include "common/types.h" +#include "video_core/amdgpu/liverpool.h" #include "video_core/amdgpu/types.h" namespace Shader { @@ -105,6 +106,8 @@ struct FragmentRuntimeInfo { auto operator<=>(const PsInput&) const noexcept = default; }; + AmdGpu::Liverpool::PsInput en_flags; + AmdGpu::Liverpool::PsInput addr_flags; u32 num_inputs; std::array inputs; struct PsColorBuffer { @@ -117,6 +120,7 @@ struct FragmentRuntimeInfo { bool operator==(const FragmentRuntimeInfo& other) const noexcept { return std::ranges::equal(color_buffers, other.color_buffers) && + en_flags.raw == other.en_flags.raw && addr_flags.raw == other.addr_flags.raw && num_inputs == other.num_inputs && std::ranges::equal(inputs.begin(), inputs.begin() + num_inputs, other.inputs.begin(), other.inputs.begin() + num_inputs); diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index 2b2f2c00a..ca3b01612 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -1071,6 +1071,28 @@ struct Liverpool { BitField<27, 1, u32> enable_postz_overrasterization; }; + union PsInput { + u32 raw; + struct { + u32 persp_sample_ena : 1; + u32 persp_center_ena : 1; + u32 persp_centroid_ena : 1; + u32 persp_pull_model_ena : 1; + u32 linear_sample_ena : 1; + u32 linear_center_ena : 1; + u32 linear_centroid_ena : 1; + u32 line_stipple_tex_ena : 1; + u32 pos_x_float_ena : 1; + u32 pos_y_float_ena : 1; + u32 pos_z_float_ena : 1; + u32 pos_w_float_ena : 1; + u32 front_face_ena : 1; + u32 ancillary_ena : 1; + u32 sample_coverage_ena : 1; + u32 pos_fixed_pt_ena : 1; + }; + }; + union Regs { struct { INSERT_PADDING_WORDS(0x2C08); @@ -1126,7 +1148,10 @@ struct Liverpool { INSERT_PADDING_WORDS(0xA191 - 0xA187); std::array ps_inputs; VsOutputConfig vs_output_config; - INSERT_PADDING_WORDS(4); + INSERT_PADDING_WORDS(1); + PsInput ps_input_ena; + PsInput ps_input_addr; + INSERT_PADDING_WORDS(1); BitField<0, 6, u32> num_interp; INSERT_PADDING_WORDS(0xA1C3 - 0xA1B6 - 1); ShaderPosFormat shader_pos_format; @@ -1388,6 +1413,8 @@ static_assert(GFX6_3D_REG_INDEX(viewports) == 0xA10F); static_assert(GFX6_3D_REG_INDEX(clip_user_data) == 0xA16F); static_assert(GFX6_3D_REG_INDEX(ps_inputs) == 0xA191); static_assert(GFX6_3D_REG_INDEX(vs_output_config) == 0xA1B1); +static_assert(GFX6_3D_REG_INDEX(ps_input_ena) == 0xA1B3); +static_assert(GFX6_3D_REG_INDEX(ps_input_addr) == 0xA1B4); static_assert(GFX6_3D_REG_INDEX(num_interp) == 0xA1B6); static_assert(GFX6_3D_REG_INDEX(shader_pos_format) == 0xA1C3); static_assert(GFX6_3D_REG_INDEX(z_export_format) == 0xA1C4); diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 82a029b95..53bdc79a6 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -123,6 +123,8 @@ Shader::RuntimeInfo PipelineCache::BuildRuntimeInfo(Shader::Stage stage) { } case Shader::Stage::Fragment: { BuildCommon(regs.ps_program); + info.fs_info.en_flags = regs.ps_input_ena; + info.fs_info.addr_flags = regs.ps_input_addr; const auto& ps_inputs = regs.ps_inputs; info.fs_info.num_inputs = regs.num_interp; for (u32 i = 0; i < regs.num_interp; i++) { From 6f543b5bd9ce19ec1143a7beffcd00e4be378821 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:48:12 +0100 Subject: [PATCH 28/89] hotfix: enable discord RPC --- CMakeLists.txt | 4 ++++ src/emulator.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 84146bb01..81fae39c2 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -878,6 +878,10 @@ target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAlloca target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") +if (ENABLE_DISCORD_RPC) + target_compile_definitions(shadps4 PRIVATE ENABLE_DISCORD_RPC) +endif() + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_compile_definitions(shadps4 PRIVATE ENABLE_USERFAULTFD) endif() diff --git a/src/emulator.cpp b/src/emulator.cpp index 8a7c04cf4..60d6e18d7 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -266,7 +266,7 @@ void Emulator::Run(const std::filesystem::path& file) { } void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string game_serial) { - constexpr std::array ModulesToLoad{ + constexpr std::array ModulesToLoad{ {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterlibSceNgs2}, {"libSceFiber.sprx", &Libraries::Fiber::RegisterlibSceFiber}, {"libSceUlt.sprx", nullptr}, From 9524f9474993885b53e963219e02e7184d33d5bb Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:07:14 +0100 Subject: [PATCH 29/89] hotfix: add missing include --- src/qt_gui/settings_dialog.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index abbd39edd..1fd4b6e8b 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -6,6 +6,9 @@ #include #include +#ifdef ENABLE_DISCORD_RPC +#include "common/discord_rpc_handler.h" +#endif #ifdef ENABLE_UPDATER #include "check_update.h" #endif From 941a668f78845290b4f5b28f2246c6cf00de765e Mon Sep 17 00:00:00 2001 From: psucien Date: Sat, 7 Dec 2024 10:30:36 +0100 Subject: [PATCH 30/89] hot-fix: obtain cmdbuf for dispatches after cache ops This fixes cmdbuf being in incorrect state after scheduler rotation on flush --- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 4e858c0d3..0471fdb0a 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -312,7 +312,6 @@ void Rasterizer::DrawIndirect(bool is_indexed, VAddr arg_address, u32 offset, u3 void Rasterizer::DispatchDirect() { RENDERER_TRACE; - const auto cmdbuf = scheduler.CommandBuffer(); const auto& cs_program = liverpool->regs.cs_program; const ComputePipeline* pipeline = pipeline_cache.GetComputePipeline(); if (!pipeline) { @@ -324,6 +323,8 @@ void Rasterizer::DispatchDirect() { } scheduler.EndRendering(); + + const auto cmdbuf = scheduler.CommandBuffer(); cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline->Handle()); cmdbuf.dispatch(cs_program.dim_x, cs_program.dim_y, cs_program.dim_z); @@ -333,7 +334,6 @@ void Rasterizer::DispatchDirect() { void Rasterizer::DispatchIndirect(VAddr address, u32 offset, u32 size) { RENDERER_TRACE; - const auto cmdbuf = scheduler.CommandBuffer(); const auto& cs_program = liverpool->regs.cs_program; const ComputePipeline* pipeline = pipeline_cache.GetComputePipeline(); if (!pipeline) { @@ -345,8 +345,11 @@ void Rasterizer::DispatchIndirect(VAddr address, u32 offset, u32 size) { } scheduler.EndRendering(); - cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline->Handle()); + const auto [buffer, base] = buffer_cache.ObtainBuffer(address + offset, size, false); + + const auto cmdbuf = scheduler.CommandBuffer(); + cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, pipeline->Handle()); cmdbuf.dispatchIndirect(buffer->Handle(), base); ResetBindings(); From 2266622dcf77422a996460d86e13cb2926ab634b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Sat, 7 Dec 2024 18:41:41 +0100 Subject: [PATCH 31/89] Support for Vulkan 1.4 (#1665) --- .gitmodules | 2 ++ CMakeLists.txt | 2 +- externals/CMakeLists.txt | 4 ++-- externals/LibAtrac9 | 2 +- externals/date | 2 +- externals/glslang | 2 +- externals/magic_enum | 2 +- externals/pugixml | 2 +- externals/sdl3 | 2 +- externals/toml11 | 2 +- externals/vma | 2 +- externals/vulkan-headers | 2 +- externals/xbyak | 2 +- externals/xxhash | 2 +- externals/zydis | 2 +- src/core/devtools/widget/common.h | 2 +- src/core/devtools/widget/frame_dump.cpp | 2 +- src/core/devtools/widget/memory_map.cpp | 2 +- src/core/devtools/widget/reg_popup.cpp | 2 +- src/core/devtools/widget/reg_view.cpp | 2 +- src/core/libraries/ajm/ajm.cpp | 2 +- src/core/libraries/ajm/ajm_instance.cpp | 2 +- src/core/libraries/audio/audioout.cpp | 2 +- src/core/libraries/avplayer/avplayer_file_streamer.cpp | 2 +- src/core/libraries/avplayer/avplayer_source.cpp | 2 +- src/core/libraries/avplayer/avplayer_state.cpp | 2 +- src/core/libraries/ime/error_dialog.cpp | 2 +- src/core/libraries/ime/ime_dialog.cpp | 3 ++- src/core/libraries/ime/ime_dialog_ui.cpp | 2 +- src/core/libraries/jpeg/jpegenc.cpp | 3 ++- src/core/libraries/save_data/dialog/savedatadialog.cpp | 3 ++- src/core/libraries/save_data/dialog/savedatadialog_ui.cpp | 2 +- src/core/libraries/save_data/save_backup.cpp | 2 +- src/core/libraries/save_data/save_instance.cpp | 2 +- src/core/libraries/save_data/savedata.cpp | 2 +- src/core/libraries/system/msgdialog.cpp | 2 +- src/core/libraries/system/sysmodule.cpp | 2 +- src/core/platform.h | 3 ++- .../backend/spirv/emit_spirv_context_get_set.cpp | 2 +- src/shader_recompiler/frontend/decode.cpp | 2 +- src/shader_recompiler/frontend/translate/translate.cpp | 2 +- src/video_core/renderer_vulkan/liverpool_to_vk.cpp | 2 +- src/video_core/renderer_vulkan/vk_platform.cpp | 2 +- src/video_core/texture_cache/tile_manager.cpp | 2 +- 44 files changed, 50 insertions(+), 44 deletions(-) diff --git a/.gitmodules b/.gitmodules index fb859c87d..8010250a9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -102,6 +102,8 @@ [submodule "externals/LibAtrac9"] path = externals/LibAtrac9 url = https://github.com/shadps4-emu/ext-LibAtrac9.git + shallow = true [submodule "externals/libpng"] path = externals/libpng url = https://github.com/pnggroup/libpng + shallow = true \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 81fae39c2..bc811fada 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,7 +120,7 @@ find_package(SDL3 3.1.2 CONFIG) find_package(stb MODULE) find_package(toml11 4.2.0 CONFIG) find_package(tsl-robin-map 1.3.0 CONFIG) -find_package(VulkanHeaders 1.3.289 CONFIG) +find_package(VulkanHeaders 1.4.303 CONFIG) find_package(VulkanMemoryAllocator 3.1.0 CONFIG) find_package(xbyak 7.07 CONFIG) find_package(xxHash 0.8.2 MODULE) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 8ccae8070..082be211a 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -35,7 +35,7 @@ else() if (NOT TARGET cryptopp::cryptopp) set(CRYPTOPP_INSTALL OFF) set(CRYPTOPP_BUILD_TESTING OFF) - set(CRYPTOPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/cryptopp/) + set(CRYPTOPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/cryptopp) add_subdirectory(cryptopp-cmake) file(COPY cryptopp DESTINATION cryptopp FILES_MATCHING PATTERN "*.h") # remove externals/cryptopp from include directories because it contains a conflicting zlib.h file @@ -216,7 +216,7 @@ endif() # Discord RPC if (ENABLE_DISCORD_RPC) set(BUILD_EXAMPLES OFF) - add_subdirectory(discord-rpc/) + add_subdirectory(discord-rpc) target_include_directories(discord-rpc INTERFACE discord-rpc/include) endif() diff --git a/externals/LibAtrac9 b/externals/LibAtrac9 index 3acdcdc78..9640129dc 160000 --- a/externals/LibAtrac9 +++ b/externals/LibAtrac9 @@ -1 +1 @@ -Subproject commit 3acdcdc78f129c2e6145331ff650fa76dd88d62c +Subproject commit 9640129dc6f2afbca6ceeca3019856e8653a5fb2 diff --git a/externals/date b/externals/date index dd8affc6d..28b7b2325 160000 --- a/externals/date +++ b/externals/date @@ -1 +1 @@ -Subproject commit dd8affc6de5755e07638bf0a14382d29549d6ee9 +Subproject commit 28b7b232521ace2c8ef3f2ad4126daec3569c14f diff --git a/externals/glslang b/externals/glslang index e61d7bb30..a0995c49e 160000 --- a/externals/glslang +++ b/externals/glslang @@ -1 +1 @@ -Subproject commit e61d7bb3006f451968714e2f653412081871e1ee +Subproject commit a0995c49ebcaca2c6d3b03efbabf74f3843decdb diff --git a/externals/magic_enum b/externals/magic_enum index 126539e13..1a1824df7 160000 --- a/externals/magic_enum +++ b/externals/magic_enum @@ -1 +1 @@ -Subproject commit 126539e13cccdc2e75ce770e94f3c26403099fa5 +Subproject commit 1a1824df7ac798177a521eed952720681b0bf482 diff --git a/externals/pugixml b/externals/pugixml index 3b1718437..4bc14418d 160000 --- a/externals/pugixml +++ b/externals/pugixml @@ -1 +1 @@ -Subproject commit 3b17184379fcaaeb7f1fbe08018b7fedf2640b3b +Subproject commit 4bc14418d12d289dd9978fdce9490a45deeb653e diff --git a/externals/sdl3 b/externals/sdl3 index 54e622c2e..3a1d76d29 160000 --- a/externals/sdl3 +++ b/externals/sdl3 @@ -1 +1 @@ -Subproject commit 54e622c2e6af456bfef382fae44c17682d5ac88a +Subproject commit 3a1d76d298db023f6cf37fb08ee766f20a4e12ab diff --git a/externals/toml11 b/externals/toml11 index f925e7f28..7f6c574ff 160000 --- a/externals/toml11 +++ b/externals/toml11 @@ -1 +1 @@ -Subproject commit f925e7f287c0008813c2294798cf9ca167fd9ffd +Subproject commit 7f6c574ff5aa1053534e7e19c0a4f22bf4c6aaca diff --git a/externals/vma b/externals/vma index 1c35ba99c..5a53a1989 160000 --- a/externals/vma +++ b/externals/vma @@ -1 +1 @@ -Subproject commit 1c35ba99ce775f8342d87a83a3f0f696f99c2a39 +Subproject commit 5a53a198945ba8260fbc58fadb788745ce6aa263 diff --git a/externals/vulkan-headers b/externals/vulkan-headers index d91597a82..6a74a7d65 160000 --- a/externals/vulkan-headers +++ b/externals/vulkan-headers @@ -1 +1 @@ -Subproject commit d91597a82f881d473887b560a03a7edf2720b72c +Subproject commit 6a74a7d65cafa19e38ec116651436cce6efd5b2e diff --git a/externals/xbyak b/externals/xbyak index d067f0d3f..4e44f4614 160000 --- a/externals/xbyak +++ b/externals/xbyak @@ -1 +1 @@ -Subproject commit d067f0d3f55696ae8bc9a25ad7012ee80f221d54 +Subproject commit 4e44f4614ddbf038f2a6296f5b906d5c72691e0f diff --git a/externals/xxhash b/externals/xxhash index d4ad85e4a..2bf8313b9 160000 --- a/externals/xxhash +++ b/externals/xxhash @@ -1 +1 @@ -Subproject commit d4ad85e4afaad5c780f54db1dc967fff5a869ffd +Subproject commit 2bf8313b934633b2a5b7e8fd239645b85e10c852 diff --git a/externals/zydis b/externals/zydis index 9d298eb80..bffbb610c 160000 --- a/externals/zydis +++ b/externals/zydis @@ -1 +1 @@ -Subproject commit 9d298eb8067ff62a237203d1e1470785033e185c +Subproject commit bffbb610cfea643b98e87658b9058382f7522807 diff --git a/src/core/devtools/widget/common.h b/src/core/devtools/widget/common.h index 4429f5581..5f669eb65 100644 --- a/src/core/devtools/widget/common.h +++ b/src/core/devtools/widget/common.h @@ -8,7 +8,7 @@ #include #include -#include +#include #include "common/bit_field.h" #include "common/io_file.h" diff --git a/src/core/devtools/widget/frame_dump.cpp b/src/core/devtools/widget/frame_dump.cpp index 86ba7b86e..055ce1333 100644 --- a/src/core/devtools/widget/frame_dump.cpp +++ b/src/core/devtools/widget/frame_dump.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include "common/io_file.h" #include "core/devtools/options.h" diff --git a/src/core/devtools/widget/memory_map.cpp b/src/core/devtools/widget/memory_map.cpp index dc8f5c2e9..7edd676e9 100644 --- a/src/core/devtools/widget/memory_map.cpp +++ b/src/core/devtools/widget/memory_map.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include "core/debug_state.h" #include "core/memory.h" diff --git a/src/core/devtools/widget/reg_popup.cpp b/src/core/devtools/widget/reg_popup.cpp index 0633e76e6..2727e1745 100644 --- a/src/core/devtools/widget/reg_popup.cpp +++ b/src/core/devtools/widget/reg_popup.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "cmd_list.h" #include "common.h" diff --git a/src/core/devtools/widget/reg_view.cpp b/src/core/devtools/widget/reg_view.cpp index a60090a8c..79b02a849 100644 --- a/src/core/devtools/widget/reg_view.cpp +++ b/src/core/devtools/widget/reg_view.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include "common.h" diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index 2396669b6..a3728039e 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -9,7 +9,7 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" -#include +#include namespace Libraries::Ajm { diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp index 4e04eea74..ea7fd5617 100644 --- a/src/core/libraries/ajm/ajm_instance.cpp +++ b/src/core/libraries/ajm/ajm_instance.cpp @@ -5,7 +5,7 @@ #include "core/libraries/ajm/ajm_instance.h" #include "core/libraries/ajm/ajm_mp3.h" -#include +#include namespace Libraries::Ajm { diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp index b92c75a8f..78b04cc90 100644 --- a/src/core/libraries/audio/audioout.cpp +++ b/src/core/libraries/audio/audioout.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include +#include #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/core/libraries/avplayer/avplayer_file_streamer.cpp b/src/core/libraries/avplayer/avplayer_file_streamer.cpp index 3323ee9b6..19faeb273 100644 --- a/src/core/libraries/avplayer/avplayer_file_streamer.cpp +++ b/src/core/libraries/avplayer/avplayer_file_streamer.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include // std::max, std::min -#include +#include #include "core/libraries/avplayer/avplayer_file_streamer.h" extern "C" { diff --git a/src/core/libraries/avplayer/avplayer_source.cpp b/src/core/libraries/avplayer/avplayer_source.cpp index 8e43e7277..950951673 100644 --- a/src/core/libraries/avplayer/avplayer_source.cpp +++ b/src/core/libraries/avplayer/avplayer_source.cpp @@ -8,7 +8,7 @@ #include "core/libraries/avplayer/avplayer_file_streamer.h" #include "core/libraries/avplayer/avplayer_source.h" -#include +#include extern "C" { #include diff --git a/src/core/libraries/avplayer/avplayer_state.cpp b/src/core/libraries/avplayer/avplayer_state.cpp index c3694eec0..143df749c 100644 --- a/src/core/libraries/avplayer/avplayer_state.cpp +++ b/src/core/libraries/avplayer/avplayer_state.cpp @@ -8,7 +8,7 @@ #include "core/libraries/avplayer/avplayer_state.h" #include "core/tls.h" -#include +#include namespace Libraries::AvPlayer { diff --git a/src/core/libraries/ime/error_dialog.cpp b/src/core/libraries/ime/error_dialog.cpp index 811f2cb99..07580fe1d 100644 --- a/src/core/libraries/ime/error_dialog.cpp +++ b/src/core/libraries/ime/error_dialog.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/core/libraries/ime/ime_dialog.cpp b/src/core/libraries/ime/ime_dialog.cpp index d6d027885..9151aa64e 100644 --- a/src/core/libraries/ime/ime_dialog.cpp +++ b/src/core/libraries/ime/ime_dialog.cpp @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include +#include + #include "common/logging/log.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" diff --git a/src/core/libraries/ime/ime_dialog_ui.cpp b/src/core/libraries/ime/ime_dialog_ui.cpp index 5957606eb..51183c79b 100644 --- a/src/core/libraries/ime/ime_dialog_ui.cpp +++ b/src/core/libraries/ime/ime_dialog_ui.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/core/libraries/jpeg/jpegenc.cpp b/src/core/libraries/jpeg/jpegenc.cpp index b664a2334..b9c88d094 100644 --- a/src/core/libraries/jpeg/jpegenc.cpp +++ b/src/core/libraries/jpeg/jpegenc.cpp @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include +#include + #include "common/alignment.h" #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/core/libraries/save_data/dialog/savedatadialog.cpp b/src/core/libraries/save_data/dialog/savedatadialog.cpp index 0ad7d7dc0..2f0619165 100644 --- a/src/core/libraries/save_data/dialog/savedatadialog.cpp +++ b/src/core/libraries/save_data/dialog/savedatadialog.cpp @@ -1,11 +1,12 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "common/elf_info.h" #include "common/logging/log.h" #include "core/libraries/libs.h" #include "core/libraries/system/commondialog.h" -#include "magic_enum.hpp" #include "savedatadialog.h" #include "savedatadialog_ui.h" diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp index 4e0d801a6..a6ca8744d 100644 --- a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include "common/elf_info.h" #include "common/singleton.h" diff --git a/src/core/libraries/save_data/save_backup.cpp b/src/core/libraries/save_data/save_backup.cpp index da5172b15..3f7969d69 100644 --- a/src/core/libraries/save_data/save_backup.cpp +++ b/src/core/libraries/save_data/save_backup.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "save_backup.h" #include "save_instance.h" diff --git a/src/core/libraries/save_data/save_instance.cpp b/src/core/libraries/save_data/save_instance.cpp index 0d6c5173c..99daf83cc 100644 --- a/src/core/libraries/save_data/save_instance.cpp +++ b/src/core/libraries/save_data/save_instance.cpp @@ -3,7 +3,7 @@ #include -#include +#include #include "common/assert.h" #include "common/config.h" diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index c515ebcbf..66899fb34 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "common/assert.h" #include "common/cstring.h" diff --git a/src/core/libraries/system/msgdialog.cpp b/src/core/libraries/system/msgdialog.cpp index 7d924e4ad..8a01f429f 100644 --- a/src/core/libraries/system/msgdialog.cpp +++ b/src/core/libraries/system/msgdialog.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include +#include #include "common/assert.h" #include "common/logging/log.h" diff --git a/src/core/libraries/system/sysmodule.cpp b/src/core/libraries/system/sysmodule.cpp index 9bed4ef31..350f1317b 100644 --- a/src/core/libraries/system/sysmodule.cpp +++ b/src/core/libraries/system/sysmodule.cpp @@ -3,7 +3,7 @@ #define MAGIC_ENUM_RANGE_MIN 0 #define MAGIC_ENUM_RANGE_MAX 300 -#include +#include #include "common/logging/log.h" #include "core/libraries/error_codes.h" diff --git a/src/core/platform.h b/src/core/platform.h index 03bd79e86..bdb50701b 100644 --- a/src/core/platform.h +++ b/src/core/platform.h @@ -7,7 +7,8 @@ #include "common/logging/log.h" #include "common/singleton.h" #include "common/types.h" -#include "magic_enum.hpp" + +#include #include #include diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index b578f0c52..d005169c4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -5,7 +5,7 @@ #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" -#include +#include namespace Shader::Backend::SPIRV { namespace { diff --git a/src/shader_recompiler/frontend/decode.cpp b/src/shader_recompiler/frontend/decode.cpp index 796bed127..a5187aebd 100644 --- a/src/shader_recompiler/frontend/decode.cpp +++ b/src/shader_recompiler/frontend/decode.cpp @@ -5,7 +5,7 @@ #include "common/assert.h" #include "shader_recompiler/frontend/decode.h" -#include "magic_enum.hpp" +#include namespace Shader::Gcn { diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index b3a47fde8..97978ff6b 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -14,7 +14,7 @@ #define MAGIC_ENUM_RANGE_MIN 0 #define MAGIC_ENUM_RANGE_MAX 1515 -#include "magic_enum.hpp" +#include namespace Shader::Gcn { diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index 2262a429a..f0f7d352c 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -6,7 +6,7 @@ #include "video_core/amdgpu/pixel_format.h" #include "video_core/renderer_vulkan/liverpool_to_vk.h" -#include +#include #define INVALID_NUMBER_FORMAT_COMBO \ LOG_ERROR(Render_Vulkan, "Unsupported number type {} for format {}", number_type, format); diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index b2a50cd44..2e717397b 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -22,7 +22,7 @@ #include "video_core/renderer_vulkan/vk_platform.h" #if VULKAN_HPP_ENABLE_DYNAMIC_LOADER_TOOL -static vk::DynamicLoader dl; +static vk::detail::DynamicLoader dl; #else extern "C" { VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance, diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index 9823cb4dc..2bc3bf282 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -14,7 +14,7 @@ #include "video_core/host_shaders/detile_m8x2_comp.h" #include -#include +#include #include namespace VideoCore { From 57e762468f7cff0c1cf130b1ac2197d2f6153fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:27:57 +0100 Subject: [PATCH 32/89] Fix + documentation update (#1689) --- CMakeLists.txt | 2 +- documents/building-windows.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bc811fada..16b49cf08 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,7 +113,7 @@ find_package(FFmpeg 5.1.2 MODULE) find_package(fmt 10.2.0 CONFIG) find_package(glslang 15 CONFIG) find_package(half 1.12.0 MODULE) -find_package(magic_enum 0.9.6 CONFIG) +find_package(magic_enum 0.9.7 CONFIG) find_package(PNG 1.6 MODULE) find_package(RenderDoc 1.6.0 MODULE) find_package(SDL3 3.1.2 CONFIG) diff --git a/documents/building-windows.md b/documents/building-windows.md index 0da630f0b..d01e7b81e 100644 --- a/documents/building-windows.md +++ b/documents/building-windows.md @@ -25,8 +25,8 @@ Once you are within the installer: Beware, this requires you to create a Qt account. If you do not want to do this, please follow the MSYS2/MinGW compilation method instead. -1. Under the current, non beta version of Qt (at the time of writing 6.7.2), select the option `MSVC 2019 64-bit` or similar. - If you are on Windows on ARM / Qualcomm Snapdragon Elite X, select `MSVC 2019 ARM64` instead. +1. Under the current, non beta version of Qt (at the time of writing 6.7.3), select the option `MSVC 2022 64-bit` or similar. + If you are on Windows on ARM / Qualcomm Snapdragon Elite X, select `MSVC 2022 ARM64` instead. Go through the installation normally. If you know what you are doing, you may unselect individual components that eat up too much disk space. @@ -35,7 +35,7 @@ Beware, this requires you to create a Qt account. If you do not want to do this, Once you are finished, you will have to configure Qt within Visual Studio: 1. Tools -> Options -> Qt -> Versions -2. Add a new Qt version and navigate it to the correct folder. Should look like so: `C:\Qt\6.7.2\msvc2019_64` +2. Add a new Qt version and navigate it to the correct folder. Should look like so: `C:\Qt\6.7.3\msvc2022_64` 3. Enable the default checkmark on the new version you just created. ### (Prerequisite) Download [**Git for Windows**](https://git-scm.com/download/win) @@ -55,16 +55,16 @@ Go through the Git for Windows installation as normal 3. If you want to build shadPS4 with the Qt Gui: 1. Click x64-Clang-Release and select "Manage Configurations" 2. Look for "CMake command arguments" and add to the text field - `-DENABLE_QT_GUI=ON -DCMAKE_PREFIX_PATH=C:\Qt\6.7.2\msvc2019_64` + `-DENABLE_QT_GUI=ON -DCMAKE_PREFIX_PATH=C:\Qt\6.7.3\msvc2022_64` (Change Qt path if you've installed it to non-default path) 3. Press CTRL+S to save and wait a moment for CMake generation 4. Change the project to build to shadps4.exe 5. Build -> Build All -Your shadps4.exe will be in `c:\path\to\source\Build\x64-Clang-Release\` +Your shadps4.exe will be in `C:\path\to\source\Build\x64-Clang-Release\` To automatically populate the necessary files to run shadPS4.exe, run in a command prompt or terminal: -`C:\Qt\6.7.2\msvc2019_64\bin\windeployqt.exe "c:\path\to\shadps4.exe"` +`C:\Qt\6.7.3\msvc2022_64\bin\windeployqt.exe "C:\path\to\shadps4.exe"` (Change Qt path if you've installed it to non-default path) ## Option 2: MSYS2/MinGW From c076ba69e8c66401e3655940bfc554cbcd21c2f2 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:28:17 -0800 Subject: [PATCH 33/89] shader_recompiler: Implement V_LSHL_B64 for immediate arguments. (#1674) --- .../frontend/translate/vector_alu.cpp | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index eb90c256e..8149230db 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -1155,15 +1155,23 @@ void Translator::V_LSHL_B64(const GcnInst& inst) { const IR::U64 src0{GetSrc64(inst.src[0])}; const IR::U64 src1{GetSrc64(inst.src[1])}; const IR::VectorReg dst_reg{inst.dst[0].code}; - if (src0.IsImmediate() && src0.U64() == -1) { - ir.SetVectorReg(dst_reg, ir.Imm32(0xFFFFFFFF)); - ir.SetVectorReg(dst_reg + 1, ir.Imm32(0xFFFFFFFF)); - return; + if (src0.IsImmediate()) { + if (src0.U64() == -1) { + // If src0 is a fixed -1, the result will always be -1. + ir.SetVectorReg(dst_reg, ir.Imm32(0xFFFFFFFF)); + ir.SetVectorReg(dst_reg + 1, ir.Imm32(0xFFFFFFFF)); + return; + } + if (src1.IsImmediate()) { + // If both src0 and src1 are immediates, we can calculate the result now. + // Note that according to the manual, only bits 4:0 are used from src1. + const u64 result = src0.U64() << (src1.U64() & 0x1F); + ir.SetVectorReg(dst_reg, ir.Imm32(static_cast(result))); + ir.SetVectorReg(dst_reg + 1, ir.Imm32(static_cast(result >> 32))); + return; + } } - ASSERT_MSG(src0.IsImmediate() && src0.U64() == 0 && src1.IsImmediate() && src1.U64() == 0, - "V_LSHL_B64 with non-zero src0 or src1 is not supported"); - ir.SetVectorReg(dst_reg, ir.Imm32(0)); - ir.SetVectorReg(dst_reg + 1, ir.Imm32(0)); + UNREACHABLE_MSG("Unimplemented V_LSHL_B64 arguments"); } void Translator::V_MUL_F64(const GcnInst& inst) { From 119e03cb585bf797cf3155216d2bb5262a66aef9 Mon Sep 17 00:00:00 2001 From: psucien Date: Sat, 7 Dec 2024 22:28:11 +0100 Subject: [PATCH 34/89] hot-fix: fix for incorrect asc qid --- src/core/libraries/gnmdriver/gnmdriver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 10d121afe..4e2db9083 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -544,7 +544,7 @@ void PS4_SYSV_ABI sceGnmDingDong(u32 gnm_vqid, u32 next_offs_dw) { .base_addr = base_addr, }); } - liverpool->SubmitAsc(vqid, acb_span); + liverpool->SubmitAsc(gnm_vqid, acb_span); *asc_queue.read_addr += acb_size; *asc_queue.read_addr %= asc_queue.ring_size_dw * 4; From cde84e4bac94cfaa4c63f38f2b0494b78c6955eb Mon Sep 17 00:00:00 2001 From: IndecisiveTurtle Date: Sat, 7 Dec 2024 23:43:52 +0200 Subject: [PATCH 35/89] shader_recompiler: Fix mistake --- src/shader_recompiler/frontend/translate/scalar_alu.cpp | 6 +++--- src/shader_recompiler/frontend/translate/translate.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/scalar_alu.cpp b/src/shader_recompiler/frontend/translate/scalar_alu.cpp index 75ad957b3..c320e00a7 100644 --- a/src/shader_recompiler/frontend/translate/scalar_alu.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_alu.cpp @@ -96,8 +96,8 @@ void Translator::EmitScalarAlu(const GcnInst& inst) { return S_BREV_B32(inst); case Opcode::S_BCNT1_I32_B64: return S_BCNT1_I32_B64(inst); - case Opcode::S_FF1_I32_B64: - return S_FF1_I32_B64(inst); + case Opcode::S_FF1_I32_B32: + return S_FF1_I32_B32(inst); case Opcode::S_AND_SAVEEXEC_B64: return S_SAVEEXEC_B64(NegateMode::None, false, inst); case Opcode::S_ORN2_SAVEEXEC_B64: @@ -575,7 +575,7 @@ void Translator::S_BCNT1_I32_B64(const GcnInst& inst) { ir.SetScc(ir.INotEqual(result, ir.Imm32(0))); } -void Translator::S_FF1_I32_B64(const GcnInst& inst) { +void Translator::S_FF1_I32_B32(const GcnInst& inst) { const IR::U32 src0{GetSrc(inst.src[0])}; const IR::U32 result{ir.Select(ir.IEqual(src0, ir.Imm32(0U)), ir.Imm32(-1), ir.FindILsb(src0))}; SetDst(inst.dst[0], result); diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index dd379d8ea..00cdd8d55 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -110,7 +110,7 @@ public: void S_NOT_B64(const GcnInst& inst); void S_BREV_B32(const GcnInst& inst); void S_BCNT1_I32_B64(const GcnInst& inst); - void S_FF1_I32_B64(const GcnInst& inst); + void S_FF1_I32_B32(const GcnInst& inst); void S_GETPC_B64(u32 pc, const GcnInst& inst); void S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& inst); From 8ee672fe32702b232728ae193abf57d9468b5de1 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 8 Dec 2024 00:10:20 +0200 Subject: [PATCH 36/89] hot-fix: Allow unpriviledged userfaultfd --- src/video_core/page_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index 80b91b825..fefae81f4 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -32,7 +32,7 @@ constexpr size_t PAGEBITS = 12; #ifdef ENABLE_USERFAULTFD struct PageManager::Impl { Impl(Vulkan::Rasterizer* rasterizer_) : rasterizer{rasterizer_} { - uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY); ASSERT_MSG(uffd != -1, "{}", Common::GetLastErrorMsg()); // Request uffdio features from kernel. From dad5953e8c3cca86152d85a92b6415a3c8f8e80f Mon Sep 17 00:00:00 2001 From: Ada Ahmed Date: Sat, 7 Dec 2024 22:52:03 +0000 Subject: [PATCH 37/89] fix: fix #1457 again by moving av_err2str to a common header (#1688) --- src/common/support/avdec.h | 17 +++++++++++++++++ src/core/libraries/ajm/ajm_mp3.cpp | 2 ++ src/core/libraries/avplayer/avplayer_source.cpp | 11 +---------- src/core/libraries/videodec/videodec2_impl.cpp | 11 +---------- src/core/libraries/videodec/videodec_impl.cpp | 11 +---------- 5 files changed, 22 insertions(+), 30 deletions(-) create mode 100644 src/common/support/avdec.h diff --git a/src/common/support/avdec.h b/src/common/support/avdec.h new file mode 100644 index 000000000..fa3483dc4 --- /dev/null +++ b/src/common/support/avdec.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// support header file for libav + +// The av_err2str macro in libavutil/error.h does not play nice with C++ +#ifdef av_err2str +#undef av_err2str +#include +av_always_inline std::string av_err2string(int errnum) { + char errbuf[AV_ERROR_MAX_STRING_SIZE]; + return av_make_error_string(errbuf, AV_ERROR_MAX_STRING_SIZE, errnum); +} +#define av_err2str(err) av_err2string(err).c_str() +#endif // av_err2str diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp index 3b464238d..2c572a01b 100644 --- a/src/core/libraries/ajm/ajm_mp3.cpp +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -12,6 +12,8 @@ extern "C" { #include } +#include "common/support/avdec.h" + namespace Libraries::Ajm { // Following tables have been reversed from AJM library diff --git a/src/core/libraries/avplayer/avplayer_source.cpp b/src/core/libraries/avplayer/avplayer_source.cpp index 950951673..cf783403c 100644 --- a/src/core/libraries/avplayer/avplayer_source.cpp +++ b/src/core/libraries/avplayer/avplayer_source.cpp @@ -18,16 +18,7 @@ extern "C" { #include } -// The av_err2str macro in libavutil/error.h does not play nice with C++ -#ifdef av_err2str -#undef av_err2str -#include -av_always_inline std::string av_err2string(int errnum) { - char errbuf[AV_ERROR_MAX_STRING_SIZE]; - return av_make_error_string(errbuf, AV_ERROR_MAX_STRING_SIZE, errnum); -} -#define av_err2str(err) av_err2string(err).c_str() -#endif // av_err2str +#include "common/support/avdec.h" namespace Libraries::AvPlayer { diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index 8daa48828..138d78af3 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -7,16 +7,7 @@ #include "common/logging/log.h" #include "core/libraries/videodec/videodec_error.h" -// The av_err2str macro in libavutil/error.h does not play nice with C++ -#ifdef av_err2str -#undef av_err2str -#include -av_always_inline std::string av_err2string(int errnum) { - char errbuf[AV_ERROR_MAX_STRING_SIZE]; - return av_make_error_string(errbuf, AV_ERROR_MAX_STRING_SIZE, errnum); -} -#define av_err2str(err) av_err2string(err).c_str() -#endif // av_err2str +#include "common/support/avdec.h" namespace Libraries::Vdec2 { diff --git a/src/core/libraries/videodec/videodec_impl.cpp b/src/core/libraries/videodec/videodec_impl.cpp index cf4846971..b5f72e9ce 100644 --- a/src/core/libraries/videodec/videodec_impl.cpp +++ b/src/core/libraries/videodec/videodec_impl.cpp @@ -8,16 +8,7 @@ #include "common/logging/log.h" #include "core/libraries/videodec/videodec_error.h" -// The av_err2str macro in libavutil/error.h does not play nice with C++ -#ifdef av_err2str -#undef av_err2str -#include -av_always_inline std::string av_err2string(int errnum) { - char errbuf[AV_ERROR_MAX_STRING_SIZE]; - return av_make_error_string(errbuf, AV_ERROR_MAX_STRING_SIZE, errnum); -} -#define av_err2str(err) av_err2string(err).c_str() -#endif // av_err2str +#include "common/support/avdec.h" namespace Libraries::Videodec { From 1940ac0fec81b88ea5bbb586e1e0edd35f6e98af Mon Sep 17 00:00:00 2001 From: auser1337 <154299690+auser1337@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:18:12 -0800 Subject: [PATCH 38/89] ajm: support for multiple contexts (#1690) * ajm: support for multiple contexts * fix sceAjmInitialize --- src/core/libraries/ajm/ajm.cpp | 56 ++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index a3728039e..3184fa64f 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -19,7 +19,7 @@ constexpr int ORBIS_AJM_CHANNELMASK_QUAD = 0x0033; constexpr int ORBIS_AJM_CHANNELMASK_5POINT1 = 0x060F; constexpr int ORBIS_AJM_CHANNELMASK_7POINT1 = 0x063F; -static std::unique_ptr context{}; +static std::unordered_map> contexts{}; u32 GetChannelMask(u32 num_channels) { switch (num_channels) { @@ -40,7 +40,13 @@ u32 GetChannelMask(u32 num_channels) { int PS4_SYSV_ABI sceAjmBatchCancel(const u32 context_id, const u32 batch_id) { LOG_INFO(Lib_Ajm, "called context_id = {} batch_id = {}", context_id, batch_id); - return context->BatchCancel(batch_id); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->BatchCancel(batch_id); } int PS4_SYSV_ABI sceAjmBatchErrorDump() { @@ -90,14 +96,26 @@ int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context_id, u8* p_batch, u32 batch_s u32* out_batch_id) { LOG_TRACE(Lib_Ajm, "called context = {}, batch_size = {:#x}, priority = {}", context_id, batch_size, priority); - return context->BatchStartBuffer(p_batch, batch_size, priority, batch_error, out_batch_id); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->BatchStartBuffer(p_batch, batch_size, priority, batch_error, out_batch_id); } int PS4_SYSV_ABI sceAjmBatchWait(const u32 context_id, const u32 batch_id, const u32 timeout, AjmBatchError* const batch_error) { LOG_TRACE(Lib_Ajm, "called context = {}, batch_id = {}, timeout = {}", context_id, batch_id, timeout); - return context->BatchWait(batch_id, timeout, batch_error); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->BatchWait(batch_id, timeout, batch_error); } int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { @@ -117,12 +135,12 @@ int PS4_SYSV_ABI sceAjmFinalize() { int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) { LOG_INFO(Lib_Ajm, "called reserved = {}", reserved); - ASSERT_MSG(context == nullptr, "Multiple contexts are currently unsupported."); if (p_context_id == nullptr || reserved != 0) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } - *p_context_id = 1; - context = std::make_unique(); + u32 id = contexts.size() + 1; + *p_context_id = id; + contexts.emplace(id, std::make_unique()); return ORBIS_OK; } @@ -135,12 +153,24 @@ int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type, AjmInstanceFlags flags, u32* out_instance) { LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}, flags = {:#x}", context_id, magic_enum::enum_name(codec_type), flags.raw); - return context->InstanceCreate(codec_type, flags, out_instance); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->InstanceCreate(codec_type, flags, out_instance); } int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context_id, u32 instance_id) { LOG_INFO(Lib_Ajm, "called context = {}, instance = {}", context_id, instance_id); - return context->InstanceDestroy(instance_id); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->InstanceDestroy(instance_id); } int PS4_SYSV_ABI sceAjmInstanceExtend() { @@ -168,7 +198,13 @@ int PS4_SYSV_ABI sceAjmModuleRegister(u32 context_id, AjmCodecType codec_type, s if (reserved != 0) { return ORBIS_AJM_ERROR_INVALID_PARAMETER; } - return context->ModuleRegister(codec_type); + + auto it = contexts.find(context_id); + if (it == contexts.end()) { + return ORBIS_AJM_ERROR_INVALID_CONTEXT; + } + + return it->second->ModuleRegister(codec_type); } int PS4_SYSV_ABI sceAjmModuleUnregister() { From 7d546f32d8d175476a9dfe80b53f0b4a66978dde Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:19:39 -0800 Subject: [PATCH 39/89] image_view: Add more BGRA storage format swizzles. (#1693) --- .../renderer_vulkan/vk_instance.cpp | 5 +++-- src/video_core/texture_cache/image_view.cpp | 21 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 49e4987db..81784eb60 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -70,8 +70,9 @@ std::unordered_map GetFormatProperties( static constexpr std::array misc_formats = { vk::Format::eA2R10G10B10UnormPack32, vk::Format::eA8B8G8R8UnormPack32, vk::Format::eA8B8G8R8SrgbPack32, vk::Format::eB8G8R8A8Unorm, - vk::Format::eB8G8R8A8Srgb, vk::Format::eR5G6B5UnormPack16, - vk::Format::eD24UnormS8Uint, + vk::Format::eB8G8R8A8Snorm, vk::Format::eB8G8R8A8Uint, + vk::Format::eB8G8R8A8Sint, vk::Format::eB8G8R8A8Srgb, + vk::Format::eR5G6B5UnormPack16, vk::Format::eD24UnormS8Uint, }; for (const auto& format : misc_formats) { if (!format_properties.contains(format)) { diff --git a/src/video_core/texture_cache/image_view.cpp b/src/video_core/texture_cache/image_view.cpp index 61cabdf11..57a58c714 100644 --- a/src/video_core/texture_cache/image_view.cpp +++ b/src/video_core/texture_cache/image_view.cpp @@ -58,11 +58,22 @@ bool IsIdentityMapping(u32 dst_sel, u32 num_components) { } vk::Format TrySwizzleFormat(vk::Format format, u32 dst_sel) { - if (format == vk::Format::eR8G8B8A8Unorm && dst_sel == 0b111100101110) { - return vk::Format::eB8G8R8A8Unorm; - } - if (format == vk::Format::eR8G8B8A8Srgb && dst_sel == 0b111100101110) { - return vk::Format::eB8G8R8A8Srgb; + // BGRA + if (dst_sel == 0b111100101110) { + switch (format) { + case vk::Format::eR8G8B8A8Unorm: + return vk::Format::eB8G8R8A8Unorm; + case vk::Format::eR8G8B8A8Snorm: + return vk::Format::eB8G8R8A8Snorm; + case vk::Format::eR8G8B8A8Uint: + return vk::Format::eB8G8R8A8Uint; + case vk::Format::eR8G8B8A8Sint: + return vk::Format::eB8G8R8A8Sint; + case vk::Format::eR8G8B8A8Srgb: + return vk::Format::eB8G8R8A8Srgb; + default: + break; + } } return format; } From 71a82199ed57245d51776e620ec21b6b4a3ea75d Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:20:05 -0800 Subject: [PATCH 40/89] shader_recompiler: Fix check for fragment depth store. (#1694) --- src/shader_recompiler/backend/spirv/emit_spirv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index 1e7032f10..23800fc49 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -284,7 +284,7 @@ void DefineEntryPoint(const IR::Program& program, EmitContext& ctx, Id main) { ctx.AddExtension("SPV_EXT_demote_to_helper_invocation"); ctx.AddCapability(spv::Capability::DemoteToHelperInvocationEXT); } - if (info.stores.Get(IR::Attribute::Depth)) { + if (info.stores.GetAny(IR::Attribute::Depth)) { ctx.AddExecutionMode(main, spv::ExecutionMode::DepthReplacing); } break; From 4fb2247196d4626bab8f2c28710b0c34cad053fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Sun, 8 Dec 2024 09:20:24 +0100 Subject: [PATCH 41/89] Better title bar for Cheats/Patches menu (#1696) --- src/qt_gui/cheats_patches.cpp | 2 +- src/qt_gui/translations/ar.ts | 4 ++-- src/qt_gui/translations/da_DK.ts | 4 ++-- src/qt_gui/translations/de.ts | 4 ++-- src/qt_gui/translations/el.ts | 4 ++-- src/qt_gui/translations/en.ts | 4 ++-- src/qt_gui/translations/es_ES.ts | 4 ++-- src/qt_gui/translations/fa_IR.ts | 4 ++-- src/qt_gui/translations/fi.ts | 4 ++-- src/qt_gui/translations/fr.ts | 34 ++++++++++++++++---------------- src/qt_gui/translations/hu_HU.ts | 4 ++-- src/qt_gui/translations/id.ts | 4 ++-- src/qt_gui/translations/it.ts | 4 ++-- src/qt_gui/translations/ja_JP.ts | 4 ++-- src/qt_gui/translations/ko_KR.ts | 4 ++-- src/qt_gui/translations/lt_LT.ts | 4 ++-- src/qt_gui/translations/nb_NO.ts | 4 ++-- src/qt_gui/translations/nl.ts | 4 ++-- src/qt_gui/translations/pl_PL.ts | 4 ++-- src/qt_gui/translations/pt_BR.ts | 4 ++-- src/qt_gui/translations/ro_RO.ts | 4 ++-- src/qt_gui/translations/ru_RU.ts | 4 ++-- src/qt_gui/translations/sq.ts | 4 ++-- src/qt_gui/translations/tr_TR.ts | 4 ++-- src/qt_gui/translations/uk_UA.ts | 4 ++-- src/qt_gui/translations/vi_VN.ts | 4 ++-- src/qt_gui/translations/zh_CN.ts | 4 ++-- src/qt_gui/translations/zh_TW.ts | 4 ++-- 28 files changed, 70 insertions(+), 70 deletions(-) diff --git a/src/qt_gui/cheats_patches.cpp b/src/qt_gui/cheats_patches.cpp index a35136f12..3e7c22451 100644 --- a/src/qt_gui/cheats_patches.cpp +++ b/src/qt_gui/cheats_patches.cpp @@ -39,7 +39,7 @@ CheatsPatches::CheatsPatches(const QString& gameName, const QString& gameSerial, m_gameSize(gameSize), m_gameImage(gameImage), manager(new QNetworkAccessManager(this)) { setupUI(); resize(500, 400); - setWindowTitle(tr("Cheats / Patches")); + setWindowTitle(tr("Cheats / Patches for ") + m_gameName); } CheatsPatches::~CheatsPatches() {} diff --git a/src/qt_gui/translations/ar.ts b/src/qt_gui/translations/ar.ts index 25e215183..45a030062 100644 --- a/src/qt_gui/translations/ar.ts +++ b/src/qt_gui/translations/ar.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - الغش / التصحيحات + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/da_DK.ts b/src/qt_gui/translations/da_DK.ts index 14c42f1d9..fa7e9c50b 100644 --- a/src/qt_gui/translations/da_DK.ts +++ b/src/qt_gui/translations/da_DK.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Snyd / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/de.ts b/src/qt_gui/translations/de.ts index 64a6c6480..0fa534bb9 100644 --- a/src/qt_gui/translations/de.ts +++ b/src/qt_gui/translations/de.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/el.ts b/src/qt_gui/translations/el.ts index e064f8c26..5233454b9 100644 --- a/src/qt_gui/translations/el.ts +++ b/src/qt_gui/translations/el.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/en.ts b/src/qt_gui/translations/en.ts index 9bf7c7188..cd5a5fe8a 100644 --- a/src/qt_gui/translations/en.ts +++ b/src/qt_gui/translations/en.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/es_ES.ts b/src/qt_gui/translations/es_ES.ts index 5d637249e..0598e04f3 100644 --- a/src/qt_gui/translations/es_ES.ts +++ b/src/qt_gui/translations/es_ES.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Trucos / Parches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/fa_IR.ts b/src/qt_gui/translations/fa_IR.ts index 55a2fdf53..3cd72cac3 100644 --- a/src/qt_gui/translations/fa_IR.ts +++ b/src/qt_gui/translations/fa_IR.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - چیت / پچ ها + Cheats / Patches for + Cheats / Patches for ا diff --git a/src/qt_gui/translations/fi.ts b/src/qt_gui/translations/fi.ts index 4d160bf6b..8c9518d0f 100644 --- a/src/qt_gui/translations/fi.ts +++ b/src/qt_gui/translations/fi.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Huijaukset / Korjaukset + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/fr.ts b/src/qt_gui/translations/fr.ts index 39cd11bf6..a9903d43c 100644 --- a/src/qt_gui/translations/fr.ts +++ b/src/qt_gui/translations/fr.ts @@ -62,7 +62,7 @@ Select which directory you want to install to. - Select which directory you want to install to. + Sélectionnez le répertoire où vous souhaitez effectuer l'installation. @@ -158,22 +158,22 @@ Delete... - Delete... + Supprimer... Delete Game - Delete Game + Supprimer jeu Delete Update - Delete Update + Supprimer MÀJ Delete DLC - Delete DLC + Supprimer DLC @@ -203,7 +203,7 @@ Game - Game + Jeu @@ -213,17 +213,17 @@ This game has no update to delete! - This game has no update to delete! + Ce jeu n'a pas de mise à jour à supprimer! Update - Update + Mise à jour This game has no DLC to delete! - This game has no DLC to delete! + Ce jeu n'a pas de DLC à supprimer! @@ -233,12 +233,12 @@ Delete %1 - Delete %1 + Supprime %1 Are you sure you want to delete %1's %2 directory? - Are you sure you want to delete %1's %2 directory? + Êtes vous sûr de vouloir supprimer le répertoire %1 %2 ? @@ -495,7 +495,7 @@ Enable Separate Update Folder - Enable Separate Update Folder + Dossier séparé pour les mises à jours @@ -510,7 +510,7 @@ Enable Discord Rich Presence - Activer Discord Rich Presence + Activer la présence Discord @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats/Patches + Cheats / Patches for + Cheats/Patchs pour @@ -1159,7 +1159,7 @@ separateUpdatesCheckBox - Enable Separate Update Folder:\nEnables installing game updates into a separate folder for easy management. + Dossier séparé pour les mises à jours:\nInstalle les mises à jours des jeux dans un dossier séparé pour une gestion plus facile. @@ -1169,7 +1169,7 @@ ps4proCheckBox - Est-ce un PS4 Pro:\nFait en sorte que l'émulateur se comporte comme un PS4 PRO, ce qui peut activer des fonctionnalités spéciales dans les jeux qui le prennent en charge. + Mode PS4 Pro:\nFait en sorte que l'émulateur se comporte comme un PS4 PRO, ce qui peut activer des fonctionnalités spéciales dans les jeux qui le prennent en charge. diff --git a/src/qt_gui/translations/hu_HU.ts b/src/qt_gui/translations/hu_HU.ts index a43b8d371..135fc4231 100644 --- a/src/qt_gui/translations/hu_HU.ts +++ b/src/qt_gui/translations/hu_HU.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Csalások / Javítások + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/id.ts b/src/qt_gui/translations/id.ts index d616f1cf3..5c6148b86 100644 --- a/src/qt_gui/translations/id.ts +++ b/src/qt_gui/translations/id.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheat / Patch + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/it.ts b/src/qt_gui/translations/it.ts index c59289314..b496cc330 100644 --- a/src/qt_gui/translations/it.ts +++ b/src/qt_gui/translations/it.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Trucchi / Patch + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/ja_JP.ts b/src/qt_gui/translations/ja_JP.ts index f4a4b15ad..3cee79951 100644 --- a/src/qt_gui/translations/ja_JP.ts +++ b/src/qt_gui/translations/ja_JP.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - チート / パッチ + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/ko_KR.ts b/src/qt_gui/translations/ko_KR.ts index 2fa3ee153..ea449ebc1 100644 --- a/src/qt_gui/translations/ko_KR.ts +++ b/src/qt_gui/translations/ko_KR.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/lt_LT.ts b/src/qt_gui/translations/lt_LT.ts index 16aaf5d86..dd467283f 100644 --- a/src/qt_gui/translations/lt_LT.ts +++ b/src/qt_gui/translations/lt_LT.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Sukčiavimai / Pataisos + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/nb_NO.ts b/src/qt_gui/translations/nb_NO.ts index e02f24182..de0a88b73 100644 --- a/src/qt_gui/translations/nb_NO.ts +++ b/src/qt_gui/translations/nb_NO.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Juks / Programrettelse + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/nl.ts b/src/qt_gui/translations/nl.ts index b0cfaff5e..399aef8be 100644 --- a/src/qt_gui/translations/nl.ts +++ b/src/qt_gui/translations/nl.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/pl_PL.ts b/src/qt_gui/translations/pl_PL.ts index 4d11c13f6..730b37124 100644 --- a/src/qt_gui/translations/pl_PL.ts +++ b/src/qt_gui/translations/pl_PL.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Kody / poprawki + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/pt_BR.ts b/src/qt_gui/translations/pt_BR.ts index f1d3631d8..7ea63b9fb 100644 --- a/src/qt_gui/translations/pt_BR.ts +++ b/src/qt_gui/translations/pt_BR.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/ro_RO.ts b/src/qt_gui/translations/ro_RO.ts index fff0bcddb..d73b1be6c 100644 --- a/src/qt_gui/translations/ro_RO.ts +++ b/src/qt_gui/translations/ro_RO.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheats / Patches + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/ru_RU.ts b/src/qt_gui/translations/ru_RU.ts index 052623235..ab7e2c40e 100644 --- a/src/qt_gui/translations/ru_RU.ts +++ b/src/qt_gui/translations/ru_RU.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Читы и патчи + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/sq.ts b/src/qt_gui/translations/sq.ts index f7144a001..bc8f24162 100644 --- a/src/qt_gui/translations/sq.ts +++ b/src/qt_gui/translations/sq.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Mashtrime / Arna + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index 335465778..5944f980f 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Hileler / Yamalar + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/uk_UA.ts b/src/qt_gui/translations/uk_UA.ts index 31bfe9dba..b28035335 100644 --- a/src/qt_gui/translations/uk_UA.ts +++ b/src/qt_gui/translations/uk_UA.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Чити та Патчі + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/vi_VN.ts b/src/qt_gui/translations/vi_VN.ts index 223cb9ed0..b5ae8bd39 100644 --- a/src/qt_gui/translations/vi_VN.ts +++ b/src/qt_gui/translations/vi_VN.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - Cheat / Bản vá + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/zh_CN.ts b/src/qt_gui/translations/zh_CN.ts index 4fe1f7c42..989e71071 100644 --- a/src/qt_gui/translations/zh_CN.ts +++ b/src/qt_gui/translations/zh_CN.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - 作弊码 / 补丁 + Cheats / Patches for + Cheats / Patches for diff --git a/src/qt_gui/translations/zh_TW.ts b/src/qt_gui/translations/zh_TW.ts index 4db00775d..b650a74ea 100644 --- a/src/qt_gui/translations/zh_TW.ts +++ b/src/qt_gui/translations/zh_TW.ts @@ -840,8 +840,8 @@ CheatsPatches - Cheats / Patches - 作弊碼 / 修補檔 + Cheats / Patches for + Cheats / Patches for From 0b672a08acd6845f9f5c36c67511454be04b7fd4 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sun, 8 Dec 2024 15:57:51 +0100 Subject: [PATCH 42/89] video_core: improve image cube heuristic --- src/video_core/texture_cache/image.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 3d5202ad6..ea298c04b 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -151,9 +151,10 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, // the texture cache should re-create the resource with the usage requested vk::ImageCreateFlags flags{vk::ImageCreateFlagBits::eMutableFormat | vk::ImageCreateFlagBits::eExtendedUsage}; - const bool can_be_cube = (info.type == vk::ImageType::e2D) && - (info.resources.layers % 6 == 0) && - (info.size.width == info.size.height); + const bool can_be_cube = + (info.type == vk::ImageType::e2D) && + (info.props.is_pow2 ? (info.resources.layers % 8) : (info.resources.layers % 6) == 0) && + (info.size.width == info.size.height); if (info.props.is_cube || can_be_cube) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } else if (info.props.is_volume) { From a88850fec6d051fec52adbd864f1ff1a6d58f227 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:02:38 +0100 Subject: [PATCH 43/89] video_core/amdgpu: fix calculation of lod range --- src/video_core/amdgpu/resource.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index a78a68391..ba87425f2 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -420,11 +420,11 @@ struct Sampler { } float MinLod() const noexcept { - return static_cast(min_lod); + return static_cast(min_lod.Value()) / 256.0f; } float MaxLod() const noexcept { - return static_cast(max_lod); + return static_cast(max_lod.Value()) / 256.0f; } }; From 1793fd4df02d4f56ebe572be26a0d50b4d66118d Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:05:36 +0100 Subject: [PATCH 44/89] format --- src/video_core/texture_cache/image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index ea298c04b..e7e1ce1da 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -153,7 +153,7 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, vk::ImageCreateFlagBits::eExtendedUsage}; const bool can_be_cube = (info.type == vk::ImageType::e2D) && - (info.props.is_pow2 ? (info.resources.layers % 8) : (info.resources.layers % 6) == 0) && + ((info.props.is_pow2 ? (info.resources.layers % 8) : (info.resources.layers % 6)) == 0) && (info.size.width == info.size.height); if (info.props.is_cube || can_be_cube) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; From fea2593ab4be8638426e4f608ebbe2314d83d9f6 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:30:33 +0100 Subject: [PATCH 45/89] The way to Unity, pt.3 (#1681) --- src/common/ntapi.cpp | 2 + src/common/ntapi.h | 20 ++++ src/core/libraries/ajm/ajm_context.cpp | 2 + src/core/libraries/kernel/kernel.cpp | 3 +- src/core/libraries/kernel/memory.cpp | 3 +- src/core/libraries/kernel/process.cpp | 15 ++- src/core/libraries/kernel/sync/semaphore.h | 42 ++++--- .../libraries/kernel/threads/event_flag.cpp | 30 ++++- .../libraries/kernel/threads/exception.cpp | 50 ++++++-- src/core/libraries/kernel/threads/pthread.cpp | 2 + .../libraries/kernel/threads/semaphore.cpp | 108 ++++++++++++++++-- src/core/libraries/save_data/save_backup.cpp | 2 +- src/core/libraries/save_data/save_memory.cpp | 2 +- src/core/linker.h | 9 ++ src/core/memory.cpp | 8 +- src/core/memory.h | 4 +- src/imgui/renderer/texture_manager.cpp | 4 + 17 files changed, 256 insertions(+), 50 deletions(-) diff --git a/src/common/ntapi.cpp b/src/common/ntapi.cpp index e0ff1cef0..c76c4657e 100644 --- a/src/common/ntapi.cpp +++ b/src/common/ntapi.cpp @@ -9,6 +9,7 @@ NtClose_t NtClose = nullptr; NtSetInformationFile_t NtSetInformationFile = nullptr; NtCreateThread_t NtCreateThread = nullptr; NtTerminateThread_t NtTerminateThread = nullptr; +NtQueueApcThreadEx_t NtQueueApcThreadEx = nullptr; namespace Common::NtApi { @@ -21,6 +22,7 @@ void Initialize() { (NtSetInformationFile_t)GetProcAddress(nt_handle, "NtSetInformationFile"); NtCreateThread = (NtCreateThread_t)GetProcAddress(nt_handle, "NtCreateThread"); NtTerminateThread = (NtTerminateThread_t)GetProcAddress(nt_handle, "NtTerminateThread"); + NtQueueApcThreadEx = (NtQueueApcThreadEx_t)GetProcAddress(nt_handle, "NtQueueApcThreadEx"); } } // namespace Common::NtApi diff --git a/src/common/ntapi.h b/src/common/ntapi.h index cb1ba7f1c..daab8440d 100644 --- a/src/common/ntapi.h +++ b/src/common/ntapi.h @@ -509,6 +509,20 @@ typedef struct _TEB { /* win32/win64 */ static_assert(offsetof(TEB, DeallocationStack) == 0x1478); /* The only member we care about at the moment */ +typedef enum _QUEUE_USER_APC_FLAGS { + QueueUserApcFlagsNone, + QueueUserApcFlagsSpecialUserApc, + QueueUserApcFlagsMaxValue +} QUEUE_USER_APC_FLAGS; + +typedef union _USER_APC_OPTION { + ULONG_PTR UserApcFlags; + HANDLE MemoryReserveHandle; +} USER_APC_OPTION, *PUSER_APC_OPTION; + +using PPS_APC_ROUTINE = void (*)(PVOID ApcArgument1, PVOID ApcArgument2, PVOID ApcArgument3, + PCONTEXT Context); + typedef u64(__stdcall* NtClose_t)(HANDLE Handle); typedef u64(__stdcall* NtSetInformationFile_t)(HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, @@ -522,10 +536,16 @@ typedef u64(__stdcall* NtCreateThread_t)(PHANDLE ThreadHandle, ACCESS_MASK Desir typedef u64(__stdcall* NtTerminateThread_t)(HANDLE ThreadHandle, u64 ExitStatus); +typedef u64(__stdcall* NtQueueApcThreadEx_t)(HANDLE ThreadHandle, + USER_APC_OPTION UserApcReserveHandle, + PPS_APC_ROUTINE ApcRoutine, PVOID ApcArgument1, + PVOID ApcArgument2, PVOID ApcArgument3); + extern NtClose_t NtClose; extern NtSetInformationFile_t NtSetInformationFile; extern NtCreateThread_t NtCreateThread; extern NtTerminateThread_t NtTerminateThread; +extern NtQueueApcThreadEx_t NtQueueApcThreadEx; namespace Common::NtApi { void Initialize(); diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp index e30e1c478..09255110c 100644 --- a/src/core/libraries/ajm/ajm_context.cpp +++ b/src/core/libraries/ajm/ajm_context.cpp @@ -3,6 +3,7 @@ #include "common/assert.h" #include "common/logging/log.h" +#include "common/thread.h" #include "core/libraries/ajm/ajm.h" #include "core/libraries/ajm/ajm_at9.h" #include "core/libraries/ajm/ajm_context.h" @@ -53,6 +54,7 @@ s32 AjmContext::ModuleRegister(AjmCodecType type) { } void AjmContext::WorkerThread(std::stop_token stop) { + Common::SetCurrentThreadName("shadPS4:AjmWorker"); while (!stop.stop_requested()) { auto batch = batch_queue.PopWait(stop); if (batch != nullptr) { diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 0b4e89fc7..bda446257 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -46,7 +46,7 @@ void KernelSignalRequest() { } static void KernelServiceThread(std::stop_token stoken) { - Common::SetCurrentThreadName("shadPS4:Kernel_ServiceThread"); + Common::SetCurrentThreadName("shadPS4:KernelServiceThread"); while (!stoken.stop_requested()) { HLE_TRACE; @@ -255,6 +255,7 @@ void RegisterKernel(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("NWtTN10cJzE", "libSceLibcInternalExt", 1, "libSceLibcInternal", 1, 1, sceLibcHeapGetTraceInfo); LIB_FUNCTION("FxVZqBAA7ks", "libkernel", 1, "libkernel", 1, 1, ps4__write); + LIB_FUNCTION("FN4gaPmuFV8", "libScePosix", 1, "libkernel", 1, 1, ps4__write); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 606c5c185..7d326cbbf 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -492,8 +492,7 @@ int PS4_SYSV_ABI sceKernelMunmap(void* addr, size_t len) { return ORBIS_OK; } auto* memory = Core::Memory::Instance(); - memory->UnmapMemory(std::bit_cast(addr), len); - return ORBIS_OK; + return memory->UnmapMemory(std::bit_cast(addr), len); } int PS4_SYSV_ABI posix_munmap(void* addr, size_t len) { diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index a657ddf98..6c29d9305 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -45,10 +45,11 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, size_t arg // Load PRX module and relocate any modules that import it. auto* linker = Common::Singleton::Instance(); - u32 handle = linker->LoadModule(path, true); - if (handle == -1) { - return ORBIS_KERNEL_ERROR_EINVAL; + u32 handle = linker->FindByName(path); + if (handle != -1) { + return handle; } + handle = linker->LoadModule(path, true); auto* module = linker->GetModule(handle); linker->RelocateAnyImports(module); @@ -60,7 +61,10 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, size_t arg // Retrieve and verify proc param according to libkernel. u64* param = module->GetProcParam(); ASSERT_MSG(!param || param[0] >= 0x18, "Invalid module param size: {}", param[0]); - module->Start(args, argp, param); + s32 ret = module->Start(args, argp, param); + if (pRes) { + *pRes = ret; + } return handle; } @@ -104,6 +108,9 @@ s32 PS4_SYSV_ABI sceKernelGetModuleInfoForUnwind(VAddr addr, int flags, LOG_INFO(Lib_Kernel, "called addr = {:#x}, flags = {:#x}", addr, flags); auto* linker = Common::Singleton::Instance(); auto* module = linker->FindByAddress(addr); + if (!module) { + return ORBIS_KERNEL_ERROR_EFAULT; + } const auto mod_info = module->GetModuleInfoEx(); // Fill in module info. diff --git a/src/core/libraries/kernel/sync/semaphore.h b/src/core/libraries/kernel/sync/semaphore.h index 884b08968..48a5dc0d8 100644 --- a/src/core/libraries/kernel/sync/semaphore.h +++ b/src/core/libraries/kernel/sync/semaphore.h @@ -87,14 +87,23 @@ public: template bool try_acquire_for(const std::chrono::duration& rel_time) { #ifdef _WIN64 - const auto rel_time_ms = std::chrono::ceil(rel_time); - const u64 timeout_ms = static_cast(rel_time_ms.count()); + const auto start_time = std::chrono::high_resolution_clock::now(); + auto rel_time_ms = std::chrono::ceil(rel_time); - if (timeout_ms == 0) { - return false; + while (rel_time_ms.count() > 0) { + u64 timeout_ms = static_cast(rel_time_ms.count()); + u64 res = WaitForSingleObjectEx(sem, timeout_ms, true); + if (res == WAIT_OBJECT_0) { + return true; + } else if (res == WAIT_IO_COMPLETION) { + auto elapsed_time = std::chrono::high_resolution_clock::now() - start_time; + rel_time_ms -= std::chrono::duration_cast(elapsed_time); + } else { + return false; + } } - return WaitForSingleObjectEx(sem, timeout_ms, true) == WAIT_OBJECT_0; + return false; #elif defined(__APPLE__) const auto rel_time_ns = std::chrono::ceil(rel_time).count(); const auto timeout = dispatch_time(DISPATCH_TIME_NOW, rel_time_ns); @@ -107,19 +116,26 @@ public: template bool try_acquire_until(const std::chrono::time_point& abs_time) { #ifdef _WIN64 - const auto now = Clock::now(); - if (now >= abs_time) { + const auto start_time = Clock::now(); + if (start_time >= abs_time) { return false; } - const auto rel_time = std::chrono::ceil(abs_time - now); - const u64 timeout_ms = static_cast(rel_time.count()); - if (timeout_ms == 0) { - return false; + auto rel_time = std::chrono::ceil(abs_time - start_time); + while (rel_time.count() > 0) { + u64 timeout_ms = static_cast(rel_time.count()); + u64 res = WaitForSingleObjectEx(sem, timeout_ms, true); + if (res == WAIT_OBJECT_0) { + return true; + } else if (res == WAIT_IO_COMPLETION) { + auto elapsed_time = Clock::now() - start_time; + rel_time -= std::chrono::duration_cast(elapsed_time); + } else { + return false; + } } - u64 res = WaitForSingleObjectEx(sem, static_cast(timeout_ms), true); - return res == WAIT_OBJECT_0; + return false; #elif defined(__APPLE__) auto abs_s = std::chrono::time_point_cast(abs_time); auto abs_ns = std::chrono::time_point_cast(abs_time) - diff --git a/src/core/libraries/kernel/threads/event_flag.cpp b/src/core/libraries/kernel/threads/event_flag.cpp index ce75bed9e..24ddcb927 100644 --- a/src/core/libraries/kernel/threads/event_flag.cpp +++ b/src/core/libraries/kernel/threads/event_flag.cpp @@ -132,6 +132,33 @@ public: m_bits &= bits; } + void Cancel(u64 setPattern, int* numWaitThreads) { + std::unique_lock lock{m_mutex}; + + while (m_status != Status::Set) { + m_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::microseconds(10)); + m_mutex.lock(); + } + + if (numWaitThreads) { + *numWaitThreads = m_waiting_threads; + } + + m_status = Status::Canceled; + m_bits = setPattern; + + m_cond_var.notify_all(); + + while (m_waiting_threads > 0) { + m_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::microseconds(10)); + m_mutex.lock(); + } + + m_status = Status::Set; + } + private: enum class Status { Set, Canceled, Deleted }; @@ -232,7 +259,8 @@ int PS4_SYSV_ABI sceKernelClearEventFlag(OrbisKernelEventFlag ef, u64 bitPattern int PS4_SYSV_ABI sceKernelCancelEventFlag(OrbisKernelEventFlag ef, u64 setPattern, int* pNumWaitThreads) { - LOG_ERROR(Kernel_Event, "(STUBBED) called"); + LOG_DEBUG(Kernel_Event, "called"); + ef->Cancel(setPattern, pNumWaitThreads); return ORBIS_OK; } diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index b6d89aae4..017984e0d 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -7,6 +7,7 @@ #include "core/libraries/libs.h" #ifdef _WIN64 +#include "common/ntapi.h" #else #include #endif @@ -64,6 +65,34 @@ void SigactionHandler(int signum, siginfo_t* inf, ucontext_t* raw_context) { handler(POSIX_SIGUSR1, &ctx); } } +#else +void ExceptionHandler(void* arg1, void* arg2, void* arg3, PCONTEXT context) { + const char* thrName = (char*)arg1; + LOG_INFO(Lib_Kernel, "Exception raised successfully on thread '{}'", thrName); + const auto handler = Handlers[POSIX_SIGUSR1]; + if (handler) { + auto ctx = Ucontext{}; + ctx.uc_mcontext.mc_r8 = context->R8; + ctx.uc_mcontext.mc_r9 = context->R9; + ctx.uc_mcontext.mc_r10 = context->R10; + ctx.uc_mcontext.mc_r11 = context->R11; + ctx.uc_mcontext.mc_r12 = context->R12; + ctx.uc_mcontext.mc_r13 = context->R13; + ctx.uc_mcontext.mc_r14 = context->R14; + ctx.uc_mcontext.mc_r15 = context->R15; + ctx.uc_mcontext.mc_rdi = context->Rdi; + ctx.uc_mcontext.mc_rsi = context->Rsi; + ctx.uc_mcontext.mc_rbp = context->Rbp; + ctx.uc_mcontext.mc_rbx = context->Rbx; + ctx.uc_mcontext.mc_rdx = context->Rdx; + ctx.uc_mcontext.mc_rax = context->Rax; + ctx.uc_mcontext.mc_rcx = context->Rcx; + ctx.uc_mcontext.mc_rsp = context->Rsp; + ctx.uc_mcontext.mc_fs = context->SegFs; + ctx.uc_mcontext.mc_gs = context->SegGs; + handler(POSIX_SIGUSR1, &ctx); + } +} #endif int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, SceKernelExceptionHandler handler) { @@ -73,9 +102,7 @@ int PS4_SYSV_ABI sceKernelInstallExceptionHandler(s32 signum, SceKernelException } ASSERT_MSG(!Handlers[POSIX_SIGUSR1], "Invalid parameters"); Handlers[POSIX_SIGUSR1] = handler; -#ifdef _WIN64 - UNREACHABLE_MSG("Missing exception implementation"); -#else +#ifndef _WIN64 struct sigaction act = {}; act.sa_flags = SA_SIGINFO | SA_RESTART; act.sa_sigaction = reinterpret_cast(SigactionHandler); @@ -91,9 +118,7 @@ int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { } ASSERT_MSG(Handlers[POSIX_SIGUSR1], "Invalid parameters"); Handlers[POSIX_SIGUSR1] = nullptr; -#ifdef _WIN64 - UNREACHABLE_MSG("Missing exception implementation"); -#else +#ifndef _WIN64 struct sigaction act = {}; act.sa_flags = SA_SIGINFO | SA_RESTART; act.sa_sigaction = nullptr; @@ -103,13 +128,18 @@ int PS4_SYSV_ABI sceKernelRemoveExceptionHandler(s32 signum) { } int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { - LOG_ERROR(Lib_Kernel, "Raising exception"); + LOG_WARNING(Lib_Kernel, "Raising exception on thread '{}'", thread->name); ASSERT_MSG(signum == POSIX_SIGUSR1, "Attempting to raise non user defined signal!"); -#ifdef _WIN64 - UNREACHABLE_MSG("Missing exception implementation"); -#else +#ifndef _WIN64 pthread_t pthr = *reinterpret_cast(thread->native_thr.GetHandle()); pthread_kill(pthr, SIGUSR2); +#else + USER_APC_OPTION option; + option.UserApcFlags = QueueUserApcFlagsSpecialUserApc; + + u64 res = NtQueueApcThreadEx(reinterpret_cast(thread->native_thr.GetHandle()), option, + ExceptionHandler, (void*)thread->name.c_str(), nullptr, nullptr); + ASSERT(res == 0); #endif return 0; } diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index b2fe09934..c83af86d0 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -540,6 +540,8 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("onNY9Byn-W8", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_join)); LIB_FUNCTION("P41kTWUS3EI", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_getschedparam)); + LIB_FUNCTION("oIRFTjoILbg", "libkernel", 1, "libkernel", 1, 1, + ORBIS(posix_pthread_setschedparam)); LIB_FUNCTION("How7B8Oet6k", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_getname_np)); LIB_FUNCTION("3kg7rT0NQIs", "libkernel", 1, "libkernel", 1, 1, posix_pthread_exit); LIB_FUNCTION("aI+OeCz8xrQ", "libkernel", 1, "libkernel", 1, 1, posix_pthread_self); diff --git a/src/core/libraries/kernel/threads/semaphore.cpp b/src/core/libraries/kernel/threads/semaphore.cpp index 5aa04f251..39c1a0233 100644 --- a/src/core/libraries/kernel/threads/semaphore.cpp +++ b/src/core/libraries/kernel/threads/semaphore.cpp @@ -111,15 +111,19 @@ public: BinarySemaphore sem; u32 priority; s32 need_count; + std::string thr_name; bool was_signaled{}; bool was_deleted{}; bool was_cancled{}; - explicit WaitingThread(s32 need_count, bool is_fifo) : sem{0}, need_count{need_count} { + explicit WaitingThread(s32 need_count, bool is_fifo) + : sem{0}, priority{0}, need_count{need_count} { // Retrieve calling thread priority for sorting into waiting threads list. if (!is_fifo) { priority = g_curthread->attr.prio; } + + thr_name = g_curthread->name; } int GetResult(bool timed_out) { @@ -232,6 +236,7 @@ int PS4_SYSV_ABI sceKernelDeleteSema(OrbisKernelSema sem) { return ORBIS_KERNEL_ERROR_ESRCH; } sem->Delete(); + delete sem; return ORBIS_OK; } @@ -246,6 +251,16 @@ int PS4_SYSV_ABI posix_sem_init(PthreadSem** sem, int pshared, u32 value) { return 0; } +int PS4_SYSV_ABI posix_sem_destroy(PthreadSem** sem) { + if (sem == nullptr || *sem == nullptr) { + *__Error() = POSIX_EINVAL; + return -1; + } + delete *sem; + *sem = nullptr; + return 0; +} + int PS4_SYSV_ABI posix_sem_wait(PthreadSem** sem) { if (sem == nullptr || *sem == nullptr) { *__Error() = POSIX_EINVAL; @@ -296,16 +311,6 @@ int PS4_SYSV_ABI posix_sem_post(PthreadSem** sem) { return 0; } -int PS4_SYSV_ABI posix_sem_destroy(PthreadSem** sem) { - if (sem == nullptr || *sem == nullptr) { - *__Error() = POSIX_EINVAL; - return -1; - } - delete *sem; - *sem = nullptr; - return 0; -} - int PS4_SYSV_ABI posix_sem_getvalue(PthreadSem** sem, int* sval) { if (sem == nullptr || *sem == nullptr) { *__Error() = POSIX_EINVAL; @@ -317,6 +322,77 @@ int PS4_SYSV_ABI posix_sem_getvalue(PthreadSem** sem, int* sval) { return 0; } +s32 PS4_SYSV_ABI scePthreadSemInit(PthreadSem** sem, int flag, u32 value, const char* name) { + if (flag != 0) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + + s32 ret = posix_sem_init(sem, 0, value); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemDestroy(PthreadSem** sem) { + s32 ret = posix_sem_destroy(sem); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemWait(PthreadSem** sem) { + s32 ret = posix_sem_wait(sem); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemTrywait(PthreadSem** sem) { + s32 ret = posix_sem_trywait(sem); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemTimedwait(PthreadSem** sem, u32 usec) { + OrbisKernelTimespec time{}; + time.tv_sec = usec / 1000000; + time.tv_nsec = (usec % 1000000) * 1000; + + s32 ret = posix_sem_timedwait(sem, &time); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemPost(PthreadSem** sem) { + s32 ret = posix_sem_post(sem); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI scePthreadSemGetvalue(PthreadSem** sem, int* sval) { + s32 ret = posix_sem_getvalue(sem, sval); + if (ret != 0) { + return ErrnoToSceKernelError(*__Error()); + } + + return ORBIS_OK; +} + void RegisterSemaphore(Core::Loader::SymbolsResolver* sym) { // Orbis LIB_FUNCTION("188x57JYp0g", "libkernel", 1, "libkernel", 1, 1, sceKernelCreateSema); @@ -328,12 +404,20 @@ void RegisterSemaphore(Core::Loader::SymbolsResolver* sym) { // Posix LIB_FUNCTION("pDuPEf3m4fI", "libScePosix", 1, "libkernel", 1, 1, posix_sem_init); + LIB_FUNCTION("cDW233RAwWo", "libScePosix", 1, "libkernel", 1, 1, posix_sem_destroy); LIB_FUNCTION("YCV5dGGBcCo", "libScePosix", 1, "libkernel", 1, 1, posix_sem_wait); LIB_FUNCTION("WBWzsRifCEA", "libScePosix", 1, "libkernel", 1, 1, posix_sem_trywait); LIB_FUNCTION("w5IHyvahg-o", "libScePosix", 1, "libkernel", 1, 1, posix_sem_timedwait); LIB_FUNCTION("IKP8typ0QUk", "libScePosix", 1, "libkernel", 1, 1, posix_sem_post); - LIB_FUNCTION("cDW233RAwWo", "libScePosix", 1, "libkernel", 1, 1, posix_sem_destroy); LIB_FUNCTION("Bq+LRV-N6Hk", "libScePosix", 1, "libkernel", 1, 1, posix_sem_getvalue); + + LIB_FUNCTION("GEnUkDZoUwY", "libkernel", 1, "libkernel", 1, 1, scePthreadSemInit); + LIB_FUNCTION("Vwc+L05e6oE", "libkernel", 1, "libkernel", 1, 1, scePthreadSemDestroy); + LIB_FUNCTION("C36iRE0F5sE", "libkernel", 1, "libkernel", 1, 1, scePthreadSemWait); + LIB_FUNCTION("H2a+IN9TP0E", "libkernel", 1, "libkernel", 1, 1, scePthreadSemTrywait); + LIB_FUNCTION("fjN6NQHhK8k", "libkernel", 1, "libkernel", 1, 1, scePthreadSemTimedwait); + LIB_FUNCTION("aishVAiFaYM", "libkernel", 1, "libkernel", 1, 1, scePthreadSemPost); + LIB_FUNCTION("DjpBvGlaWbQ", "libkernel", 1, "libkernel", 1, 1, scePthreadSemGetvalue); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/save_data/save_backup.cpp b/src/core/libraries/save_data/save_backup.cpp index 3f7969d69..5261cdb11 100644 --- a/src/core/libraries/save_data/save_backup.cpp +++ b/src/core/libraries/save_data/save_backup.cpp @@ -79,7 +79,7 @@ static void backup(const std::filesystem::path& dir_name) { } static void BackupThreadBody() { - Common::SetCurrentThreadName("shadPS4:SaveData_BackupThread"); + Common::SetCurrentThreadName("shadPS4:SaveData:BackupThread"); while (g_backup_status != WorkerStatus::Stopping) { g_backup_status = WorkerStatus::Waiting; diff --git a/src/core/libraries/save_data/save_memory.cpp b/src/core/libraries/save_data/save_memory.cpp index e9ef53761..84179bc27 100644 --- a/src/core/libraries/save_data/save_memory.cpp +++ b/src/core/libraries/save_data/save_memory.cpp @@ -66,7 +66,7 @@ static void SaveFileSafe(void* buf, size_t count, const std::filesystem::path& p } [[noreturn]] void SaveThreadLoop() { - Common::SetCurrentThreadName("shadPS4:SaveData_SaveDataMemoryThread"); + Common::SetCurrentThreadName("shadPS4:SaveData:SaveDataMemoryThread"); std::mutex mtx; while (true) { { diff --git a/src/core/linker.h b/src/core/linker.h index 3a1aeb960..d6b5d648a 100644 --- a/src/core/linker.h +++ b/src/core/linker.h @@ -85,6 +85,15 @@ public: return m_modules.at(index).get(); } + u32 FindByName(const std::filesystem::path& name) const { + for (u32 i = 0; i < m_modules.size(); i++) { + if (name == m_modules[i]->file) { + return i; + } + } + return -1; + } + u32 MaxTlsIndex() const { return max_tls_index; } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 3e1cd441f..82e4b7ad3 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -375,12 +375,12 @@ void MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { TRACK_FREE(virtual_addr, "VMEM"); } -void MemoryManager::UnmapMemory(VAddr virtual_addr, size_t size) { +s32 MemoryManager::UnmapMemory(VAddr virtual_addr, size_t size) { std::scoped_lock lk{mutex}; - UnmapMemoryImpl(virtual_addr, size); + return UnmapMemoryImpl(virtual_addr, size); } -void MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, size_t size) { +s32 MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, size_t size) { const auto it = FindVMA(virtual_addr); const auto& vma_base = it->second; ASSERT_MSG(vma_base.Contains(virtual_addr, size), @@ -415,6 +415,8 @@ void MemoryManager::UnmapMemoryImpl(VAddr virtual_addr, size_t size) { impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + size, phys_base, is_exec, has_backing, readonly_file); TRACK_FREE(virtual_addr, "VMEM"); + + return ORBIS_OK; } int MemoryManager::QueryProtection(VAddr addr, void** start, void** end, u32* prot) { diff --git a/src/core/memory.h b/src/core/memory.h index 2efa02763..364609451 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -192,7 +192,7 @@ public: void PoolDecommit(VAddr virtual_addr, size_t size); - void UnmapMemory(VAddr virtual_addr, size_t size); + s32 UnmapMemory(VAddr virtual_addr, size_t size); int QueryProtection(VAddr addr, void** start, void** end, u32* prot); @@ -250,7 +250,7 @@ private: DMemHandle Split(DMemHandle dmem_handle, size_t offset_in_area); - void UnmapMemoryImpl(VAddr virtual_addr, size_t size); + s32 UnmapMemoryImpl(VAddr virtual_addr, size_t size); private: AddressSpace impl; diff --git a/src/imgui/renderer/texture_manager.cpp b/src/imgui/renderer/texture_manager.cpp index dd233ee60..f13c995be 100644 --- a/src/imgui/renderer/texture_manager.cpp +++ b/src/imgui/renderer/texture_manager.cpp @@ -9,6 +9,7 @@ #include "common/io_file.h" #include "common/polyfill_thread.h" #include "common/stb.h" +#include "common/thread.h" #include "imgui_impl_vulkan.h" #include "texture_manager.h" @@ -81,6 +82,7 @@ RefCountedTexture::~RefCountedTexture() { } } } + RefCountedTexture::Image RefCountedTexture::GetTexture() const { if (inner == nullptr) { return {}; @@ -91,6 +93,7 @@ RefCountedTexture::Image RefCountedTexture::GetTexture() const { .height = inner->height, }; } + RefCountedTexture::operator bool() const { return inner != nullptr && inner->texture_id != nullptr; } @@ -130,6 +133,7 @@ Inner::~Inner() { } void WorkerLoop() { + Common::SetCurrentThreadName("shadPS4:ImGuiTextureManager"); std::mutex mtx; while (g_is_worker_running) { std::unique_lock lk{mtx}; From f938829f12eb81aa2ae3eeb0c7d2dca43984e718 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 8 Dec 2024 14:04:33 -0600 Subject: [PATCH 46/89] Implement sceGnmDingDongForWorkload (#1707) Seen in Final Fantasy XV. --- src/core/libraries/gnmdriver/gnmdriver.cpp | 6 +++--- src/core/libraries/gnmdriver/gnmdriver.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 4e2db9083..18035e6ce 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -550,9 +550,9 @@ void PS4_SYSV_ABI sceGnmDingDong(u32 gnm_vqid, u32 next_offs_dw) { *asc_queue.read_addr %= asc_queue.ring_size_dw * 4; } -int PS4_SYSV_ABI sceGnmDingDongForWorkload() { - LOG_ERROR(Lib_GnmDriver, "(STUBBED) called"); - return ORBIS_OK; +void PS4_SYSV_ABI sceGnmDingDongForWorkload(u32 gnm_vqid, u32 next_offs_dw, u64 workload_id) { + LOG_DEBUG(Lib_GnmDriver, "called, redirecting to sceGnmDingDong"); + sceGnmDingDong(gnm_vqid, next_offs_dw); } int PS4_SYSV_ABI sceGnmDisableMipStatsReport() { diff --git a/src/core/libraries/gnmdriver/gnmdriver.h b/src/core/libraries/gnmdriver/gnmdriver.h index 5307b3baa..017dbe3ad 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.h +++ b/src/core/libraries/gnmdriver/gnmdriver.h @@ -34,7 +34,7 @@ int PS4_SYSV_ABI sceGnmDebugHardwareStatus(); s32 PS4_SYSV_ABI sceGnmDeleteEqEvent(SceKernelEqueue eq, u64 id); int PS4_SYSV_ABI sceGnmDestroyWorkloadStream(); void PS4_SYSV_ABI sceGnmDingDong(u32 gnm_vqid, u32 next_offs_dw); -int PS4_SYSV_ABI sceGnmDingDongForWorkload(); +void PS4_SYSV_ABI sceGnmDingDongForWorkload(u32 gnm_vqid, u32 next_offs_dw, u64 workload_id); int PS4_SYSV_ABI sceGnmDisableMipStatsReport(); s32 PS4_SYSV_ABI sceGnmDispatchDirect(u32* cmdbuf, u32 size, u32 threads_x, u32 threads_y, u32 threads_z, u32 flags); From f347d3df1897af39f393c7bbfe38b8152fcef58e Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:53:29 -0800 Subject: [PATCH 47/89] image_view: Correct view format for D16Unorm images as well. (#1708) --- src/video_core/texture_cache/image_view.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/video_core/texture_cache/image_view.cpp b/src/video_core/texture_cache/image_view.cpp index 57a58c714..61f1aaafe 100644 --- a/src/video_core/texture_cache/image_view.cpp +++ b/src/video_core/texture_cache/image_view.cpp @@ -161,11 +161,12 @@ ImageView::ImageView(const Vulkan::Instance& instance, const ImageViewInfo& info if (!info.is_storage) { usage_ci.usage &= ~vk::ImageUsageFlagBits::eStorage; } - // When sampling D32 texture from shader, the T# specifies R32 Float format so adjust it. + // When sampling D32/D16 texture from shader, the T# specifies R32/R16 format so adjust it. vk::Format format = info.format; vk::ImageAspectFlags aspect = image.aspect_mask; if (image.aspect_mask & vk::ImageAspectFlagBits::eDepth && - (format == vk::Format::eR32Sfloat || format == vk::Format::eD32Sfloat)) { + (format == vk::Format::eR32Sfloat || format == vk::Format::eD32Sfloat || + format == vk::Format::eR16Unorm || format == vk::Format::eD16Unorm)) { format = image.info.pixel_format; aspect = vk::ImageAspectFlagBits::eDepth; } From 0b59ebb22fd64bd4c60d2fe409966900b617428e Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 9 Dec 2024 03:12:33 -0800 Subject: [PATCH 48/89] shader_recompiler: Implement S_ABS_I32 (#1713) --- src/shader_recompiler/frontend/translate/scalar_alu.cpp | 8 ++++++++ src/shader_recompiler/frontend/translate/translate.h | 1 + 2 files changed, 9 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/scalar_alu.cpp b/src/shader_recompiler/frontend/translate/scalar_alu.cpp index c320e00a7..5b411d83e 100644 --- a/src/shader_recompiler/frontend/translate/scalar_alu.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_alu.cpp @@ -102,6 +102,8 @@ void Translator::EmitScalarAlu(const GcnInst& inst) { return S_SAVEEXEC_B64(NegateMode::None, false, inst); case Opcode::S_ORN2_SAVEEXEC_B64: return S_SAVEEXEC_B64(NegateMode::Src1, true, inst); + case Opcode::S_ABS_I32: + return S_ABS_I32(inst); default: LogMissingOpcode(inst); } @@ -620,6 +622,12 @@ void Translator::S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& in ir.SetScc(result); } +void Translator::S_ABS_I32(const GcnInst& inst) { + const auto result = ir.IAbs(GetSrc(inst.src[0])); + SetDst(inst.dst[0], result); + ir.SetScc(ir.INotEqual(result, ir.Imm32(0))); +} + // SOPC void Translator::S_CMP(ConditionOp cond, bool is_signed, const GcnInst& inst) { diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 00cdd8d55..43f3ccef2 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -113,6 +113,7 @@ public: void S_FF1_I32_B32(const GcnInst& inst); void S_GETPC_B64(u32 pc, const GcnInst& inst); void S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& inst); + void S_ABS_I32(const GcnInst& inst); // SOPC void S_CMP(ConditionOp cond, bool is_signed, const GcnInst& inst); From 07f451650f75b01a90e48bf7d979c47da1aec26e Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Mon, 9 Dec 2024 13:47:26 -0300 Subject: [PATCH 49/89] Help - improvement (#1522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Help - improvement * Adding shadow below icons * Adding keys icon + Update changelog * color according to the selected theme * submenu 'Keys and Shortcuts' * clang * + * remove keys_shortcuts --------- Co-authored-by: ¥IGA <164882787+Xphalnos@users.noreply.github.com> --- CMakeLists.txt | 2 +- REUSE.toml | 7 +- documents/{changelog.txt => changelog.md} | 44 ++++++ src/images/discord.png | Bin 0 -> 68549 bytes src/images/github.png | Bin 0 -> 116428 bytes src/images/ko-fi.png | Bin 0 -> 39500 bytes src/images/website.png | Bin 0 -> 90853 bytes src/images/youtube.png | Bin 0 -> 39404 bytes src/qt_gui/about_dialog.cpp | 181 ++++++++++++++++++++++ src/qt_gui/about_dialog.h | 17 +- src/qt_gui/about_dialog.ui | 145 +++++++++++++++-- src/shadps4.qrc | 5 + 12 files changed, 388 insertions(+), 13 deletions(-) rename documents/{changelog.txt => changelog.md} (50%) create mode 100644 src/images/discord.png create mode 100644 src/images/github.png create mode 100644 src/images/ko-fi.png create mode 100644 src/images/website.png create mode 100644 src/images/youtube.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 16b49cf08..7de79a43d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1014,4 +1014,4 @@ if (ENABLE_QT_GUI AND CMAKE_SYSTEM_NAME STREQUAL "Linux") install(FILES "dist/net.shadps4.shadPS4.metainfo.xml" DESTINATION "share/metainfo") install(FILES ".github/shadps4.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "net.shadps4.shadPS4.png") install(FILES "src/images/net.shadps4.shadPS4.svg" DESTINATION "share/icons/hicolor/scalable/apps") -endif() +endif() \ No newline at end of file diff --git a/REUSE.toml b/REUSE.toml index 5bd21bead..747679c8b 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -12,12 +12,13 @@ path = [ "dist/net.shadps4.shadPS4_metadata.pot", "dist/net.shadps4.shadPS4.metainfo.xml", "dist/net.shadps4.shadPS4.releases.xml", - "documents/changelog.txt", + "documents/changelog.md", "documents/Quickstart/2.png", "documents/Screenshots/*", "scripts/ps4_names.txt", "src/images/about_icon.png", "src/images/controller_icon.png", + "src/images/discord.png", "src/images/dump_icon.png", "src/images/exit_icon.png", "src/images/file_icon.png", @@ -28,8 +29,10 @@ path = [ "src/images/flag_us.png", "src/images/flag_world.png", "src/images/folder_icon.png", + "src/images/github.png", "src/images/grid_icon.png", "src/images/iconsize_icon.png", + "src/images/ko-fi.png", "src/images/list_icon.png", "src/images/list_mode_icon.png", "src/images/pause_icon.png", @@ -43,6 +46,8 @@ path = [ "src/images/net.shadps4.shadPS4.svg", "src/images/themes_icon.png", "src/images/update_icon.png", + "src/images/youtube.png", + "src/images/website.png", "src/shadps4.qrc", "src/shadps4.rc", ] diff --git a/documents/changelog.txt b/documents/changelog.md similarity index 50% rename from documents/changelog.txt rename to documents/changelog.md index 6df09472d..766e1a09f 100644 --- a/documents/changelog.txt +++ b/documents/changelog.md @@ -1,3 +1,47 @@ +v0.4.0 31/10/2024 - codename divicius +================= + +- Shader recompiler fixes +- Emulated support for cpus that doesn't have SSE4.2a (intel cpus) +- Frame graph + Precise 60 fps timing +- Save data: fix nullptr & concurrent file write +- Auto Update +- Error dialog implementation +- Swapchain recreation and window resizing +- Add playback of background/title music in game list +- Kernel: Quiet sceKernelWaitEventFlag error log on timeout +- Improve keyboard navigation in game list +- core/memory: Pooled memory implementation +- Fix PKG loading +- replace trophy xml assert with error +- Refactor audio handling with range checks, buffer threshold, and lock +- audio_core: Fix return value types and shift some error handling to library +- Devtools: PM4 Explorer +- Initial support of Geometry shaders +- Working touchpad support +- net: Stub sceNetErrnoLoc +- Add support to click touchpad using back button on non PS4/5 controllers +- Multiple Install Folders +- Using a more standard data directory for linux +- video_core: Implement sceGnmInsertPushColorMarker +- ime_dialog: Initial implementation +- Network libs fixes +- Use GetSystemTimePreciseAsFileTime to fix fps timing issues +- Added adaptive mutex initializer +- Small Np + trophy fixes +- Separate Updates from Game Folder +- Minor Fixes for Separate Update Folder +- AvPlayer: Do not align w/h to 16 with vdec2 +- Improve sceSystemServiceReceiveEvent stub +- renderer_vulkan: Commize and adjust buffer bindings +- Add poll interval to libScePad +- Add more surface format mappings. +- vulkan: Report only missing format feature flags. +- IME implementation +- Videodec2 implementation +- path_util: Make sure macOS has current directory set and clean up path code. +- Load LLE modules from sys_modules/GAMEID folder + v0.3.0 23/09/2024 - codename broamic ================= diff --git a/src/images/discord.png b/src/images/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa455fd10a5cd8ae4df56f2f35e642b4c0d8b38 GIT binary patch literal 68549 zcmeEv2|Sfu*SDlPm5?M!IwVtO$CxQo$UINs;NVEcW1cdH6bYF_NRp5#AxTIQLWl@S zrjTU*)@is)clXotKJWMazW4q8(n+psUwdDB|M%K!uk~MRD^yka#I7CGI|v8}cFD_0 zs}m4xip2j#Y=t{#tNEY6Kilo)^qdF?cJ9Id-9(U(L`6WL5{}i>#px<33Y*!T;Xs?) znPNEH&e+4#1Oy`DZuV%i(-<7m6l00C6{Vl9sHR6^%|+>T_>{Pm?4>YPSUC?zjE0A@ zrkTfSGa++&aj_jDZo<&O84M1MbUR~X>m=+ZO8>cCVfYz;o0A^-`4QY{QTpTf7b10) zRFP74ju<2#2Oql`7Y{d5K!}5z4<*FS&4%RR;^yV#=HlcLV&~=-MhOUW^CQ20(TnYX zpF|wZErivjWxu`-u0-jra5#HmPEJ=>R}NQR4m(FnPHrI~Ax7h}(O+L*$92a3uNT=meZ>w?TVasH~3{SUp{Pzeg467Mf7nVbFfB70{?o6qAhH{-_>t=rae(1cC(!Z{F z=FC6zTpjaW27Wo}FFjYsIN3QnnqlDaUv}LIC*uN>|5cd&jfP}gU_O4?l)aT5&d$lo z?jJl0q`;U~SFq zT%A~cXy@zGKQ*LeXO6XS|JE54=m7X=atjG_p@ez3*|~UyxxRMhhbMn-1GJ7g8i)S3 zTKVzWAKHQSDXi*fXYOoPvWpR8_W+A1k?0c-yOHXasF`oa|1s=D*}z0 z;pfuM@rOshwV;7<*m(C_felvdBo^N%y#9lW?;XNF@%iq?x}RVE_ZQ*i0Mw7LvMP+y zNsRmRRR3_}d%NGBltVjdW1X<3HW*Duw5^jp+7V-G<}Rk`=#2TR@;^NDuNqTv!8qDG zVr_9^vS=HpUp4jpiNCj_gm%LZKm+T6`Me`QubeP=#V1PtO@I8gg#bW!j6SPQVOKGf zppXy;*XJ)0&Tlurzv26%R$|;-AUga!e1Zb}-yXI43HdJ#INM@zVyaj-jE&Q`2S4BX z-t1pc{m}Ctn)(K|stqh~W%L=Cn(uY-UBXZM#tFs`>T3bWgOoek_SeGk!$V2{8SJpX zeNGQ!V`Jy~+s7nrz!3TMQ(x|G?C@U@{Bn@r5YTW%cDBJGzq}s#`Td_e{NqC#rsQw!;Kfhui`x5I>GPeREByz6{R1ZW zF(@est7B}V zEHrIlW`Pkzqs-a)_)O94-1wgaP0a+^EqE|y0w@ehh=&^l^vlS8sQ$O6RUEOPl{d=GAYxzsD`F}cTf2;nh*6sdV;C^|wLCHh0$jC6)5I}23f&RA=s$j?tSm_ zmm2sUe0ks(BEn^6E+}ATA;@lKibAvV@}v0Jg@izcxwtWcf@od=UOv2K^|$K({0>(u zjO|YrBMOX0)HjRqOH-ep`sYTR>@0Auu<;+q?;l@HM|m3?XD6H^-jKl?k375_Tt8Zn zU)tSR@toYH#J8I@bU;)2nu`~-w#v&^ANww$Dg_T>=CrqS!hK;JkT|;yB`&t+UpWQrck%x=r$03L^PK)q z7TbSsv;Xg@oDELuAC>cMYxsXDwqN{MYkLehjb9M|Un-yfuGNI!a{o(e^)KE+P#DDp z4CXi9;y;$pe_~U(C!amH(fzX#T~R|F$5$ZXQ1?>+c)z4=d&` zM3A4ii~o_s`)^yH0;U*Vb3Q=}b}oKCFkVbiX6$G|QyzA-nT3S_m!KIJFYjju<{|4#4?WKEU4;&$pfJ z|LVWrhxC87*^Q0;dC5jReslE)uHOK(vG@;M8}azf)gQQi1JK6eKX7ft<2P4-;Q9?f z8;k$IwGofsT>XLTHvnxc{sY%WJbrWa2d>`$w6XXPTpRKD&D9^cegn|P;y-Y0#N#(t zf8hEJKpTtyz_k&N-(3BH>o)*xEdB%6Mm&CV^#`us0JO3A4_q7Z_|4THxPAlB#^OJ4 zZN%d@SAXF84L}=<|G>2okKbJVf$KK_Z7lu+*G4>kbM*(V-vG3+_zzqg@%YWvAGm%4 z(8l6FaBalnH&=h)`VBxEi~qp25s%+o{ekN@0BtP(1J_18eslE)uHOK(vG@;M8}azf z)gQQi1JK6eKX7ft<2P4-;Q9?f8;k$IwGofsT>VS9c6|R0d+@6%MCo1O_ve|b^Ph*` zy@xcDQ&%D&@Hhg$y)S@(U~wJ(9U~xc<{}_?XF@h-_?K~q!HfIF>i=>hHBy|Nj*qvVLf!^5w4jZ+;u z6w)#(m8pI_PdlJ?P=X^)3P~m*mb$Ip;r*f)6%9>bw~Z`M{KKg!j<7oVt=qP7i-}#3 zruQqgd*zR&dRgYMnIvR$zEPzlGlhSjZAgc8T}{m<5)u-V0$sB3y4%&&7t70qJ}fLq zkrBPc2{S~?oAc}^CLu{zWkyo$K-=1q5RmN%vR_?thSHV!eBZi{ANiA%nUkGc51a2#zxC#9TTwGn%SYx;Z1K+*^FCK zcXxLhOIW)2>IaQU*H_+_v&*ZU<~-Po()4dk3$7oHYg$ssOg|c>i(|oN)1E zr6Czz`&2JJOL|D`W_UXDh4bg1dy5%2TNHIPTR7EDx`bC&R#F{27*u0&UwnN|d}w_o zbv-~f!>_7J%&@{`SJ$mmo0ZzjT&HQ*R$r|jEH@<4(9pP&kno^6Q6Q~-?EUP>$(wn3 zFh}<%U52LmDsJ~qeriu$3-nx=s~;Wn zX8F}RZHM*V=;&y?pdk^=l;Kt=YBHitxw*M7`uge~J|wDIdUv$UkRz}b4RRgV6ciA! zdDv0tiXtO`LlG`>E*(+PN1{d%)od~7QlXaHK}NilTSSC7q}I=b3vrxmU|~6}YrLSW z>|Ap4LAOdHHxXJ`CFj!5o$k7omYu!r3DDbRi<#WMgYfPem-5 z(RzP+Ap@QPnc5n#X?b~B*T7(N%iNXt_~X95;ym%h#9KFeczUXF#6>AGMk!=QDrbYp zsIpN;HJaZqWMgDx)UpvMSbpZq0YF-eGGjW}dBynkB3agyW!$n#YBq5B^9i zvw|x#BTaFgFg8n$IHyc?7L9VlH#a(N+|tjp&h22;$raKeiv4a zf+bY?-Mnyz6^fYv*tlEcUT{9M=_`$Mk0wM?K7WQpPPlifA&|{`}vgp+h7_$>7inNS(b#{I@cI+5q zSo+Ok{pN>rB0Bk9EHFYfiX7RD;~nG9cU!IG@>@d~=-err$1kx-M98*l%b!Li95&{0 z#HvzgiLa3JQ7auhi;%d+tRV71pE|@yo>D@VERg;zk5YT4AxE4U6{A6JhoeZ#DA94b z?f#M1u)%y z5D31Tzng26e+)$(QEZf-nBIDQvA@#uLS0>5zhv5gQg(Z%WSR+cjKUkKd`V9CeK86_ zH#AxAAQ74wttR?X%7^AkNEayDl{rGz!zg67!|MR^N>%3P>N%Q16gy%_M6X-|6`-5f z>EAo49N+v#jwfD(ATk3h%o%2S%P5~|%kgsp)DeNqVVK3rJ(6^I;L}8i2~6}TJt}F1 zlP?SjK2>bL6NRSAP|j|TYr>32GpTsRDAbi!RrT+=Sl<_~_$Z~>g1yePwXC23O)k@F zn5W&07b{{u?g&|__IG@sK%bA#?>w3zm49vz(WYYkT|ttY2qr=v9<@d#2rIQaUl<}v zR@P~?E*1(#JF+!V!U#&Vb1_y47e?n6xtahtcw#y1aCEPDMk&Ifhs#{RoufW zqsA~HBY_AU@baKWfiz-wfsDwg*`%osd|X&KQ=t1Kwb^17I$Jw;&wz|*`OTXwOLzD3 zQdQDMbVHp*)l>#lotQi z0rBpAxadmt_OqZQ-_9=uw^|Js>z{FVbuGPJR(8dLicyOrZcm3b9~x$V#8D`v(VV_O zm(2o2O$vLhWNt_7`21aoc3cGd%GT}fyU7UY1Ua>fD_gC~_6Ixc@Ee92+bKZhLO+7Q40hQpFe*H#`$v|$0^{rMbhOjzQHp*}i zUY*OB?o$a_hy9T>@a$$a3Vc_CZyFLxB8d*+RSZUti@27XIHPL}CtK8QliM{urOJ$= z3YFgKSZ-*{pEwBOp07^JtD-XmlODailDPLPLx1a@Ir34O^G6Xl%N~~Id^UcbxwzMjY1~*prCEF zdd`Fs%{~jNH)pfP-lgemPRKg0M$t`%U*LC{NC|Kmtz7)nyAH^xJ+K}?(OY0 zgTJdenvZpIG+&CeBsPr{YLgMcBGl-xF3wQR zcvKp_N@r#zM1mr3i;z7Wo~jJ0+yr!PhD1qcEtf=Tc6d@}bBw526M(;`)R&t6; zL4MKsBwHWrCtHy)<%drCuicCpIyiImwL*6X_^s%10EE%+0iI;`hS+}(w) zG+PLq%xsfQQ$>KrK**>q)700oguOl*^EgN`lM%1(_@8Zl|J=GbO`4v&vZCTxaX|q$ zEZ?mPnU}IPSwV6xT^L=|*jA&m=a#$9>Ge}EZ< zA2J(pbSJ*=hN7MX>6p9Rgjtnm`!I)47hJF{1k*4`J4xkislpJU{q zHbra|76rN`9v&VgVAH*Sb~J}egtZ>8|=004S-W#RVLLKp_MOIc;l9!hk7T!E2O;s1> zQCdyYAALon-a#uEEq5-Q3{kTsWXpZ~V*TXao}L7cwUtHP0$rNM5l3&>grk~9a0-cr zKA+f`NFAc-; zL!Z}?2gn`Gi}W~BTNc_-lh?2wZ5Bls7Z(?L{XCb(NtbG{8&8Xhih2Ti#@X#LcUc&Y zz|6n*qnl4pXI4m5&OTUY>L8M={OWxD(9Qx~oAaZKYmVhF4OnCD%o=GVtINn&U(1!J zoh2d2%RhNq+FJZ@7(ML!6OHC=p!v2E`rW_WZBy0P+gsr<*;CvloIEAZ5eE-gvqU#) zZc?pokh%~dIb_t7Waz?7 z>xmhU)JfXASN3SnMutfkEQb}RYvE5{%^x45$M^yyQ=TCS6}rJjA( z#kUICDuPlAOY!U6>#Y)48224$6d9uf(wXg$WLI9h`>aj&md^P}nD8VvFP+T1lxN6M zD*8~9`aHw>vApRcfex5mm(5a6WZP~^XKn+vsm~g7v&Q6EZu{7+UAuPSrz_5*axF+U zL(f@|W*L42xAOHEIU$|fEAT04>aomWZTofvfr{jTrE4df(Hf-7#$Q!kJ>5LwD9e`~ zX%KU%D=iE0yvLHv(C4sB<9*e4SJ#(+S7fBHS zNo-MKY(IIN%%3_U>miq7viTVhbT?nF0=8bTV6$IUd3$==*-d}!>FLpAjCP1sIAT_s zn9oPis(_s33ep!GXIQ;~;NTCvLmoixey7Sfk1p%OoF;2#^XObHh0Hu-bR!sqqeC;F zoT3{)JQ#7z<0dYuo%4#7O9esSZ!=K)T0ElBoF_)%hzc8JVsCeMd2xBUJ1=#FvUEmk zl?{KcU1^SrnL~*NBax2XLDpo~9Oo0U`PrStFRf7aS01KatTXlQuJx2cwk;3|8J!saoY2M_H|623OF{dZ{xqv3n@;^swZo)*LmBM}yNNb!+qTVvz^u`{Fkd}K5wJhK zHDIu~X~g5{+{(ChF?E|2N~TRY1qRiK2*gl5(#q{%H2^pB>Z zn{FE}P|vxloZWpcNb(jvUf6NY<%34OIUcR!3t$-UEIT<)E+1YLVbsPn^HTh z5KmUl%~JATGa}2*)5>S6r)~DT&#crgYLxG)p5N8tM6<9jPkT9XsBG7T zAqA9D^c`bKWL)o@300OQ{AUX;zP{_Z`#6nORzOJt*ITyz18w)7KnaeHM^~A5fFjq= z>(pe8k@c?`03%>+R0P=vKcey&{w!>xd>S#8HgOw0>R4JMHJz6@!LpgUwSWpoYdhouDZ9MB*c^G__G^l zE+|j78H5H!ZmGe%ZyQ;bokE|?m)AQt`wC<-%d|kS6cipU4GoRGaI#c;dpoIa>?v4$ zEc6&jZp zwrO1OS69nTh=T@H^R&lSSG|pellhmu#Hy9E55O{69(wld8E>-kzI5!}&M96{{azoG z>~+bmw~Wr2rK!G61ct$@&wIlXqtM?XzJ9M|47Vny_ME4d>qy}VgkRE0fut-f7xB=A`XPZ7 zao@FY%a)W?dNRcMy*sw#CQ)2&kQn0~>6aBqjItr~4=-`mueQ@c=31CE)>eUou!kh1 za?xuo(VS;2IVFX*uy@k8a}3u4MCUoVWU_|xO-k)t)Dbort?%t@ZTnAwYFcZ}IAKgg z=*OKhsfbYXsWc9ydrwvM@hf%_Skk7Qof>bQUq-t|@} z)KyZ#KBY3!_I#QxG1p?sjSlM%Q&UqW9C1Yn%@(Bq>&K5D*Y@5wNXUv-|8)K=S;Mv? zqzPGc!1Bc_`VtPdIHs4-YSXf7fy{1%Z7nzYp_}$aV4?t5P{I9|$#|sq5tNfcc zS6rN&=Jazkg?VzNQdlG#0?!VtQ2C{)UT2DH^7gj3H>vhnz1w76yq0E^4{A*Kgj^t5 zgC`w~4b4Sm2%^*pp1;pL>_>K`B_)htJ}7$_pB5?R%LqJscED?52=wT|+ljC4xjXU3 zH3yf8DUaiL#3qC?!L&^~%M+?D;_wryMtl@-L7hk`AZI>eAKHC8r+h_?0qaG>7}H5PcaTy+Gi|Hn;U(-r)|ik*6&MKA~k%_w=SRVBL}cM^b`Ls^A=Q zI~ZV2qZBCp@VeK<@Xl8tXU1o2ZJz=SU6i2Ju0Wrs^b!$DMwTS55}3{mLRoc0HTC6b!0DJXG#^`GB=QT3bLt`mZ z)s=$PE4S%+LGMCPJ-*$ z%D&V9S#@!7@$~HCihZ7T#$}NUopo~eNC^GsRFHX|I<%r7+6g9Hg6g>)cQVx*-4>=! znSnaLNFBkbaQ?U?@&;_01A-)MO-uX&0@aH%^@n^*@7-I0Zq7D^O089@8*FK_1%dFI zc6E1p)G2x(RCsZD$wnD)`7@%qchADGZG*d`}`Uj(~B*W)e*vp z%?ZXJs`BNAov_HL4LQ21fwIFy$Z9Z>SH32aRHOLdA=R@O%+RYdHa3c(=?zIZZkIXgO`B>KKgizgF0C_FD=jYOGtTW0JWi$? zQoAWE-3aJIdLqXlWKbU1UI$4+@?U|?U%fhM=ewK__C<6$v${2{Ohu5a9N_+Xk6gBQ z)0_zGl7eSnL4d^iyzF5!{zPLdR00iM#^cvc2CI$<@D(B$n5k>+fuZfo9;=q=Tc$mw-`bV_Y(qP0<8e&2ybbw zzj$c97T;78fLKP2CWEg#??P&CIJ1(6p1-V+X*`Y-wzjsuQRDE^EKfVJ(z-kDiI)5O z5cE+gE~=Fepq1ArYXY`a_w@E2ICJDmC#{gWOR)MpD}d6$xEnZkEq76WAb6HiZ1SBMELtb2ZXKWGPx-Lj0@^E;P_l-k1%^Q!RF zCxKpZJi$Y9LNBA$3QT~j(~Z$g)sIIU@5*M>a1qaKu6NJ_3uYLtK@kK78Te6^b8lNS z*4N;haY*f;pSC}?nCx~$7RpvK;qKC0=~ zF%EV=Ia--ab?&K&39q&HgSG4UZ_|gP8g|cGu``p)%*Yk93Q03TQ+H1{Im?{3sxaiJ zkWpLVk8VV!w(tVuP$iooUqZ=5PFOB6TvNyfOdK~Av5({W+UoiYwDn3lL(Mm%^%>36 zMM2pe)WIzl441{OM_0sSm-DhX9LrcGvj$E3Y}F z;}mtkdhB&iVIp!&xw)xa09ond_U8EX#Kg;c;D?I$gJgE)p^x%$O41fuH&@XoJ8wta z;&;D8eO+BwL0B&CyfzC_iXvk&*iCD)6m{uc<9TWn^T0q;fVnXo!hF!ESWIF&uyN_k z??v*#yq)u19XDRbtCU@W_w{$yht!;OMOru`xBNfUWTY#@nrPz|Jye%OsF%~Re8wT?Da)_0Pzl0u_f*Co% z5Qrqj=J4r2#GovJC_VNK=S6*l|5-T(9#z&DAE13LEG)7fb3NG;md>HI&-e{Jv0RkM zMeE`)D^yrzrRZ(8aF9(1F2b8MS@&LU8W{t7X>AJM@%#|c)0&ML8tbgD9bj=V@4uuF zn3ly^*gYY^M!EcSXoh0a#k0elhaFh#9WxSQgnl$7yCoQ-{j_tpw9GZX7Ec*HLAh_A zuhVpY<*O}(e&A(}MYBb=ubc<5wkA8*vyjY#IuH@e7$rvxYGnm%^P_L)Jz6b_Dz9Oe z)RS)T?wh1*LxHV+pNp6(81(IlJncCUYhw-$j zP@N-g3^oE~wdZy~3_oXbL~8u6P-vPi^TPk#42Vxa?RQDj+E#@jh+4 z^sF=xgbo8)>r4oB9O(*}bbYmk_j z=)3-=Igzt{>`un~Xr46P{LLtxt9xKRy$}I9^kf6sV*21YLI}_eyso{9j2bs!gQ{vY zXSJsZ0U6QH+Zxxj<-$a}J(@J!)8IZu+5PT; ze9Rl50PS5}7jz5^464ESI81<2nAV{uSErJL2Duxg=*eKS1yzE-p^o z_ru#(Ur?O-*E_5gkEvwL4>oFea1vt4L47IHM~%^2I5{~jar!Pcy>g(5vlpaU&uSlg zy$^E%5v`EarmS5?Ug;9~MBwQtE;e}y0IbvXmx>&#*4!0tZ$W=j^~ zgeRl+_$Li^yr}9p995nAS)_NLOOhVGdo;T7g>jXad)!c28HB<-MN*h3WVBi;w>i?D z%BIoe7s&Nf*ZSUY$O z@-bdjgDYxLZpg17X)~Xo?15B17XY0yi;`nP_}_=P^ivU!4{tqFTlP^OI>ZTD>4|$K z=OZgr%Fc@rP8J|NZN71zyAGHREPt_ z!PnZyyq<6=&Wli2pKml5-bbXKrwyAnFp50!=Y7GINQ{Vx$Ond}z=<@}>YMS)0w+qTVujog#4glfOmgWeqwyU z1Ftfgf;)M8fclqJd+y5CH086lI2xall0tRXDW=EMmZViLR;JQuR5I<^MEE|=D&Vck zwb&??@`&2sIY)L*5D^i@#Y+<%Hvu;GJUCS&@88?M7_?u}&Fz@=MvnSXF-q_;yxq!c70KNee^^n z0k+VDKI*Oa$BAxN-{sy{YrViecvTvSUn=a?M=LY#!Y>FHPHDoHxi=7~=A|Nb52-Dv zcQAxF&uc%qEdlc$ePw|C``2~hb^-zdmS7w9K3ztpYy)Y)Fp^>kVo;6tW@cs(FqAsqMGa1i6CYKaScl>c5Dg{WoZ47lz5|x z6T`SdihIYcZIpQ7lnGXExe0bYkYyE>m8*51K7Hzc`_`&w3}^dhZc&yaZrN(y;Sh%Q z9BxXDjna91eB4PTMPw1L2_Qm#IPGYt;9aXt67Xf8vz>?_MvmbT_>EPtihHM~r!N9? zXv!EZ#KXT3K#piCJhXC=mVZn8#KZ*gD^N(5crPHPY2;?!FrD*|7C8gmZv4=Q)hI49 z5EtL^5dbfZXC`-)9U=sMwLM zj4Aaa$8*E`?mmloT5{`wyH16t$eDD65Ys)8@pP?ik58zBZMF^`ZWY9#D~gJWe&dGJjY!3OaFuR;LKj}oJNCxxj4H6u>V9dbd;q8NH)A)RNI z^C=oisy5K&W^7G95aR5F!K84m~k>YlXK&P?>!@j);`l(}h7=A6Xl@<8J9pcx- zwh=HUHv5o;zvD{X?~i-|oS?Yp(uc7Yyelvq%RD1-D>$f6=~07L&Z1fqt7el3gHdls z_n|W%(t!IWwz0AC5)u`yJjTtv`?VkkE8@YnQ+pIM)fY_&bGpW#gOBk(Pnk`(MP1i(UL-o!@+my|^z)1q6cj)_6By|<$@UN1dd1Ic z-dqr)>%hV4>Vk)ob@Gk+fSn{cLKhWbU(6raHdnwgOv|9Xja+>3A;~zONXZAa+XI$NxZEazsWhIPm{9B-ca9Cbb?esN zBgO->w>#YvJ>QFA(n_W(;_u0SeA2bzYIkpoTvc7V%tEna zdi5e?(kKZhe+0&Nc5Y;3!~ik|Ua~S5Zbv*nH^Lf;*i<9mc`k{@Ev4&>om~r2K#dBl z`c5G84-iCV`|{<>dk2WOTIP1# zI8MJ0ODSB_ahSBc2KkCA&ihR`b*M`;dY+^Dq&*l1Vs|CQBDBQXB*};-p<_vXw(E~= z`12Q6*GAKss}_>_7xcGDwxfuS-v;!JAgHj$maM$MTyRr;innH@^wzmc$2{|c1{^&n z+SX%ay|UF=MxmMJsHmu3%J3t=EJ#7N;Jn9b<(M{OYD;x0sRb2*N&DD`5AWW+8wCqX z15(zGt{T3Zm*n3{chR=*L#{G;@&kACbwiFeD;>=n&zW|fWsFxOfwU&ysTnuz6`ytB z+EJ;ks{IDwW@rp`Qn~V}>qUx_({?tHC7O$Q%cn z_mV_=WkCn^QjF)`ycS-tEo0;2reD~U2(UHT8sv4JfwUhtV~-RE;bhAtzqT6pazhK) zWEy~~PPRe`-->+wZx} z5W-FkTWK~}D0gFf8Z}aM2?>A(-?L{=nbRnVm)IdllzcGA1>P<`FHDh9jhR9X9!KjSf4-(_N<~wvgEmyC5 zp1*K`e>(#W$ze#VgzuE$bWw6h_#`%`L!?iX0y}{%HS!Ti$-zZrYso5{(QD|RzNb>* zou{3P-4pr{$3|H(f)nlnVldb!-gV1^4Q^mp3~WM5z(xV+s(CDlGmvsWEE9ZhQc|k z?nfLDFe%pGL;zuY(D5*Tus$I;2avpY_38v0B~XZVKB;mHkqVjMSOODYal;f?84r1VE}?HGXbT->(JwC?m!@TFdSHcmU|MkBqNrxl1rji8$LG_ls}G5# z=F3y_C+@N++Ao`-=IlXT?{Fn4=^-ey1K^mY4T~&?eR&BK_%^$3SG*O zAiK0=Kl=0_q-y5L)81D(yc@W&Yww*hS|xHcCHWI&;KLIyjiQ!eAjkUD4LLvxE(9)+ zaZPEe%s@D?7-cYrHM{IjCcdoAc|$`^6MJU&y+h#LtO9FvxqhhJVdzl=Uukb89YRuS zGCnmyLU3wgX}=>)2qd(=>g`Q51S9QWgzO1vHS^wr%(hLtYbq)#Cn@wR)=Q(SQDIlI zu;B6v(p<2hdOW=#unqynpx(*g+QGBkHV}gWg6EXMEM!;;*uK0Eb~HGNk&WgTZpOI` zJ6y7lSP@l-=d_*~)s~F)By+qZrd0L-{(nR?dXwwaOGv`3zLCj!suXkwd?1M#cwiW_ zSgIX)*l-MFLzvpx<;$~<;i&6OcSBAZ zj@_rr`|c<-l1~kvVqxLrn~e&weU8XJ#pAEvps;qH0oEfvAsHe|k-=3g-c@a@1#CNW zJ9fY)wIu|&4TVg9(}Hu5h7IYkVAV`aPYZ}%hR6|qX5$s|5)3Y}) zC3s-FvaJYYD?vJ`1~bLos^a4L?uQQ_W_67>JJINfH*s~wXU$NC%dCQu9tO%FPmZB2 z+X5RIdiszpce3mwl#+nP>CD@jkodZF>(*yoUEA`zcESgk(ACE5ywFw-rN>vkCc>@Kw9iY7Uv^&5-hY{+B`N3`o|tv?_<^2Q4a}9ii(N@@Z{UE zF}3;rW4l9#_ruDE&kGxV(72kp9Tp7`TBZY4L`97MQ0$#ViZAnC=7$*B1x3d875DL1T6a&3&8cYMx|HVbM)&7ma=up5;t3I$TnAzKt0Y1$*gXl)0N+Z%d?eOR)7lp=5 zu4zN&W@&E(SU9hHiccd#YC%IDtc;kuMAAP-{hVhHJLOJFN=kbej+;n|#_qtLIh~V! zu_Pm+w4^|MJRaeJxgifFqK`sIrjdbx3hDi15wg1@WD`SXtwRcDi;$VsD(VX@KI=`r zi3C8j%~gBMzk3Q{mYtPGhuX*Ib2|AxDHF}!+OwVX=u_A`MZf@=16YbgS`Iw7y`X#W zkWT2>{E~&$WAHER?cX=VQ6v=VvO!SslzUj;N1o{Zu7tsMYr}>X4yxdq_wTuE`|pK9 z&NV2$4yl(-14jrY?CdyiovH}V$zcImlfihl6=in5id(lm+A7z$TiyTR{rd!rOiYmH zl#ufP(#oI%kope^=`w;evY=1XR6(~2JBsJ+P?)@Vj^Sz2>e8jABG~_cyng|UXMS*S zP~h<4!@#pSQ0~{879kD9=)rE~a^}pL70B1_f-U4M5^=}N?^0w({o!3>ic-Q&RJ62@ z1_n%u&Z^9!KrHU)Pv(U0j@dEvoWkTZ^9h!uBi?q!`rOBkg^!J4@fqrg%`PAA4}1F$ z(~`)jnNV#du0<6+zQ&dEA;EBKI&->FKAgA!-}|uM$u`kDKAcQp-fFp3BmPw zJ2|PfYr1lLNp;DPPyJG11#ozDVf2pWG+?#BTf7@O@Ir8FyMN)fF7MCo=R=YN;)ykcY%-$;}xkG#Ppiho-6b8+8XSJBfsdFd8yK zt_{0K%FGTn*211Q;#dQ-W@UxQ$jG>I<;wkr1_ZnsGAJMd31-YQrQ_I&H4GEWeGTrq1eJW6=5%x6^?f`H)=1C*kuvp7i+i%B$x)pJLm{ey=$?^8~1P>*mgkj__$9u`(Y zAH{H&c5Vl3;)%xoNC=1=TD*kF0wa7qPm{GdA1J@$WJrYM9GAhnbCdHP4s&mmU~S$v z;bPVwpS-*IPSI>k3cA9aGV=*H#74pJ0OV1q=ZbqFBo3*7FJ8R3|LD zlAF=bu2y+(i$2YlY>=Z#95UB#m!O!X71vZO(|+gEt^In6xrtskNKL3p)N_a#$wRw= z#A2Eg72~i<2(B~r>lz2wTkz|7fx$sE-f{!OXV4xuvNhEry42IRH2jq9u8KZeHa50_ zq872uGHO8Wz;**R9mqW#ba@7OcBi{-_`yOJD7py1+|fVNUJ=5@kdP&;b%-ODIgeA?_TAl*-<8Xn z9W6qdNuBF*aNFHo@(R0XcWvEl$gwXtB!rufF9;4%pnZK0)tHc-K7CsE)G7So9a!?9 z%&wZxkY~(JY|kvdT2vR%`OuVm8WzUArV(n4o(p6;>AZ4I+ICn^m?;PswdmEqKUCTl zD1(+5NMAhG6!TD2rHZPjJacvd0`g#xnp#`$fHbiyiHWrU6j<1hI&Er|l}KgHbn#ju zg%V$`Ue$}3%h%ut3zYjnWWvnLiw3rQFf2$yGP>(@!uo*l%G&mTOR>GyYX0_R4{m3v zvy+8A`WQM=SNU!2U?JcExE@QAvB|{ToCw|qz=q8pV{uOkeEDiGoSK4+>hAoL!bkOB zoNaC7V3FT0DgtXV;Bd%2rZKcEPS$dN@ks?p16qRM1WkAyBj}KQda)6e|Qf<`>v#>*5AJ`F~_xX zEiKG8W?T=|e!6s%zPv1b4QOT{no)_Uk`%KBDV1qnLwjnuhxDtEjF^zd4vt(v4M9;i zaV+MT*t+fZwz7LwP`V#7h^NEYO@O5t-1la-G(O05bK z@d^uv;e!O9)|RIWZr>(IZRygEDxsWA4oVj}+Ek)3=?^Tn-o-%cfZsi`?$jYQD6Nwwvuhp;L zhr>!2DN=IW518{Bz2tL9iHQ|CQZe7tXDcw(U)lTN+?M0_VPc{@sEX~g^hXK(GiKu~ z53)Aqz&RR7KYIq5hYx@q(DgxYxf(jaq@o zv{-+p+cI^q6hgt=v#K{%eB(Cj`IB1mmF%23^{|+A6@tj7K0f$0m!r7@G6T-l57i^C zYwa%?_msV=JUj^oQ!>Plc_B;H)ZE-0RNe}fw4G%!VEj!unhJtgfBe>sen}5vf}JXz=9eQ~nmu zVP{kzSAS{VzfO5W2?3I))3A?{pCt9+xCOGs@kRpq$+Y{8>AMDR3?>{GfG?Djd9WEciBr#+6r2 z!kbIL&eGhCD3%-6!%Kk!UKg5#PY(Zx057aFkrxxr#;f;JnAoNSrbL0G$l@F|Q@n z=6EjmSnnaT8}?={=(I2Kx&-;`%GrDHhv~q~CPNV2uB`0)VAz8(j8eS$F-5Lo@njwk zZy?esvpVX69#TbBwCP*Xq9G4>9MiP;Nzmv3I~!H=5ABHSv@bA)MVq)AO-?nYUF_R$aI^(r*wwtx$Pu`1&M>bn9@x&`$WDdLR z)f%~N*@pr_Kuk^nHRB&Om;gOj&>S*&+moI^$Q>9 zq7TTL_oX};cx%C^$`g;XwY61-ScX`#@`;m9zIRtm?@n%4X-7fI2Vaz&;^g$SiZ<)s z#7igT5yLj8+ByT~@U|_y?1ZtgalH$r!eD^1wA21RcmKI*zVbd>uh~W>T_Yn~WvR;s zt+OucMDJ#GtTE}7p}-m7HT&geix8HuLM$p_SpMU#!x(W~&w2ekZAd7_vosUnKt&=E z=c(Nzx2@VHDshNk7~+Dh(ynT8E1V)d;c=um+<~=Hxd*$)i}|8n9#TC8Q&-3as(EI< z%h=4*6B2V?zwq-o?XtU-^PR`tL`2|(C5}Go{UI6}MMzqL>=k*P+>TpCX;eksFDE^_ zrRiJuAc$n)DBviAfCRs`t5A>idpD6X&jpUDm*tz|nw(AJ_oO2Q?X(NLR`uB^Usib^ z03QKXWc|~p%JnUW-zqZnHRR~B^*|bg6*C1jKXpWJIx}TO45t&MCYRWKIZF%+67qb! zz?{>8h>Z0X#o11M zm$-}5_Y8O(slIDM{rB7hz*`bg)d_QibgKA$ZBwEh)-iBAVHNmx8!s;}2u|*+5bZIs z;WyABQfG}Z?67veuAWo2$?ER*IyBI<2xr!Nlm;=XgC|X6#uVqvtf;(DC+FozK zvHbm5cB-kf^Iq`VqU5AIoR|cU!4w&Y&>pfuQ5#(t(sobbbRO};IcmWImsW$5jjd66 zd3iH%-j9xS21nF(3#qWi8ya6LxE&7MsCI7M^XC*| z>nlz`R~^`Q)Iry z`XiTV#f~qK<{jXCFot^#Tf+&~r!gcA*QPQVa>E7+frT1;6mdXTPp<|{TK}4>^fP3u zPFW1;kPt5l32qt|Mdd2_UDKPdyIQMsl(2@RGeamcOP$3I4x&D`C{m`Mb@kMBl*zyL zNiVi;c$fj-RWR9TvF?41PISn!x1NNb#wMl#yP6LlP&naJ1}#AYHjyo(?D+7>jGGtY z>~4vox~iUiaLP38qZUM_;dJMqy&D{1+qK=_L~zm{d0^zz<&sXDTy6J7FEKnbQfk{L zS-}v}dBc(3&OiKuc0hx5F@8_Q>q79`TREro`UzCwtTp(Jn?t2dueVwW-W8T6B_g~b zZmt&#NA4c?KK&|QX*Wm;Z122XoJz)1k z+Q#mzGCYnNC{yehety>hIw!U`ya(*~`{q0>YgKK$kk&%nX+N$HNwRpO&ob3JSOR$# zT%A1!=V%);_Chncb=Ia# zcZ1J_K~UJ_L{48JQFxtyP|z0qfKG&Mjgj>xCo|+HJyj*Z93~0;2EHG7)^PvRBTFL_ zhYzzaorBYLYp?<|fw;^IxKB}^c=5u88-?m{pa|4_3Y>C#!EXI%;VVd{F{5n2$Y~^OG(WKv3>7toAtwj{5A@+rz{k`-X2yAWMM2(&32ngoDZ2 zdD$_DkwW%1EdrL`GH_$*N>$?-6BXOIE08q3kV$>||vnJE@SB9kNLh zvXW7~-@E61Ki=~^&-t9A|Np*!4eP_Rr z+T0Xl%0NL$Iq`FD5dQ8n&Occ=tWIf!jTYTJ8nMqd(NR=L=)vLBj#r$V)K3=5*zhYH z46yK1xS7Z%4;Blk+b_?gHPkT1;*x2f+7e9}f;j_1Q03DE%WZ<+$=~ln{YJF>H0$|- zK65XxLwLIgUC|xX);BQHLqap=T8DHLW6d{ z$5!&C-lS<xL8Z-zv>?IWK?aakI~RW!=g$X&tko>8wFLO0^4`^-Dce@6JhW}#}?w7%8)@f zagX_xU|h?&6m=)6j-=N6;nzsxGfyHZ0(Aw^sX|onSX}j-Q*`5wU+pg<<4?Z7&WOUr z2`EFA*D7#l=bfM0oQ|n;?AyKSW$Xf7HXJCDj130Jk${I>mAT1csc{cVideK2@Snk> zrY!Sz;XlR(Q=m5}A^=l5H2K{b<)#kUV!Ys}ekM^BWxW{|77lE18qeR#JWLbx%wb^K)|&y3_p$3aqbSJ6%o07p>oZxPLE9s-Ck}77D@zRvLgD(d_*XVzJe7+O zjLWl9tL5nqPJ5bI|CYS!?oRhw?WE^XS{)nvRT}hw&pH7n+zY)$QC4@f@$4oLIO1ynd3qq7ax8DS z$wVx%%E(Q(P=69ipfq^tWi*7)5FK;2#1`4U4K_g1^G;}IZa}Jpv&i|+_4i0xA=oN3 z$6c7c}g5p%Y? z#XJ`SQ>}3cxx=xKWD14q=A9$i{-G;ibp8AL4T6G#7!NYYUVRR&X2D<8O_Xf}l#N6f zFtUKIT7Mp23gXL9*v=B>XVws;OG~~}tJ>rSPLhL}nVInBYrR)yrV5jwiZ^9&kxD+@ zH|s`IBW8|!6-U20UIiMZ0NriS*xZY(cPul^v%`c^Eo}80J5H_W;p<-Ceuo8o72siKp{>%m5(2(R=j>^e|Cey zWdbVw3l}cnmbp7PcYOH$!3(sqnd+RP&@T!`E7=IfQKa*K*<7fFH2vW_vzFvPD;y{& zZ%i$F9&6J7Iy^M5z5d}kBN4wt?rEc847fb_`4h&i_lvhzf1LG_cPm4Tc~B)t_WCX*ld7J6yA0x z+|1w}^H-=|RcK{*9Axg1Jmcy`snUfoA%w4~ZwxF(3c=8C{Wc$0fS9s9Zuz7jBC zSFz@{#sm4SliUAGnm!Lm9U61}D(>2E~I2-PknOS}a)vnXV?Qh8MK*|mD2Kb&~=}+ff zjW`l!inzEvm z$vmm3st$j57nhTlC$7`hRuyRU_H%v6QDCy|*UN(|@(c7MP|qGO?%&cPL#B!#ZurMd z7P%;r&uNYfM9Ni~p3V!~*WU2{{S^U*5D5X##*4h9?$nlDd(&Ia7WJxfP_7$ReIwW4 z+mY7s5MKyStIn}3a%ZnEl$ZxK0;{mcjir*y5byyM>;hlx4q-8|4b%Dy2+3iJChxuXUf?W?45$u z$DFSuCoJ>1JbCpB4PSMQP1%s?s&eX`If=9v5aBrVkt}6yW#v!Rc<^ELv?oyXLUr1V z9BuAH$C*MCw6dcrwYkfYDR9wZ#qk3&wFt=zD&>YEd<~NCG3UBz$89c{>Zy1SJuoqp zjLV_Ar+b)={rAY#;uQRoCI$2m?;szBGxtW9n|7&Tk7cnzAZNgUb9t_5QGf24A8Yf-HGbmqXl44XqZb|FfO9oSSiJUXW!W82T_ zd34Tu;dvgr=Pn(NIQNJ?ok+{9efuCr!A*s-N8pfR@~0coZq+7MNHjtBd_7Lt&um}6 z3=y{^92+@IGdYF_5CRII87;~1%Iekzg~=R$4=Z$Hz~MGu_3V7_m&i#VTn#ObR-dj4Gb zC4%l1OCQyU=E6W8xi9C zY;9`|+B5$3b-3dK@T3Nagdo}QZ(r&Q#2z*Jo|uX$<_Sb&i2CR9z#nm?I7y|GYleA# zkc6mnJ^+A#NHARV;JiUoz@2@}8O%D`Oc1}sdGhh2dU0T(w(PAq#S27MQ-?Jz9QG;) z;b~@u-Xa-M4eQ76f*gl{3U>-t7tsQs1H)bI-!K7Gs=Aw8y+*`n3dVIH)d^OSO%@TH z#adp>C?Zlh0}>JwEgKpdxH2#C%~I}u;K9sM;$heXFHK5W89hoqxA}EhR&w@>`sCu- z1}z}13iT;q!6RU(43VFUvmroe^_K=8DJE3Ch3WLU8r(W4Nx6AJp36<3$|3E8}_ zXH~>_+m{KB!NlY}ISGbo`>3#0gO7AB9@+_lp7kq{r3Mni!{bJe^G0eXni4 zXeA!8Z6gb{PW^h$JF|2@ajaTsHZS2_A@So)y+7sR*N$Vkxw%)aU88KWput@@=LH2^ zWW*BRgK|V- z;pZqsGssFMFL19!v|gnSZ72jy4@>`3JXWBG1nMib`CX8>2d)0rG78SJfA54 z!qEtQ8;Uo9gjV_xs;RjrJ>ST>qZB2W8t}*dfJt`r&+knT;)oS;aO{jU6z%kELRKVE z?W6jFGv*MHd2^z1l1lZW913C6M| z?)-%Y>?Ld@{8*fNgcS|of*>qoYq!`5+zJcwr7&aL=DODGs?yBZ+?V9RId^kmqkaXA%-LGpp#wlHsYwNq! zSzMo22Cc5gMKHLE9r^5dT7roZ>OJHa7{yVOXzVo3>B*gP?vHPu-m9FaYrap*L$T9? zUP;ZyG_Lh3Are?wVa)#f>f!CAD8qmg2MW-Nk={SQg}3r6%V&c_!sExbM>45tp9g$* zcUzz{S^TwVjtm_HDMFY6Z2wqxtrF8C!`M+u7V-6w=zMwaYZC1f_{hIveb(vm47T=| zRD11Anhw3~ZbyCf^eHWA0R|bAMGzFGsK37Ey?hvr)9EQT$;eRl+O^;IfQN9);ja-* z>E9R+-TUd+VY>GruA!F)-`Gm-I5YF6D!@ek1gK~N^P?yz=bTK5s!pYC3yW+(A}7K* z|Lpj?>GSs*Y%XMR%3m4>G!j02%3q77=}@3Etu|=@0D%Ov;$7o_tRZ=DQ2b_2T|jgG z!|rs$JR)WB@7mu#m$z#1A1|PgdLYqlSaySZ+`dm9)}T?a-K`gXUH-e#2ascDy@33w zJ?%Fca*QkKg`w?5seuX&yq&P+5Z|SXE6J>Pg`l=#CnIS^I_K_tuRnBo?5=?FqLsEg zFv$(PcDGA)dvU z|I2dCVs}1r0!Lwy_%-eG#|lpTNd-GH;pVbcWoeu5&kt!d@k(=56=9$HY?k@lhb*(- z{{7dlUr&y2d@`}+SP;Z^Q1jW?|BsS~a?wFBSJiNFBww;>rP#SV2*(aU?7A6Oh*^bZ zWBCkUl^XTqvk^z>%F1hNA*RLn!QXqweHo1|A3`D{N*%)1p^uN~qP&zN8@|L8dgTq*;&(<+Q zH6)VI(bcuwLD|}qRKAsjKu#jkl}K?V&ZZDw&GeCQi%eJM5ycRtqR77z$?X&C%hNc^ zP|*+)7qsNQ7z%O>(UZ1P#blZkblqbX2;vNY0hal{uUU7ZE)PRj zozvrGIvDHfVuFMks?dhT(YhU5aC={1qnY_y*!U=AXvC&WBbpW=UWCRooO~ki<7-VS zQmD1YVVv88S32CXE8_I5#G}OyuAIChV*rjv;jm^nc<{WM8s+vO8IC`|Ry~dMUzoA#aV0_7b+&?|k57h!g(-9bRtb-d6|V8cSE*)tVi&)? zXPwV{chk=9HlP=;>X5lP2V!l**&>&%T_IpsmY|;0`Sv6J_*X;oR~yTXMVJ@Q4Zyg_ znxS>a(%_%EaQ@hTU+JUoMAODT(C6`k$!;9N3$(@#yjMJXqcvSb*hABW9ZH5?Ki9=c zg#}59Wi@R!{-pC3~qv zTkq9fX%I_Vesy)qSuc{S;d-kVRcU&xqlMC)U3YfLHckwYy>%=DHpXbDd!u9D$ElvG zyl2|npFV$nONfm3`42jZX0&F}>4rxJb=u{=yv2K)v)Ove2ZPKMorKBr7boD^n3z5w;CRh8MIxV6O@qnoBo~T+4KHJU` zIxk7Z`a%m@ZxKQLNnaECcLCu>Kb2B4@nE1v#BFw@xk5AH8OPe8Zi#^2zKq(Ot2QT$ zjUb-|7((2im6e-yMs6oO4=bfqp{~S%QRTV3ix7w|q$OH)WS=z)e*b-n-{x)JhWn)V|X-^I*tP?@0BzhiMmYceDu~ zH_e-;OOd#}!pbXE`cX4y989yQsHnR3t?4nb!@;|7Y~b@#?ej;?EmfgB)rUYy{4epg zN!=xpw)n~Jhl*6Bgv#MZ!SXkUsuQ@Nkxz}tqVVy{>)Rz3r3VVsP`|>OrRNgCJ&?mK zy1orX;LggSe6hJX*2KG<#@DJ%p=gBvebRhQx!?eV@?dOlqiot^d}SY`GiH}mIZ`KI9swAcHSa+A?bQv^CUh>m7l zH?##crPyFr9$yrVXqqgq?Y(hQA)StIPSbO@*!?GWpL`^DTs?>1iXJzd_EWRaMWA~u z39YP&sz-~Tob~fPes<0J_hu?5Yiu(j>u#yP%j&gxbpRVQPI(vNf6^Xo?(50Y?D#x%xNxyqo4$7M>)()=T@}Ok33xYt zvMBO~eoZ3`?2vQ=mu_e~Z)hpqc;Wh9Va@;=kRV!&4L$HmDx5c6gK+k0@%pFn$ZH=1 zy|)1n`JeZL|E(O1=QuGSu{x^4bs_~3X8Jryfnn;Wv%3JNgrDQv4-OUe%yzN~GgXq6C`N^vL2$cEMV`*I{J! zhC{Kg6Ju#dEBi=BVhFq<0BZH%0coPwu)N?LB!Y7(~bO+pc_j5aWQ8&!k|%Fpg8WXjA<#0()==Flluk zy>N!hI5xiF`SFG|8^&kGIVCmVE^YmNn@M6Uu`S?h3eCNi;lgw8IQYapLMqcX@%iez z?x*DT*q)%3?TMDAeH$&FudQ-?KlC%yvORO%uL4O!CWuW{+&}Q;!wG|KE1M3tE|r)} z8+cD(e+y0h73FuZ)K7_V-+wJvmrtf$P%*JO!>*l|RuI8s5aZip>oAx)O=8q8KOdcG zgCh8`a^@G8nX6UH2Y#H=?mHx_0Y12nBeH1;!rpe2c1aVjp2z#>3FwMXzwCAMxpL9E8Rc5d1nwe5=w}hI24Kc9cm(?m#um)? zmhPE_n-5hf`sB5=q`S}Z4wen>1=yp^RW>WIC6%vY+gBiRxATAa1g=fZs|+uPXH%r% zEUH==t0*4u5ld8ssHYT>B4dyhv7)3uVVD3hEYKXeCjUt(E9 z^H$#eUG1rSX-V2f$1^8}5bPiWHiU7X|MPpX?GMhLxSwAtZ&XQL z_djU#%9U-ZS<6bCGKl%bpTYVw$6mR)58;Sc40K0DL;9?Rpb$7q-Dpad)4_G&{wN0t z7NNYRthnzrzJR_IS_1E3U|@Lt@ne`-U9{#`*}SJq#?em=u`#>wE*K6Sy4N@Rn=_{P zmPElV6)YEYasW=%!bhXxPbHOVqzXyBIsh;k0uTI}J424OD>n_43`F9Tk7zAD;DY-lsb$Lw<)U=LjFK%8n%u^h2E zix~$OjYU0mnRd%+2q&sGK&}wqHABmFzfK*orP1|%0rH>4Y2F|;WW}r6OjO>%eO2~V zfjfCFFagL%Fz7<>$m=wVQZ7r{L~W*}3m)v{KUY?+Kv@W|_^5EHp?6-Mu2jQy0^9|E zwB_y7by1U?i_7z{RcTVUgi4I%Nywa0^3_i-8-J)Sn5_X9Vk>Hm%1EW3zH*mAnp--~+1{d1D_MM9_hl|t04BH<9O>jaW+UM$rxy_rKqNXL!sLdm>59v8gj%8Liy}Y>Ly{v;^kUUN~ zQ&~gXH`9jZ&9`&RxRIdc=sc2o;=s8N2U6Ro>k(g8SX~{A*cg2tcbVg2M9)# z&~AEr|It76rNL=&@IZ(X8>Ry+S0D}nQU$!nP~Xb73NoEJdjy+~wdbE3MoWAyTqWBG%e?#?~m5ML4kw^6Km||CrcyIC&B1oKgPc~bJx#=MV8!Wpv zL0|i8C#dlF;Y*crPpsVT3@c#(z&=9Sf!QVg2+IuBy|Z4pRze$doJnDtipb$9?tfBj zn1?tXlB+g0*@&0tMt(n1bY$a)K7Z(D3v&Bx7|#DPu=`DQBz-3z>9lh>@hSj3gAkIq zy+NCysdzn@GjUIek=op;3d4@RKL5+Vzv=y|tlbocpyOyU<5{`wk0tIt)f&m~=+M~u zrdJCKr@T~h1G48jXyoGUfSfg1raA{xRB-v9ZWr@7SLFdprXN*-&)i zv2`GWC`c|1wMi)6=&jAUvS0o_zxO6>G=tnQ0^kN^(Y@$;*@BU@ii6P6O_hI%BLE6J;qyMOZ33*a-F-F}hEcG$<&|c9?R5>qnKBcE zy1&jdD#Hms<|?*`fUScFE`qljb43;& zPx||ga&(stEKI2XJ%ITaNYK~jrE6A87nhLuj9%rPTv~YN?CQJTI!)4s-7-bm3IQWN zgfASwiok{mGsFLo5mvgu6m9M!TK5t~QW2eisY_sV2z$q`@jbUTX0mW!T>ELQc{svV zql-VoDFy?aP-qa}5<#vkRAhve{}SKn1Gi5H1er46<)Q2X9Wn}S@UNw2enL+PeEoSZ zFPG;ry*gB1EV+&kH8u{`3xJL0h1X@Nw^vv4pU3##^~xc|wNuU|TF=g)J2 zc}4p8_?(+P%z6UwClL5Q1TYYz-$yRU7y|iJSi_^9GLtm~%%zj4{3Fu>;3**_Y;blj zFE8T@__tmakC^8U7^lwQ5Z)bS%iFej_gRF$k_IhUa)PV-pZjbK@Dkz1YiomTWFOcf zq%4F-+01y`5HVf?ams-D0QQ1^5F!kp&=A3Bz?u2&Wv_o#=%J`(TjR11#t|tJ-<)+^ zpVGvY`1xaS5@NC~KxfTU`uw_J91$tQYzA#LN=b%>>pQpgIv)J?oSLj&;7D|r4yUwc z#aYZW!g?YoGRQ8V?8LcsoIn&F$%G;Pp?T`{@~z?@>b6&{N(}*5?tqjDVgO}TRl)^{ zoQsE+&n!-sxj(M{s&X#FB^-iX;#Yy1f|idMI0d^Q+`xbj;6!11iPj4}2|T;lK|ta_ zjuV9wh&e)>#@!NqYJ$&<-nTfSoW}D~hLcXE{aF;AC{Dl#AH?XDetX?f&p%nq$55!_ z);24AjM7{K(>HdG{&#-R*FBzLTxU@(y>|+%IL=$7?E)$}hd_?wk?6MJS#4#!Wpniv z7{KN!+cwkK{r(m#7kw_;Hg|||D%#R|bSPB0MZF*mZP)>z5;qhG4`7_k2$A^5DyE^0 zHX(9r(L{aWalwbcKZ&;Ce%;5HvMMTj1tYd%JirT5U(1m*jP`}V1hfwRO9o+9E>zz>7{ zyhxy0aq@f2wV#Z`!itHZ$GTI6U!`>f3${iNOE@4gfV>6!qQg z!&khw7Ue$*1uJE+;*ZYlw{QFET49@^*>QLDEmNaT^hI&aaI8IvL&hHoKBG$ zo_k+nB6H@YmwMhBA9cN=VMs~NF^yBZ_yq4h8;W&($aj!mugNK9;ot$vbc{@KW(u8# z5*caM%4v$sp-hq_+ngBn=w%C02o$6IY+0@f@nCMCk6)4fXh2<>;L@?%c8pe-#72^= z##-rx@s>eFa^xXCrSy;a&98MnRA?Tf$~eQNx7&6jDU}n74YowpX_!)ZPf_ehnr2V) z3xA+*Wgv3xR9w;V$m|H@V-!RNnUv?`xIWC+W$epuoIB-P5Jfwt_$9U3DB)Q~47ViZ zy0CGH!OGH24coclya+v3<5JIKbL3(L0jc(_Z_F6JFxkDLJ7>W2?W5;;sZO~~e znX9_APH)`i+^)iV_*=>3b9YBq9mVvaPj_+}HWlUGy+@Ff zMAN=3@2{+AQ^o734DBrwB2!^D`pVKgcNQ`P7P5P`=jm9ekW*e>0CXOCHMYAx>-ZwC z42o~4S^SUj#pXB%okN%obp`;uW+4?0WM-nq??VN0@vTSHIR*Da+v}!|h4soM>upbz z8z8yUVXa&_H2<`IOaU$g?enQf*3}eLHYatJPFMSpAsaaP zjeS|cRIN+ge;2bq8==}^M=@8T&Kbp?>abH#K!BMe3_@vgBxgP|Dd6A6Jk88|Pd6ai z#CN`ulvJ-ypz8UMN3=h>ycIOA@KqGmt_$G_- z_6EK)`(Rf17J?F~$fmD~UW00+Zgc}m*i%k4LEgY)TdF}KXQF8~OHwoIdX&772EOXx}lrgRze>49RB#W{h= zAagn+uR4*fXGFti&P`Pi#Y^(cuH6DUj&tob?Z<_frVaOzY@YIj&Wbm1PCx z(==qN>ukB+aRmG9KIL$!#7w$;XUWDGzDolWruhd%jC!>O~w0yBf09Rv&;jOI(>}e&@!GX4 zOo19>v-df;b9nys+^MJ0+wj4`_uuo7*q75206`77d_2D!0n{h*4v)LJ7VxNv=|9(c zL$VED94R@7DMRKu*ug8P*r55Ae=nL7-l&-ShjK2O({~{~Sy3)UQ-6aysY!$O9VTqz zP@zPdp_%oiupyr7C0182>3K4opbq(9@t}M4Io;b~=_uA4|0u&ExVc`KzKJ|sB$}BF z(?SV<{l7Ns#tnMFIbajt!sCk8p8^0RqH{#Pvz@(grs=I*XHZeKvpxOvwY`|06>omf zDQ_$R89xa4!N;8d#~@F*Hwh$8(!&R^Y4L zuzgI?@c~Wr%d$Fm)^9N()Zy21^53Adzh~4yj2=L&3;0cfqJ`cQ5+hS*XBuSSfKi9C ztkp^Wbf7NpA8ty2>LvBd{!6R9s=Cgs1-d!V%-}(wmL)u=Px91A!oK~S|WOI(|EW$-c}By+d zf8T%xPFPSx{#RBPBGKyM4><`FCj3Ff_kx28NETpHsGh82ORv+=uzWSS>uzX=Fp*1q z6Cl*V%pny<-_Y<4z7RxAur`EZAl;<~ka+lko(_Z_T_AokvYn!<5PVy6q_^2^{aHsz`14}2GKh=D;LT}36R zgH9FNJ2vzawvT4Ah8))%ag&K6C2&ci-B@@{wW1FkFUGIDwk?G?6Aclfy^aJ{nsifv zUm@&RHGluyMBL<||FNJANpLdB;7644{lw{dIDOim zFvG*#gf1DmY*KXbQS^hit(QaN0`NdoU%+Q`#j^GFLpKO3ILCHzi-wTSt0P50A(Y>P>8%m;<2Eq#z4>KtQXm6 z(3L`%N93RoVTUk1BH$ft0wMbb+kh-F7;F5792cy0Xh<~VfF-hqpMz);>t=r5ad{M@ z#3-Rg#>Piya6}xhla6BVUQ=}3M6d*~Gi;MF=W@^+A(n>OJ^OAeFQjRinnQgClz~tf z5s$;;*AEtVcX#FkS{HBX&hX3^MJlMNslAz9bqq{>mE1h_%l+Mbd9rk)oses4F@;(| zp6I{h?rQ}|wc6~qmM?_iztux0i8n zh@z8y12+=Z2WSkBpWhEAN#68>AQGN4JkB1o^IUIBar~^Vy5XxphJ)J=6SfhJ^+J&w zR}L7&nM5Sqez|&laxqc@ZW6(}@N^M6U+51QD9rFOkl+I$1uRt1QV>r?&^kTL5-C{# z!ic0V0@?>{0ecZT4~}6FB7aoPesSLR+j7y&rbRB^^@U-{t;J9u`^*!`7RlJcRvhDL zoJ!wkX9Mw393Q4WM%rSqV^hRTuO_1R>Ckb9k(F%1w@*9o1vx*AVr}&HT@rVLm-0!;^KRf z+iy%htvWpAO=qW(_q$y&!Tm$d?9TJ&umq8``vaiv0iceAopO@*Nw@1K*&Zv7nW=Zz zFJWGc1L}%A*of>iG-o#A+$DC{ls~PgyYg@!C1pWDe!d)vb;@nuo@Z1v?a4}aOoC{- z_4CHikTDV9N%(I8%vAP^m70a^Z!tL0;n-yb_b<+V&`ktp26%~hM(|`4nq8Ck~ztRx|Fxk(P%64HMAIwu%VOar+u!<}>jDVCn%NOaOiG?19_Ef&xaz@sqFZ zL)aOcUzG_LmT}l2RRnVaj-K86v4c2$^m#Hl3!0xcsIK!Iq6?gY&C}kidRS6})^5tV z{3eM;kw`b6Do7<U|&2R-sW1h06*3+N;xYDsc6{|2d z8EhIJ1Re^0u*YyPxBf*_`=$Sp3|rpie^YBgGVn>zW&bGA%}IeX?mv3P-!SyjV8h@C z4+}*r6z=gR@_^SMp7zUt!L9EBi=;T{anRQozYc@wDB-I9pKlJA%OFidgZdrYVb!wP zYJDC|y}xq(IyEtfZ25>l|V%BWYz*jIv0!J$9!{`|Jgr zLH2&*lSJ+z-z@OgDBW}ApCS>F~=S~K8_g_?~J?!kFQsI6xUYKikuf=O+8tmN2z zY-6px?+k{EB^0GuX}EN5;J6}=Oe|w!`}Oqb;D%*kph#8cJV*O3B+!8SD^Lii-7)3+ zsZGU<8x|Js&P;i;e~N|pf-)~I07~oC7^l&U$zyVPa!&`>sh1Ym4?ar8?DqJ`6P`)e zh9f796{Z&QC`^q*v!tuhGNCL08m{`TUtJn>=OcbAgScpHz z8m`bs8I$@9?oJKN5JZ~oT5*3a)JVt{^y?gtIdNfqR}7sqU#vJyX9W^}v1USgWE$%g zoHH^f!=GfVb?`}aWG}MR3!s7|1Od3R@fdMk5LRBCQkdz5$AkpGB;1e0k`_X|u zAp191%D3){wc#g#cr$e{Uf@6nJ`ak8IKS6$KvmQJn?lUioKt%JN{(3sS*n}&OV{AM z2jFUexL^qtzky@vbQ6*8s?9BYnLk>o^`KyM*}5a*Ni=P0l4L@!P?)4 zz5i~G97CQBsLnnAggh9>wX@;6${Ps68_48Ysb;D%fzQ(5qtFl=Ejvm0 z!wcl+6$=!F8E{;jiFy{ToGt(}pQAUwZ?USuslL9jqD-O!F z3pZKv>@M1xabBKPny@x6WFS^KrtXYEYm3TX8iB!flg92OGrKT)2VHCL2_qO)1PUAzftg?f@jtahukcP_~>1> zZnDf-ak&_ji+ry-%+T`k@%d|VJaQ3;#l_o?^W7OTVnT-z}UXv#u3bU!~ zr#c!zTe0|J(#k0D&Fi^+PvAR2^ea9oQ3m1=ehqZ+q;iyW)WWed6S;5ras0+96ZQS=HdhB6Ni8N_>q7ZE#CM{J592mZ%Liw z1EGN%73DiCBvc}TB)A>ALI#>KJ_bRn5y>#S=+N>}#DKSwJV>;;3P#B*WP&M`xFD+2ll;hB0$V$A{ z#cS#|JJ+;ukyoW=l_zsm64F`Xw*o0h5Hy;cmCMWO@tny#;d?&gcp~Ldskb;FkzO6L zYA&wW=hy7LEA{GA#R4dAihx={hmni1IbjiJRd;+5`j5k(GUuvD!f8lPVjvCWz&*TU zRC6s}y?ATV<=jB4u5J;}9$%7}n3(vyveP0WL?c89ez1j9v$Rf%f4yB2y6|$B>|V_y zEf{Elh}{rhx(eQ)@7l_}O(fzAN~wv_pIS>#_;O9o@*N5u$51sC$zFp>F6A9R3N3+M zhAU-2by1OF4v)nMHx`8F~)p-Om`}x z3_Q3|aZy!n$ogSMRPeJV3qu!C#!B>46G#E+=32Ra@Qpr(gPUmgww^ECT17bro5&Tk z7{t2=@)bXQ3=Y25pghOC{u?Zz?n8PReUmaUIsil=mJj;9)VKre$@@pR*m_kc$`a{Q zW8N+E*}UagWE?Q4S4uAUVq9Y4BAzBg_!f!I5DV(0ghUY1c{qm`JZHRkgvUOkZwJ7$ zIR0)A(KbW1jvd($^&~Ab6qr>e@aNEyQf5{u652ZVfZAPOStpkULtnt zix+p0jWY(V5&CIKGsPrXkr=}s+ta=e@^JqV2p*H9*8>ds2wB_6KfBQDyRc%ARrsrY zsiJbPRpKrH6Bw{!^OEm+`K2R*m6Pe2?t6z?m={Dk(zLlzGK>=?%*t14Pyiu7Y0x{p zd+(nWJ)$GJ!)C1o+Kvx1u6&qxz}nIn6LHd|Pig=wkDvwr&!;X}bldK`C0%p*kA zLy8kmNIy&;gWmk$4ilIzfv6C1ji5uW=B%n_-$;BToA}UVCXS1X{yY_O1%u^7nM^U6 z>r>`mb<>Q%XUxOX!idj6ojiftkjS{e$+#0lPZjf7;YI`*vGh-F6~H2-p$Px>;DroV3WMGF5NAHCqxp zNFCh0#zRGR1;-V!y0DB0M;~Yt&Fu8&%tmJ?iVo9puu!fG@MJiH<&KMg{8*pq@L{8l z*;V#{)Q=8=#yTWtW^VvbiYkoAPDUgm@TM8Zyz!h)vr(a8GC0XTp;2)N zIUXHP*Vf(+{TLGwzys%Z!^D~K@F#E0W-~SQczq3&2BXOg^DS*&4>m5VQ3!eupbAvh{fRZ-#HNpiPen|o^e({fwK$xSCkn|P^l zOwPj#WMQLu^u=`I`*-_D1>i?liu)miIUrb8vHK+Fk#8>(o}yYMb}@us+HRlcbe@w* zg+ks=;H1Ph1z@+n?%DIhD$C+QWS9cRez~)T$gmReU__^f#*cvJX&#;XRwv`+RpJ$- zwApe+fEG|!tUS&ju%epZCVu9NuCi1-uq`Ezv77}=dlyZu6*{#(3>a6TB6SegD>LTj zxHnJx2KWkcP_Qo-zxNZCSOVjL?1>PBAmJM-EVD1Vm97)I7qtx^=k;387GKQjBDU8b z49)|LH=$p6tZw~0JTAm%7ZoK#-UqreoqA~-r+AA?t`pG;ZwcI&=$6ngA_oxngTubd zc6v8Ia+SvA@~=J&iLgiap4(ziM=mI*POm;<^6W=&LouFM!fSLEgIDSt>Y!TDGu%hXdeUT}b{>pq(% z5C|J3Mnv$T_8YonWK^tBEIklTcHSySnzyC9A{&~0mxv)CX2`_+fyYlMRREF@LR>6x zXqGyx?fv+>bDP{}p3nfP#FS8fOe{TW_nE`z%Vc>nC7@;k`y<{LBO^u?ye-`AI#)r% znI~_<25p}#C+!>b61XB^Uf(NQJd(*udsO_Eo~?>^iAiV@^aC2)rJYLf*& zsKuxJFl3<7=HjPltXvvA({@bgqDLcMg<@kksRjQ zLmLOLuFkbWW4lNcRjy*eh^BjT15=+v0ND`>K3w5kwqo*t)pIwGuOiEV42ca>sa-SV zSb`Vv|Ltc>UOrrg$IbI7{%EUGXGeUj$M(Yo=y;f_Y#Qj-^{uXMvlY)X>CuO3WEQrD!i2%4T348+jHwKF5z1JGg zYB^MYsMcM-L~CZBv~>{q;BWAX$I-;4LVOmMX`5t3nGSD*>EjAia45edjW1tL_Q7B< z%=>+=Q)!w(A|lQ7;db=o?PpPBGE!TLe>{6^4**Iw*B zlj|ij93*F&{h4`@il+sHoIfiw(*zAu(2-tWs@bt9I^QP}6=$BzTdXRY>j%DLB_KS! zM{P%XsgGo*eGk9 zsT4d`k$q@k7al+QY4BCAQP04J4r+%8Jv({wF0t@`dn2NaKo)_$-l^K1FfayuX}Uef z0W)&mVU{OU9vEqifEMWXz+l`3phd7I7(NoGJYFK(FqD6{*wNP0;Gwh)KUZndwcqRL zwt6wf%N*lzAVjcTuzRu`xBnq2bQVdEAA6L)c=6-w=;&)C`tBM$vTJ`|No8xN!+(Qa zAHYchkix#da{KlXev_(mP@%%2gIFNq3NI_`E7baKV_3Am>auV5KyQ`K3=X$3$Psry zCJQGR!c*boMN}b#LO{0Yx-`pXv+0IgCeOBTJV{|rkv zwrtX1Hj6cO{_Cx+t@{`ui};A!OBGZZ_uYz~w*7~g2~5wy4;#()<|0V3Jo?&pk{*@k zDjhVx$Baz_yzx$^4diiE{DIM!9yXV|Ugi4ylInKeVfp^qRle1=wGj+$G=e1B zTUTZO*;oI8TScT7T!?t~`8eLsUNSU{2XLvo?NElSiqjvuf_P^Zw_bfJlnEmRM6;lN>ICWdMwDqzKl)~{q44Le-gt{HB?dH7P< z>9poq6SdT>S+tJWhQ3FzyZ~ORrokue1mk;+$m>vL?%#&u2PAn@sMnNO7vWsn)AsRW z6ef63GB_dp`Fme!=ec*SuYaq^Ub{E0f46lsF1Wh;J=U!YziVf2Z$H!|-*Sf_{j&W6 zd$)Pw-Fx_4x7trnE+&m(6$Q&@HY@-7zF+d$>oo~h;=8P`q-!|jJM^i?pM2Dwg9M=s zR&}v*#@`g`k_v~{JBeSg92^)ZpZRF#HHQXj5^|0ujY7N|hi|RDQkRk%S+qOc;hT>0 zG7+BL?hjTl<=4a+q@o5a5E2qv5F_%Gq(#3s_u%%CA*m-`>J9yCBv-6t{C)~WWI09* z@FQJvc+)`ei1oQXm+Q73B-_i&AG~b(z1tQx>Yh7pR&}3okC+s9QxFh@k~Z)6T9zp@gq7EPS=iyfCmDdSP_Plp|XB$N8M}kA5hj}0#vjx@rk)s92 zre$Gt)Fx~7M?+v>u8$^o--Xj0t6gfD?S;mI zzrNFX)ia#=q)u^SZMMu9vLD|(v@tU-Hg;A^Ma8>QCH75yeU$T&gjvoDuioeCv}y=J z`{8*xcpf-^x8Zo|pJ!QHf0|w&`orqzJQqobP(NLkpi`L#aiFux>81Coo*!8 ztJQj)nEhp5sTKF~nM!@2xJ98wAq4dZfK^PkDk;Twlr;WaTYUb; zRstbsS!@w#G8uw`f^(uPE_s2JmhSfCUwz)qooKZUh|e^eu>H^6KdQuHwz;VW0aO0b zO?4r>m#dbG@QgO52UTV$ReRveMj!xckDsY#GqT&$-|=4nXl4Glfpk+hSD7-)x4m!7 zm63fB<{SWnU5VrFt*X&5AWp3&5Y#t6E_Q*q60oV$!?5elZ0H+vB+9xW`xR*ZVX{|= zKHqA=9u91~6cx2mne`9&Kf?$(IRueSR!@$r$t15OM6QkpNh=5Pwoi=Yepk{M)XtJ? z?1ljouMD3YEqPtyDt(_ISJxk@C^sGL?3On+Hw)d&zI^M^!!Hs%gvg`=cTC2EwYKD1 z4%*7CnvU-)TgoP1lOaa;l{Nj^*R40=oyYfmHjx@qR9HBA$&b@?PcivGFwA&A7CaP< z<|7a`QM5q3`}(u}QH|-LL!pwF^|DRaawQ%cqMS(e=|xiwjO>6H`{5B?E}ivTYoQFl z$ja8%))APinp0X^XT86UEIvgfgEN7MZu)ep`>Z1)2=rro-6<1cK0AMGOPYR0YQ)m-*q=uAVo zmqdeX3iW73Wo6A4VSXo8aRsI%GelWFe%|1X1cLv4mB&iQ+pD&tt{p<_YgRjz6*lBs zo{wcPKQxya1s85czE35b_jWOX$J_Au%9ufUzBK#Nk zwqkE>S=nAliywDUs2~#dZ6X@o5%fDQYGAxEsmA3|dm(nYvaXR0g>urys=E!<{FuNsWq@wn5;Qd>TFG(lsxtY-AUcWB%L#AvsOz>1Tu_Ly1skgepA2ehlTD3{f}cuEMmLdW z1mz^2Eg^VY_O`fsbxfq0-LtqdS~cDjkzf$+?{g*j{CY^^9N;E63XyGS64 zxXm0)vdBGYm(n`cdyb_0seDz><>;(W3cLm?qM<_RM6z6SbFQXBe(lcb1pG{Nm-ImN_u8omcpjEd@=ru zo@R#*L2g+bPM}R^W3r4=s80kB>a^oPC|_G&BmB%#)1o<2r5u(IaJX1OmbE^6D73KU$(u zvWUYi_~jZjH+weF5gFgc1up0LCZa>sNuff-tyJu?-n=1&l53TB885R6B)YbdMAPxe z$g2c`x3GiZc)OA=5#rF<7P;-H^9!3Y_bw#24cUJnH=YvZg%`#PABj~wT-ybZ!Q@Hd zonxGZp}sO8S7Bg64;Qw&{jRO8h5F0morm)+IBDsGw2OE&UaDx!3DJ6JH($Z9!ZA}b zXap`%zgDt}U#JM^5p@9Tw+=H6mEdbAR?;ID$s02yB_-#Ww)y{X6~T9%9XIOURK?vw zAbe4vX*WRK%^g6Go)rQDp;9oUSr;W6=jb(Zp?i$mSE5!Uo^dPta@rYdI_d6E#b$7f zQD-(o?UtnyYX)bO6O9>$kN(5TNhC+AM`x9>ySnB8P@DfeI@NQVR^iF`zHp#zkLTBb zx5Yn;R+4l`X;Oo6LRHXi_p+}(Ly@5ehr_2l0m_Fpl<$;He@MK9FaFE;bcx1^mYROw z-^T59X$t*57VIfQkD*_6UeFlxi7CIF6@7@UC(*EH?J6*S0OdwVcl5WP!ttc}`u^^+ z$o=Le_13Dl$Wc)Fmo~ASTzdQbI+PGR+x>V@B`o4Hkb+?g;Ge;La+XZFmEdlm`tv;_@ zWMs&%BAeW?5@b%^l0cv|y%v14@R3p;W@M(Ixin-}&rNnZ3iDsQ2e+5i0dZeGQJtRb z_z_pChB?lnfp~#!{+VU6pMiSWx;duTt-!F4_$u5};iMNA;hh4hiTbR*k(zgZX}vY8 z-&aU`{%>D1V&Bt5IT}uNW|^kzU%4>RoWcrLzpXY_IyIYSOO&0#IKvrgnZCTG=~FGC zzq&`(lMsF{S(Ri_{Ihhl{ycyFoKHIJPm80Wq2U7jh)*G7swl`TlamU4&)Z2hfdd3A zq%yIaNQ~^woh9vPCdB74V>~(y8s(SRhU|>v<}PjHBJjLu4^_KjbmWKvwDzPU&?w2Y zFZb>D%2Lp#2*3OuGOu?kvNuWnOZ=??JPtPC823~>pfS*C@q^y>X9XEPK}F#cJcR#R z93E?ub7%~-F--pdoP>mgg(Hb~*Xc%wCGN`ndpG=Hb>gcMIb%Q6=Z;L~n~st=r~&e( zUkGsFR&Ncp3z3$QA`r-in{4TojTpx^NQeG<&cWHr45{u$P_)UWtv46e*%zM|ZJnZz z`x7SNmlR@(RqFqthMl&}->5dVj3zA9K-sfGJ{i$v_e$FQ&bmdak6Fa8=X{wpGb5D| zBKkgH*j=Euua59pm-&aQXSvG&vO^{+Qt%l1LNY+ubyP}aSyV15{w|5@F(<+5(sz^U zw@>hqauc-w{TN5#T*E5Xx<_O9*#6|z7L@kgrpY4dkNH5gRnJ_GY?73ln+xME#a*V? z*YvVW61L+h`}J%uK3W&mg1lrT4BKh}qPzcuN;~I(omp2=upz#0-#$}D^gFW0fkbjT ztKUC^L+LEZ$CFstqi&S)pc+d@0>MS2Z`>n|bJV`h(wwI0W*n3xYP2l?O3LCASnpeY3 z4UJV*yL_wR>XObJ{?HEXUk>Ul-|}P2kqSF5JO7`LM2?xM=~L+FbH`Q2e>eV<<+&|- zCBc6qpT2O1KA6P{_t*oSs8be3YcDQ$M0Z_7-&~24_=RRS{+DbjrAvgY%H)~qW)vZA z#Y$}`+rYh_>z#zOAu;lJm zqV50~?9zzkRAttQ6^|+-9o7@PtF*3Kxu<3iaPrruwb`UR=^guPi!}zKEi9YX5kP;Z zD9iIkJUx)hXCq0L2L>sUg>5r+=p`M5{^0@@%}#9?I5RTLGOtH)jHmyzL%e$|~H@Uq{x>*kFcDp)(@+Bs&x zX@){ib~#^&NaNbL$-ItbglD%_Cj^Q=ul{XFl%-=$kmDvDOx-#c1T1flO-eNyvJPB@ zOL*=Me*^K2#>mFR<}7B%OobRq2FD#uqcXH|t7ppV@)aj_7Xk=Bsv1v>S9oX_UY9do zCn1q0CN4g+<5pw%^WvYD<;SeV_S+GNMxD#73flr~QgXV_4CU{Ty=$FosfclFDi|t9 z{elNo!ai~len^bDhUbq36t>w(h>2BbZr?uEyckpDiM4$P?w!#2#Lx5X-A|J>c8Pai zqCD9K^DKN{5!BcP7(7gobbnZ_ZdnPrKs!@YBpL?~iM6@Nt+GM>TIPcp=;FL!N}x`u zh3xTsH!oL+EGi9L7bfhhRGXpjngv^M zj{%R)yY~ZsY5^Gx!h$&B%DFWi8g6ecOy7RPh;((Y%Iirt*M7|FgxXIBYOcBB5tH>d z0+UWM+?%R~ErOLUSA%VcXg;HvKzO$fG)wMt&rH#bXl-ko zf=}9L>GR{>DOn1VBf*i8RlW$ppp}99RDN154+7}WJZW55ObBglZ3oe=JXSd)BO^n2 zr(+=GyffW&8%hPo+ONN$P&?z2LS1V~(HE$jwbSDwaD~XL=RT!homhM?aNB9yfBW4a zdQkIy1MD1*s)?T}Kkp%3!jA_NTZ}FCB)vYAfE1|*>V&GY-|%rNn*{w0_Yp`}+W<^@ zMK+;tFMt+UCpPpw-}0)LKA$6?6dT(=K21;h-8OPHZ11&@o6MSsZ!=u~B?*M?yp-M? zr#jTpS^-d{9s060s%mPJCP$C9nK82WC__iUdY6ohq4sN4ll?^5{fT!Qt{L5hI&BHg z;6K12E5WJ0X^|`U($1hn`24g-B0NTIa8bPS*FzXLc_9C&lV9-Y}M zI+TZxJ}jbK4koSqa<_1ci^=1PBdt|yIwnRSf&#-4T~y$rS6&vRBf05B`(j44Z#|KZ zXw-0Mvid4l#Z;E{UUTa)FHiW79(T*ZjF!`-jBHq~2C(wduP> zjkTF3gT~%{2?igy*G~D>*oAB$c&}Le?t&l?NtQsi>4zdrdL zQu5(%PwwaukMb!!tf16h5+_g>dBK--wTW7o#VKL+{{yJL04$o9adB}|nc88ypO%%~ zfb90^zLA{dqb+aby#ktf2&NS`$PTq0;nnNn`(lr)lnD#)j$-gMA3E^<-VhQBduwZj zT~-}S_wC!A5@q*ecl}=+__y>*n{pa@mC!Elb3E4bxfx2Vk)1g7~#vLa`>p!`-5`@HfzWsd|ROY#X z{t-?g&r60;IJdeevF2$mE-t5of=Y92^WO~j7B#K+StI!P^-|Ld(U0B!ewdxApQs(L z>R_`UVHoop@cB`*Y+856r%%DIoq;xO0WXSwUJmzMgcA{8ZT@96NW5)`-PDdAn1%3! zzlX>0Aa0gd5N)z#lbJr}9<$Wiy-GihxD&jOxoQdd>mH5stK9s@z4BN=y0y||FjCc2 zA??3kDq&fup{knPMvi+`EFdJz^X()tn#RECSuSgtC^v9wYpNZphV=BbwAnAvgY{}| zZl20z4r;rC><&EAyy9ME!Swjl*>#Iv`tdl}#S_zVw6QlqA74G4e43^l|j!@*q?t?O5SIP`c%4!ED-rd!EgWjrqePBVu$? zaIlO_&cS_?TcyPW>K=eCr5hA9^&1E#X($<0g<&*RidV_ESO$cg8xDH)sp$w;WS@*w zKGXs3;lxd!+!3-%VuhpG#-t#00d^)d9H|xEVAQ;nOQZFQ0h=sG)ZHU0V9LjR4iS%| zXCAtS|6+mCwr!a`pFhX01hE;vB3w8gH}0XTQj*; zNytcRknPx&c*T-@AbF)#bJEuwvA0sOj!ghi;Wt*rrrsP3>oKU$`7 zbna7aj`_uwwWeRY@Fwf=n-RCEa8e3eS!!5q!%W!le~oX#Z_*_i*0s3$Z%BHr1p~2N z`sP)bg5}JHF9a4@fAF`zbdHOQivcU>fr|J;Y`OC?54mpxy4%E^z&W<;#^L#=JyjZS%O&en6i7r-4CpU%rgLzy zG@;Ovv`LXh7e^oJcBbVrH~eH|o^gEz;_@n!8DQVW3Db7$=8T(cT6MV-1AVU$t&Adc zE5r58xnW<_FCkOHl=iiOe0U`qsx2VKe+&>63pSnnX{?fdeM{TYuumBDav5jrW6dJ`}X6PuLG-&zsyivK)3 zG<4R8Xk%+{uT0Way5Wv5CVg_7GixLLgPbt~-MXc+{(IM4>4JrqGYV4RDm*K!mFh8t zd|CJ6PG|ZBvKaS|{oE4Y0)Cb*qXEnPYG~r)_WB{nl-sD?O6R|*8zfM7Lzk#rq-1wo zwBG;wp0}zb(iai*9)>2)oFiHM7#!>X7r<Au+LfH zF4J*Q?AbVBQd7^1Q$*esP%CODP^WPF@O@B#H0#D^?K3!=;9r@vPv+s_yw*T?#`a5G ztvj{^E4CL>&u>Kga)1RmBCId?H}vV(Q0)lMprD}RRH{5U-JgdoB<+;n*B!Iv@#0bc zS%0GNfTt?D96JPdt`&d*_l}!Dh0Rd}~mqBo>_ z6_4!b)Xbu~iwmOP@R{oH9&)3;O-gBSMzdWCHJ2Il--q2q;xFEy^Ji`BpzK$fO&i&m z^jf~ObB~A3$1e1uEORZRpb&H^H6`WD_;i(FTfkFF=A^+2_9Zd(X`rBe@4RdJGOkq! zdc1{}l%oI-H|F~hAJ}YUJUKa#s9J-0d}gZTxr321GW%Sz+!$)j&lgJ?&icLoJXc7$u*qEbnw&Ak zTzDLCqs_pdufX(S7^MYXC+hTLiQgKWN=2nU+%tT=j5IdGd zwedshF>rDD+6<79ny(+Ympm{)Ys{rkMR}uniA3-7hga%0tJL2rvi|9S%|NHNzXckm z9h^sj#hv+)-kpIwCQp`IG+hK~s-q)!R?UV^`Lx=n22nl_6P6iisha z^A$S84t-cwrtZ}_nL@Q$8k05XZNSUTo%ZW0k1WqhUEvg}NKH=-8XIdj$DAoEDrzwy z4!rL7t=4LWX@_s~n>Ppi(P+0uM$RW$RTF#LZG`#BxORL;@+l#vcU9)Ouxn9He96Kg zBI~yrTgXeS7}Z;UZ8%u#DPq7W8Y3f4Z+-KLqECx*7vRJw!l?1?&~9XhS}U-uj~@nE zoyV))5@S)~akhJOiiOj58w1-#6SSI$h43h9y912JJQ$}AnO~1&GP9V>vhvH#EV1~& zIg|JfQuUjz@ow~FIHKQ=QFiO0O4jZs@dFVlG$AQF2bb(giI8uD=%w{i= z?i+zRlA#KLL?Y}X$|hyqU!`v@ALV3)OAB%SD@NGMc(}W?Gqmzd!=$nQ1;~h8~g+=F5BWDjgA`v10Ew*Tl3}TstP=zNyW_XV(->>EuDNm!h?)pE_0_nBFF3_I zj>=xM@r962|4wWhAD8ssiLT{QIb$X?xKF4{bYG)}G!{Rg;!EQO zNo~&DTNAn60d|JU>!NO-h|RBdP;K6CNy_Hlw=Z&u;5Dkv8ri!LeHcoi!pN+^B~dP9 zk5s}gC>Wune5(@s0j?UOv$LT=Mb@!^S>9(di8#QE479;eE@Joh~)2j!Apw}RLV zaVB4EpN`L;mB8SFcSa!u8}2H+8UJI)DkjybJXFA|{30}5l1IGFq%np>ozqzTJUXOV zCbOQTyHbaALaTYoGOu+6>4Gb61d5`FS&{W0;8;6Vf#tn7UNQI#tdXoOfRt(k}#gZ*-sl4w45nqc$ds zPL##LQYxg7-@ipy?)Thg?~YHMi!NubZ+|^6dhZ5y5 zP+Doi=B+Pl8n5d0EmzcP9<@$+!csY;Ef(jAA@?w5(7CjZUcg*SJUZoB#lkwIK{QEY zvSduy8M{s+;oLgQg|OYSkEnxwnQpWxu|-6G^TEW&DW*B_cK-g6tbAVq3ou^33un9^Cd4Vt96NY2U4e)?Rxt%ryzS7H!gb*j9{vmV~pk+<@5_cHC*JM(F?c+$l;IApO5+b z_-y|fSh)u~_1M;<)@>8NMn(#1Z1XF~CVhf+B*D6?huGg#P3l_RCX&UTnraAtS6_kF zyGlK8or+k!bzg`BLQyicd_A$Y0I?JFJtK2V^rVtkU)F;aO%Sk^{`*Pv#>|NM5EmyW zo&5`M?r$KQ+&a8x@%6`xWYg57o+VaG7H)-moZfnQHn|Nvu5aL%+~kB@Mp`g5!HoDUnL$aI;pT=D9cGSmd1EYwRxM-raL;9#jqP! zlQ!CwS{h+)of0-Orv8;_<-ntFKH0tQg0cB^efHy*#SdQlq{VNy!pInoS1>O+m}r?q zEguZ5WTDa{#Tf)0$^C#5w4*9wVSdD`X3pveg+jHl3MfCuZ06*_Zd$S4I)^5vVRBP! zYn-mp;>8rdH_AzdLb--_lue`R8nvSz&e+Io8!{5sht@XUL8J@f1aJyp>>M4Zj>m>U zVp`N0>ZUOW(XNbBk1Z9aQDOW?W8mjL{vX=yBFgK<*r;Z*T0H9cN?mJCDvc5A(RrO} zbG={)Kd(wWcxCBm5eN~z+M7nCoKG)5|K;vaQ1?2OSg89~#TGTG#DXL0BpUTlDG&1n zUx}Pzr93oNE4w^j;=;2|=~5QvpT>~E7iPXNVcH|izZuz|SVF@Ubj&XT;*!PcR}{IT z-v=&xbDfzf(R{HK`{^gUPAlx;&@pFjTr@YL8`xZEN)7#3Q`@>=!u&E+VR5c@U_3S}vUG7U`KeWs!-qkf>G ziMcp5$ksEzOSMT+sh84?W}$&u_@~T|7OzW`Wn_EA7R8Vh-5FNq!WY44o21wEsyBTJ zae(<+vYc^$h(o`+=~^D0RJqq0kM36-E2xk&R>k#%xtRzfdkdhFt)ow8Op$D1Tzn;) zcEw2)r|@;n!5$~kU~8Z7m85*V6~o3>ZQLxV-K8W`6Oexm|n%^aU&8>fmNOl z^vadiTsd~n;wvb9c{DM1QC?_dre!H8HTWp8ilL~O=C}9d&_os(ENmJ3Ff3i04jMQ&@?CaEWPsXVbPtO8hPLt#!Jyw=}T8E^hLyDFmMzQ+eQ*A!V8}H!J zQIRv=p=bU94tJO-sU|cyBari5&{0i3FaR_n zlQ*+z5X2LYavG}ebdJU6Fk+MS%!P$zdE%89A_(hFW_wp_dcVWekYKEDs#l`f$FMl#N^H`!%olkFTk^VqYDBpE4D3K>O3DP%Q*sJoiNnHAMnkYFrQqL~v3`P7?&e zC`0_iJ^-B2=>9+q{DQQb#i1t=ry-+a)(PWF(E2K{n>dgs3oc%XLnbJ?OLFgJTM2?S(qc- zjR(RF;rW>yfDTH~(d{ed0Qx*$=FWf+_*X)zs{dK+?Af16y1_MJ-w^p}g>Ks3&M+QL zn46Qkt0fF5&iu3cc7w}%08o5|b{C4|Jpdejs>;^E!QBn+Y7TdD{kg*L3j7ih=kJLA z$K3!I03p7gq5*)ftzrH1N|E@(d7Q0(D(nWAvpxHtWp|(iioqP+02>0#>nD~2+=SwP zR`c`voo$@pPHr|%zpx+FR|2&@aV+_ zg;|-qJHVMF6>Z@*?iLIxPS#FbU_Nmkg!?+!{!_=!*?+3uzO^6kLLA^oR27I;P;i%feEapUV=;&&Oph zAPD9XfkJtqmI9VgUa;V{=6{O+OSPPnr8^>mepL+xRKtX4hG7yLe zg#U!FfCyNQ4d=HaLTwT4hpdCSo13JwtCN$JC=im6I)1qRFL^nb ztE~qNs^IE$cDn^~Hg|P{Ap*1n6EZ@hnyjz>wjJU{ZU0RP1OgU>SP1cQ3Gtc>a)AZ; zMYt?1ghT+#v9yGU@bd}?LSW)N{}TUpPY9pexeWbXR{kaaGwn{lP0Wt^pUD5L8rNTP zv!mvR>)%u$$@`9+?*a=VSO7+?Wa|brXx`slK!z+Wn9D!UA};{33=yXOJI5%dfi_sw z+!A45l5XaJognD@?%cOq-jP65v{T?G<{<*%Gv^l+u;hZkcr5_866EKyfLaJ~2}5|T zcrCzy@ql@e`o9zZbse5IFh@k7{60MKi}DH}!_&^4Y?u1A5;rF+xTm=*?1VKC1ir?p zliv~s23`Sfeg<|`b4y!CL~95{s~xrfl>4=QOB11Fru^@Q`OW;k8RK^D{{PYzp*#5y(jmTj;g8PnEs%ZZF(^bBw*78jKHmS_ zh(q|HeC9${U@nLVn4e1k2IP3=Fd*B502iP_0=xoN{6KEJ!{A?61MFm{d-?x1^V(fs z>3<>Zhc4=ls^7@G_Ct<`=0Mm9gcXR_n zBFxd!TT;Q?!42r|eO331LjOsdI*|D|BM|(hs&6I!tw+_|3-JV6wmz_3_IiM>3ow)> z>M6-5%nt@2Li|C*vYnIPYS@YXh_g155E0%Vi`x9N^YvA$yQ3{!Qp45@=HQ0Zxqa%J zS`<{@-T8N_5Zyim4H^!>$c!40;<Bvfk!^1^sNC>bpX!0A#>e z^?xmA0CR9~^8ByGWE_AY{hycGIs4BWMnUk?heRTvzbZP*wP z)hUWvBdS5bEh)D%^vAGMJ;QeW+c*4up>K5VC_zB`O%GyVMRI50?5oiBnO_RYIypE2 zE(QPLF~6xsu=!62QP*}3wg)%&*!rMU?A*EAHr(3~=-Jr9cU)Rg^t;nPx&I$@oOOc2 zBsCRfzT&n0>wlMmBjD1|RF{?4()ud7ed@nUx&l3JUIBrxLWtk~yNtCf3^AU7lG^$G zzsp%TxC7(0D6#Ede<}5IUqGP+9Q>bU?5hKAo8bOuY-dscT+lQD~)Dh{R zd!vmPnZE2Z+9V(X45|_kFqpwWLIlj93>^L`UIz>&|AyP~0@KCY=D8>S`1Wb*CIJT6 z19&r#5EcN)Ljtn@J8i$^dIHGUwh zuNlg=-w7bNLxj#h$?^Dqhdc;BviaJR=OM$0c+#yzpei!{$Yv4<4eZ{z{Z z-5*IFg{XBvF+D8Za=c*EX+c^uOqlfDH?)#+Wx5z`R$KU$^%f3xFQE?ME z_=Ea+{K$E?JvpAxZ*d1fQNHiuCc^W!<0k(f#oli@p5SjqP2e5oul~B<$P?No57ABk zqefUzc<#yb1b=Hbh?f_+T4}$Lx1F(U50L#)H|&MI-y7Q8xAU;u1JcMDJzfNTs2H*D^G#IVzZd@akfiU)`9zSJ&VCD= z+xh0seC!_;Ilm==dm;}xQn@Gc_OJ78kCFX}?ETgl8ESTJZ|tEO+5P5wz?={=asr2c z)LIBZWD2qu??bf=d*V+7+0WQ-^Z^|`Bz=G%{z08}UI;Q1+zWdUBoBLHPk6sJe5jNY zm?Zj>xI>Nj?}a;52;38Q5C}5Y*>A1~B$}!~q6u^u`2Q^S5MN>1^I8^&FmiNlZ}bTw z$947_eE?~y62K4{(BDJQhqSVN-(i5DR-Ny~`5=7AIi#_?W`zQO3K2+!g7E=FFE#GfV(25|V;GVn>_2%Y&V{bd{1lqi>ub!Yra`%112SKG+ zdvU(sYx($)obOBafJs{98hF%uqx+veFfyL)g+5g8ZBNbzL5Au5M&EX)VVl0~(1}_x zx9`~#L?XHu@=#Mtdm@h)X(#^^c|ZGT4eH|^Uz4z3f3)S7AOHBBZ+?9DS()u`vnaZn zd%JzTg>N6N`N_u>z7yR3`pmbnN8n30f7bVCWVc`5LBuUY$^cB(e0~0XZ}_M|!@U?j zN@#C}kMyzs$?*S=f0*W<)S=%0-1p3(nvT6NhZ5Qob6>wQzVF@-|A*5F|1my?8tL5& zb10!bF(yV z%k{SSOI48e|GxY8i4Plked>p0Jc>|Y$W@`7Bkx`5@gJ+|Ft{A z*QE;E`!FLmOBNR3g+hS6jJf#uEzP;CMED_G!u;k?E*KapV9p2T6BI(c>GEH89Rgn4 z;NJbt%Rf`LQ^h}T_b+0;p#=f|_(!T9Zx3e3?OkPt!4 z$NcN;j}kJra5oK@tCsm$=bvsu{59#mE{aAhnW(45d+ipn$ zVPm_-AJ6|Nr;ph9+QEIh9wA=DCSW{3c#=b4|cgTHX?1|?LPzi^?T*v-LzgbP<($_rT6Zx4erz#O3xOrA_q zl7ut1fgliw!BR<66$J8O0fB;oL7h-X89eiR#5gVOym`K64g;ZJ9m7G;+{eE1+9S@%i2 z_FevC?}Q8Xd{&+QPD>+hYV-!@S>9Ms$WsU?1vOGDrMHX9URb-0MbsP16mz&Y_Hg1% zAeb}}je*a#aBX$T(03v~q}*`5TO&#-1bi8!_9}Ym5xrEII0y@0F13Py5o>Jsh&4K#Yc=~VP<;r+&bVTSW}4&~pI2c2kT3|oR+GMP`oK{I;I?`X!e zXh|oD-^*m!cuBXlk493FC(4{~O1c_n{K64JKSJ8-rgPCQM--c|FFt9U0h3ZyqknWJ z5qgBx@G97|S00cip==C3{aK!9(&7M7|AC|2Dd>`)r7@0jryim+lEsSZ104cm)8h}@ zC=geCxWr!77=_LHh#t4JjFu*tQmE+?^=BD2!=~s%Pc5l%;O4}oid?rV(W{&fQg6~d z@glrd5G5-dranz4I?Ze4LKJ<6n!b-)t=GUW*ffnGSGuAB5?<{>Dl{!AI>U4{Aeclv zEUqB5p)smu+JY*%ht+4=a@gyLmKPxr^ZDFq9GPWqx{3pm)kFG~&u92ET}VusBPgAz zFBM$8AKr6#JoKFTe7|Uw3wE~*p8`1KQ$YmCq{3Vde3=BlPjXe`+}-J;2dcm!IJK74 z82aUh{fHK*s)Vf|p(j-=aRr1g2isJWT0IWJtTm@Q!D)`xpHStvj$aKy!{mu4mLWgE zS@Q_LCI zCMhG%>Bp!gWTJt?(;Ay(B8C*KVnqBQ1_;xfI!voA`z96qiHqFe)m^b-wDkgI~ik zb>7#6E203L0veXVVZoof%NC*$gwp|C3C&D;>$FUe+{@=TjpNxWF~Ub(YLk?!KO6Rv zME%2&8CD~RdgYP1N@K0r_~Ufor>%M#^8TJUm32fcpNW=5H0ej`V|Ye+WoAO(Kr1O+ zDkaQK8_8xA)>9)gA})^9=xfLk>o78>Ux54YiBCYTp!-)O!n>sc$7X{=n*;|DGwze!ZD&ZAQIzV2zQwi??Z zVlo^O3KtSemqasd6>Pv>(2uG}Gb9?u%kaHWP%RMrOq>;=UC9B<$Sxo*IFoFHudDDG zhimX+F&+@Q3M!X~Nx>;0=B(K`2;*o!7jhu!Z7#=UyVF@K zeO-^=O699PSJ!FK=Pz4!RW34NI; zdb3IV!Mw(hH1*IbS<6-OL&U*on5_430G?)LWMn+=ok~;Ct|@b9)8p#RiLL6mk@0cm z<4AVmsB>s@>y7g_KhnwPy*TM$;jMG*)T1{M5vpRL&^LHI*>TFZF3>0w%;nGFJ=th! zYdPD0JGJAq2+yImq#Li#NAjIeWiJaEUk=w34#nf{Bzs6dQSe@}Ij`}BzEtCK^`Mz- zt+kR(m=d<7PR&71TfKKvOp2j1^{?MV1YSR5z;!A5TBJ^tuG%x@&=BpM7^ja?CKCDl zBK#Z*l^SCylA+<@5%1O3~qsCSSjTL(4Qts91OVkFJM$c8PbUn_Qsp_bzDSBLF_R#en z%;mw`D~6c1v84Gb&3qR^wGBQ}=&u>MaFB4tceFgs@a7d^wDNl**@_nA^?|Qjyp3N( z!X+>-z~-5{ny}Std@C>yepdy;tOQB1(9n8GO?q2jKB2|%RVYfM?XUBsKL0TGV*ws7 z$7R@-%!v~xq?N1faf2K$`taRJ<0RFNsAP zQztm-^iYy@o@=&?pGA+ZX`im224_S+?=cg*S-QCk&{;aT)FG`VYgqYG3w5`dzmVvVyh+S$p_Dd4j2X(?Dy9lsfXM z@TD&nY@Cc0gv(8WPPGTH4skc=5{+%xwL%Wj;}TIb+Yw&mIywr+1mWwp;#U9_us$p7H*-3U1ClSE$QWB1k`wkjI4NSbl$^QJd_8_y>(9Hka}MCr2cp#UQa(U z#UycBD6gb^Hd-q-?@E|;Ip2G)u@vUX+Y6Dc1in~RSXh%&CFe*;NL)zBBEu2R#1#v= z7-Z)OXEl_XSe`3s>MHzb*5!rxhayk1;o?3m>v$&SzdL zi7a46-?$tc`zour?)_{3$3X?*T8Kv__8ri>BLlzV`^Y=fdbUUUy-cUf#8aW-a|(rY zxEy!o^+zLJ@F7^}*tKvT`AjRPhNQ9F(nE#E6<_<$Uy|=S2s~FftxK+Q>&2pu!Q7=z z3EoG*bLENXn%f)~(>NXQuGjIAi6RQCMDCb3WxG-`^FBX*Bl=L;L-61MXy&Ppt{h^iEZJDu zY)eLgtx_XLu<=)?RE%ehrU2vp7(&b-j|Lo)vhw_+GtVbz_zn_vmwIp`cD>0)0opxd+%tZ0hvxT!3 z^~H_eca3jnCyfqE1xmOEEZ(4^&>%GOAH#$5~HBGS)k{};#cQlI#%(hr$zv{(*e3A+)`bN z2P-}e*Dh2Ob$%43hmJ~p(8OWQrFdUj@pC6-rmr;R_GK| z8y0${h2jZ?kWaqikuZ-=u|VdhprOBMrH zv>rM?%{nX^25ZpENb9NX=1U0RCX0Nq#-J0K!6{bRJ}ex1p3wM`9+4i;c(Kc1=XHC> z_O3|d7M@FJBv3wOgj({~UbiM*8Ba|mOZj+iNe?Oc*S=hzx(AabiJTw1zf~`*H1O)} z*zLoY@8xMW-A^VJ0#7C79=_j1_*8|5FtYnzNxGx()Ws8=P0s+c_9&V!%D>^<|8Rqh zC~&p$7Dlg6#WiAaQ8`@7fd!8g!KPakb^WTQv~!Zr*D?!?wsgJ}(NOX+$=H+Pf;%oM z)loP_(WufCI#0jbQ0~;yfm~K#JoAM30C^wNWYkkrhr>Q^wj!6T<}SaO`60DBE_;i* zU@TVk0RQo>60_h*HSBblZu*&n5pXQiS6&GYdDB*#PIXn#$yd&8x3XVJl(cjT^FSg3s{9jEzUX5WUv(^8kfSzcg`$qKTNu`<7wkxUv zZY1ECELHH-n{csa630ZzKHp~dv<15_7VB4NtXl{HV`8DwQcCU;X!Bhfo@ntTA{4sd zpEz?4op)k6@0}@oG1PuicT7;pYQ?&)GT}0JMbsuM|0+?4`lXx2^|~fD1;^LqV-6i9 zH`C;>F@)Vq-D=Eep;Viv*MFd1K=zL2MxNY}tYv0C{aeRu@(O2f7Iw?I9zS}y)u~7sPU<;A(5)uJSi4nS?*aXav<9VurDJsNY)d8?J4N?Ic^kv3_>{iNedn zGoi2)8pbnRm5FYz+8Tl;BKt?JzDD~@T4T2X9p)&7L|02awf}l*Hi}9eM20=4&(+XW(!Q# zsJuSk9B-GS-8q@%<bIMou;bS`$oowtdDrBh0B;{<-nw@0H{WkNBsFza6M z`2N1zS*u4+z4R@bFj~?R zFcq7`UaSRt%p!W(Zd2oZM%q$?-{$bG&fZgeZv>1_j~>VnSQSVJVHF$4QED4e#-=g| zHRN!AekrRcOSB=ii)~11Y$I+cFL*)}0FB)5k{9JiHu{z2k^9kV3!J)C76NylzKGC< z79Nv%CdTV6Wt}IUb(Rus>{y@D>&uq1FA1XQCC66pZ9cF(!1yr==!=+)1zeC_=#pC4 zB&)bPIi8rSRLF!=!Iay~+Z%pG*<-RL<;qfFM~SRe6blO$H8mJSrGHOfSg=hDp0{zo zGg%<8x8Pl3IS_LM!v_sUU-%5y^9A~>PEID!zfp-fo%r-fJf$(rvxEuCmXdOp+1S2a zF(a<=n1R}5*MwYQ&%W6biRe(N(UnH;hZe*Sq5wYgB-2|u#b@4BHyC@v{TlqPnaAoe zi{P_f^Eh_PO z6&zzAH=7N5Lz!*a)4QhL<=a=aS(vS&eUs}+2MG^D$_W;-mMmH7ka2uD(fa(Zxi=rT zC~DH5-j2MW8hiWU4GED#-*ISzHEptBa@#>0M3Z7KLS5I{`h2MmFVL@daMM|55trTo z{ekX!4rgrcqk`iy9%}gb4@ySR@4;+GYLjbq{9Q)NH(#nNY&p;SFA+ehE+TN!y_buB zfatbwZ}Z&y=-Sqi@==SthSi+Gt1&9_mTjf-Z`5MC29h)CUs#8oHN0#54t~Rs(a3wC zH}-Zez1VDTGyN9|&gM)&QS+Nq6ViQOR_?OW9~sj?2R)s)lavyYSMt6%H@Z|B6cj{A z8|U7l-Z}A5vUTR7hptH)HV|9S1>vW-4aZhKpS)f2{?Hs}wBNA@A>>STiM26|iehX> z78Ey+UGTB!>o`utGeL;iXWL8hj?^wBW*zz=D4&C=cVvt@ zcaC0kz3sDNm9=Y-SUKR4@k*)rl~E>Qd*=Vl_Zv^G0a6^*B`Bp(#84%+bGf{&3U@#a~thpH#jii*6agQuqpK5=YF zn!x0PhI~=Y=DKi7Id8@2FgWF1=&GCb~@_)&2SdS~VI%*RDnt=1v^3 z>Gplmuh%Q}M$96r|0zQLPNB?&avpq12@m+=PU1lt=4(j{ryUp1PEOJiO&R;5l)XdIh4a^e;ExR?Y>VWh8jlTxX}+ zkwHb4=b~M2tq9GBQ^aV1!QFIDE30pM&aHYb1*8(pq^5!5X!EE&bqQz-x88i)=P{P$YmfR_*x77rIC|oMvr=)p``W|FeuAm6Zg&$a7(WoaeT{fa zmg+B2no#?Dy?~UwE%I}xJeQP9Dnmzuk-MP{lL}1as}P+pdr4M0>>%ngzEr$9T*OHF z^e7;!l|KE{SlE3rpXm0)RFI{2Um~%l15Ve6WNZ)wzm3S>e$;HevcEKI-RKCZpAmpm zF=L938r^O0mGLppMXuEcZpt`}EQMxjnK=|G3!a;Ob;{Lrn*;%RE(V;9f&eU+o!@#* zjXy8(gN?U3XlX{73$|&*2P=leH0yI)<1z^((57F1K^eXHoS#-~!?BMjN(}OBBn1>V zF#{I6=_uj4mce6E)MhEHnVS)I9DI;9ojlnPy6}!S(LN#YAz#}dCM4|%dy0X$miNeIY0@u<=L09|Rb9$5WX#z)tj7t|k z%Uq4W1BxS+mwmvz**ffYoj$qqNL-dVZsKh*CjaGgKxc6z#`b)iU6iF{zSaBT*Kc1} z%XG>rCa+`+H8|*i zSz=$aC&Xw*1)|w6a$Ji%6mQ&nZ?3O*^I4j&VPM-_Y`Zm>FcT0ZGcCwwqbph)t>#f1 zzwDd0#p#yQ80jUTuPnj-D&*>+tlF#YSxK~8nS|_hZGM}6mPZyRw^YY4MbzJ&29l-+ z2^p5Q?}ZxtW#HZKWZZNuO*pWnJV7oKnZdKFML z+lTaEpnY2^7s(z{_nR%RyY566QK(z>l%pC$4Z9;}Hp%>{?drg0eP^-6lvqhqrVTTY z!=`_DpQ@Tq5IFYs$dVf8Q!2v}A}}MgcoihgI9TH0my$o{wf0GCN@UW_l@#!Tu=Ujo zQKkU{{qG&8hKWG}&E~^?ap9+cK|~xYp_WtJ>Q8>* zGMRW$ca8!Ru!8gl;1ovpN-)p}d%Z}_y}vL~ADtf%T$Kun>wDmH>vrOXf3E-h=ya^J z{HZHCRw0KO(>XO7<6It%U}WBq^v=kbtDQ_VZ8M1*r&L8_^+;oE2YapdPx-Alm_EOf z$zV;b;#y9^I3P0$eRPVumViA3!#|petErx^bUuKQZJ3C?tg;GAP4xQWE&s{5_a)x^ z+Seldp4<&5m3e#eJOLGPjFPIF=4`;w7ot@$qNS%>O)haP20$WoeSP)1n#uC3%R^J+ zY6V)kj2+Kz)mY2C#e>!O9Ij#iFpvJ~?Gx+C=Ms7M2TYfe+&G&w0sEU~A*JoTn_DWl z)~aovsqBy~a0>%hRNw1_l1S_oV?M%q&I(NU`{7OXQRYsb&9NFO(Xhk_z=F^%OzXs4 zdqzDS1J=53%Q57y*4>^x-V)8snC|BntQ45eNhd9Bp1B#Ucd+PSanzP!F*nmw3e1Er zY^6G6>H!NIxupVKv5#Kd7@@Lfc*W1p2pgrxhpx)ryg;JUOn;O{D5}04-_w2L{$iHd za~r@8hARfnkKfpA@0j}RYmr*zSEhO2sDi0L!SE^$<3RBFSe>A(=G?o7M*3+3mjj$E zo$mWJ2uKs1Kw~`;V|rn(>y?|6@v=v^V+CINC=K6}%kff?Au+CTJ#CU?N-KC~C98dS zc%2VlE^PIbGFm*P!c2I8fulDtQ5~?>!ef$TBgnb-@~VN1l4E!51RH+CM#Yw)Q0r{7 z)QZ$~x2Hsy3F6Zv&7X2gSDS8{O41R62(fiV$YcbGDeh9ku|c8=hFu%4;GYt?`#oKQ z4gs!9V-q#prR>xdFjsXhgt={u;!W2QOGr!`<_E*8S+71HC)D#>9EZQlX&()2%DE(r z2#NQrN*uS={VN^yS3j`EaNJJ;kpZJAvOy}zoD#;j6P=Bu#5D__kM(Q~uL#qm z-(oL=9_AK4!dB5)lOrgYy7qb|q#Wo-3#DJeIEwBp%@oV>@-rrDh`4@`9(}#VA#2QyzyPcC-r}>0fs9T# zx7zQ3$g~1FwTJ>2;)qP>CwZk5i>BY|&rqF?z+t@4thao~oWx4{x={4`*@c!{oq>1# zxyqkXVkXSuSuf1H4F&r3hmPF1S~)sW8M^?dUkbuuEM)F+Fg2G}R;kl|3{Qk!U<%X)5#PP#A`Et;@s*n`=M-bMoOnMaY#7nu5C7U(>t2Ue{3KZtce}=$!tV2|kMfiRh>i*oVNi|O zDuxi}b=>=c2Fy=<^z2T^T2>o4M>KF$tBJhl7F(%nYJdCvQvb2OtPrA@7$r_IUUh87 zo5xBIf^Y~9!sRxSr6{w#3InFZ@=U=HMYOJyv&U)iupCPkdzTz(-Wv}-bjlGs20->u zR=N5WI!?Dx^o8{X9s2caFJ74w05vd|PGKzed{tLUHekIZPsmu;*c#P?zBgAz;W3IlZ~$YA%#zjwdZg}wOvPJ`>ry}yovW_ zJL-836Mq=|#O*DlfNiqVv_Cg~>XNtNMf4YEot&_qt`756I9*OobPzN(Z&k)VmM=^t zEqdr6KBSpsP4~&&6L$0>nflT4XkEAOD?_>^H{QNkI;f^_D0Qk%!47+k$wgJ zh5sGXh7XS$y1ceNVPN530O{MwV|VCE)~>62c8RSYAG$Mj(NxEjnB-$9`U~1Srrnh_ z(Sg%<^%Q)1!Ns-UKw0b#$%OnSQc--xt<7#R<}SYaG7*wHF!nN*5?68xvb*)0J^k0K zMN<>+D=7ez)Dyv{woE}mf_Y;VLZmrbIb{Ylaj(dGG@}A^7MA0NO1EC)#not)9$X5* zVf>^S=ANL)mU`JeaNeiVt$AowuC|$%yodCfDZiSe+lnvc?b~*O1A4q0D%c&F=ntCx z*BDqoNd=0Gi+x%0J6dgX1lPHSF(^KZGN;6Q=3;13>v5(Hv3@g|H_v9RPvRWPoFdiY zvM!~Vp`8}qWC*jR$91;t%#nFo_tJH)SWlg&J+t3@xF!}45fB~NfI+W!ZE=a>ApLtU z=e<6+jk&EW;5TQS4pO$w{~)n8`f{pEvQXc0@mI%As+StWeyVbNcK%Rv)V&KiaMXZa!v{nG8Jd+cXwT;cMabUXMbv&#j0cQI|$ChvKufKGd<`*7ggWRr&33NEi!J>XZL*jDpba~y~c5zf;ZlS>#*4}Fw4 zX?Z&6xogI~6AQJ0OI<{nbhssSq>x@v|y-Tt~n zCUT-=rF}yPYmtL~$p>g+8EU1=-NxR$co1=Gx$u5;Fg1JG8KzRgj?%67Qk(rZnSHOZ zUKtu-uLy~`buv3sUYe0}YHD%)UQOwI?rel+E1;G2U2f|t(bnqSDgQv|b#K$mBhN`> z-b5WT2L-VO%&WC226P8b6_f|alJ}@yGkvpZDYbQfsYIyLw(IJ#l>vVx?2g2Aj)F`>A!aObK_{Oh?rkbWY#_;!TeZynp0XoK^1` zz`pQ&Gjplv4j%M?QR^_}($ZE%ssGT!tLvFQuc^Qcj2#OR#CetwH$R2h$_pJhCqN0K zs}s>%Gl8Ss)$9rAMn)^0OFV%qlQ&yv6Avr$a=kc6))*4Q8{1u*dK4(@GxRWU<=y7X zt38*?$mbQ>Ir0L#c{XQ-q;5}2`;^BXd!;bT7YqR&qxy=3z7CY)wwg!D1{q8D@~6zP z2jQ8ZPxKoRz8AVTJBpoDJus+ax-e?SL)jyE7@>;QWrv-x-YE`FPRljq29ml24wX*5 zRKfvdI7&%3)&dqNOV^sTtyxzhDV{TlNs-BTX zft0h#V{4GDbbSPzW`^sWIv7he;=N|=v;}?e{;KI*D-VGpHsdjsQX9c6a*%>Aa8J2` zf_Y~wgWo2BF*ky4+7--A(3fuX_xc}Of!*R`40$jifJx4PwY0HXErYp9xb)%3IrJn- zH5H+a)wQV&8=KsfLhnf-GE6cV?2|Z7+4Sdw&@0Qjq}ChhNdu>-Gu+wBjws0FZVYCe zqbH;bVQrQ$i%CKQM!Z5|w4Isa79a}EveZHCfjtxS9_mx8*94XEP6r9GmqvwU=}E1) z42SoUH>TXR-qZvF-O(e=$6j$5L}Z*`>?`oLnZIxiXTsAp3`icG%mgTNZu{Df=iXtv z9CUc@MpLd*&V=yNbz+$ZCM`-A*f=CItENNg=sgHWC%bFB!??(M+!=a8=nCe?7m`2w z@lUGLifh(LrA?E{yg5~BTSuJTYOZ=Xw`6nq)653R22nkWPXhtA^PO79<2hoPHy>qQi1S8itBZG1J-oauNoJ14Z_{{1tctux zz`H2&PPg!(M?oT-jN^&EUc+@~8fx>U&mR|%!9J!PHDJ^-VeL=ibZ1oADV;QbIt`tB zwcQB)!P`*6l8tri?hSFX=TK((T@@t8wHrm9Y=7g#)AH6BWm#&^?jj-QB4r$Jv|A&d?f5R0UTaJS}d67rAo_>v;n65IJ_ypFvn0Sx3 zcR%`zCor~!mXk_D)6AuTbC-tVM{70DwKksMb1&EA1Q`0% z)w?yNE6ndd!iJa`UpKz;>{=gx9jd^oET&9VQ%Dy=EF%~d>o|Lpc9l~gV9}z@qV0;u zNi8OcqI2j*x=O;}=Cw5;DIfPDE-~&yBN$(R^0SwG^h&Pax>|jd3V3PpSSVaGtWG@FUW* z&06Pi7@;jp7LUmbeDf?GyO;C_-*9G0f(-=&jH($&V=LLX&U{#y>Jle=02^1vW<1k- z5iK<1hJAEf^z+E~$?x$`qSXsF)SvM@hOJaPA+j~7$D<~c8j05f=pFl_7hE_e)&gIA zon3h2_2|tkU0QN8P%rIDzbFH-!|GrgktzKIZ<;9%&aDl=c%dzg%@TPG3O-WHYIWKZ zehUfGcOHcGd?D^p7Mr{v(0I;-Q`>)|nLZF)a3m&1@F(zF#OXs(CMEWvh&*@iJOijErml7^oGjFh+KMiIKanL-FMA|n$FR~QM zw(0)Zv3w>}d*BJ4k>F+F14n6wKP|PH-trfm%wH^G0!Yw#{OFX=aLCg5lQuG<29cbb zsi#G}hG@{oyH8VgfWY0eD@&5TXLDHHI^*< zPT%boKjc1hkS!!elV$Y9^su57ofs3>W-%vo&zq=aGzt)Xl~|YeSlo4HiU;mMi=oMC zG*felpyI67*^O9uau|^0Hh9VfN#XH$v<1vpECoFMAiVy)491>5`Y`j37`myz1_X8GjVT@az@EQ@w&yt zv;qyOqs!1dWuX7qqZJV`Ngy0#U72YRirRcv>I;3bCi^(e*n<$Q-J{I#R7GEt`qt=Y zOPq7?q0>50Nj6UdGyO>j#x4>!Jc;A~Z z6@i?SSo21DW((TI2w$GABO^(}wC%(lsSAx_g6O)=GRs&d%j=(7xZ2&X0RuN7F;^Bo zbBsJZH}Z_fWa4Gv&=4;bj}%&0y$lOOO7G%G_eR@9ZNUYup zFfNJ*-^Ya5OSs!(i8D!LgGPo7CY+058> zZpc#EESV$Hw&JGXpZt=v^!<8rF&j~e&xN|-Pq8GAKq(wyCadFX{ajhfH#&6K zu|yz&GRy``594v3YRo3vIhIaWm6wF=M|KKfBOM9%WnL?Cr{>eyi)~8O8nRL87x~Q0W#)+fv zBXvnk3L|un=XeiNFk44N66Y<(C6Jh$YhCt!pih40`rG!v`<#JR`sG-KmTuk_9j=wN zeRVX{D+XSgo2M(^DFrQ@^=`ove)MRooG3?vWQoDR>zZL4^@qZ`mrtC|;NW0{4V=)s zf9l@hvFryFg|Q2F2dAz8OZmb=D*XalalKC;zs+OYA+32|JT*~C^xEyttVD6XEh`_p zYwI+Psl~a;j%%A;sW;yF_>es5$6T%#eBpO#I<;_Qd@MQ8J`26(l65+dy&W6u?x~9^ z-25>C%Y0)Fj~$F(&UR-#Rk9|P;5cp$O#TSn;EQjw)=w4*y(Yo|J(;f(@%)i^zZfL) z4d)81$V#zX`qAu@>pocPTK#E^+77aT^Q~2wUTX{Iny3~;MS)!t(Z(ZGHM~H*bsTguqXpQ~i6)*3|VxPz9`C;-C)0dw{ zCJRoa-d(C?6Wd&V`AqCuX=JFUrspuNz}U&M&=SkijhPAN!Hn9j|L`+4YubO#`;aa8(2QXxEOJ9;UcL)gXUn?JMrX>nvvSM zEYNUcT*DJIODw6_%w)kpll<|am;S?^P7CyI*OhU7*4)dRiAN>YUh>q3bS8eH7ZZh9 zzSd`b%8~m?s-GkBfmZ@?-s$--kJB7@u693Dv)l5#yT)s~;vJg7E6>okI_olVd59H# z>8(rh>$IQrA`ZA3utc)^T0FCt7+Cq%3`L~3n5Da@`l71z`K~yYB zD-vXcZ&541Zedas;*xn2?5}mok2VIx@1=2xl|O~B&gO&!M^7-Nqj2vzyd3C^jrg%w z$_dq+ll^3V5hCO34p$90Z{o7H<^dA}8^Ap{bw8FWx-)Z&yX@2PHbzLn)W(#tNJg=y zkpJLvqyg61tFMC{LBo|qGq|EX!FYzVF~N`I-VS!FN-N?sa#|3cE+Br*a1e6YHnNZI zAhG4RayXYdYmYQ@QDcY|{~dx`(+p@t*{5;nKRZhY8}*-5Wrz?i7e+hR&3P%;)1*uK zsJu{dY>rGe<1efL2qRLxo>cbg$A^KPhpTs4*ya@HMs&f%KUQ$@gVrpu}V(CGfns1F^DJ!k{qx| z^aD&UJrE5aP_=K(_AFQ_4m;JFM*VR31oLQ9gf4#yE}im;_%|z(2aIaiM1syN6ThN0 z#k$qOt^LTOU)eZRW8p5(MDbeHRZpXCwD=Eb)iDR+!6T(JpXp1)TF!BLYsBYWnC*B+ zE`0g6z!%SjYNK?ggY=_K=P;B&*yh;&1($CYgrafEm|7-SE;91uzpd^FjGN#(X)Crh zu(U9G*~XQN9g}AI4qB&h5XOgK$7M8|nvJe?D09%paCtuMM0IO~T8Dxz-79GJ+{>xL zgG4VnpK3pzn7binMw1#uycD%LSu7Nl-sSGSc=lBO*{N5IUbCAV)Gwx4a|7`fdl)&H z!p)6dpPlcO9L$>}LbHzx8nAxIw>G8!_%=&AIcC|%Y&K?0JNfjRT$CNBov;?oh!#_6 z16orKFHC=M?$S!3jqYKcr%kRobGcRU&dOcAXuEE=Zl_JZwIi<{lhGw=ynm?YC`B@d zt9--YTE-ga0G;e}F2y$ojBMmE@Trt~Y(H13;+qj~P*okcQ2^t_D4bFYON^;X@#RCZ2murt}-mDCJM9U(jC%W(w)*Fp>%gihot1v zT}pRJcQ;FSDVMPs zQw764-g)f-$A$9~y{CB*TT49!NQYdZoiSt|10MhQS3Sn4P{BA99+B)X_(+|)S&;Z_ zU5gNIg(A3$NQQNVVyj5{HGc5F(c9|Pu&OcU#ZClCE4`uX5Ul!IV_PlSl6l?DdR+0X z98g|2f>Aeu8bD=HKCEvW!ZFojTQc`O=P5oBvR9~FPn-6B-cQTPGN_iH7Px!6E`r3K z1UYNJo{QY>@CHlQGV60H_+E1P%jx_5-9a%cz(4gtY$EALG8%B2;&4d*#6JJ^Hw#Bx zz_^$G2uGbvD-p=rPaY}QMl9yrktF-tT=XAWMnZ-xSNI?f%U2tgF}!;$|BHw&&I>!~ z?U~0Jr@Rm^i^k(Um;3;pvvI!jG;8*L?82UM@hST5m2l^yC>W}LJXw3}yNzLuCCbeD z=REQI6hCOm0)x-v1#}UGx_v&(i+li=e27x3KU?gVp~|yRf&fP9LE~6AmWYP&zrDPs zkM{R77=jy{GZL$G ziQpg~beS{?&Xae&PaND6j^)tIAQ3J_8X4|hV}d@b79M2oN%QRX=1D$RIxBtp+cvA zX$eTATIecI50A}XVNDydd*i^9NztvR=yg#pcxSeqHg@jBCw_0!hwDjq%%$-@R`G#K z@{0cA@~xudoe=cVx$tyu0O;tKGe|piW|UbSrqfFJ$4Ybrx-{HPh2o0*u?XMO77{f1 z*%9lcU*@5JZFu?S1U_?55zQSlC{TYBXjv3DY+>)|rPH<2oPa-Zjn93!c4)h&rZBe_ zjDD-2&`0Ih>Y=$UCb$v&K`i?CfxvVRt4JRk`ykzsDzffJ@;SZ_QRG~>z(d;#rxM=| zu74xu)wf|iB5SJ8d`)?6URBHfZQie|Z??_htCjv*$lhUe3*1EQOp!;=k`s;H*s#`- z-H>D{!HnDIgLTM9_a4CcJEX{5D0I20^=F6GC(Ej1hr?}EuNCznYgd60)8H2UaS?>PvdL9Q{T9$KOE#$v>?Kc+X zZU^?-`gnw*gbwlEhrv}N#pUeL`G(M7xVu8ik7nTfUE>W|p-DL_(g$`IvMlkF-~p8r zcNhyX$I{raRcxIs9p#|+rI0-=w9HB@20gOgArr0)OZ9Xk(}gwQtF&HYt_>f#efQ0- z%(DFxaawEs@+=KB`fI>XQe>>4zZ$QU3`<_~_M=?{sa^z!)2rchNH-9Uk($LYO3De!{{F9k6%80;5eTuldJi?ObEyD*E`@!ghS#e75 zREIb#MpLV`)C_Vdhv*J8Nm>BLIY65SednK$ozdDp7&n+SS74Y`^GT;IYz6nJk`UMV zZaaD0Ee{XWyvy7h`m@gPm>OVS^R z(CYJIEXDHOORKY;-+Lf-y-O>3Zg&mZK3!(SPCK_1%_OKh$?rkwaZSJIx+_4hIT~>@ z8jXB_t8eo68fl>cU`S+$; zB3IpOSywVN1kWMYw|9H2^abHHQ?;&oP%JYysf<0v>mp50DUMx z{-^3<-E^5>L4>lSN-R)`ce|2J2dxNM_ksP$jQ!Qph3+n1zAk|2(l$?74@sy$zS}$R z<6aB)SSXpeh~Qx_@r#XcVHvT3B?VPCjdSxqZrts6KbKn< zxsXwa?w%jJh(9m~SJ%!Sc<#MPL())!ymd3fkKkJIrpQ)L#5x%ie#XtGw$(S$OAmB3 zd5>mXYr{yInMs$)pS~7O=R*?dirfX;md7oLAiXi8{t^N=nU{ZMU}62o9OKJQ$)0H4 zCg3xD$ZCyO$+c~NeQqhE1v!M7)S@aS^Vc+!v9Gg%4kX3%pz2}~K|>1dk!HTg@rtuW z>Lq9koH;rs>5!iN+>jRrHehEJlKX@mT;h607hutPLuw)c@xLmLu?YT z`}qlF{k!(B)@TU7N~{0Xb4z3@g5Bc|R^j7MlzJ-u^_zuiGI85M4v!so|9FQerhAXL z>P>baZx@zaEm%HSEEMWuPR!cow4;G^S4e94o57(Y!A$!%y+>oIao6SCr&;gK@-;@L z5FqO&joPCcU4I{21|%1I-+lAt=sO~wEZWwh?L_M--yXp(KsWY5pWS2oNA_;mVokr& z+AWBsxx(mQE)}_Fl;>+e!^CA{OK+9undjZi<7f1m+XUTLvPn^oSiCsP%Ieq&ZSxlt_Bn@FP) zGa>s)_`KZFL`}ui<etuPaZ^ znugu=Pw&fqhz}#_+(k~z1q)shVL^3&G;J*@Fx>56r>T-QQCIoL68)5DYY%`8>aKI1 z_GYahx2_=ng79z16dm5Vw^1z{Q&%afowvU2H`v_N-6ina0sCGe)zYN+FOtqivCsOK zqbPnGYY>=yIoHf)tzzdP(5ZXU6+VkOsEZ|+82{7bC8v`+th63WXzJonr##}j8vmr) zc}bo(`ptF~wCi9HL`=%%+`$yb6Ca%Z&2`&%cd02FchvFYcs`1kV zHl0@EsrI?}EhoxX8`q*Y+D8LT4{&{QeGH4voqBYul`^SeFTp38llF_tO~dwIiuj)B zEQfpB#_$9?*vRTz9}M~$>%m789QGEO1csWkOA&;VqKeVh66s@WM60Br);oo^-dNTg zXyOGP>CPs%-*-HxqkbP9WQbfWqoy&?it>>9jVkqD%NFuA1WER(hM0pYaa_aS+i>Tu zskr=u$7wk{2IpN7A=9PUKa{QhA3WZbUujEP&zz_1&A?V=;s9a^HF+_ZMYPta$4CCa zA5C|iPDj}KeJPSC-LA;emJ3nZy}dY5X=9*8hf;S}>MHPSd%7;}@Cbzq#ysfO*K7x^ zBljP{X>3v)E}&N5=<;s=f&1M-?_slwV_7UvEh}!5{7do2%^+?a6v(xkj$1dyQJ6!d zxDp|_093P*nCOPe`};wjVK2L81HBas!IYzNa+pZJ2D|Cp>=;m?7<~N~zbZ@)sdz$e zw>#bX86DN0`zf>D1-}`U&55Dmkr;2925tN=Db_PCcUl`gKIC<~?-$OKhME>V+ln_o z=kg~_UE|kK__#g)*)i3M)QUD8=66dUBP0L)_fP81;KEjmt+e0!X;AC2`qd6%=LP&f zBw+olIhpt+d>kmN7xzX|chi()W3Ab7hFPE5ssiYndY%7>ej+ACo!k;yBH7Q#6#3p* z66i^zJK3A4_z%P3l-e+NYpFGYFpXJA`}PDl0^W*+cxjzT-qWbX_qfv7bPTQtid*I- zhnn{JKM$N$U4HGo6GnWndEYIiyGT5n8w9xXJ$u@LZZ3z#ANbDZ7VGe*%jGc>b^^N7 zbLex}RSka~B@^NUHnC18s$@xMD$t7aH4KpAnTR}{o{QDyI-;7-)p&%3^SE*3^+WjCbAs8eK z2*KgO(gP@*&liK}#LXVQdZ7qF7DlBsc-nXiNT<-m=&j)%N6~~Z@9Ih8p!)0#u`%LC zN|PhwMbRb`I9NR}OLp7x2Kj(Y>os0=%d||?1p}^BnkU($NSS?h^1IGMM^dIDs z-Sl@A|2y7Bui)^57iA%s6cX!BC{^kO9mHO15P|9$3`M~fa~(iM$+>_7)4@YgF-ddg zzFvCTR6`)GY_*zP1x#e@l{cb43Jd6hN#LlA6eS}QNPtNta18)F2h?d?iC@C%!@jW5 z#C>6-Mx*fkTv41Rcm4f=uXL-7xa?wCDD;q`hl}6Ml8S@6^}rMNJIho=x982B=2NZE zh>~tFMF$?a;i*sAMrfk(lTN)?%Wlf&)3BBe`OS;eU_w5vJygEkH_yoD=~{`csTxMn zN)PJstPi-7z&zxT4Af$o(_pd;Vy=VOoH^r1%d8Ev-=*|zY5aM?}Fa`EX-~+&CLQ#n$$-oK=#;3IXO=_G>v_2g)p~G3OVQ2oKMXN-ZI~6lDTv@I}mM~mtMDs9Q`4ZV$q`9b17n(VegIH`zMgfn;k%#-JVC^OC%B4{E zrO+%TBhf!T2dNIzZF>xVXMmvMnrPc7mLLDqnHcGnhKD<<26yipS$go0)27vLqhRL;Tvlu!l-$GJAI zro#l+BNT-t7N>A2_#je+o!Vkf|G(<7QZ4I3CaVFfj#>r(G_|PI^)IfcIjjTf_Frq=~}c>#cm%O{H~YR&`{z zk!wE+r~d_`<)#5&F{Wp^BvR#0STX^#{!Q-s4}5E4Rx?SOWU*uNLAE^oXf0mPmhO7_4Z0(;xVtX6Q5<^45UECWt=mHrU|JHJVlts%NKgfmI*vib9K(GMXP66_JJ@c@4C9-A&ve|$k4!5LI?pXb zj_k=4C6#3u(lZW|dg4T5OJ@SImifDmerasG>Ex1pXSaig(Yj`n`@2*VcA#8ove77b zaJA`ovzw`Q5c&h71jW;u4TEE0mwV(QWFg^w~n+Z}`M-Jlg&NrXWtL7Rl&Q zgureaESPEH#k0h2_U1H#Oxt$k-4Pr;ZN`w0JXaK|iaDYsa4IN+ZE{Zq>e8_=6Fl_O z$4I%#3t$5#V)6MqBx2S9;RK{}abF-;U55i|G+LL_nq}{5$uSjLli@~NPx>j=uZNY3 z{Te^zB&77D?&(c|#{6N)txnZTi`b83CVpp7EOsY1ByYb%KssDaHIA~zB zJyfrgw<#0q(T_q^IUfZWM0DWo;M;#qu6G}$pj_ifb|Zwh4TVpF;+1lRbR7+HF>(b1 zCtegduDT&!lH$Y8s1NR~wZ2EoTCS<_h$yV%jME|cJ7mCT`Sur9Ufr`uq0)Qh&l|%i zZBSeEc9ZK{UO`G#p{8Y?lx;_cj1cwMQoJ{$`e@ijNR%?7GccD6?g6f^ElnnTg-E$8 zp`k|*6u?ktvQ^O<=JzOSt3Rp7dXYo%ETYW8ZEo22^O5F=>}MXFTLVo(21}{flk9q# zL}tG49g-Tm59xNgA6I_Oihcfc?kI<<&l(9Q==e7~IcaSyc{cBjLrbkCflH0{&$p6< zSHey;;P^Ner{S*P=|fr54w<~z0Vfm?V%g5YwvgFr-m|iL4J7Ie zftaUY0vvlhq!;4kq3M~igp)J&cklJILwK(oSfe>26S3bBcboE;q)$Cf(Ll3J8%_zl z6OI4rG}F-a3|L5SSu8Q2!aaCj)L%-cF^tZ|Xu1|r@>`oS^afb3WKW$Tszq-4puM}{ zx-&*1Hh@WGMCdU>>*K_@#ml5ieh}3bIoetn7{lQE8{%r z(O5QR<*uhoCtEjC*S`t4yBX!oI2F_~2<#Wx)^>jTt+G)9DN|Ha(noyKvVS#24B;cH z2_+IIlg8sJI?AlX6K8^}ogT23qJod{8>fGd4Das`c_I1%IT4wojzSFe>3hDUrMBMW;_J5xFbH78wu!q+qY=`Rjiv; zcc1e%oFhaS(T7*YV>;+`-i0!@;|qEXakPC{;ZK~flny3v0qy2on3zvL-gTbe?p)cX zpjLeelFxNcmrUGQm%mMADr(B@o~+a7DL9&v9Hf;{I3%CVtG5MQo4yBA=^aSA5T_e6Mu7z^c^?oRPsktnUofwviK#!q#&4I1gy9>zg*t@n(f}u74BM$ z`6dG~&b}i$I}>@@*NfCP88fh|-rMU;$B~C#Ku14Mc|LvQwS9B^@Mk>ob)os-2@1?2 zuxW{|rH$%X)W%lqxDA)@;c*358WxeLeCessH*Vl|`mZ57v1LQkkR0I`4s7nQ8{~FB z#>Dxd1%VV=RD_Aj^|Xs8opa$XwAV=o(<%^L1bRy1(|zA9wP^v)uF73% z{6CopnG#*bkg9gzCD4TRLZ@|7j?+vW<}8Geu{K3FJ(pXoxq(z@0^y^fvfHmY;@OYZ zxvvM9&HoI?0TfJ_By?P!{Fu@3Q>jR=PwRDdXg;aof|l!KWdV|!=kbm*)1LD+FDMkg z`;2Uiz>HFyIt-4Kc#rHVAD%})hS1$qnbU>2Moo{@eE(tH*LnY^_*cFVFD!FA=Dty`r+^>B+yDB=6{T_Fmu3T( z{;cXym^e+?J`=Qb3(S0T@Bg?TJlo0XBu6zu4sSG7U~~^aRfCv!^m{{i+p`RrkUiF- zap1XJ+Jozdo73yVxZKV+=@3Rm&V^gesC<+W^#*&4f8}Ky38;Q$?bYD=;lwx=N+o=U zNf(we>Gn4yo>PZsbt(Gn`jeOGpN{6ofWkIeNb0Y(hNiO6)X97QFZfP3IzZ=^Tw+q$ z=9KS|*}-yI!mHgqChq2mcRkXuDb|#Sk)MZE8y>AVIjiR2sMSO$&N%ugEt_=IZ+stG z3(NUBgVs&qAMU67{;^~R$$|gtRTY{DLHol+i^6VjoN#;KGt*GbGDM~ zD1t84%V9DTQ&0aimcnw05K!EV$ePVNTdzVy65L*o_`hqG|624#=3TGWxS!u_-d2$F za?f;&DHe!hrh&Q6-RHi>vftk1c>o{*{lJSO{TM#8myOh<2Q5%4*F$qvsl$QEX64#7YKuCqUb)*5yS|zpHI;BEN{qUvwX;2LpOlVIiHF?tr*eO zc@;9t;{Ix2m!6p?g=9>Pht_$$v?Ds|%gHvA76ymC@;aTvi{c{DIX8r$5dKXPky{=b zmLe{Kw8D6p<&q=vkhy1M@pHlfFu*CNm*N)EIC0E3j0E2PN>L8ts-=O zPPq|uH3=OeLa#SY*MdSk63#n<{2Th8H#Tl;o()$l1<{$5>%lBfW9(7?KSjP|B_Q{U z>?t8Jh%{p!e;6AdNIB;?Z4aSnyT1&b(sf3ruO*<&SP{{1Sbz)R=*CXVLu$&+KK$Y@ z@N*&W``mcYnt|c+9gcJLPL-8w{qt>MG8V(sN<0DHBP~00P?4a}6lnZ5 z%V--zd9e>HiK$l^j<-WF?%zs_ff~7;0(w+@=Z`S@qc@XoXYD``Oy;)VC#}5dgypWd z(ysdvyy`5=HKhp^kn3HVLHxWi`?@x27SMz)wdMQS&dz7)+58X9f zIW2-?v%phFTi-s!uad%@TC#^*Jphf=hHhf_NpP*6Hk z^?H}gUYvWejcoEQaPh&#A@VdOD%|x^!_B4#zrQ}e*jJ94u`2TIzib^xCiI#LYpP@u zJ(AaJW}p38r>d6KS*Vh_g!xEKlKxEN%`kgIL}RT&?ha-w7IHP@ba~XCqo!^;A+GN3 zV_N9FH>r-Aw*X?jddq+%pX>dVyamLPg#|hJJ}*Tsu9)`%#q=d{(H0vT2&#|Mx)9z$ z61K5<@wUYC&OaG!dB#BgW#?A}hS6!y#f9VE#3C}wO$4r*-T*m3=EcJpfvUpT!#Z_!ovr~Uqq3i4<#(up;fol^&Y(4P)mjU~Kpty1JV<*af(!J)EPJ{m&p4_iltgEi<&I-nXry4Eu~x(w94EB} zF&4%S%DzA;gPe{J(Ou1dCb8z?ca7Yapw&m^W*8eHLU7K^6A~$CtjERVag{ly^bw57 z@wRq)z8R-_r^Wo}q>Y5zW~NrcFSqTvyx8Y(eor7sl(2&({r`rO!B8;=?=%Y*G_il^ z=509MeeY>!=lBh&y%qvih1uVCMB4nxBw`V;%qR;-Wl;v14AodsRi}XQpE9c?f_N^< zdMt;Jc++GcqUbng2O`^x%lPv?0yTOKF7|f3(IDBK5_K?3+Z=}aMmU!5>Mq{r%}u}V zvVU*iY-@p*(Av4BF?YmcXb60K#dButGE1H|YrR`b zp7DAu>6y$0EL(Cq+}mHnGk(VED3;s1NvpcEdfsTi++qOvR&hoHHViGLKAadGl^FQT zQ+Ru$bln>_diQYxLgG81w^x?lskCJla>mvRL{j(m^JNKt4Nm$TJ?aXwtfzcWKl~N8 zfBw1I{j-a;wa3y5If!yX0TO;hDMU~;y@3-K8cT+fZ8U&`^*?zbI!4^yTedlz>?1eA zffj_wbB~c84!29(ybYQ8cmmK$EWV9Qdq~!$#+0ZkY6;q1f1~av)hv0Sh(maF#xf-o>A5J8Kg7@DiZd zeVOBVF@16+rZdpzF)8Phbz&02N8;z6Cxo=G9E1ZlI098+sZ?bX{{6_UYj0Bl!>tY|?8wLBUS^-3m9if7il@=t`$a0F^R0Uxagvv+>!Z$-d0`=P6C=C9<@vOEgYG%z6Ktyym{IxE` zJRub(!Di!x?w+b){7#)S0~mG6E~(r|$oo~sKBNa5Jd>%w`sndzGX3cSoVc$rY3+7y zj7lo|2ba5K$Q(y6_kmt$%XkV|xz~OG3>9Hz%z?s9;7(Zj^$Rjx2QmPwhcJ=6;%Oi& zdhAG;p-Fu7H5kdTZGMR7seh)v}T9A&xyGrYdS{5IeF}!nWw8NFanmppd?#`eOI}v%<54 z+uIHXhM!~_L;uE>F_^w*9(t?Z#(6Alku;mLeN~MYqE!`4m}-$JDHSE*`Z+h`OizEa zpBDxX)IQNrc1p~_EWE52v^E#7BW9naI66=OV5s0X%bc?g_va%)63&e^AkKgBi$S|z zcP7uM(r5ob=cUlc+R@Bi;EMCglw*OQBt7wij1Q)b0h6!{CJW4_?W;hVaomiTVL#p4 zMRPsxbU~02T@LJfo8s8t-`>{V7OJST54wFw^!eH{8Pgp%OnANDCbLEYMGc|34TW(D} zPh4V8H#WH#z)efkP-ULxiz0K`;ha~*$JJQU+7%wU2>LjS@_~JszvRk#>iycsayQ#o zwU>i}-kG0FU-)+Qwv8x2?I^w|ejdb}O58do6@=BRRvbTH9_^k*Pj*h|q$lzWxoOww9nvldL+~8?K6zoE_B1eYSEHSv=E^{-mfu1N1-&qv|A>^&Hy7N2j1_=-C3!JVemDs92osMxEY;AOP2?EfL3Jqxa`4nHjnE$&8zi`br* zR`>^Ps@f0UC+rItzgW)<5+04!FTYr?XNI%gTn}`!gVXOy-UXP1Ppx)e3*sA6R@d0vW?fdTv3_;B+nwB-AE5L?N;YTjnX0 zv^9fDZ0t!3)C*ryH}FGu8+O8Ju+@0iCb8bfvwx*Az)#SGB7zd>9DAg|4ZzI_m%Ps| zVEClpHZ2=3>_nZd3$@t=UO)iA)4V+Ak$gv8N|uq46a$ z0B!35+cqZ%LPtVH5?>{Y?Sjflvo$AD-sMugF-#0FU2li0`xRD!BjXg%gUjiY87o;@ zG`}&)O?^RYva~8O%^}dg(Yaz;7nYGxP4xJI;u0`Vlg2g~{3i#wCD#0xm zbYUOvn0n~M1YZW>9Mc73XUCH=Sbs4$f9N&j6=j451_4nh-ea5bteI?A*B|{pUfVuO zpF{7kaQJZt392mU-}~Ec-5suw6yq7)bWGxKDY2*jKRDh2HX7R=_6kJK?h`3>jCaM8 zg#tB?1&Rb_+W_1#(i42KbGRy=>^oD)hRm92t(a>5zB@rzU2YIZddaB0ZP%|oBoWti zM>#5>clG3b`U|UuU+7r@S^+=M& zDa@h;FE&v)qQNw>W>kS8*X{5K@L0@4Z)jJ~vO!QBI!{d}lUj#1HPLkthzOZ5c#57LhGAlU3-C*9+OC8L#m2VFfjF`v)6^h2zt6UN zj{Rx#@^i0Mq!6HH?m-_*exz8M?C(}LV8E&R;CB7LEga#PD2Ed-`)DU*isTryHv85M zCAhQJ2etu)Y}?1`JNTZ~Tg#Bh0_`5Ui*>Pob0aR62!|;RUd7S&K|SAzwPO16_fywP zlihs5uRw3hDhD=+|FferIol@bx>?9;GS0E|*Oe>&!I*GQmyDg6`%GbuIMFXY_}<)^ zXAF`jLt5PoE&WM092-NnJ%ZbA3zi;>s2DkJk9x&A zWQrw5I$pDNXzm#QJ$GFZHbVp5-Y0?XlrE*8IH6;RB>1x%0tGtjQhf`3+mA)E`kGynx zc8QuJkdvv)*x+68b!IJG`T!1c*n=0#QidRW6aRLqB=b2mAJ1q-WYO2?9lw8n@UqZT z6pqH6M(-O=*hiuCo4HKXeuJfM`mc#j-&ReX8uy&*+P{Z8#X}6;{rB3>vuC1xH7_s~ zmFh8nV>FyJw(hPUnO!2%l)xDYg)F;LX};h7db#-3NdzrryW};^vgKa%<6l{yy(K{P zJ{~-b-1krks%nvrm#YSA&xNDbPkc2;fZ%`J5fbCWqA8&!z2NB`>e4qckc@;~mV^|W z!p(8(zj1WDxBtRd0{q1pe_u;TG2;fooi#w0{u9kqnI+1K#{dK`S+psr(+qL7$nt_N1!%ZxI)Y?Hckk5lkbX9;|S|yw&>P;Cmu1-sbr#rnSDU zscy<|W{GIjmu5EBg^6%V<6^^3%>qR-8qvb$b%(FVf>GzmA*jlL8`zQ0hK_|23$2E+ zinj;E+q7>(di!S%4=be#1xmvJFWW2L#X)Gpla5{aY{4ML)5A-)pVj!INpO07_9?)^ z&^iL0h0Q*!^D~4wVx(FR%H8WVb=4Z+M;D>j54u&DcW#})Pv0~dr>oN>8%^X4DVG^)4is9!J$Tda8^wE-Y&s175xqRgMfxHj#W8Dfa!lzX=HXTfUzvl>J_>I;| zwV@4VPhLV5vGsTm497%@LRAXPf`@gp%l&K~k15}%jyj_vJ;$xxv$FDgNuarYoCd%P zZMzoQG}7s$QpDkl?(4!(bM~KsVQ?#kxXGc*<1+~Krdm4mj&K>Z7=n~n-cE`@X3^D_ z@G&)Ee&#pFfzcxq!S1uJdg*DU8~p7Q8;*2LR0M(#vMx^Ha_f7D=8$;oZ31ax(gYEx zw_%se-nN3|*r>AX@-)TN{g&R1{7Lu~c!5?~7h?b3ybzd@4V0=dVMq!e8jP-Y8AX?~ z!;>3cD#70$j~H0wEwnM*sYmP9M_zK$l#I1EP(=bNi&oz#J5Z4*gSnUcl4`0lqC52% z@EM>DP~uL{smZpD(I4Pz@=YI2x?-~Kz{)WP5z$!Tmk+u>AYQ9Tuj8S93YpIc>i%Bm z9H?RH&D>I$_c}mhKT1ZtDOu?V?uN7~);{G@=4vmV4iW2HG)Pxf&KU-qaA9WAGt zCIuenDxp>A*nnKxw9Z&?E=p!#Ze~ekS;p$GMAd%2Yp)dzSS(n<2QC_?@%@7v2K!Ml z%@yN!-%KMe9Iv=#s#;%Fk>#35;1FEn>G3WS=31n5*6T$!Bei^ycF>vneurW88Fg=v zg>OCvz|qGBcew%WIfeCS$jUmm`?x6C`ABc=E3=Jx97Ew0(gJ=9A!plkVQ|)WKk*iC z$46aEB#JboqX4D2?4!svS7a&+k)CkoZ!bsF65rt@fNMeCBp?;V#|HO5J~Tr942>WO zbv}O;E+aj3zurC#?^3CiQT$?8G?R{y+UwcX!8hxRHG?a9HgCT9HzX;BOC@r7Z2Oop;~=1QR8G zoF-<2J~h7krs(soCcko*dca@(H-9Nee3M0PmzlJ``yGvy`{gdp$>66HQ_-GouudGG zQk{A`9F;L6RO(kG|Ifgzm4pY~?`;?Bm&~lTz!;2s&_J=^CPn(#S}hSZ>c>cZ_f&LG zW9N0Xd8;~(U$|i|McM`9?j9j^FH|?2+Km>8= z=3P;_ChB}Q(K}7%iUG1B_!qM#$-!5nTy}a z6x3$XYys}JolzPOMkGBeLd#_T?k;$o+6+5#v?(@&NW6DVZlKoMm@vLd8~Z)ytQD-^ zssUP&-*P|z1(tN(LP)4kwirE2;ZJ%be0Sgio~5HbdkSqQAn0UNhG-ucdB(vTaiQ_B z4~Yw5z7Ia*`*V`yh6doioQ-9ykX&ft<?~CC>uO^`emh@02>EMG60b?IYD;+3y{Cb z7NL&9q>@VOqodqg8;4(;J*q8PfQouu=^j6d{ac%Lx?3F(&0`e6%F`SoDdDOVz8q(5CbdLdS zEf%355STt$!2W!X5Z<2h7Yo3#JI{KI`QmM!|IAxAMY^pQeG+r5b{d$`>F@Oca5E|<#K4S zsP0{>*~R{w4XmC39rVogRD%be@zFE|BULDhG!Lj-r@&F(6rkUKMM;JVwZpiC^tg5U zj=0k(>45=+nw%VkICa>3zlcml9DnU@cw3LI&4)r$LjSBGez)_HtKjR;Bt@zIH1H#n z!=Vu}8V+pqYo9Wk2*)F_kHv^Q>HIXEVptMhf4FgZiwZI$5-OMUmTkevBP=WssM(bv z2^~gjDRdtaA5u!06OS0VoJsNiPha&F7v|M$fd<)spB@s>JrME{UF_rs8ZJyLjB!YM z+Tz;`>n;4a7Kum$I*1e?h@cgk3zG-O)Ty$4!7yzr-U+S|qFmY3m+Uyql2%n}8gZpJ z#npkcpPTt#n6LfuHVgFP-C<@DNAU04LtBy|hLRQjNIn8AZz4Xm3Y@f{fyW~k3OEZW zYG6j_0I@|uAy!QeilbrD_l?+^>swS*aTic8SW-%+*ky~#9BQ+NWH0sBh!1dkt-xm3 z8{l~yq$h6>0#0G&V^);rAty8z4vvNI)!%v`+T&RsO{NGlJ`NzQWC+6-x0R;44@Rf= zjC*@%1fJ@$rgs4mm1f0uK&3qgtOM;dj@A_W!%e(^K8X}PGqq5n8!4$DKZ%+9fgQ%C zlZYb!Yr5oL8$hcvE^d~2F{}IML)K!gGQK3rEKt!_ni3SA2ch7ln3C-iM|B-mGO`|q z$s{(~nOC%iu=-u$HKfd!Qgm1dJK%)N9!-F@InN@*C zV?EQ^3Z#3S1EqFu*mXvX2~7QTL=3lOQ7v5a=_FP76L}^k7d&aD1XjLlj|g^?Edto^gnB;BHOIi;dpN$K{h&Ml zvuHb53I^Gd0vaV>QG}Ban1&&OjQ>5TG@Vm6Q1yG915HrsFR8A0pa_5}IpEsvf9XMn zWs)V$nTVhYlwreC$nmr<<}n0t7kBfFXI`)se*rNZZbm4{A(_P@UzxFwS|S{9*N%n- zSgVk58{!8IU*YnM5h;|T$gMqp|7qsT0@zZN1mxx5us^qzT8N{Q8q9>Yuy^P*C5RP6 z&XlsO;MfCMKAf5;VXc|oHSD9X0~~p|cIjW#$k|KslIU40NU=B?A?P6{hr51aR@H|G$fv1hj>pGK2&c)={xZe&b})Bs=`CfeBzAEQl-n zLop?TMnGI<1V01`ywYZI8Q zhv`K}1(ovbXgs1`5WwxkAy0ub6p~J$ks<;`^kFIVr0`(LL$MB}rTDmqPnE^aEh*`6 ziEua{e#dqMtK9Nd6#(|s-n9|n{UyM?CLu|1#M=2Vq!y;v_(1`Gn;?etKtRs5ul;Gv9IdY8nJ7vY?w^rD9Y8XK?p6Sz9I}fFQ5%09HA3Hd%z@95 z!4$gym+|hzc4?ZXgzJUZ#>2b11-u8dtWp=Ew(!YF?NxD% z6CkC$<^{_49ME@n|0`d8Vx=_ZU*L~V%L{NmKNydO(-@zG+;QN6N|!0iHLInnr^#{R`>^$wpq3LYc)%L^g}9v?40qPI#h8 zCCpldRWrwRRLmq|$* zto>MIs&zHMAdoV3-4ZrK#%FLwTgu<=E?nWl3R2FZCgUKPUabgJtUeCFhfsHkj_Dm`2uoSMZSB7x*nM$MyNkLoqrcJ?sp-Vv9 zi4(=ljE1oEhS#zKR9C=M{a8?PZZrf|<` z$s*z$xFZdIUII1*9F`UX(af4*ZpXvu56$8!c+ z14>HfVODED1yYDE5sj4&X`tpT8|v6dEAK#FpRoZ|-3^SX2|1BTSO2(@YKq7f#(OwZ zQjqD-fdNAOpTQ`lIiD73xLe=e&elzrzK`H0WP#kR%t9apM|uJ|Gywbbo8=qNi0iil zS&-#IcZsc$R9ts3K&YPxBSBiXQDy&wdJqM1A8MPv5Z=ohYGkzq?Bh}@fw?po$>-Cz zhWx_e4H|DnK&&7dsvjwF|8aE90d>A@ICrwmlWlX^wX|yKWG>sb?N*ChUTxK~?ccI( zTi?6y@ALNb@aA=2_vPmWKUctRwTVP7!F(K_dm`l8ixV<^Ect4TkJ8hIYe|CP(aZGG zfC+qph?KT2+1B>iQiayV#Wk?w2R4L(~}3Y9X5(MG)AVJaM6Z=^T_hO69T zukI#~`g5odR0bXvZ5Y-+s!XXzR+_-1r$8=yJS3IOXg`f9b+zIVP|$Skm&^!XW`LV& z5K{3m>7}fP75+merxj19nLOBWAzeP8KCS^J#(C|xJFfHRL`;pDfRV464QeT+HLNz% zKyAESySN4abfL&A z!mwLjmswl*<6psyxE}=IU-~77gQyXaFKpy8m!7vlA;xh*fq0Ea9YdD{5M4F^Wf8^1vHZ zSLgO?EFnZej4*2e6@?U#*DPIFESIEC68Ku9Zd$~4qxK27jdgyscCj`iP90%&07p2m zN?D015GFw>8>0$G`hS~DZ~wxf~tR%Vto+28@d#3`1U8{=pE)RWHhH4Oh8|x zufqxdU5*I>|4lFlLJB=IF&dXC3m-_|@?f)oS=;SWMG#VNv|;^Vv2m-k0|IR;rFLnC z!g|t4zxe>s051^7Fb<&>+WIU~sH)3pwPF*sn=@g!`J&ea+Twu)mq>GBN@YDxz~FN* z<||KP;3CNdz_4tm{O9C>J;*{0JmO)hB5)t!0sdia2DP5ujgvJ%=Cg&zgP5zuc1kgf?)NlVtedgc}Gy}PxeV{Sq607)(tF6F%Q-Z*|cd*o2pY3Vt5pi*<%DWGm(^!HU%gP z$04;UyuB7X+;QEQtwS{j*m2<~G%@i1Bpl6jeHi8VM+j@;4 zb*H8sevJun>`TRmm`)y9<6|}3fPg_{yHIc;`dA})iE?{eb|cqI?>z3P!>E;bG;qh; z$TV@Z03t96`&Mn1rKds4W7g!yEasDnXvu`V2uy~p#f8!(0}lfEJ~9BoC=L^dtAkKD z_E$nyG|nj#LArW(0qMKq8bkd=1K8sqK#US19CaVq9Yf*Dzc8f54Mz}8G!FSe$h?Vb_uCB!Jf!@Ohv z{mNiz4X2;n%6&Gy`Y7FnRy4vms7!aY+fhnJgDIJ335X>^;3cAS`1?b1vcqwqF_vy7 z_-w!VQEgbGg6TbXzO->z!ih|#dH}Dq_X9@@>{!kFFK_=|vdh6M><#0}Wu(Y6$T^xm ztw014OG6P7Ab+CF@TbjLlusm%&MMgV^j+sU2RTcOnOMQwY}DZDd{KFmK8|?313^Si z9LXU|oB=m3cok#ETqu&7X5pGRz-{Gl>rGA?7-2}*Nd!ujIAH*(f{Uz_B@-vMW*J%A zZ_tY34J<{GZqMu!^jF#j- z`|M4@gvuW^n*95epJW4KhD)EQ;9ZbwJT(tQz?~b$5-YVGjaPJ?aEJDTvP>Qs9PihofSrH_jHIKnt!cmM*|u&i!@P}baO>;fmsjG=wfy7dd0 zRjf*V+Ax9vbZ|j8HY;E%g0~LT_=-y$nfm@%@pDdNuL1Xc*a~Cehx7_rIuh}J~@E-t2OG+ zHgRm7JuU>v6~iln8VT5JDbiV8K@qY#xCNTX7OYzWMU+pmF^!V*Guvy$S70R z$iUmj9Kg+B==!DLG!mhD$dmLfR~DdgOq(M^>INiNo z%^?#uRsLJFa>%VPvU3D3bX;2!$bb4ZGvM|(V;I_akO9d=wNYs<|1~S*mrN8M6p`z= zFaEI$oPGDx0x9PP-V(Tn*+&4LI~kG1XGB zAZm%!9iZkaQS~dC&VC0$Nj4|i%lDl;(cRAE)rty3rCv1=}oQEqy|`|Qs28mJU{p0{h?cm z3l@{M0W5eVuWvSSl<0PyKFIZCSarK+1m-jb3|uzd>SHr0 zfW*Vx&_kupxjVg43XiJ!&nLlJW{PA|Gw2Ey6nz4f9 z!=!5DnH)#blk;6fLKWPuSpc(738{!eX~>>?W{`mNIY)_TH{ppI?@_i(8%nwd%T1<* zu0hjl+#;?=Ai_BfIF&_7ROM}SYz`8qIthoOx7DX;ZVjTU37sHj=%{8?AF7X-F&4Uf zhUD2SSLcKNegC4z_}BxYsJbsW`sz!Y`?RQ_U5x)GbdJ1{o#3I0sMBJCy!B2j(C`0w zldQBc5~!Sy24NpZp&jQ4QXcuXDxfH_nNDIPvagho*uuuZo;36cE8VD{b#Fd^7P;I| zO;GDf3Jh+e?z*GTrE(ef&KR2>1V7Tc_h ze@T`Tc*;$XwzBsgK{h=rvM0yC5*B+T1WPB_ZoDGsdhnY{^EatW`1IuhAaY0?6ogex zdK;fjl5Vf{31pit;DvmT312RXE%{3_sN5MR8c&_nqM3so18PkgV!|Zw8q`@R732s5 zRRYF`MUm7Yo|p3grwk?q5jvQmL;qX%;Q`^mR~e7P_0wLW+JYZ?VFjLWD^^DMO@yv; zB0gE(8YDUjRI5oYP}J6@sKBLglWdNZDQUf~U-+xqXuWH&oMO4d+I61_V=wI|VJvc@ zSe@z>eHHJ^&8!T|MFfvwf{DlooVN-FT$wY84;RhH419?a4)Izj>-z7HPhSh($(@=W zcTt-N3*4nQ%WY2kR7>w~IFUyOAqVy>^hta$G}fi6_C!k@BC8Wn{%4^P6mNaoD9j+h*&r|Ngr#Y1g`I=hU^BL1 zr#y?wmpY@MV-QC7jr^9f9Z0RqUZF_V7F@)GlaJ3+B%5QPNrC>xl6R|m0W=>0juWr(e`!am94I=oY2sv@h=GIOU6Du#B zG1th>gF!CEM6T`^?P!L9QV#;lP{%D3Dn)!MVYqaxa3zf#%qSeGv zI+zV^BC=q#7fy7XnrHz6?Nmi(aQm3horHyCQ!y3g8ytCd6rG)ywV$dJgX9iVml9m`T3&se^6khp!Vy55@u8k9D@EXgLpb8 z@>%N*>_|)v+`UWAGrG*T%7N%UBZKLI%85l8Ja*+E(F}(1qWdu#x;!l6-_6)7kxB;; zFg~~hM3~j5O*11Fi3{sk9PrD(hzP#yYhi;Q3wXb*ET0qjR(Xq>v>RFs$SEu%Z3#Wi zisj=Xz8j2Z32!lK1?B$S;x?=I7$6((v+bKWmj>S5ZMfzj0LZCupO^X1QpFp2i2H;?ShNhdYyhtAK<=VVW{43B*1R-MB#1 zB27R3B+M#^nqc{P@{NQ5B6`dXYujNuf`k0W#`GC};fc~O*!5t;ghnzdp0GI{15<6F zGsqm(0IuY_zT2dMzB=peHvqD{KMF5+lwYZbIzKh!k)og$e#y0`Hldh}K%#4^Ve(Rpl2zpbBlN8;ft~K- z6Xy=T$BJl*$~fGo0&y_mFAX+c_+;|^!<@80$@$F&({_K-mBHFf!)1ngeSnIbmi`C+ z0Eb*ZAV5LxtguIa5k{-P*lUgRs!a@HAEyliX!%BE8gl`$@yf3L&-{#|a_ViNI#XSe zK&c{8jXi4@$4t_KoX{nOo&p@4E>n1N1Ys|zTDJO&RvtHcCwr#VW!iv+G9qY$5vBMUM z0f=|+I1%W=aL1_FXfY^>@DJgIC6-nD^Qz8e?hN9?*g(OW(ANms|`gWN=5-PB63(rKFfY@qa8%&_` zg<|m#8Mcz=hQ2w-a#WcKz_vYH1)x%xl4loO%Lj~6u{?A5|Ds6R#Xq5>MYVj6D%@N4cta`;ydXcJA_ ztni=^W%6SexuwW^wKZAORU_Zk_OHOH{2APhVz$Lwo%)df~5Dh0}%Y$bbqrw$Xga8^{Y;Gg!^SjCkh+4x)tlF4A3IR)1-`t?Swn zU8sF?^+p|w)yo@w5uEr2jmHBIHt#(+=#`6_!jwVH)%qOLCM4VPxp z<=@~OXlE^(pyUiAg-2Fwol7?Ie~$VU1p0i78&E{%RsqbJOl5na;02nlAMxRtPLuy7 z2!_@xYWnC=YK_IB^j#7;e2s4c(w1m)Hn3iZ+>pxuEc<$~(Olmyg*W3DePCykl+`(? z-lAI1xRSK3_w2}4|J)#V9#>!qYyz8$c3?*+Rr7N zpB17XlOSi1w)|E55x$gA`fD>{aqgq!u>zNox$iE&#pLn>ZuxXKAwb*h5F!veIg8z$ zL2{RJv|O7cgu(I+&3{!21=$1Xg7`bdF?j_AE>d$6TaO|dHsxInWE|zk_M{hj{0bt_ z|1MSFY;$s$GDhisyi0u1t<&dj8f`)kz%q(|zSTxstDigxoyopY#FuNd+hw<41H$2O+tKm zV1IjJ|6B1K#`S$ZfJVz@m#N4M^*tz5V8E1LatJniz<1cjBZYUWAH|22JDr(&oZ3`& zp8ts#sGm4(f?Lqdav(00XA#tN3{|gtB^<^ttTexgd?0MECkKBs(Za=z^U%}Y*cbK?uLg~i_RLbfvypRW0)eS6i zIpAFxv|?-)Iulgx{ABFxGs++!7v8?b`BQF2VzdV|*H|F#+4vY)zMEu*1$~Wo#)+#I zR(7~*FbVw;w;3UY6KJKF-Ti085890yuQV!MGF>s9i7Jcz$9qzM+ zBbb{cdBHnb`>Bx#D(nKiKn}%#2Muf<|8O7B`ohGkp9f>$W}IKqIU9fxjphiwkG+w> z8!mzIcdeSVe_Gj6>WVUDd}xQ720Zf^{^tQL;U_I%8KIS3h)vLi&nt^5Db}4~F!e1S5`iNI_K0d;h( zoA4c$qCp{}Bgpl~`4>U)jLl(0hCV981I8!6OTSh3_wKSoi9+va@8DQ)N98&mPn3CJ zy7LR!-8Nw!4<^`L&www^(+T|Oe3XQG>>5J=pNmynwnRtr^bKrM3$<_WPk2U3oK0rS zzYfjrRR2kbdGf+PDAZblBwh+MOcj=Rp?`4LJZ8V0`aPs9^Ot^rrd3dr*YWF`SlD$v z(6_hC?$xE2U@5TEME9T#vmeJexkEs<0UR0h0o8Gvr5Kn&pCkb1&jn9W!mBlN z?d(N*VXyd>3<5(13BHiOCym8fc+eka#*)!7HpfL|Pu=u~Wy~%O^0RfV3c?EzcYEEQ zXdtpXbEcA1nj%|7M+RfQ;Xhw+`SIWY719R%lbl)T5xV$)h_Z+jjH|vF6u!PdZ=S0G zxB>zf=D?n;1JWEpLbr;Y3ooOqEl5>EAwY7R=r1soPc8{6yRh?vIeT${UxTO_pD$RH z?pwS9l8f8(;>%pz{mnWOJhP_Rb=C!g2ai3#T)KDfOiWf$;tcR`>|N5oFjL)hfFT`bL?TXzi(2mR*7|%;I%H|!^}z)F9Wi0!a3!)I1QyN%b@T-G zh!UFu3z#>*g=miPjfe|f_6 zxn)g@27MSb1oRG1=9MfAv{fb(s@ZRBTk8PamE#v<6BG~ec zvS85SCXEx~dr%^T=2#-m4H`e(i@UO5-F=zAtpvH=3mv+^IFXcatw_|D&AV8kcUh|tSqt$_T<#f5U(cqGpQSAC*T|KdLFApN=!GiJt|0bSQ2Ik}3PBL^( z?ZK%c>(i?H5dZxC^L||-#ZV`Q{^8HLs7{f)bcB4hI~^AoJB1QdC;#yWw&8#OncB6M ze`)pn>oMjtZ~w*T%crq3do{J?C1p=XJiUIZSQsb?%+Gc-NkYj&8pl(go4vNqC~68K zN93`v&dB#su!*R+Umkt$3(o!a$vI)6>JNeb!eLn3PM}9ArS6O;_wn1(l zd6#E3E|+$Q`tZ?Q{(|UTlmM%eJMbn~47xzTVaQF#K+oU7T5FOF`hMa{C#!dq?U#oe zyZg?Cb8u!10^61N$L?(|Y6VILCNI+6K>2iX#%ytZ#~w@5^>8hmk@`LE(a8dZX3uO_ zSBw}|)r?UNq;*mrkukorTot>i;&EO(wY4Qb0rj4q|JEC`;Hwt@fWPDHz3?Kc& zWLw(OA=Q_?_0}@0HTv_mN|M&qk*a3oYj zc=#qY6|z($yI~}PXU_EkM%SHE?Ju(P_EJM0llX+pOrX=2oytnv=Q;>ZDXi+of4i3a z7_^<#GBkyy6J)%?X2=$&k(wl?PG!Q}^-Q}iufqAaIa;r8X#9Q|TCvU9R_2igCFefF zQ!ySt`F+$1dQdYSU%q%FMNh=oC3`hWTfxvMg1n3lJ+G{sQUfW+?)8}6fyqd8Z;Eto zm#+*>1>sV#hP|o85O-R9Ik9QQCNrx+dKma(SVnBuIy2+yoV|Zd92P)8uJgc)KG`4z zJhqP?j_wI-FPLkFt92nT1qTmVM*4EIGQKx!PRppD9`9`P;*wRYd?pQT?qG0YGvQHU zJpxn--a}Y52v{(2;Y3s~xs9-%#!1lfEV!9}z1)2NDK(LYb8*@h6!CDCdO8?ys8!k? z;r6Q_>7wV!@!vnje~-}q{~trW8f`c}&`J$~*CeH$tpy%gMp`q-jQ$Wp_QK1~YRTYmD}ZDfWA1jEHNh3bhqOhEd%w{enS;Win_ZOfvw~hrWh4H)81z0C zEwnSi<#v-NuI|+m#~dG5^ABVKi2s(||E{LHIK=5k0tfMnAi$wcJE^ti+b?x@<9+GbttdCDf7|W3@7P`p1^<4FFdDaME$W_EH%_7e zq0JYvi95F5Yv`^W=ipl1R?ZrK#-O*QMG+F;*nP?NC%1YOoS$BiYw2Tz3xRh@h+G_f zPsAX~yTPe1cr5bQ-sAZCoqMk4X&{>(zU?xBt+}J~$-w*Gt2)nU=eg%-S6lY6Ttv2s zrxF*mN5%*1*QfTF)VvZW5}xcFrijhVmSJ@6cJ{^L2^H>SC0H-=xW?VTzqH%kS?Ykl zlaX-1D_^zMJBAiJ1|PJC7rL_H|CaGB#aU(nWaw5<}PyVZH?cvog zGad4Sit!F;i4O2G!!BmK!JeIcnGtd-T!?q@s7k^WTi^yVM7}q^ttpkL{f8ZUwdl$W zr(dG62?C5mN3XHo(kA(uZ{;@x+WfEA#_)DE7rz^?$qI_p8FVSLTxpsIa@&UYZ=fx| zYe*#3YRJHTs(R9R40mF2Nep~Pg);~&&m^@Io_9Ojv1DhVF1hwLv7p9XZBs;aE+~tR zS|JXGOl)ut&>HOvs-{rtm$MJ)B&VQC&foJJ5T2>5T(676Lf@}n()x;y@7q23=7#%S z>pxo(Wc5P(2o8GcDaFUxVS@JLpFLbs2)ts^MN$~W$cGb zB|Lb#8>U-yuEQ*EQ_yIKSZMdbC%aPa(2#LRP0Ot@-@29jziY)VrL(-C_ABNe2^r$k zI^TEwsMR(t4s@(01iN+_4D&Xh?aPw6ynTRy7VKUP_Cf5IjIWj36a-wuIw@5->vb!5 z`*vz*7?Z>21#fai!t{`>Z`D>h287nvCB0JQx$xt*D_dks@t3{^1;m>?&1Oq6xs;<#;z~?EjY;kNcW?%GuArl zL!IvZe*!uJ-6Vcpyds6S*XKNy@8k3^2=U3$h;)U;MEc%-Wc(-nxvl1WUr23%4ky0-6 z=Fv6zRC7;)(;K~b4eb)=-4lxJGNYAp#j6B_&fwXV zP9!3KL9^54p3C6Hv7yvUSDSH6%j6Mq=Ozf_6783U#K*Cy4;-up1BaXbM!P)xgrf(H z@yx>Ilx|KnKX8eS+^bF-@d}v-a)N_?oc~Q4v{TC$3vkNROi#bZ9vJf^B&@8mrv8cf z{Fuj`x5wzwkSydK1Jj)r<(e)lp7NCAm}xSxbP#E+O+(~B{vY9U=-&NuhF*5 z#B(d?iP=vEdLQVy%qS3D*ZySx+EP>@Y*i~l@7{Iz8$0%Qp1yCZopu|X@s@upzAAFc z6_yeD>z-J-*xIg0;WaDY^oMNV>OfiB57_K~5!+T0@-JJtXlX>?%A+ehHM$aHH1>q6 zI9&H=ywx}-6>Ajy4y)SvBVtrPF>hhDg~||(MQ&7G@!^@UA zEDxcojFu&#x?wl{A(}F~sECs5AWpZNut9&KftltD@vBj}t=IhS|Pagbmb> zja0CsiN6~Waa^C&v2yi)xhs=x|?r(8o4Y}e5DmmI=2 zv@A3%42lAMgj=|UE>YYDSK~Y*0};}@_+0Q-`Lw-u9e)?dk79p9;0&A-*RE|Ck zwLjvPYnmZ1LD^=pqX_(YRhZ1+&{+6$wLdb)x?(bq&@HIJ zq*y4U{?qT9DjWN+@j@ffkkSymVKH@~TXs@YRBm~3G6S;D`* zUAfg^wDB6eyf9Aw_oEV?$>L@ROsER`{NVhS3azcYUDSDIot-e_@!}^Uu8$q}euNlUr8Ne*I5+5<= zaw!LR@2;`r4$N^qCB>WF)_x{A)^`=Y*|svPiMrK!bY(}*=M%sn*g;;C?^gNs^HZX~ zGZu<`-Klgyd5;tSCg^L2Tk7ZI3zzp6Nke^s9PW$5ll8{2kWWQ{;jV~#& zfiGk*Q`S#xSe0X9xF)B2K0%Zi zOS2h97y5V;F7QteltWPhp-M2_>oSj=_tZ?}%PWKTi08c`(*ej1pSl!0nzVT*%9ZN< z+731>s!JY*U;=lk>i?FT=v{b?I(1G$pJhijNAt>jqtEihKdxXAx{OgP zV7G*KebsuM44HQSd?JZ0PxP>S~*r8od)&~0#N zhlHJdl%|2TIs1oxV~4f&TLi-A2WqihGzZ)|f>TGf9ypn=+$J{S>cR#)ROgz#pFEt! ze?)zhB%?K*fV*yX7(6V=bbb=--p~6=U0$1RyiRwmeN$rOQ5+O&BU)0Oh^Oz;v8y$l z#I06qKVIT+R-egfPFibsz9>RIyzlXaZDKC>dzjo=ID)FL0p5u@?0tQlr+}2PD&A)> ze9`PpjwtLWK{WnX+wS*{r^hVVmk>U_hIq27QaG8YBpMr}y^q%emq7ulm$c=AgJ@6D zru%(vGod&)CGG`(dzqRPOE1mqG3#=Do>cNl5@Z~1CUfBn%DFdanEvpQlLQ>}e9z7} zl`Q9tn^DP2sbpj6D`#o<42YHyw0j&B>5+TMaLsU?&ZoUe#f0;KvDQ@MZ|Vm_*hiRVp;Ji4Z}p?XJn z%y4pS6$b~-4^?|n!b6h0-#@bdnX->`x`K0V75ja=rACw#+D0iGPXaxB8LBc+79u?s zpH@caQDx3ew;QOWeZT=7w*^92Yabf3nmIH5Me&tz5sWIQk z)$5%6kC<;kvE@gV>t~J>43j8~MTOouVu7wqmd^S5M~)wrmLz|?lG!zT(ePU2poUwQ zw$Q!MoU=6BRLSC5%wTi%l+chvp(Kh-yT5dI4@ltbBV-B;+OWB%ipg4uifMhUnof%5 z6Cal>oA+qhEjfxwCV(^GLS)zZ`}@TFxMj9!>EfC4vFJGEX3r&Ua*^}a>(S-kyEqqp zoL3*P^|+dEn;^s)eMx0n__lD2&Z}PR<_vHXf6d1KG$l!f);RS6Go8qq5mWhh1P}SP zn{&QIgaE*Bc(8XQYG2LFU@(5n9<1_vO@29TrduU4^Vf0Tf{4+ypR&|duaURU{@bx# zK%2q)h|j3hi2#) z>XUo{y@LxKdkIY2^?0?5Lj_LH+8w2$jM?4)$1+Hv`YR7+bW>i8pAE+3Iaa?24hP}%0QV17 zqh(yy(auP-vLz`xuSi<029K-ZZ#^3e23QE%Cy-GS8#caeOtRtz(6EO()>ytO|NdWR4sQcQ&_OkMUJsIck>sZ*vxur*xVg znj_b2Mf>Vth@|68bxH9w#1&4ba}gVTw`*j(s+Le)@TVdQ9aM2BTS*30SD6a3vRDZ2 z3cyvAq=CTGeKJTGk#J_Hsa;*$bObHKDr`;#2{tiwuV^&0LF!QAM4kJ>W(1MPNd zp3T=^n5*=?kzBt9yYH6nMjn%Qwy+hF$6UcPq|i(atJOaYvgCWEDlRCyIUqji@?2-K zYzCUK13F#nG8bL6TT12qP}Rhe>Y}+PC|6Oj-m zy>8Q^P^&8?xokoQUDU3kX(qLs<18(j;yB;k;7s3g(iwKSzf-RIO51WVJf`nsh|OiUH?FWb6$ zd;9zQdnHV;%%Y&5h#)~?2!|!9(8V-SSk!zL@QyXB7Z;PocfziE1ZGBxzfP);4Wb@DT?@o@zvuS)k0WLloEYuFNti*lSiNkefAJ|lGklFLGMEq_(JV5EzR)AnhN*)FyLpP&MI8T@kmC7v*h`m5rGsp($nV{;elKI`ev^= z(`cQH8}w0v>^4nPMU*$~qpBcYRq2=S#7^CYqN@y6%^&id5e~j3LL(fSMRCeHSrTc? z{B99*IITm%r$|#X_h#^%O^y^}J0{mlvIF#L#((`YBkGnU!YKNhrl7$`S__#}A z*q12Ls#2cQiOF0DyA`v<$OG-X=I9`;r)}kiDyX}kR+e7^22PVAhT;CYvs}huN>naS z^TnzLyN518iWJdPk}LfK$M3mZUqa}Q(?4Hl(+3TFP3#Xt!oEU_)E=G0`2UA-$9Aj1 zFSF~MQ9MqM>KyFoB5uJ@u?wwQ?;K#%9*6eAi6H)MHdEL1Y37i&jZc5lvpBX~K8bSq zwW&Z(m;K2T{mh?Hcwu4_2`yl*WFJhd&5@blS%dy*?fWbVM*2N#8g(<2g?#q9c=O)I z-`+yr0jW3p{uj;eVNIFDZwB9RGM^dft)Z;;V}pv)++W~wJ4^W^eRzfbqgGP>YtOe{ zorB4?y?#Y)OVhxC2&zvAR0-=;!b?>7f@wj9vX>En_cribdlkdK;Z3W{uwqYiuQT^f z?ljBw$>bo%x%_G+V%nCj5pk4%UDRKIVi$Y=f`R%2hxr`GNEqYr zBX_uC6Gc~n<~qjA?-}CwPx-=TQh0Vjsf>74E(Jqc)5yUaM!gsJi?he*e)pMniN3B5 zA95Xxp(jOuXSK6F>#Mr6UynUbuXvvE(;iMh;?xc{crfZmZMhZ2HNy)V*rI_z}r4r&df5n%AS0gSj zeM8X=;tquoOaJ~icNAkXf#2PMly&ky`p2@YwmB&!OS^m56chnC7AjijAAWB&G#_}~ z-7s&rir3(iON+y> zpNAH*h63%Y!9cnP%wJP7OCohH0xD+%69YP>v7`LU@6)amn0m?2y0-B8S`0oyvv z=q=X`E1y*V5R}Q-8#zQS{;_R7kE`+F?X0MSRvoaN?-^fcc*0F4=h<1rmNl5fX^bdqCdH4M=_NaJ$&V;3q}# z35IVkAi}+f7MuuC-Itt?KOVUz85sQuFf)>8W!uJQ<&UNQ_lMjt80V}4t0*MJn~OHa z<%m;om|;jhSQuT(!hbffH&tsWK3fN_vS(wg#6x^b7I`n=+zxWPN%6XxLp>of?)Bx5 zKfH)B-SCOQ-P;u+BC(;S8dk{XgDaZ!9K!5RokE&&86Dq${9SiSKEFg&+KWa-%eb^c zy*zj3`u6ulheg)DY(LxAbL2rx?ISC{Wr4}m7=?3k=VTfUQFA3y^-P=;2meq_Wy9Ml z1^X-49?MPKEwH3R?>`{Bm5Q?!4lQ170ox+YpN)Er=v71D&lV@VitMtyej& zzponqrAV}ke0CV9rVJK^rZ^MKP6fATzEZ z&vuXgC!0L08RWa-=`F zud1lhI=6oBI%*yGaZGXE&(>|g&FGA%JY5Mr_xFBp;%!l6TW+CQ=PSu{!A#l4oFVs( z3#6RrG}ZU9-z3egwG8NiHyXL&)2^g1_xr4-toL;2)gcUf{K&>!`d}T51wos1NkOUP zVYzboje_hvcpS%j<2M);@%wON+mZi%e~@wvxEkr+j8ck$y1%lUTaQM3zU(m>=MU<& zFNV;hgT*sv<7Sk{+u(}PzE|vnM6+eHm7@rvVD9WTPLd^%0%=_nuVBDOgqu(f6W`PK zshWjHvzn%b=wp_gb|VAdFaAVSXHOG!;<^3t+)hVUA9{W`Mz<3?2V(5NVW!(z-W5j2#j7+h_6X5 z8lBsyn!WF}Z>NDzuSEb-vqR2t9vIG0aNX4JB^g8Yw6%VYZCJ%Y(UNGNIt4s_L#mlx ziRIx9k0v6(-ZZ!C7y0^v&5kGNbh-Z&1I{JQ384$1yvgMU)fG@Uo!>T)m z_9bkxB}oI*^1FK!n7#|gn!mjmMr1_lntw#kNlB|N649GCU9aeRZWAAOCO`+s88_0h zkog~M=RXgaon8ls{wYh%e|K04oDogbn_`k7qur*CC*WJ|rfDbYi23UmmVB6y?m^hl z?V5-Z_cNr``@&iIh~pto`iPAxy-QPw>2NHB^#vZ|Bh_!3L=O$5oa>thbal9pKYHqi zhUba!zw%%HPX0Yp60lus`Eq@V<1`QB#j4#a;qRcNqx6JtJEXuq!k>D^E_LPknd_r~ zL6-$B?=|KT>d&y*><(Q3Kao>j8Vn`A<;#+M_3eKoU1NA;U9+9owr$&*Ol;e>ZBK05 z6LVr?qKR!M6T9Qi?f1LC`#IHhHtJNZwQBF4?@92`yx|D<=8(_wIeTtr> zUVXDcTscgO`o&xrL^C4xy>DxuB!=1?x)O@#-hn6I{rEUfZ&K4a$y5U6;J&czPV+*s z>OL?OouLx9>c21G`5yQ>TH;K*0&~4$%CnSAQd)Yn+bs&W{lxdxAokCkQuyuWQK=vy^dLXX0H& z#6iB&VHJsA@(8=xZp#);VIrqrGq4L#zP|W7li2h9J~8ZV&9MmIdqe%GV;2&=A z>;-E*`Z{+wK)051RqZ8&a2<`Ms3$D$#Q*Uxmx(98r+X5la5@_@SQ5Q} zu_L~8Y7(E&+>ivL@4EbJjIuWSmFe0xd$%VLX7NEc){zG{= zq8eP#^c*Ahi07R6zZtkWJnJ#hTnV;@bEJ{w>kdr0MomHl5Ofn1v$~OcR6}{Ug`dN_ zXwG!LHUBCJE3JZLOh2s?JSABEfJ%e~JN>`YAe#x^nEN}V3s9TLzMkzHM}~+g*q;J&=7DW|`5_iK>_)X|d+|&+*Fpc5i0j7B z5Dj4av~<0wVZK+Mx;)+!XwLYsXN)=m8JHK-`T2Noe&sQKDgQZSoUPoc38gb`P8(Yv zOWJ}TG(dBHyREbR$Kd3&=ESFHs5^vrsbU<$;xJWgWq75#ys7?cnsQ*ROdRvV>HRs8Y6svJ1&P1t6$mtPI^$~omq@o$Ax7^(6@eLabx)H_yKT!TXETj zh^%b)fhRD(C?Fy>ycs{OBv`ihG;TPXx3u-jGdc3(RVRL>vj+-}rH0zQF!Q`9jZl8e zAA)X!`^1ie-WengJmsj!_EnUj$CM}CcqG*p3JDY{0&E00{m%b+?1G%kwWz1(4ar>Z z^ZutEp<5_jGda?UQ@n2l<|1#FCNhNORxo!>XpClX>Xjz%=qm5y?omiRwZ0jbr=lOD zcEWLm$y+nb*ppRfl6Xg2!dX4iH~r`df%?*wV)24B^CuT-+)lsOqM;jOA<<0_!5`L; z$vh&MzdX>HPn^^|#f75qQfQT#vN>_j57;%oa1h>J+<fkd`Q{2H+l_dVpZznU{ z@wt3*(*QoYH0A0)3Q+Q#&M{wqUAbrOgz8UW^iOVFz|wPqa~WglPxuDc#ba7)u#I^X zKqUJ)9``*d$(*?7LEB$gY2nBTMpGf!%4*0V*HYXsS=6fYXzV z)j&d)`4=fn>;p^rwm);;J)@eyX~L*tq?KEi&PhMKtD*o2)mRv&%VJqZ4YA_LcWp%_ z%Vl`TMHx<(<2;|fWz~RyLp`pjv0i^C@w8KTUK!n~%|h?1r9T|qDmG5q@5G|T^6xT+ zoyqWD8y_)Of&yddRbp+oP92k-ara#8Eh#ap6aZ@Pv6{6kp6fE>HU=*|2EKoKHS137iB~o7b;x7RA5M zq@(6p9_+YV924yFh6QK(Prf8^&L&hx3#6(8^w`esYm2C{f^jetY?#cjz|gZ((93Fa zjTa1+Zajxq?ZleimM|Jx+i-^D>p(G&wV1m(}1(tGhuNX(#jqIw~jBUVfx- zU3C!@m#3aP*;+_7eZ&TyHWY<{mLQ!&JimBXHR(eUnZUkxaK9PVZh@Px^^wxqo>O#% z>Y%y`O)DkcnsmB9UaeVOYZ0Sp`1%0y%zpWTojX_tDf|hsBt3Z!UkR?$yZiBl15RsmV>I#gmcET>fnB7z!f(TPlQ~M&Rs2T z$N3GD*Pym7l*<)dKNz-|e6d&lU9(;^X-cg#e8tmb)Gnm7OSCncEtPJn5=CLS9A;<3 zdmv72K&OYle6v^nb3^ZvitV(4z!>5y&IxUtS9A0V&1&`MsBex1tch5cszt3dRx%|4 z^UpUd`D16A(@~mLDTQ5(8;7sIxD06*EI%o0DbYPYi=UkGbmseoYxdx2n8cMj3eSu; zr~*d=YIEEU(MQoLktTSk|`Mne_^qh-~09O+n*o45pDOAZC?`_$98s$kCsEq%*8fi&%!U zxaQ$pU6kn}LMtl$LN}5%6PsQ@e=)-=uRZps|6Z0XXg#>pZBPz*m|}<0x9vOPTpK!p z4Z^7(SbknB`vIf1o;Dy*Ix7Gw(k5ncg>EE%Cn415-rxkIh6`jH?kC1gxb4x|&0v}s zXyDA+0gD6F!sKFI`=OL3a22jg0}>d-+YdlBrR|SUTZaIpAw!y zR`pvQty{x{$#Hb0VP_v;3%mU%;ssjZSOuTcL~^+?4sX(;k#uDR6&64!pZBaI1S>5z zf?gX^`;Y4kJ72U4=|KBt{7B5_#V*T54375bFrrp&JB?N8rtrsPMLpH9N0+rPzs=4m z%+mDKxOW(KpJGzsy^QU+k8hrq3*_S`**{I)lj8#T6ZmEOqiuCHER7K}PAISTO|87o z$y^7k{LpS79&87j6AQopRWc8<6W@zETe_Fw(Q~cos36mZWkE%#>dw#;Ub{KHxQPHe z{n)bgRx>p^q%MLwRpdApz~oqafn0~103o2| zV)}RUFg#9kE8$el-QgR=+MrXK()2D^Y!&v{hEx=vqzs?Z0ea%HG}!_xM)9H1V%8Kd z^?CVJ-0M&&AvumiP7C%%($#I~U%ed{$88jC%p-^yq16yGJD#J>9J3$5lk-m{>(aT* zq)h(Jd+*e!muo?2g{ifP{_=vtc`46eqb7TK2@VJL$l?JsOE?8w^a)h?u#{pZl6qip zNO5S-+keK`nr1e_lzM&Zu1<1>Xks5Zrx1FEN*{b}@l3hCa<~KkunESL*D}OFh#b(b-2!Ysw(`-9TA1P9jqvai4@b`HkpyYE@2BXFv zr@|>C=Rn}BhE^T($z*>=a$xlkLrPyO+M*Hlie<0!6^Ol+Q)6RTVdF9khnmH zLrp(Wtuc44Y1Ox?|LN`C9aXEcev`J*yyWpd`hnfZVxnhNYxx0^VePLIZ?L#$mXW6p z0UrgyQV+C$n$oJ}YDLZv(ruzW&#!boX^C&Cmh|_|oN>lv_JO)*zSho}Bs&1t=}~{2 zSTE^;;vSm1V@Tf`BfkFoWf7tV^73W(^SfW*y6Teja0q+kQQp>I8^IYZA(k2U9Kt57 z<)m(YH#y!_{QLH8j~^X;XNV3ghsNdo0`n^JIAl}}?ULYg5+SV1`Vz9q*gQ}8eBFe` zujLKBJ(rD#(D1xo<){KC7N`S7#~+I<`g*|8)t+xvRZ09;Bv^9?vOEz>9sc7Sm-^od zXUZaJs!VBu_NMa0j3yY|*;oHrV6waIbRv)XC0F$Vj;ge}G7PqkEOK;}mEF`n z#HlcIW_=Q$5S9izLZh^2m<><4>fa(UPi7FQIo!0S4^si}LopLpgBv(ynv92jwJdvc zX#+Rb?dT>y97o8#MsAUgSWUZyfu*N<%z)Vkmo-6jXQL3ojJ>_c=eD}@dLoB~4z&Wr zlXk1jIv06p_c5PB66Q73UeqaTQw)L^q%mA2CPxSiRR@MotAQOsK_el$y(@*+_5}p~ z9~=VSF*Mrr@Ex#4zsiVff&s0@j)cx(pwng6qdH zS_=CU0zpL%4|Qy{np+9PF!t&rP9s7#qX0RJ=g*tEkG~20P$Hrg1AovI59$B4l`8d? zXtg!0W{;`UCXPNZ)wd>GoG5TNnt&Asi&1FQT0q^n<}D(YZLxi6ST2^F6!?D*I&xWr zm!^Sa6A4d(VhCW_VER8`)siQ#lS~j}vx8INGKha<`gfgM)EhB|u9QJF1P_+RobN9? zNosj6fr^d;YKW(5UtdeXIQ6?pnyRj1y4^|2&kEWmgUZjGvpxJ>fo6m z=LqB1Y>IRc99IR0q)Rp}C66Lcu|c53?9ncG{b5%G*3-s9@+Q8h6QM(SVGIr4hHzO& zY26JNKGgwCdTpb#JbXji{prcV!qYhO%JHeeQ+Vq@{~)=F_v5Fs1X9f_^mgk5)mW!z zu8?m$-9Il+Hk>eM&6ZFk`%1V}`W#BePU|=q-ltljXg9`~)BJk3@hb@wEQ1TfhEH=w zTbyY>rV??E_!YavfD0a+jRcX}c=NEaOVJSX{<#mQ*<=Izv5t`xK?=N7+iK^ZcdTQ) z3OK8s@+`xM(=jnmOhl+6q2Y9uDYiK_ zQY%zPN09#@!U~qbN?)3jX5y;qQi9;%+Sh9ANG6pQh4n`Jwd+1vD?7@#2wcXmT&8A&{taMhKN4#4pqsj#xL|8iJO~S zMW0+dj$Jks(xqoe*%8 z3qS*>YoPcjQoM$_6q>|(=K#?zAaFayNZn|yh@(mi80!H*#*kVP8Rq`0_etZkn%nlx3g%ov_D@fb~W>ED<&A}RKeTe zAHi1CWPUjvPw`|vY9KMoD5h|+L+aB4)fRMk3B3dC!yK7dj*!ZK%)(!9$fskZQPxU) z%XG)jNIpa8xPECbu-m@r=%UpE#9=CDInlv#8U&trY3c2LIhOoYpks0}JYC%(04LTm z3g74^fDUtL=7Iu+P72khUf3l0EH1TtK+3Ovpj5x#Jrmc#KC8aFVwOf6$~+XJu9D!Z zsne5JL+AmeZ0mm6X=;$kyU*m;HS3) z|F=478+nL(&UQt!BD{Swfb-YPt%PE;1bYr7xL6aZfitI1VE#fK&P|Zd)u!z&HKqg5 zFPVXUtWNlG0HOGphFBphRj-p_LHy~SLoh$@>+l|`p(X8@!RC|@En~6`3l?%zVuXK; zTD;6|T~E%iy}nbz`@Y*6R{LNib#NdRmGBWFco}fHM zvWI?gl%eNjB*;iaL%`x`Z^10NpQw!e@~Hz0WJ0ChGvjw~W`-9S!ea2;g~9n%0lg?t z)1GBOt$T!yaHhw5!zE=yQ*!z*3(k9}s5P=;>kv_8LiJj9q5Bl}u>pL@QWqC@O= zKb9^@!oaL5YG6nWipZ|PClCdi;P%O6!MwNTLlDeUwaN@XH`6DWT9)iD#^tFXGJ(Yf zJY?wi<^Di$#Z8NN=-!@Bf_|N+>*%|K8-L9svVZ2!CUtg-*MUv#j$a&2G42E6<;cKA z8SPieAz>%fh>Eb?mJp%|G`wfGR5||{@#bVGa4|C5ovltOevU{~d}g87gMbFj=N0gM z@=97eHbHwN&tsem`fgih!4pX7DkHPdoxePvc4Z%8EXVZzajUq0>(!@K&2E)IA5#^*P*2|9 zZh!R~Xi3|jAz*R0^a9q>h0`$J;T-NR(L&AX|2d~)p3jiP{;0?8x*LTeWaOo4=cTd~ zFe^GBB!P817Y@VScf$Rsxubt=v1H+R?+0XBJi3?1xYH@Hg3xP5aS);)JjJbB8n3_x z&82yIOFmwldIQkdeR`0ozd^Xjp;y|n3GvUzb{yrmxp)Z#?gQJfL{krkUO!`Q{nTkE2-Me=RRRy5!Az zYq$LtC8Z@q*W^>%*kl%YS*TtO6>1{Sd-sEZ?|9kUKTDVDd%74rhcI%Xpg#_&>ao8s zxQecJqvkW%TW!6=a+QQ!z5h1GJ1z?+fdCmg)c9vI(-wZ&z$oB#ljYH<-_ZU185ypl zLoiAH7e$l*O{!ev`RrC@{D-jB9ENS$;`&^acQ!ZoDKXj)@8H-xnCr|s&3gW#t##NY62BH^ zl>u^Zhvlj6b99k^wi0;R!etR?Fs#9bfnppYCAJ4Bc0R#!gru}|I^ZG3#mVWs@Ih{5 z=^Xeg^0gvwj}Ht0UpVafvl}M34%~p`GRsHzqowBE9Y&Hj$69WK$TIk&!AhmjFN}Eq z`0){vDD7FK`X;UFFwhzctTGWwZ@tSX@Owca*wuvKQBhZE{;3$OQwy z)rTKnh9lLd386%}SJvX3_LWUfoKvk`8=+g@Tqv0_(S`#H`C1Xh<@o1>+hF_i&j!!; zs-`fpq1f{RMo@P2^?vW%h*1OJULCbfyj;oXC!m889i4zCCwU{DmONBj9=7=C zT4?VVBHB1t?N$9^+6=l!_c^m4Q}FoQT?;ts0WSyv#=@{6vEPK4n_}{QHDA3y7vZp> z?!%j4VmT&V?30cUWMW{ee6y2-m66Q=HGR-*<_g0o>TW3qJ-koPMj-nEOnWInz3M*m z^v3DhWODvm_1!$xt%#Y+mP%c$@0eEqE2Z~3!4JZ>n*spq*w}7r=Zv0Kx0le zGNcY+l-kCHzS-=w=p-lXQ36a3|2{z15xY0d(g}Q&Gs9~5(WO=5PONtAUT{R)KX5R+ z&iNF^XhKJ($UYKgiU^AB!bz_5kycK;_hGwBfxNtyrfefWY4=8fBWO)Rv8>3 zaioM^I0p+1CrDIuzqt1#RS6had$8z)xQaCciOjMrp|S50U1)Z%WRpuzgiw^{8k*H3 zgRwbE3JV$f+WtH6Db|ti>oHcTAM^5B5iWWZFSsB%SO9wz$$36H10qo~PLtT1UAG>= zlFiRtsk-4|759UxZn4VP8mz6tPiKLpaiuco)@qh$1-|+h@AKluQ70g!%8Zbs_ya2m0Un32G;LDDdk*&}nQ*+;JyUf5Ja$Rysr;BbBP5xvKfy*RCrp(8`d;Vp+ z6RVsfUy^A-)79CDPXr+b`iD%{mAZdH-9K4&#Yxy~GAe2-?eCurLaV+bZmBdwvAN+) zI+Wj-Po~Qoc5xITnH}52Cl*c8bB>bx-*|9T0wwdlB0bvAsnn$Kab ziHy8Zm43*zsj<1qif6HME0uUBJJhCBvY^6V3#D*#3akqXHUsAhb{js%@o#UXi)iVJ z4wT6D_##dtHdcF5etkAqxA5T+x**UaHYkt05^aAV5LY@@EE)%YLmI&N;S&O5lYaW2 zwO);d;OHnhV^C{ml%Wo=U_D(3$MCXp;D0)_Q|xKXpoAr%G7;)*|w_Hs~@=GDh>;V0H!2104HnRn#<2H=6lhdvu(T* z!~kjSe$rZ=7`sI&y?9w+haX;57GZZ;?vp$XfdP8vMETQ}Gjh@22Gr-zl17BEDHa~w zq8)w+Tj^T~YVbg2MlNoOCkg6iB>r$!DydDn7Jt*H(y{YOUHxa8*RUYEZZEx7mgiAh zl)CCIirxYVKG@qRW6U4F%H6m4_CCPILiTLF0;%8mGeZV1evLOAt~46)I*93cTW z%Rb`fpPnPJQKA7dh`_7CqZzn%oA=a-!*%4ig$%}0f%-VoU@*x~?606#?_ry%hH6zp z?{<~#h9kN-OpsTSdefX^D{J{aMIQdj9rlr6(7Ih+A_Lm973`E)0d*ux!d3zGr4w|Q z)YRBuwGz8fpc=5JV_#*X>HW7hEt+7vP?h#gjVYm(kc*q`dZ30R9a5uow*W{z+j*S3EB9u%Xyydr;M0f)%*ls<&XH^ei zq~@;3%wVkq0s}{<4~zDUu5YG#-(+W>UBD+)%BI+~&E6KZ1SJ0a7JH@reFzkO=?@fi z2{7_y!J%Li?cZO<`i~BWDfafk@6_Fl@P~!9_whtR_!YODaS)&O#`Z2Em>@keM0i(i)8o$vg&LojBs<;V4C&{V%LU-d@d|GEtVDW01jPK^Te>>u<^Q`whD}_S zzxar51sOGWnci3ujuh^l=n5Vis00VvhHBYxR@=!Wz=J!j$u}%8#soOfCfub1vm01U z23CvltS$0tMzas#*wP_BU8D$LZ<>Wq5gt=gK)SfFSrsCqni2*0Ps-YpnS`J=lG@m$ zz!+$nUd@wy4A0=ybcJPUe;?pLZ=^Jt2FIUu6#NKhf0UZ_ti4q?S(dhnGO?4kF+hrQ zi(t^-r>xPL(BxJYejmSw&v3~S0~FDvW09jW+D^xYgdpHXdw}x4*h)kCtFnPVoy>^g z!B#S+hW-=94ZaocJJ0-?IEq}2K|2*96#RNWJhJW5T4G?l^SIJfB%Us@BL1%VT@nJUb{%s{PY{go0N+Sljk@LssdnCqN~sDUhb8FaSs)PS z+})*oF&*_*NNX_qq|^vv=QVJ4VQ3wn-YWNHVs+eq*5W7Gj7zwZ&KRimW}MyDGLs^J zTrCWbNwExdb$ z^W9ox#ULJ?;Z+%$LX{bQ&*prlB@<=Y>AH8P%b`PzNS{zhEQCy#Vi%A!oppJE;|FOL z4{r36RM1Q8Rliz(g1IF;*la%h9~-_mBpzJ9hxhy%_^}~Sni#Jh*gk`)YP0*5x^J6E z25^hivNmU8(4NxNZBk`gs@&FxPK2oB8-z$RRTc`mnI8Q!I!G#pV#PP>OI#wEQ6tJK z@<_1Td?VGr_R+k0H9n0BUZCf8v(97`$blW#`$ogFTJ~h28}vG>`5@K)-eCBiV{;A{ zG&kk{aF^l zPM`1~4udLT$UIMx|6g&gV!dkwSjmLGeEojRATv3p;rr@A@m{p%pfr&+)jM2pbG!dt zBGC7NA=*Tx|NFp$#9(Xw=gcm_4hgWc~+kt2U% z3{!_FY*}&Fs!u74-PL|)xuH&Cg8sa?AxrwG4~dRVO~~QXd;XdTT7Q!D*N#aD4?ax~ z)teQ1M@e|PFMMKL%nl{i5XBm5c@9i1Oh(o!&ea3xoiUz0EW#b=6ezBmU~DqBzx1$fm{(a0jtCux^ecdYr{t>920{ zDJ6k&V+>x(pX`!pXpy+`Hao^BmW?vd^rpRUb6^*wB}-+qZZ^JKHE9bSfghS z*=7ANbwN7p3>!VUb}|LmvZAhRYN5u(DtvuMEm$H3_t+G!00zjOU`aA)Es|HixLu&d zDSiIkV(jQ%l??o1u9@2}$Jcj{KWIsAJEIjri^y5VWQ4buy$!fKw=&x-vOAuVf}IlI z2M`dV_MW1G9KAtNq2?klln1#J@|RT;T_w=Z7Crt#MnnrkH`u&JsF%)2VODg}G%YqC zR$e{%RQt&9l1!mrLwr+M8~Cv1=li1|0@c}2xzXopzc4et{~m{iXQS8@5aFg#nPVC| z6KDT?rteTnohI-PkxJ>)RyTAuHUaX4A3&Z(bpGDf+YZ1vAO81C5RPo`LuHVs4Jdv( zep~3dMZ)y2N|e7;(P_KZ%oluR@!z36s>*v`&YMxWTvx?HZzly7)F?6vP5KV)tpEN} z1N1pRiMQQh!;8DQenB{@b71z*g7e+F1dT2$5@Lo44ezc!?)VlLc z8jMDf@H~g6B3EEV&h6>a(oGPO%IK&Z1RQ!v&?)BPX4Rz<$ZCMa1q;M`l<7TTc<{rF zw09U*1CKWqU?K{JVR4sB)h#lm&`OCXeYWQB*mH^1QeVs0j60;ra!r+cm7zlRXP;Pv5hPoYPsF^xbU@f1&a%-oX85mJ8})KL>@lS-h#&=*A$WVy`SHTZl}fYNBUUohMI;whD2V zXFBl@UU8-grim;aO{g3q0RapFPA)GuYFW7N!Z-19wQaP!tWcYmi z+YUzBgL+TBEUcn_uhD~6h|$8~ZxRtl^vIlXf3css8@8;4PbBeYk#a!0taa3W8Ad=u!vF7N?q^SJ7dJadN{Q{J^tB%p~RF zjFs&}h;vQAeQGu47@{rEI*qex?p){hk>)JFwp=7e8~r|ExM ztS-(3z6@FZHUBQ|n$n1VrMKos&GR##*gP+<{2yuwW~LZ49@V)V0~f1u7UX;uBwJN7 z#|frA{~1*zwI~Kv9GfB(F%l=`XAydGzguM48YM_;Zd(OZU~z?YC@S01j$Ifd7}P;yD8-z_hCY=Zjs z_Lob+G!faV(=H_)Kbl;m_r5VBbK|F?S6fRXdHr>Yo0ox67I!-#>Ija?ZhmGaCev(I z#hxC5jc~^un`(t;-&(2B+hO(#{7~azCKrS42;ML8^XfH6k?`9jAu%jbJC-(7tdY_= zh{ZO^vCbLft`O-_M=p$tQUqPtSc-^2AtoqccjksfL1meOi8CM@z#~mcCQDbE$KD(uk3?@N?S;Wu^rmnkUfGSF1*qmg>0Nm=J#AtuHX_^m z9u>u*{{_e?j@bu@bgi6M9i{O2KNvuU(xDz`rv8>xOBPR;WnlH+ca$bRt@=9=H7UQ| z@mr`*O|D+G!OFb8CT+PkBPx6LF&7JrxG2wKtk6Gjeah@=0}J#~Vg|R~vOa2Iny)CH z?F;aUx1xM-$y=JGc7oLbcwOByCirP*Xc_ciJ}ph1Pg$j3&=|~G6*dQ))vcX~8wELT zGni#cjv&K<;h;d-bsz1`B*!E_2hH`#=ygIoE%-#)+>!G|dD+=h4(pRLD-QXLf_)tOW+hbm47DJo~vC&^BWv^?5gm-AU_1=w?L%O2WLon3~+o-X}= zceVRJK2*Zkxl=PpzRT`$!E-FfYB+aGO1e%$g;p6Gm%)>^Ece+EV8k>CZ9kIt!{xPG zY=xpavkeauYy(u*y%lz0X1DaK!_JoG&2tmV-W0SB;jZ-|t@h{W z%Ym>+s#27`uUofbuNklZ!a56jj&sjmRpk8JzFSkNKxw(5EJP)N2KDg#_dzeYVek;r ztE7sSLXXHmu<9=2fK!Hk`uAEh?aO%7+k=HcExkgcWsTS2*#i8k6=4*OO5Gv2>&Q?8 zIB^6>HU5hA%aj$jU0;*g8@U9pu@M_k4&%N#c%lDl>*i(fykphCEZ2U_N=+pV>xcu* zoPgRWn)I8i?4e-M11I_A&!N%R^2{!m{uQPi1!NCzdjv<{!6FsUlw%K|H$g#BW7`+xhu?RXVu=TK0`Q5O`%I@&ZtH(sxWiy(I_8=U22i1a*JNE-UdD z{HwbyG=dM=dNqj05U<9A3XK+^Sa&rWJ^t;)=;JaW?5D2yU#iDPm^X7TQv1(n^J{dG zC#QvG5Lm65zSyPe%-~%W9T;lrQvhcp@0NQ9E_BwSexBa_I%O4T^yGR&x0)0*jZV1n17RJkeKW^m+YuirOG$IU}*~QQdibjJf~&rL~2(UfQIHj?6NMh_kZZS z`EWhc+ve%A$OM(|3wq*XQz%#GHQjILg#n$La28Ju_sh@-O{+ z5YD;llulm$>?&yJd5SmseR)ObdZY=e%U$E++!XbBx7814+qFySt(-U{;?K-St0G zA2#h%^H5VLP2nIzvx`~pRPzfKbvyi>XWx~88vzXgqFcCR#)EN1oHQ4~$B^0} zI4vibtNi-j4W(E0Vc~_0hFMb_R@)hdf9c=vNZw zEhfkbEJ0aj$1jb4)t{egN{{tR@MUzzb!1H-(zn4$=vXUcq)Tn{E6ICD&_QlG8MSc_ zlu&c%17@ljXN)dVt9K^AD{ENu~8Cu6Gcpt~2t1Nr421IQ|H!hWDnx zRr922;P-zOvxje51w4_#eD*!fOkKFQ(Diq5dPIqW7 zx(FT8!$M+ge8e&^0_!NEuU!Xp?zX#8C3RB7eouP+c7k38QyVzo*nh#G1nwqh&js3A zQ9dVLiOfvcV^)pcD>;S<{z`0$@rddB!r)-_Nc7&sW6t}QnHipaRR};XJvzNNzIIk5 z%JN&Fx1lx~Un3?1m^p!#R?jh3B0)VhOj@s1OP7=^551$<35xxzxMtE9AqNW~vFV!b zk)i3c#fmk?Na0u0l_b6%$9g;o-V_I(@F2|CTijQODX8P?ZQg*%dRHeYJ z%9fd^JF5^p9rcR(0fs~HnG^dMK(p%R%Tn{#I-^!jb*IyEm*f)&Y#-xqkol(rtIf*7 z&%5$yA%5EY(D3zgNF#aPY=+P2C<|xxc{pm*R%T<>B&24i(q{(|pS!UhV^6Q^>JgR4 zb-MCJ_ThTmG?JzM@t;FT%!;cyTEu<${2o^Xr##P590mbL9+o8xi zs4*g@;dM=Xdp8nToXRhz`N`Ylp@F*?@fCY|y z1hz%MV7TVd{CcD}tG|iQu$T@0868qH>Kao~hG(HNXz4#+t514*#G{3vPxl z8@t;-a7fjM9_`Mvw&t>z;&ap0=$u&)2zCC5JPX?2+mI#saKKcbMSnZ@t%-XAa@lB_Ce&9(vvan#| z60(I|$YN^B68W!`^jo((q4`nKew!z|ib=S|Zcb72j{PIE8li$|R z$VJ%SdGG8H!ToD+sos$``C$xq`LDO_8m0i z_e6&BM0y?pXW3UYB7Wy16?f?QHsi(YKu;I7#tg}{(9kktc1PEgxQqJLo86E3g2a-_ zD2xMc1TX}6NX4M3D1yyR#U4OB1&V}T>ELDbt*P<=bAmD`V`sf{mR8~J13o6KUbQdD zKvSK`!fGpP^RFrBJNX{(1D@XN1$%?Myo4m%lKq-4jB>+K8;ibiId}OE%C{N>Jf_0z zGHwPvZUi?rfPNrN-;@3)+|j}EvxFw4&!lS_@i!VUpu+DM0jFAd1RBm2tn6QI<+ z-`=5Q6m;Qd4zz$C3+}aT2h|UkLtTeNN+jMLHC$-ghy)vdK%pFS%WzrL+j%{E41_(@ zdoj+P!u>ZO@J6V*I9f=8u9j2DDE)!|`sQubn<`CYh#HGfmz_xGJ$=r`ux{E3t$Tch z$h<%cUm0|Ceg=tkXZ1`}Vb81b>03d}K?M!fBLgk#Q%w(*K`x5Z*WBCDq>P{X%71S% zu3o&T0R*U3UVRz~{=OM3w(yi`I`NdIu@(!-HX$Om<$L-!me{Z7acSP4QN}=KET)p; zTzjqG(S76pa(jz$>SQ)ENx`P?GT_aBtH6|r2hL5f0Z@%b! z(#Rz)879arq=Wv{yZA10=)K)=;i_2e1mcqbCZmHIBQ9LBQdg@w6!FAXP-YPQ2n_1N zX6xsr^WeGlRr*G$o=Epsi?yGTKKHWo(~? z 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 literal 0 HcmV?d00001 diff --git a/src/images/ko-fi.png b/src/images/ko-fi.png new file mode 100644 index 0000000000000000000000000000000000000000..d19991b5f3a895e1e3ee4b23d08ec2716024fd9d GIT binary patch literal 39500 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 O;yKsX?Wn7K +#include +#include +#include +#include +#include +#include #include "about_dialog.h" +#include "main_window_themes.h" #include "ui_about_dialog.h" AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDialog) { ui->setupUi(this); + preloadImages(); + + ui->image_1->setAttribute(Qt::WA_Hover, true); + ui->image_2->setAttribute(Qt::WA_Hover, true); + ui->image_3->setAttribute(Qt::WA_Hover, true); + ui->image_4->setAttribute(Qt::WA_Hover, true); + ui->image_5->setAttribute(Qt::WA_Hover, true); + + ui->image_1->installEventFilter(this); + ui->image_2->installEventFilter(this); + ui->image_3->installEventFilter(this); + ui->image_4->installEventFilter(this); + ui->image_5->installEventFilter(this); } AboutDialog::~AboutDialog() { delete ui; } + +void AboutDialog::preloadImages() { + originalImages[0] = ui->image_1->pixmap().copy(); + originalImages[1] = ui->image_2->pixmap().copy(); + originalImages[2] = ui->image_3->pixmap().copy(); + originalImages[3] = ui->image_4->pixmap().copy(); + originalImages[4] = ui->image_5->pixmap().copy(); + + for (int i = 0; i < 5; ++i) { + QImage image = originalImages[i].toImage(); + for (int y = 0; y < image.height(); ++y) { + for (int x = 0; x < image.width(); ++x) { + QColor color = image.pixelColor(x, y); + color.setRed(255 - color.red()); + color.setGreen(255 - color.green()); + color.setBlue(255 - color.blue()); + image.setPixelColor(x, y, color); + } + } + invertedImages[i] = QPixmap::fromImage(image); + } + updateImagesForCurrentTheme(); +} + +void AboutDialog::updateImagesForCurrentTheme() { + Theme currentTheme = static_cast(Config::getMainWindowTheme()); + bool isDarkTheme = (currentTheme == Theme::Dark || currentTheme == Theme::Green || + currentTheme == Theme::Blue || currentTheme == Theme::Violet); + if (isDarkTheme) { + ui->image_1->setPixmap(invertedImages[0]); + ui->image_2->setPixmap(invertedImages[1]); + ui->image_3->setPixmap(invertedImages[2]); + ui->image_4->setPixmap(invertedImages[3]); + ui->image_5->setPixmap(invertedImages[4]); + } else { + ui->image_1->setPixmap(originalImages[0]); + ui->image_2->setPixmap(originalImages[1]); + ui->image_3->setPixmap(originalImages[2]); + ui->image_4->setPixmap(originalImages[3]); + ui->image_5->setPixmap(originalImages[4]); + } +} + +bool AboutDialog::eventFilter(QObject* obj, QEvent* event) { + if (event->type() == QEvent::Enter) { + if (obj == ui->image_1) { + if (isDarkTheme()) { + ui->image_1->setPixmap(originalImages[0]); + } else { + ui->image_1->setPixmap(invertedImages[0]); + } + applyHoverEffect(ui->image_1); + } else if (obj == ui->image_2) { + if (isDarkTheme()) { + ui->image_2->setPixmap(originalImages[1]); + } else { + ui->image_2->setPixmap(invertedImages[1]); + } + applyHoverEffect(ui->image_2); + } else if (obj == ui->image_3) { + if (isDarkTheme()) { + ui->image_3->setPixmap(originalImages[2]); + } else { + ui->image_3->setPixmap(invertedImages[2]); + } + applyHoverEffect(ui->image_3); + } else if (obj == ui->image_4) { + if (isDarkTheme()) { + ui->image_4->setPixmap(originalImages[3]); + } else { + ui->image_4->setPixmap(invertedImages[3]); + } + applyHoverEffect(ui->image_4); + } else if (obj == ui->image_5) { + if (isDarkTheme()) { + ui->image_5->setPixmap(originalImages[4]); + } else { + ui->image_5->setPixmap(invertedImages[4]); + } + applyHoverEffect(ui->image_5); + } + } else if (event->type() == QEvent::Leave) { + if (obj == ui->image_1) { + if (isDarkTheme()) { + ui->image_1->setPixmap(invertedImages[0]); + } else { + ui->image_1->setPixmap(originalImages[0]); + } + removeHoverEffect(ui->image_1); + } else if (obj == ui->image_2) { + if (isDarkTheme()) { + ui->image_2->setPixmap(invertedImages[1]); + } else { + ui->image_2->setPixmap(originalImages[1]); + } + removeHoverEffect(ui->image_2); + } else if (obj == ui->image_3) { + if (isDarkTheme()) { + ui->image_3->setPixmap(invertedImages[2]); + } else { + ui->image_3->setPixmap(originalImages[2]); + } + removeHoverEffect(ui->image_3); + } else if (obj == ui->image_4) { + if (isDarkTheme()) { + ui->image_4->setPixmap(invertedImages[3]); + } else { + ui->image_4->setPixmap(originalImages[3]); + } + removeHoverEffect(ui->image_4); + } else if (obj == ui->image_5) { + if (isDarkTheme()) { + ui->image_5->setPixmap(invertedImages[4]); + } else { + ui->image_5->setPixmap(originalImages[4]); + } + removeHoverEffect(ui->image_5); + } + } else if (event->type() == QEvent::MouseButtonPress) { + if (obj == ui->image_1) { + QDesktopServices::openUrl(QUrl("https://github.com/shadps4-emu/shadPS4")); + } else if (obj == ui->image_2) { + QDesktopServices::openUrl(QUrl("https://discord.gg/bFJxfftGW6")); + } else if (obj == ui->image_3) { + QDesktopServices::openUrl(QUrl("https://www.youtube.com/@shadPS4/videos")); + } else if (obj == ui->image_4) { + QDesktopServices::openUrl(QUrl("https://ko-fi.com/shadps4")); + } else if (obj == ui->image_5) { + QDesktopServices::openUrl(QUrl("https://shadps4.net")); + } + return true; + } + return QDialog::eventFilter(obj, event); +} + +void AboutDialog::applyHoverEffect(QLabel* label) { + QColor shadowColor = isDarkTheme() ? QColor(0, 0, 0) : QColor(169, 169, 169); + QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(5); + shadow->setXOffset(2); + shadow->setYOffset(2); + shadow->setColor(shadowColor); + label->setGraphicsEffect(shadow); +} + +void AboutDialog::removeHoverEffect(QLabel* label) { + QColor shadowColor = isDarkTheme() ? QColor(50, 50, 50) : QColor(169, 169, 169); + QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(3); + shadow->setXOffset(0); + shadow->setYOffset(0); + shadow->setColor(shadowColor); + label->setGraphicsEffect(shadow); +} + +bool AboutDialog::isDarkTheme() const { + Theme currentTheme = static_cast(Config::getMainWindowTheme()); + return currentTheme == Theme::Dark || currentTheme == Theme::Green || + currentTheme == Theme::Blue || currentTheme == Theme::Violet; +} diff --git a/src/qt_gui/about_dialog.h b/src/qt_gui/about_dialog.h index 8c802221b..42e8d557a 100644 --- a/src/qt_gui/about_dialog.h +++ b/src/qt_gui/about_dialog.h @@ -3,7 +3,11 @@ #pragma once +#include #include +#include +#include +#include namespace Ui { class AboutDialog; @@ -15,7 +19,18 @@ class AboutDialog : public QDialog { public: explicit AboutDialog(QWidget* parent = nullptr); ~AboutDialog(); + bool eventFilter(QObject* obj, QEvent* event); private: Ui::AboutDialog* ui; -}; \ No newline at end of file + + void preloadImages(); + void updateImagesForCurrentTheme(); + void applyHoverEffect(QLabel* label); + void removeHoverEffect(QLabel* label); + + bool isDarkTheme() const; + + QPixmap originalImages[5]; + QPixmap invertedImages[5]; +}; diff --git a/src/qt_gui/about_dialog.ui b/src/qt_gui/about_dialog.ui index e2e76f4c4..19840e452 100644 --- a/src/qt_gui/about_dialog.ui +++ b/src/qt_gui/about_dialog.ui @@ -9,7 +9,7 @@ 0 0 780 - 320 + 310 @@ -22,14 +22,14 @@ - 10 - 30 + 15 + 15 271 - 261 + 271 - QFrame::Shape::NoFrame + QFrame::NoFrame @@ -45,7 +45,7 @@ 310 - 40 + 15 171 41 @@ -64,9 +64,9 @@ 310 - 90 + 60 451 - 101 + 70 @@ -85,9 +85,9 @@ 310 - 180 + 130 451 - 101 + 70 @@ -102,6 +102,131 @@ true + + + + 310 + 210 + 80 + 80 + + + + ArrowCursor + + + QFrame::NoFrame + + + + + + :/images/github.png + + + true + + + + + + 400 + 210 + 80 + 80 + + + + ArrowCursor + + + QFrame::NoFrame + + + + + + :/images/discord.png + + + true + + + + + + 490 + 210 + 80 + 80 + + + + ArrowCursor + + + QFrame::NoFrame + + + + + + :/images/youtube.png + + + true + + + + + + 580 + 210 + 80 + 80 + + + + ArrowCursor + + + QFrame::NoFrame + + + + + + :/images/ko-fi.png + + + true + + + + + + 670 + 210 + 80 + 80 + + + + ArrowCursor + + + QFrame::NoFrame + + + + + + :/images/website.png + + + true + + diff --git a/src/shadps4.qrc b/src/shadps4.qrc index e328f2c42..30f234ed8 100644 --- a/src/shadps4.qrc +++ b/src/shadps4.qrc @@ -25,5 +25,10 @@ images/flag_us.png images/flag_world.png images/flag_china.png + images/github.png + images/discord.png + images/ko-fi.png + images/youtube.png + images/website.png From f623613d120d6b0dd66dcdb5584ca569d4ff2b0a Mon Sep 17 00:00:00 2001 From: Martin <67326368+Martini-141@users.noreply.github.com> Date: Mon, 9 Dec 2024 17:53:25 +0100 Subject: [PATCH 50/89] Update nb translations (#1712) * update nb_NO.ts * small grammar changes * revert to nb.ts --- src/qt_gui/translations/{nb_NO.ts => nb.ts} | 72 ++++++++++----------- 1 file changed, 36 insertions(+), 36 deletions(-) rename src/qt_gui/translations/{nb_NO.ts => nb.ts} (94%) diff --git a/src/qt_gui/translations/nb_NO.ts b/src/qt_gui/translations/nb.ts similarity index 94% rename from src/qt_gui/translations/nb_NO.ts rename to src/qt_gui/translations/nb.ts index de0a88b73..303baadaf 100644 --- a/src/qt_gui/translations/nb_NO.ts +++ b/src/qt_gui/translations/nb.ts @@ -90,7 +90,7 @@ The value for location to install games is not valid. - Verdien for mappen for å installere spill er ikke gyldig. + Stien for å installere spillet er ikke gyldig. @@ -123,7 +123,7 @@ Open Game Folder - Åpne Spillmappe + Åpne Spillmappen @@ -208,7 +208,7 @@ requiresEnableSeparateUpdateFolder_MSG - Denne funksjonen krever 'Aktiver seperat oppdateringsmappe' konfigurasjonsalternativet. Hvis du vil bruke denne funksjonen, vennligst aktiver den. + Denne funksjonen krever 'Aktiver seperat oppdateringsmappe' konfigurasjonsalternativet. Hvis du vil bruke denne funksjonen, må du aktiver den. @@ -261,7 +261,7 @@ Check for Updates - Sjekk etter oppdateringer + Se etter oppdateringer @@ -500,7 +500,7 @@ Show Splash - Vis Velkomst + Vis Velkomstbilde @@ -655,7 +655,7 @@ Check for Updates at Startup - Sjekk etter oppdateringer ved oppstart + Se etter oppdateringer ved oppstart @@ -665,7 +665,7 @@ Check for Updates - Sjekk for oppdateringer + Se etter oppdateringer @@ -718,7 +718,7 @@ Patches Downloaded Successfully! - Programrettelser lastet ned vellykket! + Programrettelser ble lastet ned! @@ -828,7 +828,7 @@ Game successfully installed at %1 - Spillet ble installert vellykket på %1 + Spillet ble installert i %1 @@ -841,12 +841,12 @@ Cheats / Patches for - Cheats / Patches for + Juks / Programrettelser for defaultTextEdit_MSG - Juks/programrettelse er eksperimentelle.\nBruk med forsiktighet.\n\nLast ned juks individuelt ved å velge pakkebrønn og klikke på nedlastingsknappen.\nPå fanen programrettelse kan du laste ned alle programrettelser samtidig, velge hvilke du ønsker å bruke, og lagre valget ditt.\n\nSiden vi ikke utvikler Juksene/Programrettelsene,\nvær vennlig å rapportere problemer til juks-utvikleren.\n\nHar du laget en ny juks? Besøk:\nhttps://github.com/shadps4-emu/ps4_cheats + Juks/programrettelse er eksperimentelle.\nBruk med forsiktighet.\n\nLast ned juks individuelt ved å velge pakkebrønn og klikke på nedlastingsknappen.\nPå fanen programrettelse kan du laste ned alle programrettelser samtidig, velge hvilke du ønsker å bruke, og lagre valget ditt.\n\nSiden vi ikke utvikler Juksene/Programrettelsene,\nvær vennlig å rapportere problemer til jukse/programrettelse utvikleren.\n\nHar du laget en ny juks? Besøk:\nhttps://github.com/shadps4-emu/ps4_cheats @@ -871,7 +871,7 @@ Select Cheat File: - Velg juksfil: + Velg juksefil: @@ -896,7 +896,7 @@ You can delete the cheats you don't want after downloading them. - Du kan slette jukser du ikke ønsker etter å ha lastet dem ned. + Du kan slette juksene du ikke ønsker etter å ha lastet dem ned. @@ -971,12 +971,12 @@ Options saved successfully. - Alternativer lagret vellykket. + Alternativer ble lagret. Invalid Source - Ugyldig kilde + Ugyldig Kilde @@ -986,7 +986,7 @@ File Exists - Filen eksisterer + Filen Eksisterer @@ -996,17 +996,17 @@ Failed to save file: - Kunne ikke lagre fil: + Kunne ikke lagre filen: Failed to download file: - Kunne ikke laste ned fil: + Kunne ikke laste ned filen: Cheats Not Found - Jukser ikke funnet + Fant ikke juksene @@ -1016,12 +1016,12 @@ Cheats Downloaded Successfully - Jukser lastet ned vellykket + Juksene ble lastet ned CheatsDownloadedSuccessfully_MSG - Du har lastet ned jukser vellykket for denne versjonen av spillet fra den valgte pakkebrønnen. Du kan prøve å laste ned fra en annen pakkebrønn, hvis det er tilgjengelig, vil det også være mulig å bruke det ved å velge filen fra listen. + Du har lastet ned jukser for denne versjonen av spillet fra den valgte pakkebrønnen. Du kan prøve å laste ned fra en annen pakkebrønn, hvis det er tilgjengelig, vil det også være mulig å bruke det ved å velge filen fra listen. @@ -1041,7 +1041,7 @@ DownloadComplete_MSG - Oppdateringer lastet ned vellykket! Alle programrettelsene tilgjengelige for alle spill har blitt lastet ned, det er ikke nødvendig å laste dem ned individuelt for hvert spill som skjer med jukser. Hvis programrettelsen ikke vises, kan det hende at den ikke finnes for den spesifikke serienummeret og versjonen av spillet. + Programrettelser ble lastet ned! Alle programrettelsene tilgjengelige for alle spill har blitt lastet ned, det er ikke nødvendig å laste dem ned individuelt for hvert spill som skjer med jukser. Hvis programrettelsen ikke vises, kan det hende at den ikke finnes for den spesifikke serienummeret og versjonen av spillet. @@ -1076,7 +1076,7 @@ Failed to open file: - Kunne ikke åpne fil: + Kunne ikke åpne filen: @@ -1111,7 +1111,7 @@ Can't apply cheats before the game is started - Kan ikke bruke juksetriks før spillet er startet. + Kan ikke bruke juksene før spillet er startet. @@ -1154,7 +1154,7 @@ fullscreenCheckBox - Aktiver fullskjerm:\nSetter automatisk spillvinduet i fullskjermmodus.\nDette kan slås av ved å trykke på F11-tasten. + Aktiver fullskjerm:\nSetter spillvinduet automatisk i fullskjermmodus.\nDette kan slås av ved å trykke på F11-tasten. @@ -1164,12 +1164,12 @@ showSplashCheckBox - Vis startskjerm:\nViser spillets startskjerm (et spesialbilde) når spillet starter. + Vis Velkomstbilde:\nViser spillets velkomstbilde (et spesialbilde) når spillet starter. ps4proCheckBox - Er PS4 Pro:\nFår emulatoren til å fungere som en PS4 PRO, noe som kan aktivere spesielle funksjoner i spill som støtter dette. + Er PS4 Pro:\nFår etterligneren til å fungere som en PS4 PRO, noe som kan aktivere spesielle funksjoner i spill som støtter dette. @@ -1199,7 +1199,7 @@ GUIgroupBox - Spille tittelmusikk:\nHvis et spill støtter det, aktiverer spesiell musikk når du velger spillet i menyen. + Spille tittelmusikk:\nHvis et spill støtter det, så aktiveres det spesiell musikk når du velger spillet i menyen. @@ -1254,7 +1254,7 @@ graphicsAdapterGroupBox - Grafikkenhet:\nI systemer med flere GPU-er, velg GPU-en etterligneren skal bruke fra rullegardinlisten,\neller velg "Auto Select" for å bestemme det automatisk. + Grafikkenhet:\nI systemer med flere GPU-er, velg GPU-en etterligneren skal bruke fra rullegardinlisten,\neller velg "Auto Select" for å bestemme den automatisk. @@ -1264,7 +1264,7 @@ heightDivider - Vblank Skillelinje:\nBildehastigheten som etterligneren oppdaterer ved, multipliseres med dette tallet. Endring av dette kan ha negative effekter, som å øke hastigheten på spillet, eller ødelegge kritisk spillfunksjonalitet som ikke forventer at dette endres! + Vblank Skillelinje:\nBildehastigheten som etterligneren oppdaterer ved, multipliseres med dette tallet. Endring av dette kan ha negative effekter, som å øke hastigheten av spillet, eller ødelegge kritisk spillfunksjonalitet som ikke forventer at dette endres! @@ -1274,12 +1274,12 @@ nullGpuCheckBox - Aktiver Null GPU:\nFor teknisk feilsøking deaktiverer spillgjengivelse som om det ikke var noe grafikkort. + Aktiver Null GPU:\nFor teknisk feilsøking deaktiverer spillets-gjengivelse som om det ikke var noe grafikkort. gameFoldersBox - Spillmapper:\nListen over mapper for å sjekke installerte spill. + Spillmapper:\nListen over mapper som brukes for å se etter installerte spill. @@ -1299,12 +1299,12 @@ vkValidationCheckBox - Aktiver Vulkan valideringslag:\nAktiverer et system som validerer tilstanden til Vulkan-gjengiveren og logger informasjon om dens indre tilstand. Dette vil redusere ytelsen og sannsynligvis endre etterlignerens oppførsel. + Aktiver Vulkan valideringslag:\nAktiverer et system som validerer tilstanden til Vulkan-gjengiveren og logger informasjon om dens indre tilstand. Dette vil redusere ytelsen og sannsynligvis endre etterlignerens atferd. vkSyncValidationCheckBox - Aktiver Vulkan synkronisering validering:\nAktiverer et system som validerer frekvens tiden av Vulkan-gjengivelsensoppgaver. Dette vil redusere ytelsen og sannsynligvis endre etterlignerens oppførsel. + Aktiver Vulkan synkronisering validering:\nAktiverer et system som validerer frekvens tiden av Vulkan-gjengivelsensoppgaver. Dette vil redusere ytelsen og sannsynligvis endre etterlignerens atferd. @@ -1365,7 +1365,7 @@ Auto Updater - Automatisk oppdaterer + Automatisk oppdaterering @@ -1435,7 +1435,7 @@ Check for Updates at Startup - Sjekk etter oppdateringer ved oppstart + Se etter oppdateringer ved oppstart From f1b23c616e266fdc726b7e352faa5b0ec897361a Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Mon, 9 Dec 2024 17:11:11 -0300 Subject: [PATCH 51/89] Devtools - Shader editing (#1705) * devtools: shader editing and compiling * devtools: patch shader at runtime * devtools: shader editing load patch even with config disabled --- src/common/string_util.cpp | 4 + src/common/string_util.h | 2 + src/core/debug_state.cpp | 11 +- src/core/debug_state.h | 42 +++- src/core/devtools/options.h | 4 +- src/core/devtools/widget/common.h | 31 ++- src/core/devtools/widget/shader_list.cpp | 230 +++++++++++++++--- src/core/devtools/widget/shader_list.h | 28 ++- src/core/devtools/widget/text_editor.cpp | 49 +++- src/core/devtools/widget/text_editor.h | 1 + src/sdl_window.cpp | 15 ++ src/sdl_window.h | 5 + .../renderer_vulkan/vk_compute_pipeline.cpp | 2 +- .../renderer_vulkan/vk_compute_pipeline.h | 24 +- .../renderer_vulkan/vk_graphics_pipeline.h | 2 + .../renderer_vulkan/vk_pipeline_cache.cpp | 80 ++++-- .../renderer_vulkan/vk_pipeline_cache.h | 27 +- src/video_core/renderer_vulkan/vk_presenter.h | 8 + .../renderer_vulkan/vk_rasterizer.h | 4 + 19 files changed, 466 insertions(+), 103 deletions(-) diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 6d5a254cd..4658d0ef4 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -37,6 +37,10 @@ std::vector SplitString(const std::string& str, char delimiter) { return output; } +std::string_view U8stringToString(std::u8string_view u8str) { + return std::string_view{reinterpret_cast(u8str.data()), u8str.size()}; +} + #ifdef _WIN32 static std::wstring CPToUTF16(u32 code_page, std::string_view input) { const auto size = diff --git a/src/common/string_util.h b/src/common/string_util.h index 23e82b93c..18972de44 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -16,6 +16,8 @@ void ToLowerInPlace(std::string& str); std::vector SplitString(const std::string& str, char delimiter); +std::string_view U8stringToString(std::u8string_view u8str); + #ifdef _WIN32 [[nodiscard]] std::string UTF16ToUTF8(std::wstring_view input); [[nodiscard]] std::wstring UTF8ToUTF16W(std::string_view str); diff --git a/src/core/debug_state.cpp b/src/core/debug_state.cpp index 562cb62e8..649624924 100644 --- a/src/core/debug_state.cpp +++ b/src/core/debug_state.cpp @@ -177,9 +177,10 @@ void DebugStateImpl::PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, } } -void DebugStateImpl::CollectShader(const std::string& name, std::span spv, - std::span raw_code) { - shader_dump_list.emplace_back(name, std::vector{spv.begin(), spv.end()}, - std::vector{raw_code.begin(), raw_code.end()}); - std::ranges::sort(shader_dump_list, {}, &ShaderDump::name); +void DebugStateImpl::CollectShader(const std::string& name, vk::ShaderModule module, + std::span spv, std::span raw_code, + std::span patch_spv, bool is_patched) { + shader_dump_list.emplace_back(name, module, std::vector{spv.begin(), spv.end()}, + std::vector{raw_code.begin(), raw_code.end()}, + std::vector{patch_spv.begin(), patch_spv.end()}, is_patched); } diff --git a/src/core/debug_state.h b/src/core/debug_state.h index 759755b52..fa2e5cd9d 100644 --- a/src/core/debug_state.h +++ b/src/core/debug_state.h @@ -12,7 +12,7 @@ #include "common/types.h" #include "video_core/amdgpu/liverpool.h" -#include "video_core/renderer_vulkan/vk_pipeline_cache.h" +#include "video_core/renderer_vulkan/vk_graphics_pipeline.h" #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -76,29 +76,46 @@ struct FrameDump { struct ShaderDump { std::string name; + vk::ShaderModule module; + std::vector spv; - std::vector raw_code; + std::vector isa; + std::vector patch_spv; + std::string patch_source{}; + + bool loaded_data = false; + bool is_patched = false; std::string cache_spv_disasm{}; - std::string cache_raw_disasm{}; + std::string cache_isa_disasm{}; + std::string cache_patch_disasm{}; - ShaderDump(std::string name, std::vector spv, std::vector raw_code) - : name(std::move(name)), spv(std::move(spv)), raw_code(std::move(raw_code)) {} + ShaderDump(std::string name, vk::ShaderModule module, std::vector spv, + std::vector isa, std::vector patch_spv, bool is_patched) + : name(std::move(name)), module(module), spv(std::move(spv)), isa(std::move(isa)), + patch_spv(std::move(patch_spv)), is_patched(is_patched) {} ShaderDump(const ShaderDump& other) = delete; ShaderDump(ShaderDump&& other) noexcept - : name{std::move(other.name)}, spv{std::move(other.spv)}, - raw_code{std::move(other.raw_code)}, cache_spv_disasm{std::move(other.cache_spv_disasm)}, - cache_raw_disasm{std::move(other.cache_raw_disasm)} {} + : name{std::move(other.name)}, module{std::move(other.module)}, spv{std::move(other.spv)}, + isa{std::move(other.isa)}, patch_spv{std::move(other.patch_spv)}, + patch_source{std::move(other.patch_source)}, + cache_spv_disasm{std::move(other.cache_spv_disasm)}, + cache_isa_disasm{std::move(other.cache_isa_disasm)}, + cache_patch_disasm{std::move(other.cache_patch_disasm)} {} ShaderDump& operator=(const ShaderDump& other) = delete; ShaderDump& operator=(ShaderDump&& other) noexcept { if (this == &other) return *this; name = std::move(other.name); + module = std::move(other.module); spv = std::move(other.spv); - raw_code = std::move(other.raw_code); + isa = std::move(other.isa); + patch_spv = std::move(other.patch_spv); + patch_source = std::move(other.patch_source); cache_spv_disasm = std::move(other.cache_spv_disasm); - cache_raw_disasm = std::move(other.cache_raw_disasm); + cache_isa_disasm = std::move(other.cache_isa_disasm); + cache_patch_disasm = std::move(other.cache_patch_disasm); return *this; } }; @@ -186,8 +203,9 @@ public: void PushRegsDump(uintptr_t base_addr, uintptr_t header_addr, const AmdGpu::Liverpool::Regs& regs, bool is_compute = false); - void CollectShader(const std::string& name, std::span spv, - std::span raw_code); + void CollectShader(const std::string& name, vk::ShaderModule module, std::span spv, + std::span raw_code, std::span patch_spv, + bool is_patched); }; } // namespace DebugStateType diff --git a/src/core/devtools/options.h b/src/core/devtools/options.h index 70e1d137b..a859a2eec 100644 --- a/src/core/devtools/options.h +++ b/src/core/devtools/options.h @@ -10,8 +10,8 @@ struct ImGuiTextBuffer; namespace Core::Devtools { struct TOptions { - std::string disassembler_cli_isa{"clrxdisasm --raw \"{src}\""}; - std::string disassembler_cli_spv{"spirv-cross -V \"{src}\""}; + std::string disassembler_cli_isa{"clrxdisasm --raw {src}"}; + std::string disassembler_cli_spv{"spirv-cross -V {src}"}; bool frame_dump_render_on_collapse{false}; }; diff --git a/src/core/devtools/widget/common.h b/src/core/devtools/widget/common.h index 5f669eb65..75eb55301 100644 --- a/src/core/devtools/widget/common.h +++ b/src/core/devtools/widget/common.h @@ -117,7 +117,7 @@ static bool IsDrawCall(AmdGpu::PM4ItOpcode opcode) { inline std::optional exec_cli(const char* cli) { std::array buffer{}; std::string output; - const auto f = popen(cli, "r"); + const auto f = popen(cli, "rt"); if (!f) { pclose(f); return {}; @@ -129,21 +129,27 @@ inline std::optional exec_cli(const char* cli) { return output; } -inline std::string RunDisassembler(const std::string& disassembler_cli, - const std::vector& shader_code) { +template +inline std::string RunDisassembler(const std::string& disassembler_cli, const T& shader_code, + bool* success = nullptr) { std::string shader_dis; if (disassembler_cli.empty()) { shader_dis = "No disassembler set"; + if (success) { + *success = false; + } } else { auto bin_path = std::filesystem::temp_directory_path() / "shadps4_tmp_shader.bin"; constexpr std::string_view src_arg = "{src}"; - std::string cli = disassembler_cli; + std::string cli = disassembler_cli + " 2>&1"; const auto pos = cli.find(src_arg); if (pos == std::string::npos) { - DebugState.ShowDebugMessage("Disassembler CLI does not contain {src} argument\n" + - disassembler_cli); + shader_dis = "Disassembler CLI does not contain {src} argument"; + if (success) { + *success = false; + } } else { cli.replace(pos, src_arg.size(), "\"" + bin_path.string() + "\""); Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Write); @@ -151,9 +157,16 @@ inline std::string RunDisassembler(const std::string& disassembler_cli, file.Close(); auto result = exec_cli(cli.c_str()); - shader_dis = result.value_or("Could not disassemble shader"); - if (shader_dis.empty()) { - shader_dis = "Disassembly empty or failed"; + if (result) { + shader_dis = result.value(); + if (success) { + *success = true; + } + } else { + if (success) { + *success = false; + } + shader_dis = "Could not disassemble shader"; } std::filesystem::remove(bin_path); diff --git a/src/core/devtools/widget/shader_list.cpp b/src/core/devtools/widget/shader_list.cpp index b056880dd..80c939718 100644 --- a/src/core/devtools/widget/shader_list.cpp +++ b/src/core/devtools/widget/shader_list.cpp @@ -1,66 +1,221 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "shader_list.h" #include #include "common.h" #include "common/config.h" +#include "common/path_util.h" +#include "common/string_util.h" #include "core/debug_state.h" #include "core/devtools/options.h" #include "imgui/imgui_std.h" +#include "sdl_window.h" +#include "video_core/renderer_vulkan/vk_presenter.h" +#include "video_core/renderer_vulkan/vk_rasterizer.h" + +extern std::unique_ptr presenter; using namespace ImGui; namespace Core::Devtools::Widget { -void ShaderList::DrawShader(DebugStateType::ShaderDump& value) { - if (!loaded_data) { - loaded_data = true; - if (value.cache_raw_disasm.empty()) { - value.cache_raw_disasm = RunDisassembler(Options.disassembler_cli_isa, value.raw_code); - } - isa_editor.SetText(value.cache_raw_disasm); +ShaderList::Selection::Selection(int index) : index(index) { + isa_editor.SetPalette(TextEditor::GetDarkPalette()); + isa_editor.SetReadOnly(true); + glsl_editor.SetPalette(TextEditor::GetDarkPalette()); + glsl_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::GLSL()); + presenter->GetWindow().RequestKeyboard(); +} +ShaderList::Selection::~Selection() { + presenter->GetWindow().ReleaseKeyboard(); +} + +void ShaderList::Selection::ReloadShader(DebugStateType::ShaderDump& value) { + auto& spv = value.is_patched ? value.patch_spv : value.spv; + if (spv.empty()) { + return; + } + auto& cache = presenter->GetRasterizer().GetPipelineCache(); + if (const auto m = cache.ReplaceShader(value.module, spv); m) { + value.module = *m; + } +} + +bool ShaderList::Selection::DrawShader(DebugStateType::ShaderDump& value) { + if (!value.loaded_data) { + value.loaded_data = true; + if (value.cache_isa_disasm.empty()) { + value.cache_isa_disasm = RunDisassembler(Options.disassembler_cli_isa, value.isa); + } if (value.cache_spv_disasm.empty()) { value.cache_spv_disasm = RunDisassembler(Options.disassembler_cli_spv, value.spv); } - spv_editor.SetText(value.cache_spv_disasm); + if (!value.patch_spv.empty() && value.cache_patch_disasm.empty()) { + value.cache_patch_disasm = RunDisassembler("spirv-dis {src}", value.patch_spv); + } + patch_path = + Common::FS::GetUserPath(Common::FS::PathType::ShaderDir) / "patch" / value.name; + patch_bin_path = patch_path; + patch_bin_path += ".spv"; + patch_path += ".glsl"; + if (std::filesystem::exists(patch_path)) { + std::ifstream file{patch_path}; + value.patch_source = + std::string{std::istreambuf_iterator{file}, std::istreambuf_iterator{}}; + } + + value.is_patched = !value.patch_spv.empty(); + if (!value.is_patched) { // No patch + isa_editor.SetText(value.cache_isa_disasm); + glsl_editor.SetText(value.cache_spv_disasm); + } else { + isa_editor.SetText(value.cache_patch_disasm); + isa_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::SPIRV()); + glsl_editor.SetText(value.patch_source); + glsl_editor.SetReadOnly(false); + } } - if (SmallButton("<-")) { - selected_shader = -1; + char name[64]; + snprintf(name, sizeof(name), "Shader %s", value.name.c_str()); + SetNextWindowSize({450.0f, 600.0f}, ImGuiCond_FirstUseEver); + if (!Begin(name, &open, ImGuiWindowFlags_NoNav)) { + End(); + return open; } - SameLine(); + Text("%s", value.name.c_str()); SameLine(0.0f, 7.0f); - if (BeginCombo("Shader type", showing_isa ? "ISA" : "SPIRV", ImGuiComboFlags_WidthFitPreview)) { - if (Selectable("SPIRV")) { - showing_isa = false; + if (Checkbox("Enable patch", &value.is_patched)) { + if (value.is_patched) { + if (value.patch_source.empty()) { + value.patch_source = value.cache_spv_disasm; + } + isa_editor.SetText(value.cache_patch_disasm); + isa_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::SPIRV()); + glsl_editor.SetText(value.patch_source); + glsl_editor.SetReadOnly(false); + if (!value.patch_spv.empty()) { + ReloadShader(value); + } + } else { + isa_editor.SetText(value.cache_isa_disasm); + isa_editor.SetLanguageDefinition(TextEditor::LanguageDefinition()); + glsl_editor.SetText(value.cache_spv_disasm); + glsl_editor.SetReadOnly(true); + ReloadShader(value); } - if (Selectable("ISA")) { - showing_isa = true; - } - EndCombo(); } - if (showing_isa) { - isa_editor.Render("ISA", GetContentRegionAvail()); + if (value.is_patched) { + if (BeginCombo("Shader type", showing_bin ? "SPIRV" : "GLSL", + ImGuiComboFlags_WidthFitPreview)) { + if (Selectable("GLSL")) { + showing_bin = false; + } + if (Selectable("SPIRV")) { + showing_bin = true; + } + EndCombo(); + } } else { - spv_editor.Render("SPIRV", GetContentRegionAvail()); + if (BeginCombo("Shader type", showing_bin ? "ISA" : "GLSL", + ImGuiComboFlags_WidthFitPreview)) { + if (Selectable("GLSL")) { + showing_bin = false; + } + if (Selectable("ISA")) { + showing_bin = true; + } + EndCombo(); + } } -} -ShaderList::ShaderList() { - isa_editor.SetPalette(TextEditor::GetDarkPalette()); - isa_editor.SetReadOnly(true); - spv_editor.SetPalette(TextEditor::GetDarkPalette()); - spv_editor.SetReadOnly(true); - spv_editor.SetLanguageDefinition(TextEditor::LanguageDefinition::GLSL()); + if (value.is_patched) { + bool save = false; + bool compile = false; + SameLine(0.0f, 3.0f); + if (Button("Save")) { + save = true; + } + SameLine(); + if (Button("Save & Compile")) { + save = true; + compile = true; + } + if (save) { + value.patch_source = glsl_editor.GetText(); + std::ofstream file{patch_path, std::ios::binary | std::ios::trunc}; + file << value.patch_source; + std::string msg = "Patch saved to "; + msg += Common::U8stringToString(patch_path.u8string()); + DebugState.ShowDebugMessage(msg); + } + if (compile) { + static std::map stage_arg = { + {"vs", "vert"}, + {"gs", "geom"}, + {"fs", "frag"}, + {"cs", "comp"}, + }; + auto stage = stage_arg.find(value.name.substr(0, 2)); + if (stage == stage_arg.end()) { + DebugState.ShowDebugMessage(std::string{"Invalid shader stage: "} + + value.name.substr(0, 2)); + } else { + std::string cmd = + fmt::format("glslc --target-env=vulkan1.3 --target-spv=spv1.6 " + "-fshader-stage={} {{src}} -o \"{}\"", + stage->second, Common::U8stringToString(patch_bin_path.u8string())); + bool success = false; + auto res = RunDisassembler(cmd, value.patch_source, &success); + if (!res.empty() || !success) { + DebugState.ShowDebugMessage("Compilation failed:\n" + res); + } else { + Common::FS::IOFile file{patch_bin_path, Common::FS::FileAccessMode::Read}; + value.patch_spv.resize(file.GetSize() / sizeof(u32)); + file.Read(value.patch_spv); + value.cache_patch_disasm = + RunDisassembler("spirv-dis {src}", value.patch_spv, &success); + if (!success) { + DebugState.ShowDebugMessage("Decompilation failed (Compile was ok):\n" + + res); + } else { + isa_editor.SetText(value.cache_patch_disasm); + ReloadShader(value); + } + } + } + } + } + + if (showing_bin) { + isa_editor.Render(value.is_patched ? "SPIRV" : "ISA", GetContentRegionAvail()); + } else { + glsl_editor.Render("GLSL", GetContentRegionAvail()); + } + + End(); + return open; } void ShaderList::Draw() { + for (auto it = open_shaders.begin(); it != open_shaders.end();) { + auto& selection = *it; + auto& shader = DebugState.shader_dump_list[selection.index]; + if (!selection.DrawShader(shader)) { + it = open_shaders.erase(it); + } else { + ++it; + } + } + SetNextWindowSize({500.0f, 600.0f}, ImGuiCond_FirstUseEver); if (!Begin("Shader list", &open)) { End(); @@ -73,18 +228,19 @@ void ShaderList::Draw() { return; } - if (selected_shader >= 0) { - DrawShader(DebugState.shader_dump_list[selected_shader]); - End(); - return; - } - auto width = GetContentRegionAvail().x; int i = 0; for (const auto& shader : DebugState.shader_dump_list) { - if (ButtonEx(shader.name.c_str(), {width, 20.0f}, ImGuiButtonFlags_NoHoveredOnFocus)) { - selected_shader = i; - loaded_data = false; + char name[128]; + if (shader.is_patched) { + snprintf(name, sizeof(name), "%s (PATCH ON)", shader.name.c_str()); + } else if (!shader.patch_spv.empty()) { + snprintf(name, sizeof(name), "%s (PATCH OFF)", shader.name.c_str()); + } else { + snprintf(name, sizeof(name), "%s", shader.name.c_str()); + } + if (ButtonEx(name, {width, 20.0f}, ImGuiButtonFlags_NoHoveredOnFocus)) { + open_shaders.emplace_back(i); } i++; } diff --git a/src/core/devtools/widget/shader_list.h b/src/core/devtools/widget/shader_list.h index 5a47f656d..2534ded35 100644 --- a/src/core/devtools/widget/shader_list.h +++ b/src/core/devtools/widget/shader_list.h @@ -6,20 +6,32 @@ #include "core/debug_state.h" #include "text_editor.h" +#include + namespace Core::Devtools::Widget { class ShaderList { - int selected_shader = -1; - TextEditor isa_editor{}; - TextEditor spv_editor{}; - bool loaded_data = false; - bool showing_isa = false; + struct Selection { + explicit Selection(int index); + ~Selection(); - void DrawShader(DebugStateType::ShaderDump& value); + void ReloadShader(DebugStateType::ShaderDump& value); + + bool DrawShader(DebugStateType::ShaderDump& value); + + int index; + TextEditor isa_editor{}; + TextEditor glsl_editor{}; + bool open = true; + bool showing_bin = false; + + std::filesystem::path patch_path; + std::filesystem::path patch_bin_path; + }; + + std::vector open_shaders{}; public: - ShaderList(); - bool open = false; void Draw(); diff --git a/src/core/devtools/widget/text_editor.cpp b/src/core/devtools/widget/text_editor.cpp index 07f2f658d..7171cac47 100644 --- a/src/core/devtools/widget/text_editor.cpp +++ b/src/core/devtools/widget/text_editor.cpp @@ -1059,7 +1059,8 @@ void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) { if (!mIgnoreImGuiChild) ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | - ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoNav); if (mHandleKeyboardInputs) { HandleKeyboardInputs(); @@ -2331,4 +2332,50 @@ const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() { return langDef; } +// Source: https://github.com/dfranx/ImGuiColorTextEdit/blob/master/TextEditor.cpp +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SPIRV() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + /* + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ + \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", + PaletteIndex::Punctuation)); + */ + + langDef.mTokenRegexStrings.push_back(std::make_pair( + "L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[ =\\t]Op[a-zA-Z]*", PaletteIndex::Keyword)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("%[_a-zA-Z0-9]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = ";"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = false; + + langDef.mName = "SPIR-V"; + + inited = true; + } + return langDef; +} + } // namespace Core::Devtools::Widget diff --git a/src/core/devtools/widget/text_editor.h b/src/core/devtools/widget/text_editor.h index 5c3f29f11..aa81d0d23 100644 --- a/src/core/devtools/widget/text_editor.h +++ b/src/core/devtools/widget/text_editor.h @@ -161,6 +161,7 @@ public: : mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true) {} static const LanguageDefinition& GLSL(); + static const LanguageDefinition& SPIRV(); }; TextEditor(); diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp index d95e8d634..f6b57436f 100644 --- a/src/sdl_window.cpp +++ b/src/sdl_window.cpp @@ -168,6 +168,21 @@ void WindowSDL::InitTimers() { SDL_AddTimer(100, &PollController, controller); } +void WindowSDL::RequestKeyboard() { + if (keyboard_grab == 0) { + SDL_StartTextInput(window); + } + keyboard_grab++; +} + +void WindowSDL::ReleaseKeyboard() { + ASSERT(keyboard_grab > 0); + keyboard_grab--; + if (keyboard_grab == 0) { + SDL_StopTextInput(window); + } +} + void WindowSDL::OnResize() { SDL_GetWindowSizeInPixels(window, &width, &height); ImGui::Core::OnResize(); diff --git a/src/sdl_window.h b/src/sdl_window.h index 78d0e582f..78d4bbc39 100644 --- a/src/sdl_window.h +++ b/src/sdl_window.h @@ -41,6 +41,8 @@ struct WindowSystemInfo { }; class WindowSDL { + int keyboard_grab = 0; + public: explicit WindowSDL(s32 width, s32 height, Input::GameController* controller, std::string_view window_title); @@ -69,6 +71,9 @@ public: void WaitEvent(); void InitTimers(); + void RequestKeyboard(); + void ReleaseKeyboard(); + private: void OnResize(); void OnKeyPress(const SDL_Event* event); diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 09d4e4195..8d495ab06 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -13,7 +13,7 @@ namespace Vulkan { ComputePipeline::ComputePipeline(const Instance& instance_, Scheduler& scheduler_, DescriptorHeap& desc_heap_, vk::PipelineCache pipeline_cache, - u64 compute_key_, const Shader::Info& info_, + ComputePipelineKey compute_key_, const Shader::Info& info_, vk::ShaderModule module) : Pipeline{instance_, scheduler_, desc_heap_, pipeline_cache, true}, compute_key{compute_key_} { auto& info = stages[int(Shader::Stage::Compute)]; diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.h b/src/video_core/renderer_vulkan/vk_compute_pipeline.h index ca429b58d..1c28e461c 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.h @@ -17,15 +17,33 @@ class Instance; class Scheduler; class DescriptorHeap; +struct ComputePipelineKey { + size_t value; + + friend bool operator==(const ComputePipelineKey& lhs, const ComputePipelineKey& rhs) { + return lhs.value == rhs.value; + } + friend bool operator!=(const ComputePipelineKey& lhs, const ComputePipelineKey& rhs) { + return !(lhs == rhs); + } +}; + class ComputePipeline : public Pipeline { public: ComputePipeline(const Instance& instance, Scheduler& scheduler, DescriptorHeap& desc_heap, - vk::PipelineCache pipeline_cache, u64 compute_key, const Shader::Info& info, - vk::ShaderModule module); + vk::PipelineCache pipeline_cache, ComputePipelineKey compute_key, + const Shader::Info& info, vk::ShaderModule module); ~ComputePipeline(); private: - u64 compute_key; + ComputePipelineKey compute_key; }; } // namespace Vulkan + +template <> +struct std::hash { + std::size_t operator()(const Vulkan::ComputePipelineKey& key) const noexcept { + return std::hash{}(key.value); + } +}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index 91ffe4ea4..2834fceb7 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + #include #include "common/types.h" diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 53bdc79a6..276e4ef29 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -189,10 +189,19 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline() { } const auto [it, is_new] = graphics_pipelines.try_emplace(graphics_key); if (is_new) { - it.value() = graphics_pipeline_pool.Create(instance, scheduler, desc_heap, graphics_key, - *pipeline_cache, infos, fetch_shader, modules); + it.value() = + std::make_unique(instance, scheduler, desc_heap, graphics_key, + *pipeline_cache, infos, fetch_shader, modules); + if (Config::collectShadersForDebug()) { + for (auto stage = 0; stage < MaxShaderStages; ++stage) { + if (infos[stage]) { + auto& m = modules[stage]; + module_related_pipelines[m].emplace_back(graphics_key); + } + } + } } - return it->second; + return it->second.get(); } const ComputePipeline* PipelineCache::GetComputePipeline() { @@ -201,10 +210,14 @@ const ComputePipeline* PipelineCache::GetComputePipeline() { } const auto [it, is_new] = compute_pipelines.try_emplace(compute_key); if (is_new) { - it.value() = compute_pipeline_pool.Create(instance, scheduler, desc_heap, *pipeline_cache, - compute_key, *infos[0], modules[0]); + it.value() = std::make_unique( + instance, scheduler, desc_heap, *pipeline_cache, compute_key, *infos[0], modules[0]); + if (Config::collectShadersForDebug()) { + auto& m = modules[0]; + module_related_pipelines[m].emplace_back(compute_key); + } } - return it->second; + return it->second.get(); } bool PipelineCache::RefreshGraphicsKey() { @@ -401,7 +414,7 @@ bool PipelineCache::RefreshComputeKey() { Shader::Backend::Bindings binding{}; const auto* cs_pgm = &liverpool->regs.cs_program; const auto cs_params = Liverpool::GetParams(*cs_pgm); - std::tie(infos[0], modules[0], fetch_shader, compute_key) = + std::tie(infos[0], modules[0], fetch_shader, compute_key.value) = GetProgram(Shader::Stage::Compute, cs_params, binding); return true; } @@ -417,17 +430,23 @@ vk::ShaderModule PipelineCache::CompileModule(Shader::Info& info, const auto ir_program = Shader::TranslateProgram(code, pools, info, runtime_info, profile); auto spv = Shader::Backend::SPIRV::EmitSPIRV(profile, runtime_info, ir_program, binding); DumpShader(spv, info.pgm_hash, info.stage, perm_idx, "spv"); + + vk::ShaderModule module; + auto patch = GetShaderPatch(info.pgm_hash, info.stage, perm_idx, "spv"); - if (patch) { - spv = *patch; + const bool is_patched = patch && Config::patchShaders(); + if (is_patched) { LOG_INFO(Loader, "Loaded patch for {} shader {:#x}", info.stage, info.pgm_hash); + module = CompileSPV(*patch, instance.GetDevice()); + } else { + module = CompileSPV(spv, instance.GetDevice()); } - const auto module = CompileSPV(spv, instance.GetDevice()); - const auto name = fmt::format("{}_{:#x}_{}", info.stage, info.pgm_hash, perm_idx); + const auto name = fmt::format("{}_{:#018x}_{}", info.stage, info.pgm_hash, perm_idx); Vulkan::SetObjectName(instance.GetDevice(), module, name); if (Config::collectShadersForDebug()) { - DebugState.CollectShader(name, spv, code); + DebugState.CollectShader(name, module, spv, code, patch ? *patch : std::span{}, + is_patched); } return module; } @@ -438,17 +457,17 @@ PipelineCache::GetProgram(Shader::Stage stage, Shader::ShaderParams params, const auto runtime_info = BuildRuntimeInfo(stage); auto [it_pgm, new_program] = program_cache.try_emplace(params.hash); if (new_program) { - Program* program = program_pool.Create(stage, params); + it_pgm.value() = std::make_unique(stage, params); + auto& program = it_pgm.value(); auto start = binding; const auto module = CompileModule(program->info, runtime_info, params.code, 0, binding); const auto spec = Shader::StageSpecialization(program->info, runtime_info, profile, start); program->AddPermut(module, std::move(spec)); - it_pgm.value() = program; return std::make_tuple(&program->info, module, spec.fetch_shader_data, HashCombine(params.hash, 0)); } - Program* program = it_pgm->second; + auto& program = it_pgm.value(); auto& info = program->info; info.RefreshFlatBuf(); const auto spec = Shader::StageSpecialization(info, runtime_info, profile, binding); @@ -469,6 +488,34 @@ PipelineCache::GetProgram(Shader::Stage stage, Shader::ShaderParams params, HashCombine(params.hash, perm_idx)); } +std::optional PipelineCache::ReplaceShader(vk::ShaderModule module, + std::span spv_code) { + std::optional new_module{}; + for (const auto& [_, program] : program_cache) { + for (auto& m : program->modules) { + if (m.module == module) { + const auto& d = instance.GetDevice(); + d.destroyShaderModule(m.module); + m.module = CompileSPV(spv_code, d); + new_module = m.module; + } + } + } + if (module_related_pipelines.contains(module)) { + auto& pipeline_keys = module_related_pipelines[module]; + for (auto& key : pipeline_keys) { + if (std::holds_alternative(key)) { + auto& graphics_key = std::get(key); + graphics_pipelines.erase(graphics_key); + } else if (std::holds_alternative(key)) { + auto& compute_key = std::get(key); + compute_pipelines.erase(compute_key); + } + } + } + return new_module; +} + void PipelineCache::DumpShader(std::span code, u64 hash, Shader::Stage stage, size_t perm_idx, std::string_view ext) { if (!Config::dumpShaders()) { @@ -488,9 +535,6 @@ void PipelineCache::DumpShader(std::span code, u64 hash, Shader::Stag std::optional> PipelineCache::GetShaderPatch(u64 hash, Shader::Stage stage, size_t perm_idx, std::string_view ext) { - if (!Config::patchShaders()) { - return {}; - } using namespace Common::FS; const auto patch_dir = GetUserPath(PathType::ShaderDir) / "patch"; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index e4a8abd4f..c5c2fc98e 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "shader_recompiler/profile.h" #include "shader_recompiler/recompiler.h" @@ -11,6 +12,13 @@ #include "video_core/renderer_vulkan/vk_graphics_pipeline.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" +template <> +struct std::hash { + std::size_t operator()(const vk::ShaderModule& module) const noexcept { + return std::hash{}(reinterpret_cast((VkShaderModule)module)); + } +}; + namespace Shader { struct Info; } @@ -52,6 +60,9 @@ public: GetProgram(Shader::Stage stage, Shader::ShaderParams params, Shader::Backend::Bindings& binding); + std::optional ReplaceShader(vk::ShaderModule module, + std::span spv_code); + private: bool RefreshGraphicsKey(); bool RefreshComputeKey(); @@ -74,17 +85,19 @@ private: vk::UniquePipelineLayout pipeline_layout; Shader::Profile profile{}; Shader::Pools pools; - tsl::robin_map program_cache; - Common::ObjectPool program_pool; - Common::ObjectPool graphics_pipeline_pool; - Common::ObjectPool compute_pipeline_pool; - tsl::robin_map compute_pipelines; - tsl::robin_map graphics_pipelines; + tsl::robin_map> program_cache; + tsl::robin_map> compute_pipelines; + tsl::robin_map> graphics_pipelines; std::array infos{}; std::array modules{}; std::optional fetch_shader{}; GraphicsPipelineKey graphics_key{}; - u64 compute_key{}; + ComputePipelineKey compute_key{}; + + // Only if Config::collectShadersForDebug() + tsl::robin_map>> + module_related_pipelines; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_presenter.h b/src/video_core/renderer_vulkan/vk_presenter.h index 4d9226dec..4c29af0f0 100644 --- a/src/video_core/renderer_vulkan/vk_presenter.h +++ b/src/video_core/renderer_vulkan/vk_presenter.h @@ -53,6 +53,10 @@ public: return pp_settings.gamma; } + Frontend::WindowSDL& GetWindow() const { + return window; + } + Frame* PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup& attribute, VAddr cpu_address, bool is_eop) { auto desc = VideoCore::TextureCache::VideoOutDesc{attribute, cpu_address}; @@ -90,6 +94,10 @@ public: draw_scheduler.Flush(info); } + Rasterizer& GetRasterizer() const { + return *rasterizer.get(); + } + private: void CreatePostProcessPipeline(); Frame* PrepareFrameInternal(VideoCore::ImageId image_id, bool is_eop = true); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index fe8aceba7..1936276a2 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -54,6 +54,10 @@ public: u64 Flush(); void Finish(); + PipelineCache& GetPipelineCache() { + return pipeline_cache; + } + private: RenderState PrepareRenderState(u32 mrt_mask); void BeginRendering(const GraphicsPipeline& pipeline, RenderState& state); From cd9fc5d0e935ac12f469d609b1a643964fb33129 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:08:53 -0800 Subject: [PATCH 52/89] thread: Apply alternate signal stack to created threads. (#1724) --- src/core/thread.cpp | 30 +++++++++++++++++++++++++++--- src/core/thread.h | 1 + 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/core/thread.cpp b/src/core/thread.cpp index f87e3c8dc..0d0804cea 100644 --- a/src/core/thread.cpp +++ b/src/core/thread.cpp @@ -1,15 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "libraries/kernel/threads/pthread.h" -#include "thread.h" - +#include "common/alignment.h" #include "core/libraries/kernel/threads/pthread.h" +#include "thread.h" #ifdef _WIN64 #include #include "common/ntapi.h" #else +#include #include #endif @@ -113,6 +113,17 @@ void NativeThread::Exit() { NtTerminateThread(nullptr, 0); #else + // Disable and free the signal stack. + constexpr stack_t sig_stack = { + .ss_flags = SS_DISABLE, + }; + sigaltstack(&sig_stack, nullptr); + + if (sig_stack_ptr) { + free(sig_stack_ptr); + sig_stack_ptr = nullptr; + } + pthread_exit(nullptr); #endif } @@ -122,6 +133,19 @@ void NativeThread::Initialize() { tid = GetCurrentThreadId(); #else tid = (u64)pthread_self(); + + // Set up an alternate signal handler stack to avoid overflowing small thread stacks. + const size_t page_size = getpagesize(); + const size_t sig_stack_size = Common::AlignUp(std::max(64_KB, MINSIGSTKSZ), page_size); + ASSERT_MSG(posix_memalign(&sig_stack_ptr, page_size, sig_stack_size) == 0, + "Failed to allocate signal stack: {}", errno); + + const stack_t sig_stack = { + .ss_sp = sig_stack_ptr, + .ss_size = sig_stack_size, + .ss_flags = 0, + }; + ASSERT_MSG(sigaltstack(&sig_stack, nullptr) == 0, "Failed to set signal stack: {}", errno); #endif } diff --git a/src/core/thread.h b/src/core/thread.h index 3bac0e699..bd777a2e6 100644 --- a/src/core/thread.h +++ b/src/core/thread.h @@ -37,6 +37,7 @@ private: void* native_handle; #else uintptr_t native_handle; + void* sig_stack_ptr; #endif u64 tid; }; From aa5293e3ad6d4fd6b83da219c6ee5d11d24fe5eb Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Tue, 10 Dec 2024 08:12:58 -0300 Subject: [PATCH 53/89] Delete Patches Button (#1722) --- src/qt_gui/cheats_patches.cpp | 39 ++++++++++++++++++++++++++++++++ src/qt_gui/translations/pt_BR.ts | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/qt_gui/cheats_patches.cpp b/src/qt_gui/cheats_patches.cpp index 3e7c22451..446b5a0ea 100644 --- a/src/qt_gui/cheats_patches.cpp +++ b/src/qt_gui/cheats_patches.cpp @@ -51,6 +51,9 @@ void CheatsPatches::setupUI() { QString CHEATS_DIR_QString; Common::FS::PathToQString(CHEATS_DIR_QString, Common::FS::GetUserPath(Common::FS::PathType::CheatsDir)); + QString PATCHS_DIR_QString; + Common::FS::PathToQString(PATCHS_DIR_QString, + Common::FS::GetUserPath(Common::FS::PathType::PatchesDir)); QString NameCheatJson = m_gameSerial + "_" + m_gameVersion + ".json"; m_cheatFilePath = CHEATS_DIR_QString + "/" + NameCheatJson; @@ -237,9 +240,45 @@ void CheatsPatches::setupUI() { }); patchesControlLayout->addWidget(patchesButton); + QPushButton* deletePatchButton = new QPushButton(tr("Delete File")); + connect(deletePatchButton, &QPushButton::clicked, [this, PATCHS_DIR_QString]() { + QStringListModel* model = qobject_cast(patchesListView->model()); + if (!model) { + return; + } + QItemSelectionModel* selectionModel = patchesListView->selectionModel(); + if (!selectionModel) { + return; + } + QModelIndexList selectedIndexes = selectionModel->selectedIndexes(); + if (selectedIndexes.isEmpty()) { + QMessageBox::warning(this, tr("Delete File"), tr("No files selected.")); + return; + } + QModelIndex selectedIndex = selectedIndexes.first(); + QString selectedFileName = model->data(selectedIndex).toString(); + + int ret = QMessageBox::warning( + this, tr("Delete File"), + QString(tr("Do you want to delete the selected file?\\n%1").replace("\\n", "\n")) + .arg(selectedFileName), + QMessageBox::Yes | QMessageBox::No); + + if (ret == QMessageBox::Yes) { + QString fileName = selectedFileName.split('|').first().trimmed(); + QString directoryName = selectedFileName.split('|').last().trimmed(); + QString filePath = PATCHS_DIR_QString + "/" + directoryName + "/" + fileName; + + QFile::remove(filePath); + createFilesJson(directoryName); + populateFileListPatches(); + } + }); + QPushButton* saveButton = new QPushButton(tr("Save")); connect(saveButton, &QPushButton::clicked, this, &CheatsPatches::onSaveButtonClicked); + patchesControlLayout->addWidget(deletePatchButton); patchesControlLayout->addWidget(saveButton); patchesLayout->addLayout(patchesControlLayout); diff --git a/src/qt_gui/translations/pt_BR.ts b/src/qt_gui/translations/pt_BR.ts index 7ea63b9fb..dfe785673 100644 --- a/src/qt_gui/translations/pt_BR.ts +++ b/src/qt_gui/translations/pt_BR.ts @@ -841,7 +841,7 @@ Cheats / Patches for - Cheats / Patches for + Cheats / Patches para From bf41ab6c40a1236550e26c64ae897d0d74a33968 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:13:34 -0800 Subject: [PATCH 54/89] memory: Handle 0 alignment in MemoryManager::Allocate (#1692) --- src/core/memory.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 82e4b7ad3..980beee79 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -96,12 +96,12 @@ PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, size_t siz PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, u64 alignment, int memory_type) { std::scoped_lock lk{mutex}; + alignment = alignment > 0 ? alignment : 16_KB; auto dmem_area = FindDmemArea(search_start); const auto is_suitable = [&] { - const auto aligned_base = alignment > 0 ? Common::AlignUp(dmem_area->second.base, alignment) - : dmem_area->second.base; + const auto aligned_base = Common::AlignUp(dmem_area->second.base, alignment); const auto alignment_size = aligned_base - dmem_area->second.base; const auto remaining_size = dmem_area->second.size >= alignment_size ? dmem_area->second.size - alignment_size : 0; @@ -114,7 +114,7 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, // Align free position PAddr free_addr = dmem_area->second.base; - free_addr = alignment > 0 ? Common::AlignUp(free_addr, alignment) : free_addr; + free_addr = Common::AlignUp(free_addr, alignment); // Add the allocated region to the list and commit its pages. auto& area = CarveDmemArea(free_addr, size)->second; From 41fd1c84cf51fbb8b3784e038c72d4bb37b8f406 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 10 Dec 2024 04:43:32 -0800 Subject: [PATCH 55/89] semaphore: Use handles to properly handle semaphore double-delete. (#1728) --- .../libraries/kernel/threads/semaphore.cpp | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/core/libraries/kernel/threads/semaphore.cpp b/src/core/libraries/kernel/threads/semaphore.cpp index 39c1a0233..f25a76c2b 100644 --- a/src/core/libraries/kernel/threads/semaphore.cpp +++ b/src/core/libraries/kernel/threads/semaphore.cpp @@ -9,6 +9,7 @@ #include "core/libraries/kernel/sync/semaphore.h" #include "common/logging/log.h" +#include "common/slot_vector.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/orbis_error.h" #include "core/libraries/kernel/posix_error.h" @@ -188,7 +189,9 @@ public: bool is_fifo; }; -using OrbisKernelSema = OrbisSem*; +using OrbisKernelSema = Common::SlotId; + +static Common::SlotVector> orbis_sems; s32 PS4_SYSV_ABI sceKernelCreateSema(OrbisKernelSema* sem, const char* pName, u32 attr, s32 initCount, s32 maxCount, const void* pOptParam) { @@ -196,47 +199,48 @@ s32 PS4_SYSV_ABI sceKernelCreateSema(OrbisKernelSema* sem, const char* pName, u3 LOG_ERROR(Lib_Kernel, "Semaphore creation parameters are invalid!"); return ORBIS_KERNEL_ERROR_EINVAL; } - *sem = new OrbisSem(initCount, maxCount, pName, attr == 1); + *sem = orbis_sems.insert( + std::move(std::make_unique(initCount, maxCount, pName, attr == 1))); return ORBIS_OK; } s32 PS4_SYSV_ABI sceKernelWaitSema(OrbisKernelSema sem, s32 needCount, u32* pTimeout) { - if (!sem) { + if (!orbis_sems.is_allocated(sem)) { return ORBIS_KERNEL_ERROR_ESRCH; } - return sem->Wait(true, needCount, pTimeout); + return orbis_sems[sem]->Wait(true, needCount, pTimeout); } s32 PS4_SYSV_ABI sceKernelSignalSema(OrbisKernelSema sem, s32 signalCount) { - if (!sem) { + if (!orbis_sems.is_allocated(sem)) { return ORBIS_KERNEL_ERROR_ESRCH; } - if (!sem->Signal(signalCount)) { + if (!orbis_sems[sem]->Signal(signalCount)) { return ORBIS_KERNEL_ERROR_EINVAL; } return ORBIS_OK; } s32 PS4_SYSV_ABI sceKernelPollSema(OrbisKernelSema sem, s32 needCount) { - if (!sem) { + if (!orbis_sems.is_allocated(sem)) { return ORBIS_KERNEL_ERROR_ESRCH; } - return sem->Wait(false, needCount, nullptr); + return orbis_sems[sem]->Wait(false, needCount, nullptr); } int PS4_SYSV_ABI sceKernelCancelSema(OrbisKernelSema sem, s32 setCount, s32* pNumWaitThreads) { - if (!sem) { + if (!orbis_sems.is_allocated(sem)) { return ORBIS_KERNEL_ERROR_ESRCH; } - return sem->Cancel(setCount, pNumWaitThreads); + return orbis_sems[sem]->Cancel(setCount, pNumWaitThreads); } int PS4_SYSV_ABI sceKernelDeleteSema(OrbisKernelSema sem) { - if (!sem) { + if (!orbis_sems.is_allocated(sem)) { return ORBIS_KERNEL_ERROR_ESRCH; } - sem->Delete(); - delete sem; + orbis_sems[sem]->Delete(); + orbis_sems.erase(sem); return ORBIS_OK; } From e5e1aba24172b4cf41e1c4ea56adf0b0984b815b Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 10 Dec 2024 04:44:08 -0800 Subject: [PATCH 56/89] renderer_vulkan: Introduce shader HLE system with copy shader implementation. (#1683) * renderer_vulkan: Introduce shader HLE system with copy shader implementation. Co-authored-by: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> * buffer_cache: Handle obtaining buffer views partially within buffers. * vk_shader_hle: Make more efficient --------- Co-authored-by: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> --- CMakeLists.txt | 2 + src/video_core/buffer_cache/buffer_cache.cpp | 10 +- src/video_core/buffer_cache/buffer_cache.h | 3 +- .../renderer_vulkan/vk_rasterizer.cpp | 6 + .../renderer_vulkan/vk_rasterizer.h | 8 + src/video_core/renderer_vulkan/vk_scheduler.h | 4 + .../renderer_vulkan/vk_shader_hle.cpp | 139 ++++++++++++++++++ .../renderer_vulkan/vk_shader_hle.h | 20 +++ .../texture_cache/texture_cache.cpp | 8 +- 9 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 src/video_core/renderer_vulkan/vk_shader_hle.cpp create mode 100644 src/video_core/renderer_vulkan/vk_shader_hle.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7de79a43d..b057f55d6 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -738,6 +738,8 @@ set(VIDEO_CORE src/video_core/amdgpu/liverpool.cpp src/video_core/renderer_vulkan/vk_resource_pool.h src/video_core/renderer_vulkan/vk_scheduler.cpp src/video_core/renderer_vulkan/vk_scheduler.h + src/video_core/renderer_vulkan/vk_shader_hle.cpp + src/video_core/renderer_vulkan/vk_shader_hle.h src/video_core/renderer_vulkan/vk_shader_util.cpp src/video_core/renderer_vulkan/vk_shader_util.h src/video_core/renderer_vulkan/vk_swapchain.cpp diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 1abdb230b..e9fc06493 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -360,7 +360,8 @@ std::pair BufferCache::ObtainBuffer(VAddr device_addr, u32 size, b return {&buffer, buffer.Offset(device_addr)}; } -std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size) { +std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, bool prefer_gpu) { + // Check if any buffer contains the full requested range. const u64 page = gpu_addr >> CACHING_PAGEBITS; const BufferId buffer_id = page_table[page]; if (buffer_id) { @@ -370,6 +371,13 @@ std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size) return {&buffer, buffer.Offset(gpu_addr)}; } } + // If no buffer contains the full requested range but some buffer within was GPU-modified, + // fall back to ObtainBuffer to create a full buffer and avoid losing GPU modifications. + // This is only done if the request prefers to use GPU memory, otherwise we can skip it. + if (prefer_gpu && memory_tracker.IsRegionGpuModified(gpu_addr, size)) { + return ObtainBuffer(gpu_addr, size, false, false); + } + // In all other cases, just do a CPU copy to the staging buffer. const u32 offset = staging_buffer.Copy(gpu_addr, size, 16); return {&staging_buffer, offset}; } diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 3dab95db7..e62913413 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -96,7 +96,8 @@ public: BufferId buffer_id = {}); /// Attempts to obtain a buffer without modifying the cache contents. - [[nodiscard]] std::pair ObtainViewBuffer(VAddr gpu_addr, u32 size); + [[nodiscard]] std::pair ObtainViewBuffer(VAddr gpu_addr, u32 size, + bool prefer_gpu); /// Return true when a region is registered on the cache [[nodiscard]] bool IsRegionRegistered(VAddr addr, size_t size); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 0471fdb0a..33358b850 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -8,6 +8,7 @@ #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_hle.h" #include "video_core/texture_cache/image_view.h" #include "video_core/texture_cache/texture_cache.h" #include "vk_rasterizer.h" @@ -318,6 +319,11 @@ void Rasterizer::DispatchDirect() { return; } + const auto& cs = pipeline->GetStage(Shader::Stage::Compute); + if (ExecuteShaderHLE(cs, liverpool->regs, *this)) { + return; + } + if (!BindResources(pipeline)) { return; } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 1936276a2..9214372ee 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -28,6 +28,14 @@ public: AmdGpu::Liverpool* liverpool); ~Rasterizer(); + [[nodiscard]] Scheduler& GetScheduler() noexcept { + return scheduler; + } + + [[nodiscard]] VideoCore::BufferCache& GetBufferCache() noexcept { + return buffer_cache; + } + [[nodiscard]] VideoCore::TextureCache& GetTextureCache() noexcept { return texture_cache; } diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 1140bfbc2..45a9228c9 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -10,6 +10,10 @@ #include "video_core/renderer_vulkan/vk_master_semaphore.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" +namespace tracy { +class VkCtxScope; +} + namespace Vulkan { class Instance; diff --git a/src/video_core/renderer_vulkan/vk_shader_hle.cpp b/src/video_core/renderer_vulkan/vk_shader_hle.cpp new file mode 100644 index 000000000..df9d40f07 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_shader_hle.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "shader_recompiler/info.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_hle.h" + +#include "vk_rasterizer.h" + +namespace Vulkan { + +static constexpr u64 COPY_SHADER_HASH = 0xfefebf9f; + +bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Regs& regs, + Rasterizer& rasterizer) { + auto& scheduler = rasterizer.GetScheduler(); + auto& buffer_cache = rasterizer.GetBufferCache(); + + // Copy shader defines three formatted buffers as inputs: control, source, and destination. + const auto ctl_buf_sharp = info.texture_buffers[0].GetSharp(info); + const auto src_buf_sharp = info.texture_buffers[1].GetSharp(info); + const auto dst_buf_sharp = info.texture_buffers[2].GetSharp(info); + const auto buf_stride = src_buf_sharp.GetStride(); + ASSERT(buf_stride == dst_buf_sharp.GetStride()); + + struct CopyShaderControl { + u32 dst_idx; + u32 src_idx; + u32 end; + }; + static_assert(sizeof(CopyShaderControl) == 12); + ASSERT(ctl_buf_sharp.GetStride() == sizeof(CopyShaderControl)); + const auto ctl_buf = reinterpret_cast(ctl_buf_sharp.base_address); + + static std::vector copies; + copies.clear(); + copies.reserve(regs.cs_program.dim_x); + + for (u32 i = 0; i < regs.cs_program.dim_x; i++) { + const auto& [dst_idx, src_idx, end] = ctl_buf[i]; + const u32 local_dst_offset = dst_idx * buf_stride; + const u32 local_src_offset = src_idx * buf_stride; + const u32 local_size = (end + 1) * buf_stride; + copies.emplace_back(local_src_offset, local_dst_offset, local_size); + } + + scheduler.EndRendering(); + + static constexpr vk::MemoryBarrier READ_BARRIER{ + .srcAccessMask = vk::AccessFlagBits::eMemoryWrite, + .dstAccessMask = vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eTransferWrite, + }; + static constexpr vk::MemoryBarrier WRITE_BARRIER{ + .srcAccessMask = vk::AccessFlagBits::eTransferWrite, + .dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite, + }; + scheduler.CommandBuffer().pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, READ_BARRIER, {}, {}); + + static constexpr vk::DeviceSize MaxDistanceForMerge = 64_MB; + u32 batch_start = 0; + u32 batch_end = 1; + + while (batch_end < copies.size()) { + // Place first copy into the current batch + const auto& copy = copies[batch_start]; + auto src_offset_min = copy.srcOffset; + auto src_offset_max = copy.srcOffset + copy.size; + auto dst_offset_min = copy.dstOffset; + auto dst_offset_max = copy.dstOffset + copy.size; + + for (int i = batch_start + 1; i < copies.size(); i++) { + // Compute new src and dst bounds if we were to batch this copy + const auto [src_offset, dst_offset, size] = copies[i]; + auto new_src_offset_min = std::min(src_offset_min, src_offset); + auto new_src_offset_max = std::max(src_offset_max, src_offset + size); + if (new_src_offset_max - new_src_offset_min > MaxDistanceForMerge) { + continue; + } + + auto new_dst_offset_min = std::min(dst_offset_min, dst_offset); + auto new_dst_offset_max = std::max(dst_offset_max, dst_offset + size); + if (new_dst_offset_max - new_dst_offset_min > MaxDistanceForMerge) { + continue; + } + + // We can batch this copy + src_offset_min = new_src_offset_min; + src_offset_max = new_src_offset_max; + dst_offset_min = new_dst_offset_min; + dst_offset_max = new_dst_offset_max; + if (i != batch_end) { + std::swap(copies[i], copies[batch_end]); + } + ++batch_end; + } + + // Obtain buffers for the total source and destination ranges. + const auto [src_buf, src_buf_offset] = + buffer_cache.ObtainBuffer(src_buf_sharp.base_address + src_offset_min, + src_offset_max - src_offset_min, false, false); + const auto [dst_buf, dst_buf_offset] = + buffer_cache.ObtainBuffer(dst_buf_sharp.base_address + dst_offset_min, + dst_offset_max - dst_offset_min, true, false); + + // Apply found buffer base. + const auto vk_copies = std::span{copies}.subspan(batch_start, batch_end - batch_start); + for (auto& copy : vk_copies) { + copy.srcOffset = copy.srcOffset - src_offset_min + src_buf_offset; + copy.dstOffset = copy.dstOffset - dst_offset_min + dst_buf_offset; + } + + // Execute buffer copies. + LOG_TRACE(Render_Vulkan, "HLE buffer copy: src_size = {}, dst_size = {}", + src_offset_max - src_offset_min, dst_offset_max - dst_offset_min); + scheduler.CommandBuffer().copyBuffer(src_buf->Handle(), dst_buf->Handle(), vk_copies); + batch_start = batch_end; + ++batch_end; + } + + scheduler.CommandBuffer().pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, WRITE_BARRIER, {}, {}); + + return true; +} + +bool ExecuteShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Regs& regs, + Rasterizer& rasterizer) { + switch (info.pgm_hash) { + case COPY_SHADER_HASH: + return ExecuteCopyShaderHLE(info, regs, rasterizer); + default: + return false; + } +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_hle.h b/src/video_core/renderer_vulkan/vk_shader_hle.h new file mode 100644 index 000000000..fda9b1735 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_shader_hle.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "video_core/amdgpu/liverpool.h" + +namespace Shader { +struct Info; +} + +namespace Vulkan { + +class Rasterizer; + +/// Attempts to execute a shader using HLE if possible. +bool ExecuteShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Regs& regs, + Rasterizer& rasterizer); + +} // namespace Vulkan diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 1670648b3..0e5bbc1f3 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -466,6 +466,9 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule const auto& num_mips = image.info.resources.levels; ASSERT(num_mips == image.info.mips_layout.size()); + const bool is_gpu_modified = True(image.flags & ImageFlagBits::GpuModified); + const bool is_gpu_dirty = True(image.flags & ImageFlagBits::GpuDirty); + boost::container::small_vector image_copy{}; for (u32 m = 0; m < num_mips; m++) { const u32 width = std::max(image.info.size.width >> m, 1u); @@ -475,8 +478,6 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule const auto& mip = image.info.mips_layout[m]; // Protect GPU modified resources from accidental CPU reuploads. - const bool is_gpu_modified = True(image.flags & ImageFlagBits::GpuModified); - const bool is_gpu_dirty = True(image.flags & ImageFlagBits::GpuDirty); if (is_gpu_modified && !is_gpu_dirty) { const u8* addr = std::bit_cast(image.info.guest_address); const u64 hash = XXH3_64bits(addr + mip.offset, mip.size); @@ -515,7 +516,8 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule const VAddr image_addr = image.info.guest_address; const size_t image_size = image.info.guest_size_bytes; - const auto [vk_buffer, buf_offset] = buffer_cache.ObtainViewBuffer(image_addr, image_size); + const auto [vk_buffer, buf_offset] = + buffer_cache.ObtainViewBuffer(image_addr, image_size, is_gpu_dirty); // The obtained buffer may be written by a shader so we need to emit a barrier to prevent RAW // hazard if (auto barrier = vk_buffer->GetBarrier(vk::AccessFlagBits2::eTransferRead, From 00543fe640b211c73ca1d3b02cc5e84d735f3ace Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Tue, 10 Dec 2024 10:40:54 -0300 Subject: [PATCH 57/89] Remove game from list after deletion (#1730) --- src/qt_gui/gui_context_menus.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index 7da7341da..6eef1230c 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -360,6 +360,7 @@ public: QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { dir.removeRecursively(); + widget->removeRow(itemID); } } } From ea8ad359477e73e14c1f4d89c2e96d0064b28512 Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Tue, 10 Dec 2024 13:57:30 -0300 Subject: [PATCH 58/89] Fix delete cheats button (#1731) --- src/qt_gui/cheats_patches.cpp | 26 +++++++++++++++++++++++--- src/qt_gui/cheats_patches.h | 1 + 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/qt_gui/cheats_patches.cpp b/src/qt_gui/cheats_patches.cpp index 446b5a0ea..2fea0b6ea 100644 --- a/src/qt_gui/cheats_patches.cpp +++ b/src/qt_gui/cheats_patches.cpp @@ -955,15 +955,33 @@ void CheatsPatches::createFilesJson(const QString& repository) { jsonFile.close(); } -void CheatsPatches::addCheatsToLayout(const QJsonArray& modsArray, const QJsonArray& creditsArray) { +void CheatsPatches::clearListCheats() { QLayoutItem* item; while ((item = rightLayout->takeAt(0)) != nullptr) { - delete item->widget(); - delete item; + QWidget* widget = item->widget(); + if (widget) { + delete widget; + } else { + QLayout* layout = item->layout(); + if (layout) { + QLayoutItem* innerItem; + while ((innerItem = layout->takeAt(0)) != nullptr) { + QWidget* innerWidget = innerItem->widget(); + if (innerWidget) { + delete innerWidget; + } + delete innerItem; + } + delete layout; + } + } } m_cheats.clear(); m_cheatCheckBoxes.clear(); +} +void CheatsPatches::addCheatsToLayout(const QJsonArray& modsArray, const QJsonArray& creditsArray) { + clearListCheats(); int maxWidthButton = 0; for (const QJsonValue& modValue : modsArray) { @@ -1056,6 +1074,8 @@ void CheatsPatches::addCheatsToLayout(const QJsonArray& modsArray, const QJsonAr } void CheatsPatches::populateFileListCheats() { + clearListCheats(); + QString cheatsDir; Common::FS::PathToQString(cheatsDir, Common::FS::GetUserPath(Common::FS::PathType::CheatsDir)); diff --git a/src/qt_gui/cheats_patches.h b/src/qt_gui/cheats_patches.h index b07e828c2..4217436f6 100644 --- a/src/qt_gui/cheats_patches.h +++ b/src/qt_gui/cheats_patches.h @@ -36,6 +36,7 @@ public: const QString& m_gameVersion, bool showMessageBox); void downloadPatches(const QString repository, const bool showMessageBox); void createFilesJson(const QString& repository); + void clearListCheats(); void compatibleVersionNotice(const QString repository); signals: From b8a443c728c50c778ea29eca94747f9321e8949b Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 10 Dec 2024 21:15:43 +0100 Subject: [PATCH 59/89] Fix compiling due to typedefs varying across platforms (#1729) * Fix compiling on modern C++ compilers https://github.com/shadps4-emu/shadPS4/commit/cd9fc5d0e935ac12f469d609b1a643964fb33129 broke it * Fix order * Test * Test putting flags in old order * Remove designated initializer --- src/core/thread.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/thread.cpp b/src/core/thread.cpp index 0d0804cea..07681e6b9 100644 --- a/src/core/thread.cpp +++ b/src/core/thread.cpp @@ -140,13 +140,12 @@ void NativeThread::Initialize() { ASSERT_MSG(posix_memalign(&sig_stack_ptr, page_size, sig_stack_size) == 0, "Failed to allocate signal stack: {}", errno); - const stack_t sig_stack = { - .ss_sp = sig_stack_ptr, - .ss_size = sig_stack_size, - .ss_flags = 0, - }; + stack_t sig_stack; + sig_stack.ss_sp = sig_stack_ptr; + sig_stack.ss_size = sig_stack_size; + sig_stack.ss_flags = 0; ASSERT_MSG(sigaltstack(&sig_stack, nullptr) == 0, "Failed to set signal stack: {}", errno); #endif } -} // namespace Core \ No newline at end of file +} // namespace Core From e36c4d5f75cf1457a23c8fea7d45cd22cd61fc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:16:16 +0100 Subject: [PATCH 60/89] Displays "Never Played" if the game has never been played (#1697) * Displays "Never Played" if the game has never been played * Update nb.ts + pt_BR.ts --- src/qt_gui/game_list_frame.cpp | 4 ++-- src/qt_gui/translations/ar.ts | 5 +++++ src/qt_gui/translations/da_DK.ts | 5 +++++ src/qt_gui/translations/de.ts | 5 +++++ src/qt_gui/translations/el.ts | 5 +++++ src/qt_gui/translations/en.ts | 5 +++++ src/qt_gui/translations/es_ES.ts | 5 +++++ src/qt_gui/translations/fa_IR.ts | 5 +++++ src/qt_gui/translations/fi.ts | 5 +++++ src/qt_gui/translations/fr.ts | 5 +++++ src/qt_gui/translations/hu_HU.ts | 5 +++++ src/qt_gui/translations/id.ts | 5 +++++ src/qt_gui/translations/it.ts | 5 +++++ src/qt_gui/translations/ja_JP.ts | 5 +++++ src/qt_gui/translations/ko_KR.ts | 5 +++++ src/qt_gui/translations/lt_LT.ts | 5 +++++ src/qt_gui/translations/nb.ts | 5 +++++ src/qt_gui/translations/nl.ts | 5 +++++ src/qt_gui/translations/pl_PL.ts | 5 +++++ src/qt_gui/translations/pt_BR.ts | 5 +++++ src/qt_gui/translations/ro_RO.ts | 5 +++++ src/qt_gui/translations/ru_RU.ts | 5 +++++ src/qt_gui/translations/sq.ts | 5 +++++ src/qt_gui/translations/tr_TR.ts | 5 +++++ src/qt_gui/translations/uk_UA.ts | 5 +++++ src/qt_gui/translations/vi_VN.ts | 5 +++++ src/qt_gui/translations/zh_CN.ts | 5 +++++ src/qt_gui/translations/zh_TW.ts | 5 +++++ 28 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp index 99628b083..47bfbfef9 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -31,7 +31,7 @@ GameListFrame::GameListFrame(std::shared_ptr game_info_get, QWidg this->setColumnWidth(4, 90); // Firmware this->setColumnWidth(5, 90); // Size this->setColumnWidth(6, 90); // Version - this->setColumnWidth(7, 100); // Play Time + this->setColumnWidth(7, 120); // Play Time QStringList headers; headers << tr("Icon") << tr("Name") << tr("Serial") << tr("Region") << tr("Firmware") << tr("Size") << tr("Version") << tr("Play Time") << tr("Path"); @@ -105,7 +105,7 @@ void GameListFrame::PopulateGameList() { QString playTime = GetPlayTime(m_game_info->m_games[i].serial); if (playTime.isEmpty()) { m_game_info->m_games[i].play_time = "0:00:00"; - SetTableItem(i, 7, "0"); + SetTableItem(i, 7, tr("Never Played")); } else { QStringList timeParts = playTime.split(':'); int hours = timeParts[0].toInt(); diff --git a/src/qt_gui/translations/ar.ts b/src/qt_gui/translations/ar.ts index 45a030062..3f861187e 100644 --- a/src/qt_gui/translations/ar.ts +++ b/src/qt_gui/translations/ar.ts @@ -1359,6 +1359,11 @@ Play Time وقت اللعب + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/da_DK.ts b/src/qt_gui/translations/da_DK.ts index fa7e9c50b..3539159e2 100644 --- a/src/qt_gui/translations/da_DK.ts +++ b/src/qt_gui/translations/da_DK.ts @@ -1359,6 +1359,11 @@ Play Time Spilletid + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/de.ts b/src/qt_gui/translations/de.ts index 0fa534bb9..f34402ac9 100644 --- a/src/qt_gui/translations/de.ts +++ b/src/qt_gui/translations/de.ts @@ -1359,6 +1359,11 @@ Play Time Spielzeit + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/el.ts b/src/qt_gui/translations/el.ts index 5233454b9..65cee641a 100644 --- a/src/qt_gui/translations/el.ts +++ b/src/qt_gui/translations/el.ts @@ -1359,6 +1359,11 @@ Play Time Χρόνος παιχνιδιού + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/en.ts b/src/qt_gui/translations/en.ts index cd5a5fe8a..7ae583040 100644 --- a/src/qt_gui/translations/en.ts +++ b/src/qt_gui/translations/en.ts @@ -1359,6 +1359,11 @@ Play Time Play Time + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/es_ES.ts b/src/qt_gui/translations/es_ES.ts index 0598e04f3..3d1f291a6 100644 --- a/src/qt_gui/translations/es_ES.ts +++ b/src/qt_gui/translations/es_ES.ts @@ -1359,6 +1359,11 @@ Play Time Tiempo de Juego + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/fa_IR.ts b/src/qt_gui/translations/fa_IR.ts index 3cd72cac3..58de03346 100644 --- a/src/qt_gui/translations/fa_IR.ts +++ b/src/qt_gui/translations/fa_IR.ts @@ -1359,6 +1359,11 @@ Play Time زمان بازی + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/fi.ts b/src/qt_gui/translations/fi.ts index 8c9518d0f..0a7f2b250 100644 --- a/src/qt_gui/translations/fi.ts +++ b/src/qt_gui/translations/fi.ts @@ -1359,6 +1359,11 @@ Play Time Peliaika + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/fr.ts b/src/qt_gui/translations/fr.ts index a9903d43c..fad90622a 100644 --- a/src/qt_gui/translations/fr.ts +++ b/src/qt_gui/translations/fr.ts @@ -1359,6 +1359,11 @@ Play Time Temps de jeu + + + Never Played + Jamais joué + CheckUpdate diff --git a/src/qt_gui/translations/hu_HU.ts b/src/qt_gui/translations/hu_HU.ts index 135fc4231..937e3f188 100644 --- a/src/qt_gui/translations/hu_HU.ts +++ b/src/qt_gui/translations/hu_HU.ts @@ -1359,6 +1359,11 @@ Play Time Játékidő + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/id.ts b/src/qt_gui/translations/id.ts index 5c6148b86..80873daa9 100644 --- a/src/qt_gui/translations/id.ts +++ b/src/qt_gui/translations/id.ts @@ -1359,6 +1359,11 @@ Play Time Waktu Bermain + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/it.ts b/src/qt_gui/translations/it.ts index b496cc330..9094a7ed5 100644 --- a/src/qt_gui/translations/it.ts +++ b/src/qt_gui/translations/it.ts @@ -1359,6 +1359,11 @@ Play Time Tempo di Gioco + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/ja_JP.ts b/src/qt_gui/translations/ja_JP.ts index 3cee79951..ad1f383fe 100644 --- a/src/qt_gui/translations/ja_JP.ts +++ b/src/qt_gui/translations/ja_JP.ts @@ -1359,6 +1359,11 @@ Play Time プレイ時間 + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/ko_KR.ts b/src/qt_gui/translations/ko_KR.ts index ea449ebc1..a528db295 100644 --- a/src/qt_gui/translations/ko_KR.ts +++ b/src/qt_gui/translations/ko_KR.ts @@ -1359,6 +1359,11 @@ Play Time Play Time + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/lt_LT.ts b/src/qt_gui/translations/lt_LT.ts index dd467283f..4a2820399 100644 --- a/src/qt_gui/translations/lt_LT.ts +++ b/src/qt_gui/translations/lt_LT.ts @@ -1359,6 +1359,11 @@ Play Time Žaidimo laikas + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/nb.ts b/src/qt_gui/translations/nb.ts index 303baadaf..028646740 100644 --- a/src/qt_gui/translations/nb.ts +++ b/src/qt_gui/translations/nb.ts @@ -1359,6 +1359,11 @@ Play Time Spilletid + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/nl.ts b/src/qt_gui/translations/nl.ts index 399aef8be..b66cb94e4 100644 --- a/src/qt_gui/translations/nl.ts +++ b/src/qt_gui/translations/nl.ts @@ -1359,6 +1359,11 @@ Play Time Speeltijd + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/pl_PL.ts b/src/qt_gui/translations/pl_PL.ts index 730b37124..8236cf720 100644 --- a/src/qt_gui/translations/pl_PL.ts +++ b/src/qt_gui/translations/pl_PL.ts @@ -1359,6 +1359,11 @@ Play Time Czas gry + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/pt_BR.ts b/src/qt_gui/translations/pt_BR.ts index dfe785673..5faccf6c5 100644 --- a/src/qt_gui/translations/pt_BR.ts +++ b/src/qt_gui/translations/pt_BR.ts @@ -1359,6 +1359,11 @@ Play Time Tempo Jogado + + + Never Played + Nunca jogado + CheckUpdate diff --git a/src/qt_gui/translations/ro_RO.ts b/src/qt_gui/translations/ro_RO.ts index d73b1be6c..2439e69e2 100644 --- a/src/qt_gui/translations/ro_RO.ts +++ b/src/qt_gui/translations/ro_RO.ts @@ -1359,6 +1359,11 @@ Play Time Timp de Joacă + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/ru_RU.ts b/src/qt_gui/translations/ru_RU.ts index ab7e2c40e..ccee34517 100644 --- a/src/qt_gui/translations/ru_RU.ts +++ b/src/qt_gui/translations/ru_RU.ts @@ -1359,6 +1359,11 @@ Play Time Времени в игре + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/sq.ts b/src/qt_gui/translations/sq.ts index bc8f24162..4a02298e8 100644 --- a/src/qt_gui/translations/sq.ts +++ b/src/qt_gui/translations/sq.ts @@ -1359,6 +1359,11 @@ Play Time Koha e luajtjes + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index 5944f980f..83a45e529 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -1359,6 +1359,11 @@ Play Time Oynama Süresi + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/uk_UA.ts b/src/qt_gui/translations/uk_UA.ts index b28035335..805fff151 100644 --- a/src/qt_gui/translations/uk_UA.ts +++ b/src/qt_gui/translations/uk_UA.ts @@ -1359,6 +1359,11 @@ Play Time Час у грі + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/vi_VN.ts b/src/qt_gui/translations/vi_VN.ts index b5ae8bd39..1ac3d042d 100644 --- a/src/qt_gui/translations/vi_VN.ts +++ b/src/qt_gui/translations/vi_VN.ts @@ -1359,6 +1359,11 @@ Play Time Thời gian chơi + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/zh_CN.ts b/src/qt_gui/translations/zh_CN.ts index 989e71071..19fb8edff 100644 --- a/src/qt_gui/translations/zh_CN.ts +++ b/src/qt_gui/translations/zh_CN.ts @@ -1359,6 +1359,11 @@ Play Time 游戏时间 + + + Never Played + Never Played + CheckUpdate diff --git a/src/qt_gui/translations/zh_TW.ts b/src/qt_gui/translations/zh_TW.ts index b650a74ea..fbd6d624d 100644 --- a/src/qt_gui/translations/zh_TW.ts +++ b/src/qt_gui/translations/zh_TW.ts @@ -1359,6 +1359,11 @@ Play Time 遊玩時間 + + + Never Played + Never Played + CheckUpdate From 14c2be8c670286080e6277a413d03d5019f1ed21 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:57:11 +0800 Subject: [PATCH 61/89] Add default value for Separate Update Folder (#1735) Co-authored-by: rainmakerv2 <30595646+jpau02@users.noreply.github.com> --- src/common/config.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/config.cpp b/src/common/config.cpp index eae8897c8..3db98a438 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -743,6 +743,7 @@ void setDefaultValues() { emulator_language = "en"; m_language = 1; gpuId = -1; + separateupdatefolder = false; } } // namespace Config From 51bf98a7b5178f9578c395bb92ad8e965833dc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:07:33 +0100 Subject: [PATCH 62/89] Fix for R4G4B4A4UnormPack16 Tiled image (#1738) --- src/video_core/texture_cache/tile_manager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index 2bc3bf282..fc3d35e3e 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -174,6 +174,7 @@ vk::Format DemoteImageFormatForDetiling(vk::Format format) { switch (format) { case vk::Format::eR8Unorm: return vk::Format::eR8Uint; + case vk::Format::eR4G4B4A4UnormPack16: case vk::Format::eR8G8Unorm: case vk::Format::eR16Sfloat: case vk::Format::eR16Unorm: From 2a953391ef9a1ccd0df8b84a9ab365b0c5d2a701 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:40:45 +0100 Subject: [PATCH 63/89] liverpool: implement Rewind and IndirectBuffer packets --- src/video_core/amdgpu/liverpool.cpp | 35 +++++++++++++++++++++++++++++ src/video_core/amdgpu/pm4_cmds.h | 13 +++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index a4eae8e7a..8db2d63c4 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -610,6 +610,17 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); break; } + case PM4ItOpcode::Rewind: { + const PM4CmdRewind* rewind = reinterpret_cast(header); + while (!rewind->Valid()) { + mapped_queues[GfxQueueId].cs_state = regs.cs_program; + TracyFiberLeave; + co_yield {}; + TracyFiberEnter(dcb_task_name); + regs.cs_program = mapped_queues[GfxQueueId].cs_state; + } + break; + } case PM4ItOpcode::WaitRegMem: { const auto* wait_reg_mem = reinterpret_cast(header); // ASSERT(wait_reg_mem->engine.Value() == PM4CmdWaitRegMem::Engine::Me); @@ -630,6 +641,19 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); + auto task = ProcessGraphics( + {indirect_buffer->Address(), indirect_buffer->ib_size}, {}); + while (!task.handle.done()) { + task.handle.resume(); + + TracyFiberLeave; + co_yield {}; + TracyFiberEnter(dcb_task_name); + }; + break; + } case PM4ItOpcode::IncrementDeCounter: { ++cblock.de_count; break; @@ -730,6 +754,17 @@ Liverpool::Task Liverpool::ProcessCompute(std::span acb, int vqid) { case PM4ItOpcode::AcquireMem: { break; } + case PM4ItOpcode::Rewind: { + const PM4CmdRewind* rewind = reinterpret_cast(header); + while (!rewind->Valid()) { + mapped_queues[vqid].cs_state = regs.cs_program; + TracyFiberLeave; + co_yield {}; + TracyFiberEnter(acb_task_name); + regs.cs_program = mapped_queues[vqid].cs_state; + } + break; + } case PM4ItOpcode::SetShReg: { const auto* set_data = reinterpret_cast(header); std::memcpy(®s.reg_array[ShRegWordOffset + set_data->reg_offset], header + 2, diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index be6751285..238e09fad 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -418,6 +418,19 @@ struct PM4DmaData { } }; +struct PM4CmdRewind { + PM4Type3Header header; + union { + u32 raw; + BitField<24, 1, u32> offload_enable; ///< Enable offload polling valid bit to IQ + BitField<31, 1, u32> valid; ///< Set when subsequent packets are valid + }; + + bool Valid() const { + return valid; + } +}; + struct PM4CmdWaitRegMem { enum class Engine : u32 { Me = 0u, Pfp = 1u }; enum class MemSpace : u32 { Register = 0u, Memory = 1u }; From 8db1beaec6dc964d3d743af4807a104321963d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:44:18 +0100 Subject: [PATCH 64/89] Displays FPS before frame latency (#1736) --- src/core/devtools/layer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index 2c2099f4d..776f3377d 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -254,7 +254,7 @@ void L::DrawAdvanced() { void L::DrawSimple() { const auto io = GetIO(); - Text("Frame time: %.3f ms (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + Text("%.1f FPS (%.2f ms)", io.Framerate, 1000.0f / io.Framerate); } static void LoadSettings(const char* line) { @@ -338,6 +338,7 @@ void L::Draw() { const auto fn = DebugState.flip_frame_count.load(); frame_graph.AddFrame(fn, io.DeltaTime); } + if (IsKeyPressed(ImGuiKey_F10, false)) { if (io.KeyCtrl) { show_advanced_debug = !show_advanced_debug; From e612e881ac2bd3112c38dc0660771447e5e62218 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:10:40 -0800 Subject: [PATCH 65/89] renderer_vulkan: Bind null color attachments when target is masked out. (#1740) * renderer_vulkan: Bind null color attachments when target is masked out. * Simplify setting null color attachment --- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 5 +++-- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 11 ++++++++--- src/video_core/renderer_vulkan/vk_scheduler.h | 2 -- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 276e4ef29..b9f318f7a 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -279,7 +279,7 @@ bool PipelineCache::RefreshGraphicsKey() { // recompiler. for (auto cb = 0u, remapped_cb = 0u; cb < Liverpool::NumColorBuffers; ++cb) { auto const& col_buf = regs.color_buffers[cb]; - if (skip_cb_binding || !col_buf) { + if (skip_cb_binding || !col_buf || !regs.color_target_mask.GetMask(cb)) { continue; } const auto base_format = @@ -385,7 +385,8 @@ bool PipelineCache::RefreshGraphicsKey() { // Second pass to fill remain CB pipeline key data for (auto cb = 0u, remapped_cb = 0u; cb < Liverpool::NumColorBuffers; ++cb) { auto const& col_buf = regs.color_buffers[cb]; - if (skip_cb_binding || !col_buf || (key.mrt_mask & (1u << cb)) == 0) { + if (skip_cb_binding || !col_buf || !regs.color_target_mask.GetMask(cb) || + (key.mrt_mask & (1u << cb)) == 0) { key.color_formats[cb] = vk::Format::eUndefined; key.mrt_swizzles[cb] = Liverpool::ColorBuffer::SwapMode::Standard; continue; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 33358b850..496ea5163 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -103,6 +103,14 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { continue; } + // If the color buffer is still bound but rendering to it is disabled by the target + // mask, we need to prevent the render area from being affected by unbound render target + // extents. + if (!regs.color_target_mask.GetMask(col_buf_id)) { + state.color_attachments[state.num_color_attachments++].imageView = VK_NULL_HANDLE; + continue; + } + const auto& hint = liverpool->last_cb_extent[col_buf_id]; auto& [image_id, desc] = cb_descs.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{col_buf, hint}); @@ -118,7 +126,6 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { const auto mip = image_view.info.range.base.level; state.width = std::min(state.width, std::max(image.info.size.width >> mip, 1u)); state.height = std::min(state.height, std::max(image.info.size.height >> mip, 1u)); - state.color_images[state.num_color_attachments] = image.image; state.color_attachments[state.num_color_attachments++] = { .imageView = *image_view.image_view, .imageLayout = vk::ImageLayout::eUndefined, @@ -153,7 +160,6 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { state.width = std::min(state.width, image.info.size.width); state.height = std::min(state.height, image.info.size.height); - state.depth_image = image.image; state.depth_attachment = { .imageView = *image_view.image_view, .imageLayout = vk::ImageLayout::eUndefined, @@ -716,7 +722,6 @@ void Rasterizer::BeginRendering(const GraphicsPipeline& pipeline, RenderState& s auto& image = texture_cache.GetImage(view.image_id); state.color_attachments[cb_index].imageView = *view.image_view; state.color_attachments[cb_index].imageLayout = image.last_state.layout; - state.color_images[cb_index] = image.image; const auto mip = view.info.range.base.level; state.width = std::min(state.width, std::max(image.info.size.width >> mip, 1u)); diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 45a9228c9..cdd33745a 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -20,9 +20,7 @@ class Instance; struct RenderState { std::array color_attachments{}; - std::array color_images{}; vk::RenderingAttachmentInfo depth_attachment{}; - vk::Image depth_image{}; u32 num_color_attachments{}; bool has_depth{}; bool has_stencil{}; From 14f7dc3527de3249b2356e48dba4c54f58a2a114 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:11:24 -0800 Subject: [PATCH 66/89] cache: Invalidate pages for file reads. (#1726) * cache: Invalidate pages for file reads. * texture_cache: Simplify invalidate intersection check. * vk_rasterizer: Make aware of mapped memory ranges. * buffer_cache: Remove redundant page calculations. Called functions will convert to page numbers/addresses themselves. * file_system: Simplify memory invalidation and add a few missed cases. --- src/core/libraries/kernel/file_system.cpp | 29 ++++++++++++------- src/core/libraries/kernel/file_system.h | 3 ++ src/core/libraries/kernel/kernel.cpp | 26 ++--------------- src/core/memory.cpp | 6 ++++ src/core/memory.h | 2 ++ src/video_core/page_manager.cpp | 21 +++++--------- .../renderer_vulkan/vk_rasterizer.cpp | 22 ++++++++++++-- .../renderer_vulkan/vk_rasterizer.h | 4 ++- .../texture_cache/texture_cache.cpp | 19 +++++++----- src/video_core/texture_cache/texture_cache.h | 2 +- 10 files changed, 74 insertions(+), 60 deletions(-) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 2a65255fb..5ba9976c6 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -1,22 +1,22 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + #include "common/assert.h" #include "common/logging/log.h" #include "common/scope_exit.h" #include "common/singleton.h" +#include "core/devices/logger.h" +#include "core/devices/nop_device.h" #include "core/file_sys/fs.h" #include "core/libraries/kernel/file_system.h" #include "core/libraries/kernel/orbis_error.h" #include "core/libraries/libs.h" +#include "core/memory.h" #include "kernel.h" -#include -#include - -#include "core/devices/logger.h" -#include "core/devices/nop_device.h" - namespace D = Core::Devices; using FactoryDevice = std::function(u32, const char*, int, u16)>; @@ -201,7 +201,7 @@ int PS4_SYSV_ABI posix_close(int d) { return result; } -size_t PS4_SYSV_ABI sceKernelWrite(int d, const void* buf, size_t nbytes) { +s64 PS4_SYSV_ABI sceKernelWrite(int d, const void* buf, size_t nbytes) { auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(d); if (file == nullptr) { @@ -246,6 +246,15 @@ int PS4_SYSV_ABI sceKernelUnlink(const char* path) { return ORBIS_OK; } +size_t ReadFile(Common::FS::IOFile& file, void* buf, size_t nbytes) { + const auto* memory = Core::Memory::Instance(); + // Invalidate up to the actual number of bytes that could be read. + const auto remaining = file.GetSize() - file.Tell(); + memory->InvalidateMemory(reinterpret_cast(buf), std::min(nbytes, remaining)); + + return file.ReadRaw(buf, nbytes); +} + size_t PS4_SYSV_ABI _readv(int d, const SceKernelIovec* iov, int iovcnt) { auto* h = Common::Singleton::Instance(); auto* file = h->GetFile(d); @@ -264,7 +273,7 @@ size_t PS4_SYSV_ABI _readv(int d, const SceKernelIovec* iov, int iovcnt) { } size_t total_read = 0; for (int i = 0; i < iovcnt; i++) { - total_read += file->f.ReadRaw(iov[i].iov_base, iov[i].iov_len); + total_read += ReadFile(file->f, iov[i].iov_base, iov[i].iov_len); } return total_read; } @@ -351,7 +360,7 @@ s64 PS4_SYSV_ABI sceKernelRead(int d, void* buf, size_t nbytes) { if (file->type == Core::FileSys::FileType::Device) { return file->device->read(buf, nbytes); } - return file->f.ReadRaw(buf, nbytes); + return ReadFile(file->f, buf, nbytes); } int PS4_SYSV_ABI posix_read(int d, void* buf, size_t nbytes) { @@ -541,7 +550,7 @@ s64 PS4_SYSV_ABI sceKernelPreadv(int d, SceKernelIovec* iov, int iovcnt, s64 off } size_t total_read = 0; for (int i = 0; i < iovcnt; i++) { - total_read += file->f.ReadRaw(iov[i].iov_base, iov[i].iov_len); + total_read += ReadFile(file->f, iov[i].iov_base, iov[i].iov_len); } return total_read; } diff --git a/src/core/libraries/kernel/file_system.h b/src/core/libraries/kernel/file_system.h index dcbb3957d..6443962ff 100644 --- a/src/core/libraries/kernel/file_system.h +++ b/src/core/libraries/kernel/file_system.h @@ -65,6 +65,9 @@ constexpr int ORBIS_KERNEL_O_DSYNC = 0x1000; constexpr int ORBIS_KERNEL_O_DIRECT = 0x00010000; constexpr int ORBIS_KERNEL_O_DIRECTORY = 0x00020000; +s64 PS4_SYSV_ABI sceKernelWrite(int d, const void* buf, size_t nbytes); +s64 PS4_SYSV_ABI sceKernelRead(int d, void* buf, size_t nbytes); + void RegisterFileSystem(Core::Loader::SymbolsResolver* sym); } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index bda446257..b05c96fad 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -133,33 +133,11 @@ void PS4_SYSV_ABI sceLibcHeapGetTraceInfo(HeapInfoInfo* info) { } s64 PS4_SYSV_ABI ps4__write(int d, const char* buf, std::size_t nbytes) { - auto* h = Common::Singleton::Instance(); - auto* file = h->GetFile(d); - if (file == nullptr) { - return ORBIS_KERNEL_ERROR_EBADF; - } - std::scoped_lock lk{file->m_mutex}; - if (file->type == Core::FileSys::FileType::Device) { - return file->device->write(buf, nbytes); - } - return file->f.WriteRaw(buf, nbytes); + return sceKernelWrite(d, buf, nbytes); } s64 PS4_SYSV_ABI ps4__read(int d, void* buf, u64 nbytes) { - if (d == 0) { - return static_cast( - strlen(std::fgets(static_cast(buf), static_cast(nbytes), stdin))); - } - auto* h = Common::Singleton::Instance(); - auto* file = h->GetFile(d); - if (file == nullptr) { - return ORBIS_KERNEL_ERROR_EBADF; - } - std::scoped_lock lk{file->m_mutex}; - if (file->type == Core::FileSys::FileType::Device) { - return file->device->read(buf, nbytes); - } - return file->f.ReadRaw(buf, nbytes); + return sceKernelRead(d, buf, nbytes); } struct OrbisKernelUuid { diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 980beee79..41db7df4b 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -587,6 +587,12 @@ void MemoryManager::NameVirtualRange(VAddr virtual_addr, size_t size, std::strin it->second.name = name; } +void MemoryManager::InvalidateMemory(const VAddr addr, const u64 size) const { + if (rasterizer) { + rasterizer->InvalidateMemory(addr, size); + } +} + VAddr MemoryManager::SearchFree(VAddr virtual_addr, size_t size, u32 alignment) { // If the requested address is below the mapped range, start search from the lowest address auto min_search_address = impl.SystemManagedVirtualBase(); diff --git a/src/core/memory.h b/src/core/memory.h index 364609451..a9f2df322 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -211,6 +211,8 @@ public: void NameVirtualRange(VAddr virtual_addr, size_t size, std::string_view name); + void InvalidateMemory(VAddr addr, u64 size) const; + private: VMAHandle FindVMA(VAddr target) { return std::prev(vma_map.upper_bound(target)); diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index fefae81f4..556555c25 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -114,8 +114,7 @@ struct PageManager::Impl { // Notify rasterizer about the fault. const VAddr addr = msg.arg.pagefault.address; - const VAddr addr_page = GetPageAddr(addr); - rasterizer->InvalidateMemory(addr, addr_page, PAGESIZE); + rasterizer->InvalidateMemory(addr, 1); } } @@ -135,17 +134,14 @@ struct PageManager::Impl { } void OnMap(VAddr address, size_t size) { - owned_ranges += boost::icl::interval::right_open(address, address + size); + // No-op } void OnUnmap(VAddr address, size_t size) { - owned_ranges -= boost::icl::interval::right_open(address, address + size); + // No-op } void Protect(VAddr address, size_t size, bool allow_write) { - ASSERT_MSG(owned_ranges.find(address) != owned_ranges.end(), - "Attempted to track non-GPU memory at address {:#x}, size {:#x}.", address, - size); auto* memory = Core::Memory::Instance(); auto& impl = memory->GetAddressSpace(); impl.Protect(address, size, @@ -155,17 +151,13 @@ struct PageManager::Impl { static bool GuestFaultSignalHandler(void* context, void* fault_address) { const auto addr = reinterpret_cast(fault_address); - const bool is_write = Common::IsWriteError(context); - if (is_write && owned_ranges.find(addr) != owned_ranges.end()) { - const VAddr addr_aligned = GetPageAddr(addr); - rasterizer->InvalidateMemory(addr, addr_aligned, PAGESIZE); - return true; + if (Common::IsWriteError(context)) { + return rasterizer->InvalidateMemory(addr, 1); } return false; } inline static Vulkan::Rasterizer* rasterizer; - inline static boost::icl::interval_set owned_ranges; }; #endif @@ -210,6 +202,9 @@ void PageManager::UpdatePagesCachedCount(VAddr addr, u64 size, s32 delta) { const VAddr interval_start_addr = boost::icl::first(interval) << PageShift; const VAddr interval_end_addr = boost::icl::last_next(interval) << PageShift; const u32 interval_size = interval_end_addr - interval_start_addr; + ASSERT_MSG(rasterizer->IsMapped(interval_start_addr, interval_size), + "Attempted to track non-GPU memory at address {:#x}, size {:#x}.", + interval_start_addr, interval_size); if (delta > 0 && count == delta) { impl->Protect(interval_start_addr, interval_size, false); } else if (delta < 0 && count == -delta) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 496ea5163..5fd0d99a4 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -841,12 +841,27 @@ u32 Rasterizer::ReadDataFromGds(u32 gds_offset) { return value; } -void Rasterizer::InvalidateMemory(VAddr addr, VAddr addr_aligned, u64 size) { - buffer_cache.InvalidateMemory(addr_aligned, size); - texture_cache.InvalidateMemory(addr, addr_aligned, size); +bool Rasterizer::InvalidateMemory(VAddr addr, u64 size) { + if (!IsMapped(addr, size)) { + // Not GPU mapped memory, can skip invalidation logic entirely. + return false; + } + buffer_cache.InvalidateMemory(addr, size); + texture_cache.InvalidateMemory(addr, size); + return true; +} + +bool Rasterizer::IsMapped(VAddr addr, u64 size) { + if (size == 0) { + // There is no memory, so not mapped. + return false; + } + return mapped_ranges.find(boost::icl::interval::right_open(addr, addr + size)) != + mapped_ranges.end(); } void Rasterizer::MapMemory(VAddr addr, u64 size) { + mapped_ranges += boost::icl::interval::right_open(addr, addr + size); page_manager.OnGpuMap(addr, size); } @@ -854,6 +869,7 @@ void Rasterizer::UnmapMemory(VAddr addr, u64 size) { buffer_cache.InvalidateMemory(addr, size); texture_cache.UnmapMemory(addr, size); page_manager.OnGpuUnmap(addr, size); + mapped_ranges -= boost::icl::interval::right_open(addr, addr + size); } void Rasterizer::UpdateDynamicState(const GraphicsPipeline& pipeline) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 9214372ee..ec1b5e134 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -54,7 +54,8 @@ public: void InlineData(VAddr address, const void* value, u32 num_bytes, bool is_gds); u32 ReadDataFromGds(u32 gsd_offset); - void InvalidateMemory(VAddr addr, VAddr addr_aligned, u64 size); + bool InvalidateMemory(VAddr addr, u64 size); + bool IsMapped(VAddr addr, u64 size); void MapMemory(VAddr addr, u64 size); void UnmapMemory(VAddr addr, u64 size); @@ -100,6 +101,7 @@ private: VideoCore::TextureCache texture_cache; AmdGpu::Liverpool* liverpool; Core::MemoryManager* memory; + boost::icl::interval_set mapped_ranges; PipelineCache pipeline_cache; boost::container::static_vector< diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 0e5bbc1f3..66132753d 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -56,24 +56,27 @@ void TextureCache::MarkAsMaybeDirty(ImageId image_id, Image& image) { UntrackImage(image_id); } -void TextureCache::InvalidateMemory(VAddr addr, VAddr page_addr, size_t size) { +void TextureCache::InvalidateMemory(VAddr addr, size_t size) { std::scoped_lock lock{mutex}; - ForEachImageInRegion(page_addr, size, [&](ImageId image_id, Image& image) { + const auto end = addr + size; + const auto pages_start = PageManager::GetPageAddr(addr); + const auto pages_end = PageManager::GetNextPageAddr(addr + size - 1); + ForEachImageInRegion(pages_start, pages_end - pages_start, [&](ImageId image_id, Image& image) { const auto image_begin = image.info.guest_address; const auto image_end = image.info.guest_address + image.info.guest_size_bytes; - const auto page_end = page_addr + size; - if (image_begin <= addr && addr < image_end) { - // This image was definitely accessed by this page fault. - // Untrack image, so the range is unprotected and the guest can write freely + if (image_begin < end && addr < image_end) { + // Start or end of the modified region is in the image, or the image is entirely within + // the modified region, so the image was definitely accessed by this page fault. + // Untrack the image, so that the range is unprotected and the guest can write freely. image.flags |= ImageFlagBits::CpuDirty; UntrackImage(image_id); - } else if (page_end < image_end) { + } else if (pages_end < image_end) { // This page access may or may not modify the image. // We should not mark it as dirty now. If it really was modified // it will receive more invalidations on its other pages. // Remove tracking from this page only. UntrackImageHead(image_id); - } else if (image_begin < page_addr) { + } else if (image_begin < pages_start) { // This page access does not modify the image but the page should be untracked. // We should not mark this image as dirty now. If it really was modified // it will receive more invalidations on its other pages. diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 676ede777..3ef81a699 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -95,7 +95,7 @@ public: ~TextureCache(); /// Invalidates any image in the logical page range. - void InvalidateMemory(VAddr addr, VAddr page_addr, size_t size); + void InvalidateMemory(VAddr addr, size_t size); /// Marks an image as dirty if it exists at the provided address. void InvalidateMemoryFromGPU(VAddr address, size_t max_size); From 0211b7e38e448801b99cd4855b188b3800a4a2e5 Mon Sep 17 00:00:00 2001 From: slick-daddy <129640104+slick-daddy@users.noreply.github.com> Date: Wed, 11 Dec 2024 22:11:34 +0300 Subject: [PATCH 67/89] Minor Translation Fixes (#1691) * Update tr_TR.ts Minor translation fixes. Mainly contains capitalization and small changes. * Update tr_TR.ts --- src/qt_gui/translations/tr_TR.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index 83a45e529..4c77bc16a 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -351,7 +351,7 @@ Download Cheats/Patches - Hileler / Yamanlar İndir + Hileleri/Yamaları İndir @@ -505,7 +505,7 @@ Is PS4 Pro - PS4 Pro mu + PS4 Pro @@ -545,12 +545,12 @@ Hide Cursor - İmleci gizle + İmleci Gizle Hide Cursor Idle Timeout - İmleç için hareketsizlik zaman aşımı + İmleç İçin Hareketsizlik Zaman Aşımı @@ -665,7 +665,7 @@ Check for Updates - Güncellemeleri kontrol et + Güncellemeleri Kontrol Et @@ -703,7 +703,7 @@ Download Patches For All Games - Tüm Oyunlar İçin Yamanları İndir + Tüm Oyunlar İçin Yamaları İndir @@ -758,7 +758,7 @@ Patch detected! - Yamanın tespit edildi! + Yama tespit edildi! @@ -941,7 +941,7 @@ Unable to open files.json for reading. - files.json dosyasını okumak için açılamadı. + files.json dosyası okumak için açılamadı. @@ -1169,7 +1169,7 @@ ps4proCheckBox - PS4 Pro Mu:\nEmülatörü bir PS4 PRO gibi çalıştırır; bu, bunu destekleyen oyunlarda özel özellikleri etkinleştirebilir. + PS4 Pro:\nEmülatörü bir PS4 PRO gibi çalıştırır; bu, bunu destekleyen oyunlarda özel özellikleri etkinleştirebilir. @@ -1493,4 +1493,4 @@ Güncelleme betiği dosyası oluşturulamadı - \ No newline at end of file + From b82993c56805245fc75742a3e0a72db770cdf054 Mon Sep 17 00:00:00 2001 From: "Daniel R." <47796739+polybiusproxy@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:12:35 +0100 Subject: [PATCH 68/89] core/kernel: implement condvar signalto --- src/core/libraries/kernel/threads/condvar.cpp | 15 ++++++++++++--- src/core/libraries/kernel/threads/pthread.h | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/kernel/threads/condvar.cpp b/src/core/libraries/kernel/threads/condvar.cpp index 2927899d9..853526559 100644 --- a/src/core/libraries/kernel/threads/condvar.cpp +++ b/src/core/libraries/kernel/threads/condvar.cpp @@ -177,7 +177,7 @@ int PS4_SYSV_ABI posix_pthread_cond_reltimedwait_np(PthreadCondT* cond, PthreadM return cvp->Wait(mutex, THR_RELTIME, usec); } -int PthreadCond::Signal() { +int PthreadCond::Signal(Pthread* thread) { Pthread* curthread = g_curthread; SleepqLock(this); @@ -187,7 +187,8 @@ int PthreadCond::Signal() { return 0; } - Pthread* td = sq->sq_blocked.front(); + Pthread* td = thread ? thread : sq->sq_blocked.front(); + PthreadMutex* mp = td->mutex_obj; has_user_waiters = SleepqRemove(sq, td); @@ -262,7 +263,13 @@ int PthreadCond::Broadcast() { int PS4_SYSV_ABI posix_pthread_cond_signal(PthreadCondT* cond) { PthreadCond* cvp{}; CHECK_AND_INIT_COND - return cvp->Signal(); + return cvp->Signal(nullptr); +} + +int PS4_SYSV_ABI posix_pthread_cond_signalto_np(PthreadCondT* cond, Pthread* thread) { + PthreadCond* cvp{}; + CHECK_AND_INIT_COND + return cvp->Signal(thread); } int PS4_SYSV_ABI posix_pthread_cond_broadcast(PthreadCondT* cond) { @@ -358,6 +365,8 @@ void RegisterCond(Core::Loader::SymbolsResolver* sym) { ORBIS(posix_pthread_cond_reltimedwait_np)); LIB_FUNCTION("g+PZd2hiacg", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_cond_destroy)); + LIB_FUNCTION("o69RpYO-Mu0", "libkernel", 1, "libkernel", 1, 1, + ORBIS(posix_pthread_cond_signalto_np)); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/pthread.h b/src/core/libraries/kernel/threads/pthread.h index 456c2ef37..089156776 100644 --- a/src/core/libraries/kernel/threads/pthread.h +++ b/src/core/libraries/kernel/threads/pthread.h @@ -123,7 +123,7 @@ struct PthreadCond { int Wait(PthreadMutexT* mutex, const OrbisKernelTimespec* abstime, u64 usec = 0); - int Signal(); + int Signal(Pthread* thread); int Broadcast(); }; using PthreadCondT = PthreadCond*; From 0a9c437ec80ece0f8e1ab222b85537c1bb49b46e Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Wed, 11 Dec 2024 21:17:55 +0200 Subject: [PATCH 69/89] hot-fix: Enforce minimum stack size of 64KB Fixes some crashes in BB from unity pt 1 --- src/core/libraries/kernel/threads/pthread.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index c83af86d0..08886c6eb 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -243,6 +243,13 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(PthreadT* thread, const PthreadAtt static int TidCounter = 1; new_thread->tid = ++TidCounter; + if (new_thread->attr.stackaddr_attr == 0) { + /* Enforce minimum stack size of 64 KB */ + static constexpr size_t MinimumStack = 64_KB; + auto& stacksize = new_thread->attr.stacksize_attr; + stacksize = std::max(stacksize, MinimumStack); + } + if (thread_state->CreateStack(&new_thread->attr) != 0) { /* Insufficient memory to create a stack: */ thread_state->Free(curthread, new_thread); From 714605c6a7d980868171c76121a23db328edd001 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:51:39 -0800 Subject: [PATCH 70/89] renderer_vulkan: Require exact image format for resolve pass. (#1742) --- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 6 ++++-- src/video_core/texture_cache/texture_cache.cpp | 13 ++++++++++--- src/video_core/texture_cache/texture_cache.h | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 5fd0d99a4..bfcdc9538 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -790,8 +790,10 @@ void Rasterizer::Resolve() { mrt0_hint}; VideoCore::TextureCache::RenderTargetDesc mrt1_desc{liverpool->regs.color_buffers[1], mrt1_hint}; - auto& mrt0_image = texture_cache.GetImage(texture_cache.FindImage(mrt0_desc)); - auto& mrt1_image = texture_cache.GetImage(texture_cache.FindImage(mrt1_desc)); + auto& mrt0_image = + texture_cache.GetImage(texture_cache.FindImage(mrt0_desc, VideoCore::FindFlags::ExactFmt)); + auto& mrt1_image = + texture_cache.GetImage(texture_cache.FindImage(mrt1_desc, VideoCore::FindFlags::ExactFmt)); VideoCore::SubresourceRange mrt0_range; mrt0_range.base.layer = liverpool->regs.color_buffers[0].view.slice_start; diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 66132753d..153314d2b 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -324,6 +324,10 @@ ImageId TextureCache::FindImage(BaseDesc& desc, FindFlags flags) { !IsVulkanFormatCompatible(info.pixel_format, cache_image.info.pixel_format)) { continue; } + if (True(flags & FindFlags::ExactFmt) && + info.pixel_format != cache_image.info.pixel_format) { + continue; + } ASSERT((cache_image.info.type == info.type || info.size == Extent3D{1, 1, 1} || True(flags & FindFlags::RelaxFmt))); image_id = cache_id; @@ -348,9 +352,12 @@ ImageId TextureCache::FindImage(BaseDesc& desc, FindFlags flags) { } if (image_id) { - Image& image_resoved = slot_images[image_id]; - - if (image_resoved.info.resources < info.resources) { + Image& image_resolved = slot_images[image_id]; + if (True(flags & FindFlags::ExactFmt) && + info.pixel_format != image_resolved.info.pixel_format) { + // Cannot reuse this image as we need the exact requested format. + image_id = {}; + } else if (image_resolved.info.resources < info.resources) { // The image was clearly picked up wrong. FreeImage(image_id); image_id = {}; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 3ef81a699..430415ed2 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -28,6 +28,7 @@ enum class FindFlags { RelaxDim = 1 << 1, ///< Do not check the dimentions of image, only address. RelaxSize = 1 << 2, ///< Do not check that the size matches exactly. RelaxFmt = 1 << 3, ///< Do not check that format is compatible. + ExactFmt = 1 << 4, ///< Require the format to be exactly the same. }; DECLARE_ENUM_FLAG_OPERATORS(FindFlags) From 7f4265834a3112e53133783d62627d683611ba71 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Thu, 12 Dec 2024 03:33:49 +0200 Subject: [PATCH 71/89] hot-fix: Fix race in rwlock Resetting the owner should be before the lock is unlocked, otherwise a waiter might lock and set a new owner before its reset. --- src/core/libraries/kernel/threads/rwlock.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/threads/rwlock.cpp b/src/core/libraries/kernel/threads/rwlock.cpp index affaaf994..ff211e48c 100644 --- a/src/core/libraries/kernel/threads/rwlock.cpp +++ b/src/core/libraries/kernel/threads/rwlock.cpp @@ -177,13 +177,13 @@ int PS4_SYSV_ABI posix_pthread_rwlock_unlock(PthreadRwlockT* rwlock) { } if (prwlock->owner == curthread) { - prwlock->lock.unlock(); prwlock->owner = nullptr; + prwlock->lock.unlock(); } else { - prwlock->lock.unlock_shared(); if (prwlock->owner == nullptr) { curthread->rdlock_count--; } + prwlock->lock.unlock_shared(); } return 0; From 5789d1a5fbf5033ff08c2ed356087e1869d08296 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:47:07 -0800 Subject: [PATCH 72/89] playgo: Lower scePlayGoGetLocus log to debug. (#1748) --- src/core/libraries/playgo/playgo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/playgo/playgo.cpp b/src/core/libraries/playgo/playgo.cpp index 13d6f991f..848533ff7 100644 --- a/src/core/libraries/playgo/playgo.cpp +++ b/src/core/libraries/playgo/playgo.cpp @@ -137,8 +137,8 @@ s32 PS4_SYSV_ABI scePlayGoGetLanguageMask(OrbisPlayGoHandle handle, s32 PS4_SYSV_ABI scePlayGoGetLocus(OrbisPlayGoHandle handle, const OrbisPlayGoChunkId* chunkIds, uint32_t numberOfEntries, OrbisPlayGoLocus* outLoci) { - LOG_INFO(Lib_PlayGo, "called handle = {}, chunkIds = {}, numberOfEntries = {}", handle, - *chunkIds, numberOfEntries); + LOG_DEBUG(Lib_PlayGo, "called handle = {}, chunkIds = {}, numberOfEntries = {}", handle, + *chunkIds, numberOfEntries); if (handle != PlaygoHandle) { return ORBIS_PLAYGO_ERROR_BAD_HANDLE; From 3d1e332c6f0e67e558d541a65fedf80cf1c9c27f Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 12 Dec 2024 01:05:59 -0800 Subject: [PATCH 73/89] renderer_vulkan: Disable culling for RectList. (#1749) --- .../renderer_vulkan/liverpool_to_vk.cpp | 27 +++++++++++++++++++ .../renderer_vulkan/liverpool_to_vk.h | 2 ++ .../renderer_vulkan/vk_graphics_pipeline.cpp | 4 ++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index f0f7d352c..fa8d28ba0 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -65,6 +65,33 @@ vk::CompareOp CompareOp(Liverpool::CompareFunc func) { } } +bool IsPrimitiveCulled(AmdGpu::PrimitiveType type) { + switch (type) { + case AmdGpu::PrimitiveType::TriangleList: + case AmdGpu::PrimitiveType::TriangleFan: + case AmdGpu::PrimitiveType::TriangleStrip: + case AmdGpu::PrimitiveType::PatchPrimitive: + case AmdGpu::PrimitiveType::AdjTriangleList: + case AmdGpu::PrimitiveType::AdjTriangleStrip: + case AmdGpu::PrimitiveType::QuadList: + case AmdGpu::PrimitiveType::QuadStrip: + case AmdGpu::PrimitiveType::Polygon: + return true; + case AmdGpu::PrimitiveType::None: + case AmdGpu::PrimitiveType::PointList: + case AmdGpu::PrimitiveType::LineList: + case AmdGpu::PrimitiveType::LineStrip: + case AmdGpu::PrimitiveType::AdjLineList: + case AmdGpu::PrimitiveType::AdjLineStrip: + case AmdGpu::PrimitiveType::RectList: // Screen-aligned rectangles that are not culled + case AmdGpu::PrimitiveType::LineLoop: + return false; + default: + UNREACHABLE(); + return true; + } +} + vk::PrimitiveTopology PrimitiveType(AmdGpu::PrimitiveType type) { switch (type) { case AmdGpu::PrimitiveType::PointList: diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.h b/src/video_core/renderer_vulkan/liverpool_to_vk.h index 287ba691e..ebd09f0ee 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.h +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.h @@ -18,6 +18,8 @@ vk::StencilOp StencilOp(Liverpool::StencilFunc op); vk::CompareOp CompareOp(Liverpool::CompareFunc func); +bool IsPrimitiveCulled(AmdGpu::PrimitiveType type); + vk::PrimitiveTopology PrimitiveType(AmdGpu::PrimitiveType type); vk::PolygonMode PolygonMode(Liverpool::PolygonMode mode); diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index d53204c77..814657e5d 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -110,7 +110,9 @@ GraphicsPipeline::GraphicsPipeline(const Instance& instance_, Scheduler& schedul .depthClampEnable = false, .rasterizerDiscardEnable = false, .polygonMode = LiverpoolToVK::PolygonMode(key.polygon_mode), - .cullMode = LiverpoolToVK::CullMode(key.cull_mode), + .cullMode = LiverpoolToVK::IsPrimitiveCulled(key.prim_type) + ? LiverpoolToVK::CullMode(key.cull_mode) + : vk::CullModeFlagBits::eNone, .frontFace = key.front_face == Liverpool::FrontFace::Clockwise ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise, From 7aa868562c86564fc9ba94fc3a727c283a2bda99 Mon Sep 17 00:00:00 2001 From: Osyotr Date: Thu, 12 Dec 2024 16:45:59 +0300 Subject: [PATCH 74/89] video_core: add eR5G5B5A1UnormPack16 support to the detiler (#1741) --- src/video_core/texture_cache/tile_manager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index fc3d35e3e..d8d23c400 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -175,6 +175,7 @@ vk::Format DemoteImageFormatForDetiling(vk::Format format) { case vk::Format::eR8Unorm: return vk::Format::eR8Uint; case vk::Format::eR4G4B4A4UnormPack16: + case vk::Format::eR5G5B5A1UnormPack16: case vk::Format::eR8G8Unorm: case vk::Format::eR16Sfloat: case vk::Format::eR16Unorm: From ec8e5d5ef17870165e8e8ac6a02f29a56ac76060 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:45:18 -0800 Subject: [PATCH 75/89] renderer_vulkan: Fix some color attachment indexing issues. (#1755) --- .../renderer_vulkan/vk_graphics_pipeline.cpp | 8 ++--- .../renderer_vulkan/vk_graphics_pipeline.h | 1 + .../renderer_vulkan/vk_pipeline_cache.cpp | 34 +++++++++++++------ .../renderer_vulkan/vk_platform.cpp | 1 + .../renderer_vulkan/vk_rasterizer.cpp | 1 + 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 814657e5d..795537574 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -229,17 +229,15 @@ GraphicsPipeline::GraphicsPipeline(const Instance& instance_, Scheduler& schedul }); } - const auto it = std::ranges::find(key.color_formats, vk::Format::eUndefined); - const u32 num_color_formats = std::distance(key.color_formats.begin(), it); const vk::PipelineRenderingCreateInfoKHR pipeline_rendering_ci = { - .colorAttachmentCount = num_color_formats, + .colorAttachmentCount = key.num_color_attachments, .pColorAttachmentFormats = key.color_formats.data(), .depthAttachmentFormat = key.depth_format, .stencilAttachmentFormat = key.stencil_format, }; std::array attachments; - for (u32 i = 0; i < num_color_formats; i++) { + for (u32 i = 0; i < key.num_color_attachments; i++) { const auto& control = key.blend_controls[i]; const auto src_color = LiverpoolToVK::BlendFactor(control.color_src_factor); const auto dst_color = LiverpoolToVK::BlendFactor(control.color_dst_factor); @@ -292,7 +290,7 @@ GraphicsPipeline::GraphicsPipeline(const Instance& instance_, Scheduler& schedul const vk::PipelineColorBlendStateCreateInfo color_blending = { .logicOpEnable = false, .logicOp = vk::LogicOp::eCopy, - .attachmentCount = num_color_formats, + .attachmentCount = key.num_color_attachments, .pAttachments = attachments.data(), .blendConstants = std::array{1.0f, 1.0f, 1.0f, 1.0f}, }; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index 2834fceb7..703a0680e 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -29,6 +29,7 @@ using Liverpool = AmdGpu::Liverpool; struct GraphicsPipelineKey { std::array stage_hashes; + u32 num_color_attachments; std::array color_formats; std::array color_num_formats; std::array mrt_swizzles; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index b9f318f7a..0fa77e19b 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -268,6 +268,7 @@ bool PipelineCache::RefreshGraphicsKey() { // `RenderingInfo` is assumed to be initialized with a contiguous array of valid color // attachments. This might be not a case as HW color buffers can be bound in an arbitrary // order. We need to do some arrays compaction at this stage + key.num_color_attachments = 0; key.color_formats.fill(vk::Format::eUndefined); key.color_num_formats.fill(AmdGpu::NumberFormat::Unorm); key.blend_controls.fill({}); @@ -277,11 +278,19 @@ bool PipelineCache::RefreshGraphicsKey() { // First pass of bindings check to idenitfy formats and swizzles and pass them to rhe shader // recompiler. - for (auto cb = 0u, remapped_cb = 0u; cb < Liverpool::NumColorBuffers; ++cb) { + for (auto cb = 0u; cb < Liverpool::NumColorBuffers; ++cb) { auto const& col_buf = regs.color_buffers[cb]; - if (skip_cb_binding || !col_buf || !regs.color_target_mask.GetMask(cb)) { + if (skip_cb_binding || !col_buf) { + // No attachment bound and no incremented index. continue; } + + const auto remapped_cb = key.num_color_attachments++; + if (!regs.color_target_mask.GetMask(cb)) { + // Bound to null handle, skip over this attachment index. + continue; + } + const auto base_format = LiverpoolToVK::SurfaceFormat(col_buf.info.format, col_buf.NumFormat()); key.color_formats[remapped_cb] = @@ -290,8 +299,6 @@ bool PipelineCache::RefreshGraphicsKey() { if (base_format == key.color_formats[remapped_cb]) { key.mrt_swizzles[remapped_cb] = col_buf.info.comp_swap.Value(); } - - ++remapped_cb; } fetch_shader = std::nullopt; @@ -385,10 +392,18 @@ bool PipelineCache::RefreshGraphicsKey() { // Second pass to fill remain CB pipeline key data for (auto cb = 0u, remapped_cb = 0u; cb < Liverpool::NumColorBuffers; ++cb) { auto const& col_buf = regs.color_buffers[cb]; - if (skip_cb_binding || !col_buf || !regs.color_target_mask.GetMask(cb) || - (key.mrt_mask & (1u << cb)) == 0) { - key.color_formats[cb] = vk::Format::eUndefined; - key.mrt_swizzles[cb] = Liverpool::ColorBuffer::SwapMode::Standard; + if (skip_cb_binding || !col_buf) { + // No attachment bound and no incremented index. + continue; + } + + if (!regs.color_target_mask.GetMask(cb) || (key.mrt_mask & (1u << cb)) == 0) { + // Attachment is masked out by either color_target_mask or shader mrt_mask. In the case + // of the latter we need to change format to undefined, and either way we need to + // increment the index for the null attachment binding. + key.color_formats[remapped_cb] = vk::Format::eUndefined; + key.mrt_swizzles[remapped_cb] = Liverpool::ColorBuffer::SwapMode::Standard; + ++remapped_cb; continue; } @@ -397,10 +412,9 @@ bool PipelineCache::RefreshGraphicsKey() { !col_buf.info.blend_bypass); key.write_masks[remapped_cb] = vk::ColorComponentFlags{regs.color_target_mask.GetMask(cb)}; key.cb_shader_mask.SetMask(remapped_cb, regs.color_shader_mask.GetMask(cb)); + ++remapped_cb; num_samples = std::max(num_samples, 1u << col_buf.attrib.num_samples_log2); - - ++remapped_cb; } // It seems that the number of samples > 1 set in the AA config doesn't mean we're always diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index 2e717397b..f5e513611 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -44,6 +44,7 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsCallback( case 0xc81ad50e: case 0xb7c39078: case 0x32868fde: // vkCreateBufferView(): pCreateInfo->range does not equal VK_WHOLE_SIZE + case 0x1012616b: // `VK_FORMAT_UNDEFINED` does not match fragment shader output type return VK_FALSE; default: break; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index bfcdc9538..9abf1b527 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -100,6 +100,7 @@ RenderState Rasterizer::PrepareRenderState(u32 mrt_mask) { // an unnecessary transition and may result in state conflict if the resource is already // bound for reading. if ((mrt_mask & (1 << col_buf_id)) == 0) { + state.color_attachments[state.num_color_attachments++].imageView = VK_NULL_HANDLE; continue; } From 1e3d034f9682956e5737126ef8b97d54cdf08d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Cea=20L=C3=B3pez?= Date: Thu, 12 Dec 2024 21:45:56 +0100 Subject: [PATCH 76/89] Fix HLE buffer copy not executed when there's only 1 copy. (#1754) --- src/video_core/renderer_vulkan/vk_shader_hle.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_hle.cpp b/src/video_core/renderer_vulkan/vk_shader_hle.cpp index df9d40f07..d1d4f9af3 100644 --- a/src/video_core/renderer_vulkan/vk_shader_hle.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_hle.cpp @@ -60,7 +60,7 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg static constexpr vk::DeviceSize MaxDistanceForMerge = 64_MB; u32 batch_start = 0; - u32 batch_end = 1; + u32 batch_end = copies.size() > 1 ? 1 : 0; while (batch_end < copies.size()) { // Place first copy into the current batch @@ -72,7 +72,7 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg for (int i = batch_start + 1; i < copies.size(); i++) { // Compute new src and dst bounds if we were to batch this copy - const auto [src_offset, dst_offset, size] = copies[i]; + const auto& [src_offset, dst_offset, size] = copies[i]; auto new_src_offset_min = std::min(src_offset_min, src_offset); auto new_src_offset_max = std::max(src_offset_max, src_offset + size); if (new_src_offset_max - new_src_offset_min > MaxDistanceForMerge) { From 2a19d915e8b8420c92ad819775cd9ae6dd7aa734 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 12 Dec 2024 22:46:20 +0200 Subject: [PATCH 77/89] fix for detecting more that 2 players and play both with player 1 keys (#1750) --- src/core/libraries/pad/pad.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index ec4186f11..98f086dd9 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -155,6 +155,9 @@ int PS4_SYSV_ABI scePadGetFeatureReport() { } int PS4_SYSV_ABI scePadGetHandle(s32 userId, s32 type, s32 index) { + if (userId == -1) { + return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + } LOG_DEBUG(Lib_Pad, "(DUMMY) called"); return 1; } @@ -246,6 +249,9 @@ int PS4_SYSV_ABI scePadMbusTerm() { int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenParam* pParam) { LOG_INFO(Lib_Pad, "(DUMMY) called user_id = {} type = {} index = {}", userId, type, index); + if (userId == -1) { + return ORBIS_PAD_ERROR_DEVICE_NO_HANDLE; + } if (Config::getUseSpecialPad()) { if (type != ORBIS_PAD_PORT_TYPE_SPECIAL) return ORBIS_PAD_ERROR_DEVICE_NOT_CONNECTED; @@ -346,6 +352,9 @@ int PS4_SYSV_ABI scePadReadHistory() { } int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) { + if (handle == ORBIS_PAD_ERROR_DEVICE_NO_HANDLE) { + return ORBIS_PAD_ERROR_INVALID_HANDLE; + } auto* controller = Common::Singleton::Instance(); int connectedCount = 0; bool isConnected = false; From 3f1061de5613c0c4a74d6394a6493491280bc03f Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Fri, 13 Dec 2024 04:46:31 +0800 Subject: [PATCH 78/89] Resubmit - Prevent settings from being saved when close button is pressed instead of save (#1747) * Do not save settings when close button pressed instead of save * Update src/common/config.h Co-authored-by: TheTurtle * Revert "Update src/common/config.h" This reverts commit 125303ea8674b25e93a4c4cf7b93a0357eac19f4. --------- Co-authored-by: rainmakerv2 <30595646+jpau02@users.noreply.github.com> Co-authored-by: TheTurtle --- src/common/config.cpp | 52 ++++-- src/common/config.h | 4 +- src/emulator.cpp | 2 +- src/qt_gui/main_window.cpp | 6 +- src/qt_gui/settings_dialog.cpp | 291 ++++++++++++++++----------------- src/qt_gui/settings_dialog.h | 2 + 6 files changed, 194 insertions(+), 163 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 3db98a438..4d07ba29f 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -422,6 +422,10 @@ void setEmulatorLanguage(std::string language) { emulator_language = language; } +void setGameInstallDirs(const std::vector& settings_install_dirs_config) { + settings_install_dirs = settings_install_dirs_config; +} + u32 getMainWindowGeometryX() { return main_window_geometry_x; } @@ -673,14 +677,6 @@ void save(const std::filesystem::path& path) { data["Vulkan"]["crashDiagnostic"] = vkCrashDiagnostic; data["Debug"]["DebugDump"] = isDebugDump; data["Debug"]["CollectShader"] = isShaderDebug; - data["GUI"]["theme"] = mw_themes; - data["GUI"]["iconSize"] = m_icon_size; - data["GUI"]["sliderPos"] = m_slider_pos; - data["GUI"]["iconSizeGrid"] = m_icon_size_grid; - data["GUI"]["sliderPosGrid"] = m_slider_pos_grid; - data["GUI"]["gameTableMode"] = m_table_mode; - data["GUI"]["mw_width"] = m_window_size_W; - data["GUI"]["mw_height"] = m_window_size_H; std::vector install_dirs; for (const auto& dirString : settings_install_dirs) { @@ -690,6 +686,43 @@ void save(const std::filesystem::path& path) { data["GUI"]["addonInstallDir"] = std::string{fmt::UTF(settings_addon_install_dir.u8string()).data}; + data["GUI"]["emulatorLanguage"] = emulator_language; + data["Settings"]["consoleLanguage"] = m_language; + + std::ofstream file(path, std::ios::binary); + file << data; + file.close(); +} + +void saveMainWindow(const std::filesystem::path& path) { + toml::value data; + + std::error_code error; + if (std::filesystem::exists(path, error)) { + try { + std::ifstream ifs; + ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); + ifs.open(path, std::ios_base::binary); + data = toml::parse(ifs, std::string{fmt::UTF(path.filename().u8string()).data}); + } catch (const std::exception& ex) { + fmt::print("Exception trying to parse config file. Exception: {}\n", ex.what()); + return; + } + } else { + if (error) { + fmt::print("Filesystem error: {}\n", error.message()); + } + fmt::print("Saving new configuration file {}\n", fmt::UTF(path.u8string())); + } + + data["GUI"]["mw_width"] = m_window_size_W; + data["GUI"]["mw_height"] = m_window_size_H; + data["GUI"]["theme"] = mw_themes; + data["GUI"]["iconSize"] = m_icon_size; + data["GUI"]["sliderPos"] = m_slider_pos; + data["GUI"]["iconSizeGrid"] = m_icon_size_grid; + data["GUI"]["sliderPosGrid"] = m_slider_pos_grid; + data["GUI"]["gameTableMode"] = m_table_mode; data["GUI"]["geometry_x"] = main_window_geometry_x; data["GUI"]["geometry_y"] = main_window_geometry_y; data["GUI"]["geometry_w"] = main_window_geometry_w; @@ -697,9 +730,6 @@ void save(const std::filesystem::path& path) { data["GUI"]["pkgDirs"] = m_pkg_viewer; data["GUI"]["elfDirs"] = m_elf_viewer; data["GUI"]["recentFiles"] = m_recent_files; - data["GUI"]["emulatorLanguage"] = emulator_language; - - data["Settings"]["consoleLanguage"] = m_language; std::ofstream file(path, std::ios::binary); file << data; diff --git a/src/common/config.h b/src/common/config.h index d98c94480..ff3b3703f 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -13,6 +13,7 @@ enum HideCursorState : s16 { Never, Idle, Always }; void load(const std::filesystem::path& path); void save(const std::filesystem::path& path); +void saveMainWindow(const std::filesystem::path& path); bool isNeoMode(); bool isFullscreenMode(); @@ -67,6 +68,7 @@ void setNeoMode(bool enable); void setUserName(const std::string& type); void setUpdateChannel(const std::string& type); void setSeparateUpdateEnabled(bool use); +void setGameInstallDirs(const std::vector& settings_install_dirs_config); void setCursorState(s16 cursorState); void setCursorHideTimeout(int newcursorHideTimeout); @@ -128,4 +130,4 @@ void setDefaultValues(); // settings u32 GetLanguage(); -}; // namespace Config +}; // namespace Config \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index 60d6e18d7..eeac5973a 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -100,7 +100,7 @@ Emulator::Emulator() { Emulator::~Emulator() { const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); + Config::saveMainWindow(config_dir / "config.toml"); } void Emulator::Run(const std::filesystem::path& file) { diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 4c40084d3..3eb629c0b 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -35,7 +35,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi MainWindow::~MainWindow() { SaveWindowState(); const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); + Config::saveMainWindow(config_dir / "config.toml"); } bool MainWindow::Init() { @@ -1006,7 +1006,7 @@ void MainWindow::AddRecentFiles(QString filePath) { } Config::setRecentFiles(vec); const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); + Config::saveMainWindow(config_dir / "config.toml"); CreateRecentGameActions(); // Refresh the QActions. } @@ -1077,4 +1077,4 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) { } } return QMainWindow::eventFilter(obj, event); -} +} \ No newline at end of file diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 1fd4b6e8b..e67c14ccb 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -12,12 +12,13 @@ #ifdef ENABLE_UPDATER #include "check_update.h" #endif +#include #include "common/logging/backend.h" #include "common/logging/filter.h" +#include "common/logging/formatter.h" #include "main_window.h" #include "settings_dialog.h" #include "ui_settings_dialog.h" - QStringList languageNames = {"Arabic", "Czech", "Danish", @@ -94,13 +95,18 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this, config_dir](QAbstractButton* button) { if (button == ui->buttonBox->button(QDialogButtonBox::Save)) { + UpdateSettings(); Config::save(config_dir / "config.toml"); QWidget::close(); } else if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) { + UpdateSettings(); Config::save(config_dir / "config.toml"); } else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { Config::setDefaultValues(); + Config::save(config_dir / "config.toml"); LoadValuesFromConfig(); + } else if (button == ui->buttonBox->button(QDialogButtonBox::Close)) { + ResetInstallFolders(); } if (Common::Log::IsActive()) { Common::Log::Filter filter; @@ -119,35 +125,6 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge // GENERAL TAB { - connect(ui->userNameLineEdit, &QLineEdit::textChanged, this, - [](const QString& text) { Config::setUserName(text.toStdString()); }); - - connect(ui->consoleLanguageComboBox, QOverload::of(&QComboBox::currentIndexChanged), - this, [](int index) { - if (index >= 0 && index < languageIndexes.size()) { - int languageCode = languageIndexes[index]; - Config::setLanguage(languageCode); - } - }); - - connect(ui->fullscreenCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setFullscreenMode(val); }); - - connect(ui->separateUpdatesCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setSeparateUpdateEnabled(val); }); - - connect(ui->showSplashCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setShowSplash(val); }); - - connect(ui->ps4proCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setNeoMode(val); }); - - connect(ui->logTypeComboBox, &QComboBox::currentTextChanged, this, - [](const QString& text) { Config::setLogType(text.toStdString()); }); - - connect(ui->logFilterLineEdit, &QLineEdit::textChanged, this, - [](const QString& text) { Config::setLogFilter(text.toStdString()); }); - #ifdef ENABLE_UPDATER connect(ui->updateCheckBox, &QCheckBox::stateChanged, this, [](int state) { Config::setAutoUpdate(state == Qt::Checked); }); @@ -163,74 +140,12 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge ui->updaterGroupBox->setVisible(false); ui->GUIgroupBox->setMaximumSize(265, 16777215); #endif - - connect(ui->playBGMCheckBox, &QCheckBox::stateChanged, this, [](int val) { - Config::setPlayBGM(val); - if (val == Qt::Unchecked) { - BackgroundMusicPlayer::getInstance().stopMusic(); - } - }); - - connect(ui->BGMVolumeSlider, &QSlider::valueChanged, this, [](float val) { - Config::setBGMvolume(val); - BackgroundMusicPlayer::getInstance().setVolume(val); - }); - -#ifdef ENABLE_DISCORD_RPC - connect(ui->discordRPCCheckbox, &QCheckBox::stateChanged, this, [](int val) { - Config::setEnableDiscordRPC(val); - auto* rpc = Common::Singleton::Instance(); - if (val == Qt::Checked) { - rpc->init(); - rpc->setStatusIdling(); - } else { - rpc->shutdown(); - } - }); -#endif } // Input TAB { connect(ui->hideCursorComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - [this](s16 index) { - Config::setCursorState(index); - OnCursorStateChanged(index); - }); - - connect(ui->idleTimeoutSpinBox, &QSpinBox::valueChanged, this, - [](int index) { Config::setCursorHideTimeout(index); }); - - connect(ui->backButtonBehaviorComboBox, QOverload::of(&QComboBox::currentIndexChanged), - this, [this](int index) { - if (index >= 0 && index < ui->backButtonBehaviorComboBox->count()) { - QString data = ui->backButtonBehaviorComboBox->itemData(index).toString(); - Config::setBackButtonBehavior(data.toStdString()); - } - }); - } - - // GPU TAB - { - // First options is auto selection -1, so gpuId on the GUI will always have to subtract 1 - // when setting and add 1 when getting to select the correct gpu in Qt - connect(ui->graphicsAdapterBox, &QComboBox::currentIndexChanged, this, - [](int index) { Config::setGpuId(index - 1); }); - - connect(ui->widthSpinBox, &QSpinBox::valueChanged, this, - [](int val) { Config::setScreenWidth(val); }); - - connect(ui->heightSpinBox, &QSpinBox::valueChanged, this, - [](int val) { Config::setScreenHeight(val); }); - - connect(ui->vblankSpinBox, &QSpinBox::valueChanged, this, - [](int val) { Config::setVblankDiv(val); }); - - connect(ui->dumpShadersCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setDumpShaders(val); }); - - connect(ui->nullGpuCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setNullGpu(val); }); + [this](s16 index) { OnCursorStateChanged(index); }); } // PATH TAB @@ -262,21 +177,6 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge }); } - // DEBUG TAB - { - connect(ui->debugDump, &QCheckBox::stateChanged, this, - [](int val) { Config::setDebugDump(val); }); - - connect(ui->vkValidationCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setVkValidation(val); }); - - connect(ui->vkSyncValidationCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setVkSyncValidation(val); }); - - connect(ui->rdocCheckBox, &QCheckBox::stateChanged, this, - [](int val) { Config::setRdocEnabled(val); }); - } - // Descriptions { // General @@ -323,40 +223,69 @@ SettingsDialog::SettingsDialog(std::span physical_devices, QWidge } void SettingsDialog::LoadValuesFromConfig() { - ui->consoleLanguageComboBox->setCurrentIndex( - std::distance( - languageIndexes.begin(), - std::find(languageIndexes.begin(), languageIndexes.end(), Config::GetLanguage())) % - languageIndexes.size()); - ui->emulatorLanguageComboBox->setCurrentIndex(languages[Config::getEmulatorLanguage()]); - ui->hideCursorComboBox->setCurrentIndex(Config::getCursorState()); - OnCursorStateChanged(Config::getCursorState()); - ui->idleTimeoutSpinBox->setValue(Config::getCursorHideTimeout()); - ui->graphicsAdapterBox->setCurrentIndex(Config::getGpuId() + 1); - ui->widthSpinBox->setValue(Config::getScreenWidth()); - ui->heightSpinBox->setValue(Config::getScreenHeight()); - ui->vblankSpinBox->setValue(Config::vblankDiv()); - ui->dumpShadersCheckBox->setChecked(Config::dumpShaders()); - ui->nullGpuCheckBox->setChecked(Config::nullGpu()); - ui->playBGMCheckBox->setChecked(Config::getPlayBGM()); - ui->BGMVolumeSlider->setValue((Config::getBGMvolume())); - ui->discordRPCCheckbox->setChecked(Config::getEnableDiscordRPC()); - ui->fullscreenCheckBox->setChecked(Config::isFullscreenMode()); - ui->separateUpdatesCheckBox->setChecked(Config::getSeparateUpdateEnabled()); - ui->showSplashCheckBox->setChecked(Config::showSplash()); - ui->ps4proCheckBox->setChecked(Config::isNeoMode()); - ui->logTypeComboBox->setCurrentText(QString::fromStdString(Config::getLogType())); - ui->logFilterLineEdit->setText(QString::fromStdString(Config::getLogFilter())); - ui->userNameLineEdit->setText(QString::fromStdString(Config::getUserName())); - ui->debugDump->setChecked(Config::debugDump()); - ui->vkValidationCheckBox->setChecked(Config::vkValidationEnabled()); - ui->vkSyncValidationCheckBox->setChecked(Config::vkValidationSyncEnabled()); - ui->rdocCheckBox->setChecked(Config::isRdocEnabled()); + std::filesystem::path userdir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + std::error_code error; + if (!std::filesystem::exists(userdir / "Config.toml", error)) { + Config::load(userdir / "Config.toml"); + return; + } + + try { + std::ifstream ifs; + ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); + const toml::value data = toml::parse(userdir / "Config.toml"); + } catch (std::exception& ex) { + fmt::print("Got exception trying to load config file. Exception: {}\n", ex.what()); + return; + } + + const toml::value data = toml::parse(userdir / "Config.toml"); + const QVector languageIndexes = {21, 23, 14, 6, 18, 1, 12, 22, 2, 4, 25, 24, 29, 5, 0, 9, + 15, 16, 17, 7, 26, 8, 11, 20, 3, 13, 27, 10, 19, 30, 28}; + + ui->consoleLanguageComboBox->setCurrentIndex( + std::distance(languageIndexes.begin(), + std::find(languageIndexes.begin(), languageIndexes.end(), + toml::find_or(data, "Settings", "consoleLanguage", 6))) % + languageIndexes.size()); + ui->emulatorLanguageComboBox->setCurrentIndex( + languages[toml::find_or(data, "GUI", "emulatorLanguage", "en")]); + ui->hideCursorComboBox->setCurrentIndex(toml::find_or(data, "Input", "cursorState", 1)); + OnCursorStateChanged(toml::find_or(data, "Input", "cursorState", 1)); + ui->idleTimeoutSpinBox->setValue(toml::find_or(data, "Input", "cursorHideTimeout", 5)); + // First options is auto selection -1, so gpuId on the GUI will always have to subtract 1 + // when setting and add 1 when getting to select the correct gpu in Qt + ui->graphicsAdapterBox->setCurrentIndex(toml::find_or(data, "Vulkan", "gpuId", -1) + 1); + ui->widthSpinBox->setValue(toml::find_or(data, "GPU", "screenWidth", 1280)); + ui->heightSpinBox->setValue(toml::find_or(data, "GPU", "screenHeight", 720)); + ui->vblankSpinBox->setValue(toml::find_or(data, "GPU", "vblankDivider", 1)); + ui->dumpShadersCheckBox->setChecked(toml::find_or(data, "GPU", "dumpShaders", false)); + ui->nullGpuCheckBox->setChecked(toml::find_or(data, "GPU", "nullGpu", false)); + ui->playBGMCheckBox->setChecked(toml::find_or(data, "General", "playBGM", false)); + ui->BGMVolumeSlider->setValue(toml::find_or(data, "General", "BGMvolume", 50)); + ui->discordRPCCheckbox->setChecked( + toml::find_or(data, "General", "enableDiscordRPC", true)); + ui->fullscreenCheckBox->setChecked(toml::find_or(data, "General", "Fullscreen", false)); + ui->separateUpdatesCheckBox->setChecked( + toml::find_or(data, "General", "separateUpdateEnabled", false)); + ui->showSplashCheckBox->setChecked(toml::find_or(data, "General", "showSplash", false)); + ui->ps4proCheckBox->setChecked(toml::find_or(data, "General", "isPS4Pro", false)); + ui->logTypeComboBox->setCurrentText( + QString::fromStdString(toml::find_or(data, "General", "logType", "async"))); + ui->logFilterLineEdit->setText( + QString::fromStdString(toml::find_or(data, "General", "logFilter", ""))); + ui->userNameLineEdit->setText( + QString::fromStdString(toml::find_or(data, "General", "userName", "shadPS4"))); + ui->debugDump->setChecked(toml::find_or(data, "Debug", "DebugDump", false)); + ui->vkValidationCheckBox->setChecked(toml::find_or(data, "Vulkan", "validation", false)); + ui->vkSyncValidationCheckBox->setChecked( + toml::find_or(data, "Vulkan", "validation_sync", false)); + ui->rdocCheckBox->setChecked(toml::find_or(data, "Vulkan", "rdocEnable", false)); #ifdef ENABLE_UPDATER - ui->updateCheckBox->setChecked(Config::autoUpdate()); - std::string updateChannel = Config::getUpdateChannel(); + ui->updateCheckBox->setChecked(toml::find_or(data, "General", "autoUpdate", false)); + std::string updateChannel = toml::find_or(data, "General", "updateChannel", ""); if (updateChannel != "Release" && updateChannel != "Nightly") { if (Common::isRelease) { updateChannel = "Release"; @@ -367,18 +296,13 @@ void SettingsDialog::LoadValuesFromConfig() { ui->updateComboBox->setCurrentText(QString::fromStdString(updateChannel)); #endif - for (const auto& dir : Config::getGameInstallDirs()) { - QString path_string; - Common::FS::PathToQString(path_string, dir); - QListWidgetItem* item = new QListWidgetItem(path_string); - ui->gameFoldersListWidget->addItem(item); - } - - QString backButtonBehavior = QString::fromStdString(Config::getBackButtonBehavior()); + QString backButtonBehavior = QString::fromStdString( + toml::find_or(data, "Input", "backButtonBehavior", "left")); int index = ui->backButtonBehaviorComboBox->findData(backButtonBehavior); ui->backButtonBehaviorComboBox->setCurrentIndex(index != -1 ? index : 0); ui->removeFolderButton->setEnabled(!ui->gameFoldersListWidget->selectedItems().isEmpty()); + ResetInstallFolders(); } void SettingsDialog::InitializeEmulatorLanguages() { @@ -554,3 +478,76 @@ bool SettingsDialog::eventFilter(QObject* obj, QEvent* event) { } return QDialog::eventFilter(obj, event); } + +void SettingsDialog::UpdateSettings() { + + const QVector TouchPadIndex = {"left", "center", "right", "none"}; + Config::setBackButtonBehavior(TouchPadIndex[ui->backButtonBehaviorComboBox->currentIndex()]); + Config::setNeoMode(ui->ps4proCheckBox->isChecked()); + Config::setFullscreenMode(ui->fullscreenCheckBox->isChecked()); + Config::setPlayBGM(ui->playBGMCheckBox->isChecked()); + Config::setNeoMode(ui->ps4proCheckBox->isChecked()); + Config::setLogType(ui->logTypeComboBox->currentText().toStdString()); + Config::setLogFilter(ui->logFilterLineEdit->text().toStdString()); + Config::setUserName(ui->userNameLineEdit->text().toStdString()); + Config::setCursorState(ui->hideCursorComboBox->currentIndex()); + Config::setCursorHideTimeout(ui->idleTimeoutSpinBox->value()); + Config::setGpuId(ui->graphicsAdapterBox->currentIndex() - 1); + Config::setBGMvolume(ui->BGMVolumeSlider->value()); + Config::setLanguage(languageIndexes[ui->consoleLanguageComboBox->currentIndex()]); + Config::setEnableDiscordRPC(ui->discordRPCCheckbox->isChecked()); + Config::setScreenWidth(ui->widthSpinBox->value()); + Config::setScreenHeight(ui->heightSpinBox->value()); + Config::setVblankDiv(ui->vblankSpinBox->value()); + Config::setDumpShaders(ui->dumpShadersCheckBox->isChecked()); + Config::setNullGpu(ui->nullGpuCheckBox->isChecked()); + Config::setSeparateUpdateEnabled(ui->separateUpdatesCheckBox->isChecked()); + Config::setShowSplash(ui->showSplashCheckBox->isChecked()); + Config::setDebugDump(ui->debugDump->isChecked()); + Config::setVkValidation(ui->vkValidationCheckBox->isChecked()); + Config::setVkSyncValidation(ui->vkSyncValidationCheckBox->isChecked()); + Config::setRdocEnabled(ui->rdocCheckBox->isChecked()); + Config::setAutoUpdate(ui->updateCheckBox->isChecked()); + Config::setUpdateChannel(ui->updateComboBox->currentText().toStdString()); + +#ifdef ENABLE_DISCORD_RPC + auto* rpc = Common::Singleton::Instance(); + if (Config::getEnableDiscordRPC()) { + rpc->init(); + rpc->setStatusIdling(); + } else { + rpc->shutdown(); + } +#endif + + BackgroundMusicPlayer::getInstance().setVolume(ui->BGMVolumeSlider->value()); + ResetInstallFolders(); +} + +void SettingsDialog::ResetInstallFolders() { + + std::filesystem::path userdir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + const toml::value data = toml::parse(userdir / "Config.toml"); + + if (data.contains("GUI")) { + const toml::value& gui = data.at("GUI"); + const auto install_dir_array = + toml::find_or>(gui, "installDirs", {}); + std::vector settings_install_dirs_config = {}; + + for (const auto& dir : install_dir_array) { + if (std::find(settings_install_dirs_config.begin(), settings_install_dirs_config.end(), + dir) == settings_install_dirs_config.end()) { + settings_install_dirs_config.push_back(dir); + } + } + + for (const auto& dir : settings_install_dirs_config) { + QString path_string; + Common::FS::PathToQString(path_string, dir); + QListWidgetItem* item = new QListWidgetItem(path_string); + ui->gameFoldersListWidget->addItem(item); + } + Config::setGameInstallDirs(settings_install_dirs_config); + } +} \ No newline at end of file diff --git a/src/qt_gui/settings_dialog.h b/src/qt_gui/settings_dialog.h index 8cdded980..987b35d45 100644 --- a/src/qt_gui/settings_dialog.h +++ b/src/qt_gui/settings_dialog.h @@ -31,6 +31,8 @@ signals: private: void LoadValuesFromConfig(); + void UpdateSettings(); + void ResetInstallFolders(); void InitializeEmulatorLanguages(); void OnLanguageChanged(int index); void OnCursorStateChanged(s16 index); From 5be807fc8ac6bada55c37428a51cee081bf64498 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Fri, 13 Dec 2024 00:31:49 +0200 Subject: [PATCH 79/89] hot-fix: Fix order of operands --- src/shader_recompiler/backend/spirv/emit_spirv_image.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp index fe2660705..736410dcd 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_image.cpp @@ -130,8 +130,8 @@ Id EmitImageSampleDrefExplicitLod(EmitContext& ctx, IR::Inst* inst, u32 handle, const Id sampler = ctx.OpLoad(ctx.sampler_type, ctx.samplers[handle >> 16]); const Id sampled_image = ctx.OpSampledImage(texture.sampled_type, image, sampler); ImageOperands operands; - operands.AddOffset(ctx, offset); operands.Add(spv::ImageOperandsMask::Lod, lod); + operands.AddOffset(ctx, offset); const Id sample = ctx.OpImageSampleDrefExplicitLod(result_type, sampled_image, coords, dref, operands.mask, operands.operands); const Id sample_typed = texture.is_integer ? ctx.OpBitcast(ctx.F32[1], sample) : sample; From 91d57e830be312e5296fa7bfc265dfe87f13582c Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:27:36 +0800 Subject: [PATCH 80/89] Fix lowercase filenames fox Linux (#1760) Fix uppercase config filenames Co-authored-by: rainmakerv2 <30595646+jpau02@users.noreply.github.com> --- src/qt_gui/settings_dialog.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index e67c14ccb..f74f86435 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -226,21 +226,21 @@ void SettingsDialog::LoadValuesFromConfig() { std::filesystem::path userdir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); std::error_code error; - if (!std::filesystem::exists(userdir / "Config.toml", error)) { - Config::load(userdir / "Config.toml"); + if (!std::filesystem::exists(userdir / "config.toml", error)) { + Config::load(userdir / "config.toml"); return; } try { std::ifstream ifs; ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit); - const toml::value data = toml::parse(userdir / "Config.toml"); + const toml::value data = toml::parse(userdir / "config.toml"); } catch (std::exception& ex) { fmt::print("Got exception trying to load config file. Exception: {}\n", ex.what()); return; } - const toml::value data = toml::parse(userdir / "Config.toml"); + const toml::value data = toml::parse(userdir / "config.toml"); const QVector languageIndexes = {21, 23, 14, 6, 18, 1, 12, 22, 2, 4, 25, 24, 29, 5, 0, 9, 15, 16, 17, 7, 26, 8, 11, 20, 3, 13, 27, 10, 19, 30, 28}; @@ -527,7 +527,7 @@ void SettingsDialog::UpdateSettings() { void SettingsDialog::ResetInstallFolders() { std::filesystem::path userdir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - const toml::value data = toml::parse(userdir / "Config.toml"); + const toml::value data = toml::parse(userdir / "config.toml"); if (data.contains("GUI")) { const toml::value& gui = data.at("GUI"); From f587931ed387efbf83e8e947bf9859885bbac297 Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:52:54 +0800 Subject: [PATCH 81/89] Fix for adding game folders (#1761) Co-authored-by: rainmakerv2 <30595646+jpau02@users.noreply.github.com> --- src/qt_gui/settings_dialog.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index f74f86435..09d3674f7 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -521,7 +521,6 @@ void SettingsDialog::UpdateSettings() { #endif BackgroundMusicPlayer::getInstance().setVolume(ui->BGMVolumeSlider->value()); - ResetInstallFolders(); } void SettingsDialog::ResetInstallFolders() { From bab00dbca8be8c0f3fb8433dea1b7bad24012a71 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Fri, 13 Dec 2024 18:23:01 +0200 Subject: [PATCH 82/89] kernel: Fix module finding Patch by Elbread --- src/core/libraries/kernel/process.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index 6c29d9305..ba7964bc4 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -50,6 +50,9 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, size_t arg return handle; } handle = linker->LoadModule(path, true); + if (handle == -1) { + return ORBIS_KERNEL_ERROR_EINVAL; + } auto* module = linker->GetModule(handle); linker->RelocateAnyImports(module); From 8acefd25e77d527eb4a250572ed91161c342e144 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Fri, 13 Dec 2024 18:26:16 +0200 Subject: [PATCH 83/89] hot-fix the hot-fix --- src/core/libraries/kernel/process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/process.cpp b/src/core/libraries/kernel/process.cpp index ba7964bc4..97cc01ebc 100644 --- a/src/core/libraries/kernel/process.cpp +++ b/src/core/libraries/kernel/process.cpp @@ -51,7 +51,7 @@ s32 PS4_SYSV_ABI sceKernelLoadStartModule(const char* moduleFileName, size_t arg } handle = linker->LoadModule(path, true); if (handle == -1) { - return ORBIS_KERNEL_ERROR_EINVAL; + return ORBIS_KERNEL_ERROR_ESRCH; } auto* module = linker->GetModule(handle); linker->RelocateAnyImports(module); From cfbd8691261e4d0b06a6ed29ec68c333ef6cc8d1 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Fri, 13 Dec 2024 18:28:19 +0200 Subject: [PATCH 84/89] texture_cache: Improve support for stencil reads (#1758) * texture_cache: Improve support for stencil reads * libraries: Supress some spammy logs * core: Support loading font libraries * texture_cache: Remove assert --- src/core/libraries/audio3d/audio3d.cpp | 2 +- src/core/libraries/gnmdriver/gnmdriver.cpp | 2 +- .../libraries/libc_internal/libc_internal.cpp | 12 +++++++++++ src/emulator.cpp | 7 +++++-- src/video_core/amdgpu/liverpool.h | 4 ++++ .../renderer_vulkan/vk_rasterizer.cpp | 18 ++++++++++------ src/video_core/texture_cache/image.cpp | 4 +++- src/video_core/texture_cache/image.h | 5 +++++ src/video_core/texture_cache/image_info.cpp | 3 +++ src/video_core/texture_cache/image_info.h | 2 +- src/video_core/texture_cache/image_view.cpp | 2 +- .../texture_cache/texture_cache.cpp | 21 +++++++++++++++++++ 12 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/core/libraries/audio3d/audio3d.cpp b/src/core/libraries/audio3d/audio3d.cpp index 44670d87b..d896524c6 100644 --- a/src/core/libraries/audio3d/audio3d.cpp +++ b/src/core/libraries/audio3d/audio3d.cpp @@ -80,7 +80,7 @@ int PS4_SYSV_ABI sceAudio3dPortGetAttributesSupported(OrbisAudio3dPortId uiPortI int PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId uiPortId, u32* pQueueLevel, u32* pQueueAvailable) { - LOG_INFO(Lib_Audio3d, "uiPortId = {}", uiPortId); + LOG_TRACE(Lib_Audio3d, "uiPortId = {}", uiPortId); return ORBIS_OK; } diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index 18035e6ce..dbf085fb3 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -971,7 +971,7 @@ s32 PS4_SYSV_ABI sceGnmFindResourcesPublic() { } void PS4_SYSV_ABI sceGnmFlushGarlic() { - LOG_WARNING(Lib_GnmDriver, "(STUBBED) called"); + LOG_TRACE(Lib_GnmDriver, "(STUBBED) called"); } int PS4_SYSV_ABI sceGnmGetCoredumpAddress() { diff --git a/src/core/libraries/libc_internal/libc_internal.cpp b/src/core/libraries/libc_internal/libc_internal.cpp index eb6046c7a..8453a78b9 100644 --- a/src/core/libraries/libc_internal/libc_internal.cpp +++ b/src/core/libraries/libc_internal/libc_internal.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include "common/assert.h" #include "common/logging/log.h" @@ -65,6 +66,15 @@ char* PS4_SYSV_ABI internal_strncpy(char* dest, const char* src, std::size_t cou return std::strncpy(dest, src, count); } +int PS4_SYSV_ABI internal_strncpy_s(char* dest, size_t destsz, const char* src, size_t count) { +#ifdef _WIN64 + return strncpy_s(dest, destsz, src, count); +#else + std::strcpy(dest, src); + return 0; +#endif +} + char* PS4_SYSV_ABI internal_strcat(char* dest, const char* src) { return std::strcat(dest, src); } @@ -237,6 +247,8 @@ void RegisterlibSceLibcInternal(Core::Loader::SymbolsResolver* sym) { internal_strlen); LIB_FUNCTION("6sJWiWSRuqk", "libSceLibcInternal", 1, "libSceLibcInternal", 1, 1, internal_strncpy); + LIB_FUNCTION("YNzNkJzYqEg", "libSceLibcInternal", 1, "libSceLibcInternal", 1, 1, + internal_strncpy_s); LIB_FUNCTION("Ls4tzzhimqQ", "libSceLibcInternal", 1, "libSceLibcInternal", 1, 1, internal_strcat); LIB_FUNCTION("ob5xAW4ln-0", "libSceLibcInternal", 1, "libSceLibcInternal", 1, 1, diff --git a/src/emulator.cpp b/src/emulator.cpp index eeac5973a..c517bc284 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -266,7 +266,7 @@ void Emulator::Run(const std::filesystem::path& file) { } void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string game_serial) { - constexpr std::array ModulesToLoad{ + constexpr std::array ModulesToLoad{ {{"libSceNgs2.sprx", &Libraries::Ngs2::RegisterlibSceNgs2}, {"libSceFiber.sprx", &Libraries::Fiber::RegisterlibSceFiber}, {"libSceUlt.sprx", nullptr}, @@ -276,7 +276,10 @@ void Emulator::LoadSystemModules(const std::filesystem::path& file, std::string {"libSceDiscMap.sprx", &Libraries::DiscMap::RegisterlibSceDiscMap}, {"libSceRtc.sprx", &Libraries::Rtc::RegisterlibSceRtc}, {"libSceJpegEnc.sprx", &Libraries::JpegEnc::RegisterlibSceJpegEnc}, - {"libSceCesCs.sprx", nullptr}}}; + {"libSceCesCs.sprx", nullptr}, + {"libSceFont.sprx", nullptr}, + {"libSceFontFt.sprx", nullptr}, + {"libSceFreeTypeOt.sprx", nullptr}}}; std::vector found_modules; const auto& sys_module_path = Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir); diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index ca3b01612..9bc3454d8 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -431,6 +431,10 @@ struct Liverpool { return u64(z_read_base) << 8; } + u64 StencilAddress() const { + return u64(stencil_read_base) << 8; + } + u32 NumSamples() const { return 1u << z_info.num_samples; // spec doesn't say it is a log2 } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 9abf1b527..eb2ef3600 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -616,18 +616,24 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin auto& [image_id, desc] = image_bindings.emplace_back(std::piecewise_construct, std::tuple{}, std::tuple{tsharp, image_desc}); image_id = texture_cache.FindImage(desc); - auto& image = texture_cache.GetImage(image_id); - if (image.binding.is_bound) { + auto* image = &texture_cache.GetImage(image_id); + if (image->depth_id) { + // If this image has an associated depth image, it's a stencil attachment. + // Redirect the access to the actual depth-stencil buffer. + image_id = image->depth_id; + image = &texture_cache.GetImage(image_id); + } + if (image->binding.is_bound) { // The image is already bound. In case if it is about to be used as storage we need // to force general layout on it. - image.binding.force_general |= image_desc.is_storage; + image->binding.force_general |= image_desc.is_storage; } - if (image.binding.is_target) { + if (image->binding.is_target) { // The image is already bound as target. Since we read and output to it need to force // general layout too. - image.binding.force_general = 1u; + image->binding.force_general = 1u; } - image.binding.is_bound = 1u; + image->binding.is_bound = 1u; } // Second pass to re-bind images that were updated after binding diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index e7e1ce1da..03339d280 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -145,8 +145,10 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, const ImageInfo& info_) : instance{&instance_}, scheduler{&scheduler_}, info{info_}, image{instance->GetDevice(), instance->GetAllocator()} { + if (info.pixel_format == vk::Format::eUndefined) { + return; + } mip_hashes.resize(info.resources.levels); - ASSERT(info.pixel_format != vk::Format::eUndefined); // Here we force `eExtendedUsage` as don't know all image usage cases beforehand. In normal case // the texture cache should re-create the resource with the usage requested vk::ImageCreateFlags flags{vk::ImageCreateFlagBits::eMutableFormat | diff --git a/src/video_core/texture_cache/image.h b/src/video_core/texture_cache/image.h index a1b1b007f..473dd731e 100644 --- a/src/video_core/texture_cache/image.h +++ b/src/video_core/texture_cache/image.h @@ -92,6 +92,10 @@ struct Image { return image_view_ids[std::distance(image_view_infos.begin(), it)]; } + void AssociateDepth(ImageId image_id) { + depth_id = image_id; + } + boost::container::small_vector GetBarriers( vk::ImageLayout dst_layout, vk::Flags dst_mask, vk::PipelineStageFlags2 dst_stage, std::optional subres_range); @@ -116,6 +120,7 @@ struct Image { VAddr track_addr_end = 0; std::vector image_view_infos; std::vector image_view_ids; + ImageId depth_id{}; // Resource state tracking struct { diff --git a/src/video_core/texture_cache/image_info.cpp b/src/video_core/texture_cache/image_info.cpp index 0ed36ee39..1445d41cd 100644 --- a/src/video_core/texture_cache/image_info.cpp +++ b/src/video_core/texture_cache/image_info.cpp @@ -298,6 +298,9 @@ ImageInfo::ImageInfo(const AmdGpu::Liverpool::DepthBuffer& buffer, u32 num_slice resources.layers = num_slices; meta_info.htile_addr = buffer.z_info.tile_surface_en ? htile_address : 0; + stencil_addr = buffer.StencilAddress(); + stencil_size = pitch * size.height * sizeof(u8); + guest_address = buffer.Address(); const auto depth_slice_sz = buffer.GetDepthSliceSize(); guest_size_bytes = depth_slice_sz * num_slices; diff --git a/src/video_core/texture_cache/image_info.h b/src/video_core/texture_cache/image_info.h index e12ae3be1..a657310a8 100644 --- a/src/video_core/texture_cache/image_info.h +++ b/src/video_core/texture_cache/image_info.h @@ -69,7 +69,7 @@ struct ImageInfo { } props{}; // Surface properties with impact on various calculation factors vk::Format pixel_format = vk::Format::eUndefined; - vk::ImageType type = vk::ImageType::e1D; + vk::ImageType type = vk::ImageType::e2D; SubresourceExtent resources; Extent3D size{1, 1, 1}; u32 num_bits{}; diff --git a/src/video_core/texture_cache/image_view.cpp b/src/video_core/texture_cache/image_view.cpp index 61f1aaafe..12ad201d1 100644 --- a/src/video_core/texture_cache/image_view.cpp +++ b/src/video_core/texture_cache/image_view.cpp @@ -170,7 +170,7 @@ ImageView::ImageView(const Vulkan::Instance& instance, const ImageViewInfo& info format = image.info.pixel_format; aspect = vk::ImageAspectFlagBits::eDepth; } - if (image.aspect_mask & vk::ImageAspectFlagBits::eStencil && format == vk::Format::eR8Unorm) { + if (image.aspect_mask & vk::ImageAspectFlagBits::eStencil && format == vk::Format::eR8Uint) { format = image.info.pixel_format; aspect = vk::ImageAspectFlagBits::eStencil; } diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 153314d2b..897d6f67e 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -443,6 +443,27 @@ ImageView& TextureCache::FindDepthTarget(BaseDesc& desc) { } } + // If there is a stencil attachment, link depth and stencil. + if (desc.info.stencil_addr != 0) { + ImageId stencil_id{}; + ForEachImageInRegion(desc.info.stencil_addr, desc.info.stencil_size, + [&](ImageId image_id, Image& image) { + if (image.info.guest_address == desc.info.stencil_addr) { + stencil_id = image_id; + } + }); + if (!stencil_id) { + ImageInfo info{}; + info.guest_address = desc.info.stencil_addr; + info.guest_size_bytes = desc.info.stencil_size; + info.size = desc.info.size; + stencil_id = slot_images.insert(instance, scheduler, info); + RegisterImage(stencil_id); + } + Image& image = slot_images[stencil_id]; + image.AssociateDepth(image_id); + } + return RegisterImageView(image_id, desc.view_info); } From 306279901fccb634b7722de1b4cc17f70dd70f6b Mon Sep 17 00:00:00 2001 From: MajorP93 Date: Fri, 13 Dec 2024 17:30:16 +0100 Subject: [PATCH 85/89] ci: Use link-time optimization for building (#1636) * ci: Use link-time optimization for building * cmake: Set CMP0069 policy to new for external dependencies * This enables LTO also when building external dependencies that do not handle CMP0069 in their CMake scripts. --- .github/workflows/build.yml | 12 ++++++------ externals/CMakeLists.txt | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 878c10868..bacfbea0d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,7 @@ jobs: arch: amd64 - name: Configure CMake - run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS @@ -143,7 +143,7 @@ jobs: arch: amd64 - name: Configure CMake - run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS @@ -201,7 +201,7 @@ jobs: variant: sccache - name: Configure CMake - run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(sysctl -n hw.ncpu) @@ -265,7 +265,7 @@ jobs: variant: sccache - name: Configure CMake - run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(sysctl -n hw.ncpu) @@ -312,7 +312,7 @@ jobs: key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - name: Configure CMake - run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc) @@ -368,7 +368,7 @@ jobs: key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - name: Configure CMake - run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 082be211a..e1e67f235 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -8,6 +8,9 @@ set_directory_properties(PROPERTIES SYSTEM ON ) +# Set CMP0069 policy to "NEW" in order to ensure consistent behavior when building external targets with LTO enabled +set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) + if (MSVC) # Silence "deprecation" warnings add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS) From 028be3ba5d7da1a0782c053f43cf606c78d9b71b Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:49:07 -0800 Subject: [PATCH 86/89] shader_recompiler: Emulate unnormalized sampler coordinates in shader. (#1762) * shader_recompiler: Emulate unnormalized sampler coordinates in shader. * Address review comments. --- .../spirv/emit_spirv_floating_point.cpp | 8 ++++ .../backend/spirv/emit_spirv_instructions.h | 2 + .../frontend/translate/vector_memory.cpp | 1 + src/shader_recompiler/ir/ir_emitter.cpp | 14 +++++++ src/shader_recompiler/ir/ir_emitter.h | 1 + src/shader_recompiler/ir/opcodes.inc | 2 + .../ir/passes/resource_tracking_pass.cpp | 41 ++++++++++++++----- src/shader_recompiler/ir/reg.h | 3 +- src/shader_recompiler/specialization.h | 16 ++++++++ src/video_core/texture_cache/sampler.cpp | 2 +- 10 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp index e822eabef..1e8f31ddc 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp @@ -87,6 +87,14 @@ Id EmitFPMul64(EmitContext& ctx, IR::Inst* inst, Id a, Id b) { return Decorate(ctx, inst, ctx.OpFMul(ctx.F64[1], a, b)); } +Id EmitFPDiv32(EmitContext& ctx, IR::Inst* inst, Id a, Id b) { + return Decorate(ctx, inst, ctx.OpFDiv(ctx.F32[1], a, b)); +} + +Id EmitFPDiv64(EmitContext& ctx, IR::Inst* inst, Id a, Id b) { + return Decorate(ctx, inst, ctx.OpFDiv(ctx.F64[1], a, b)); +} + Id EmitFPNeg16(EmitContext& ctx, Id value) { return ctx.OpFNegate(ctx.F16[1], value); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index cc3db880c..071b430d5 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -189,6 +189,8 @@ Id EmitFPMin64(EmitContext& ctx, Id a, Id b); Id EmitFPMul16(EmitContext& ctx, IR::Inst* inst, Id a, Id b); Id EmitFPMul32(EmitContext& ctx, IR::Inst* inst, Id a, Id b); Id EmitFPMul64(EmitContext& ctx, IR::Inst* inst, Id a, Id b); +Id EmitFPDiv32(EmitContext& ctx, IR::Inst* inst, Id a, Id b); +Id EmitFPDiv64(EmitContext& ctx, IR::Inst* inst, Id a, Id b); Id EmitFPNeg16(EmitContext& ctx, Id value); Id EmitFPNeg32(EmitContext& ctx, Id value); Id EmitFPNeg64(EmitContext& ctx, Id value); diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index b7ad3b36b..74b9c905d 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -527,6 +527,7 @@ IR::Value EmitImageSample(IR::IREmitter& ir, const GcnInst& inst, const IR::Scal info.has_offset.Assign(flags.test(MimgModifier::Offset)); info.has_lod.Assign(flags.any(MimgModifier::Lod)); info.is_array.Assign(mimg.da); + info.is_unnormalized.Assign(mimg.unrm); if (gather) { info.gather_comp.Assign(std::bit_width(mimg.dmask) - 1); diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 78e7f2289..5fa20b744 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -692,6 +692,20 @@ F32F64 IREmitter::FPMul(const F32F64& a, const F32F64& b) { } } +F32F64 IREmitter::FPDiv(const F32F64& a, const F32F64& b) { + if (a.Type() != b.Type()) { + UNREACHABLE_MSG("Mismatching types {} and {}", a.Type(), b.Type()); + } + switch (a.Type()) { + case Type::F32: + return Inst(Opcode::FPDiv32, a, b); + case Type::F64: + return Inst(Opcode::FPDiv64, a, b); + default: + ThrowInvalidType(a.Type()); + } +} + F32F64 IREmitter::FPFma(const F32F64& a, const F32F64& b, const F32F64& c) { if (a.Type() != b.Type() || a.Type() != c.Type()) { UNREACHABLE_MSG("Mismatching types {}, {}, and {}", a.Type(), b.Type(), c.Type()); diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index cbd3780de..e6608cba7 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -158,6 +158,7 @@ public: [[nodiscard]] F32F64 FPAdd(const F32F64& a, const F32F64& b); [[nodiscard]] F32F64 FPSub(const F32F64& a, const F32F64& b); [[nodiscard]] F32F64 FPMul(const F32F64& a, const F32F64& b); + [[nodiscard]] F32F64 FPDiv(const F32F64& a, const F32F64& b); [[nodiscard]] F32F64 FPFma(const F32F64& a, const F32F64& b, const F32F64& c); [[nodiscard]] F32F64 FPAbs(const F32F64& value); diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 0283ccd0f..60232a3a1 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -184,6 +184,8 @@ OPCODE(FPMin32, F32, F32, OPCODE(FPMin64, F64, F64, F64, ) OPCODE(FPMul32, F32, F32, F32, ) OPCODE(FPMul64, F64, F64, F64, ) +OPCODE(FPDiv32, F32, F32, F32, ) +OPCODE(FPDiv64, F64, F64, F64, ) OPCODE(FPNeg32, F32, F32, ) OPCODE(FPNeg64, F64, F64, ) OPCODE(FPRecip32, F32, F32, ) diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 89c5c78a0..995851049 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -420,26 +420,29 @@ void PatchImageSampleInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& descriptors, const IR::Inst* producer, const u32 image_binding, const AmdGpu::Image& image) { // Read sampler sharp. This doesn't exist for IMAGE_LOAD/IMAGE_STORE instructions - const u32 sampler_binding = [&] { + const auto [sampler_binding, sampler] = [&] -> std::pair { ASSERT(producer->GetOpcode() == IR::Opcode::CompositeConstructU32x2); const IR::Value& handle = producer->Arg(1); // Inline sampler resource. if (handle.IsImmediate()) { LOG_WARNING(Render_Vulkan, "Inline sampler detected"); - return descriptors.Add(SamplerResource{ + const auto inline_sampler = AmdGpu::Sampler{.raw0 = handle.U32()}; + const auto binding = descriptors.Add(SamplerResource{ .sharp_idx = std::numeric_limits::max(), - .inline_sampler = AmdGpu::Sampler{.raw0 = handle.U32()}, + .inline_sampler = inline_sampler, }); + return {binding, inline_sampler}; } // Normal sampler resource. const auto ssharp_handle = handle.InstRecursive(); const auto& [ssharp_ud, disable_aniso] = TryDisableAnisoLod0(ssharp_handle); const auto ssharp = TrackSharp(ssharp_ud, info); - return descriptors.Add(SamplerResource{ + const auto binding = descriptors.Add(SamplerResource{ .sharp_idx = ssharp, .associated_image = image_binding, .disable_aniso = disable_aniso, }); + return {binding, info.ReadUdSharp(ssharp)}; }(); IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; @@ -539,28 +542,46 @@ void PatchImageSampleInstruction(IR::Block& block, IR::Inst& inst, Info& info, } }(); + const auto unnormalized = sampler.force_unnormalized || inst_info.is_unnormalized; + // Query dimensions of image if needed for normalization. + // We can't use the image sharp because it could be bound to a different image later. + const auto dimensions = + unnormalized ? ir.ImageQueryDimension(ir.Imm32(image_binding), ir.Imm32(0u), ir.Imm1(false)) + : IR::Value{}; + const auto get_coord = [&](u32 idx, u32 dim_idx) -> IR::Value { + const auto coord = get_addr_reg(idx); + if (unnormalized) { + // Normalize the coordinate for sampling, dividing by its corresponding dimension. + return ir.FPDiv(coord, + ir.BitCast(IR::U32{ir.CompositeExtract(dimensions, dim_idx)})); + } + return coord; + }; + // Now we can load body components as noted in Table 8.9 Image Opcodes with Sampler const IR::Value coords = [&] -> IR::Value { switch (image.GetType()) { case AmdGpu::ImageType::Color1D: // x addr_reg = addr_reg + 1; - return get_addr_reg(addr_reg - 1); + return get_coord(addr_reg - 1, 0); case AmdGpu::ImageType::Color1DArray: // x, slice [[fallthrough]]; case AmdGpu::ImageType::Color2D: // x, y addr_reg = addr_reg + 2; - return ir.CompositeConstruct(get_addr_reg(addr_reg - 2), get_addr_reg(addr_reg - 1)); + return ir.CompositeConstruct(get_coord(addr_reg - 2, 0), get_coord(addr_reg - 1, 1)); case AmdGpu::ImageType::Color2DArray: // x, y, slice [[fallthrough]]; case AmdGpu::ImageType::Color2DMsaa: // x, y, frag - [[fallthrough]]; + addr_reg = addr_reg + 3; + return ir.CompositeConstruct(get_coord(addr_reg - 3, 0), get_coord(addr_reg - 2, 1), + get_addr_reg(addr_reg - 1)); case AmdGpu::ImageType::Color3D: // x, y, z addr_reg = addr_reg + 3; - return ir.CompositeConstruct(get_addr_reg(addr_reg - 3), get_addr_reg(addr_reg - 2), - get_addr_reg(addr_reg - 1)); + return ir.CompositeConstruct(get_coord(addr_reg - 3, 0), get_coord(addr_reg - 2, 1), + get_coord(addr_reg - 1, 2)); case AmdGpu::ImageType::Cube: // x, y, face addr_reg = addr_reg + 3; - return PatchCubeCoord(ir, get_addr_reg(addr_reg - 3), get_addr_reg(addr_reg - 2), + return PatchCubeCoord(ir, get_coord(addr_reg - 3, 0), get_coord(addr_reg - 2, 1), get_addr_reg(addr_reg - 1), false, inst_info.is_array); default: UNREACHABLE(); diff --git a/src/shader_recompiler/ir/reg.h b/src/shader_recompiler/ir/reg.h index 3004d2b86..ca2e9ceb9 100644 --- a/src/shader_recompiler/ir/reg.h +++ b/src/shader_recompiler/ir/reg.h @@ -40,7 +40,8 @@ union TextureInstInfo { BitField<6, 2, u32> gather_comp; BitField<8, 1, u32> has_derivatives; BitField<9, 1, u32> is_array; - BitField<10, 1, u32> is_gather; + BitField<10, 1, u32> is_unnormalized; + BitField<11, 1, u32> is_gather; }; union BufferInstInfo { diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index 2a3bd62f4..bc8627c1c 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -49,6 +49,12 @@ struct FMaskSpecialization { auto operator<=>(const FMaskSpecialization&) const = default; }; +struct SamplerSpecialization { + bool force_unnormalized = false; + + auto operator<=>(const SamplerSpecialization&) const = default; +}; + /** * Alongside runtime information, this structure also checks bound resources * for compatibility. Can be used as a key for storing shader permutations. @@ -67,6 +73,7 @@ struct StageSpecialization { boost::container::small_vector tex_buffers; boost::container::small_vector images; boost::container::small_vector fmasks; + boost::container::small_vector samplers; Backend::Bindings start{}; explicit StageSpecialization(const Info& info_, RuntimeInfo runtime_info_, @@ -107,6 +114,10 @@ struct StageSpecialization { spec.width = sharp.width; spec.height = sharp.height; }); + ForEachSharp(samplers, info->samplers, + [](auto& spec, const auto& desc, AmdGpu::Sampler sharp) { + spec.force_unnormalized = sharp.force_unnormalized; + }); } void ForEachSharp(auto& spec_list, auto& desc_list, auto&& func) { @@ -175,6 +186,11 @@ struct StageSpecialization { return false; } } + for (u32 i = 0; i < samplers.size(); i++) { + if (samplers[i] != other.samplers[i]) { + return false; + } + } return true; } }; diff --git a/src/video_core/texture_cache/sampler.cpp b/src/video_core/texture_cache/sampler.cpp index e47f53abf..9f4bc7a7e 100644 --- a/src/video_core/texture_cache/sampler.cpp +++ b/src/video_core/texture_cache/sampler.cpp @@ -25,7 +25,7 @@ Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sample .minLod = sampler.MinLod(), .maxLod = sampler.MaxLod(), .borderColor = LiverpoolToVK::BorderColor(sampler.border_color_type), - .unnormalizedCoordinates = bool(sampler.force_unnormalized), + .unnormalizedCoordinates = false, // Handled in shader due to Vulkan limitations. }; auto [sampler_result, smplr] = instance.GetDevice().createSamplerUnique(sampler_ci); ASSERT_MSG(sampler_result == vk::Result::eSuccess, "Failed to create sampler: {}", From 722a0e36be3486d2084bae557bc6722d7b895b3d Mon Sep 17 00:00:00 2001 From: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com> Date: Fri, 13 Dec 2024 21:49:37 +0200 Subject: [PATCH 87/89] graphics: Improve handling of color buffer and storage image swizzles (#1763) * liverpool_to_vk: Remove wrong component swap formats * shader_recompiler: Handle storage and buffer format swizzles * shader_recompiler: Skip unsupported depth export * image_view: Remove image format swizzle * Platform support is not always guaranteed --- .../frontend/translate/export.cpp | 5 +++ .../ir/passes/resource_tracking_pass.cpp | 42 +++++++++++++++++++ src/shader_recompiler/specialization.h | 11 ++++- src/video_core/amdgpu/resource.h | 9 ++++ .../renderer_vulkan/liverpool_to_vk.cpp | 9 ---- src/video_core/texture_cache/image_view.cpp | 39 ----------------- 6 files changed, 66 insertions(+), 49 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/export.cpp b/src/shader_recompiler/frontend/translate/export.cpp index f82f8fc1b..f4914577d 100644 --- a/src/shader_recompiler/frontend/translate/export.cpp +++ b/src/shader_recompiler/frontend/translate/export.cpp @@ -13,6 +13,11 @@ void Translator::EmitExport(const GcnInst& inst) { const auto& exp = inst.control.exp; const IR::Attribute attrib{exp.target}; + if (attrib == IR::Attribute::Depth && exp.en != 1) { + LOG_WARNING(Render_Vulkan, "Unsupported depth export"); + return; + } + const std::array vsrc = { IR::VectorReg(inst.src[0].code), IR::VectorReg(inst.src[1].code), diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 995851049..398579ad4 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -137,6 +137,35 @@ bool IsImageInstruction(const IR::Inst& inst) { } } +IR::Value SwizzleVector(IR::IREmitter& ir, auto sharp, IR::Value texel) { + boost::container::static_vector comps; + for (u32 i = 0; i < 4; i++) { + switch (sharp.GetSwizzle(i)) { + case AmdGpu::CompSwizzle::Zero: + comps.emplace_back(ir.Imm32(0.f)); + break; + case AmdGpu::CompSwizzle::One: + comps.emplace_back(ir.Imm32(1.f)); + break; + case AmdGpu::CompSwizzle::Red: + comps.emplace_back(ir.CompositeExtract(texel, 0)); + break; + case AmdGpu::CompSwizzle::Green: + comps.emplace_back(ir.CompositeExtract(texel, 1)); + break; + case AmdGpu::CompSwizzle::Blue: + comps.emplace_back(ir.CompositeExtract(texel, 2)); + break; + case AmdGpu::CompSwizzle::Alpha: + comps.emplace_back(ir.CompositeExtract(texel, 3)); + break; + default: + UNREACHABLE(); + } + } + return ir.CompositeConstruct(comps[0], comps[1], comps[2], comps[3]); +}; + class Descriptors { public: explicit Descriptors(Info& info_) @@ -388,6 +417,15 @@ void PatchTextureBufferInstruction(IR::Block& block, IR::Inst& inst, Info& info, IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; inst.SetArg(0, ir.Imm32(binding)); ASSERT(!buffer.swizzle_enable && !buffer.add_tid_enable); + + // Apply dst_sel swizzle on formatted buffer instructions + if (inst.GetOpcode() == IR::Opcode::StoreBufferFormatF32) { + inst.SetArg(2, SwizzleVector(ir, buffer, inst.Arg(2))); + } else { + const auto inst_info = inst.Flags(); + const auto texel = ir.LoadBufferFormat(inst.Arg(0), inst.Arg(1), inst_info); + inst.ReplaceUsesWith(SwizzleVector(ir, buffer, texel)); + } } IR::Value PatchCubeCoord(IR::IREmitter& ir, const IR::Value& s, const IR::Value& t, @@ -732,6 +770,10 @@ void PatchImageInstruction(IR::Block& block, IR::Inst& inst, Info& info, Descrip }(); inst.SetArg(1, coords); + if (inst.GetOpcode() == IR::Opcode::ImageWrite) { + inst.SetArg(2, SwizzleVector(ir, image, inst.Arg(2))); + } + if (inst_info.has_lod) { ASSERT(inst.GetOpcode() == IR::Opcode::ImageFetch); ASSERT(image.GetType() != AmdGpu::ImageType::Color2DMsaa && diff --git a/src/shader_recompiler/specialization.h b/src/shader_recompiler/specialization.h index bc8627c1c..9b5dd8fa1 100644 --- a/src/shader_recompiler/specialization.h +++ b/src/shader_recompiler/specialization.h @@ -31,6 +31,7 @@ struct BufferSpecialization { struct TextureBufferSpecialization { bool is_integer = false; + u32 dst_select = 0; auto operator<=>(const TextureBufferSpecialization&) const = default; }; @@ -38,8 +39,12 @@ struct TextureBufferSpecialization { struct ImageSpecialization { AmdGpu::ImageType type = AmdGpu::ImageType::Color2D; bool is_integer = false; + u32 dst_select = 0; - auto operator<=>(const ImageSpecialization&) const = default; + bool operator==(const ImageSpecialization& other) const { + return type == other.type && is_integer == other.is_integer && + (dst_select != 0 ? dst_select == other.dst_select : true); + } }; struct FMaskSpecialization { @@ -103,11 +108,15 @@ struct StageSpecialization { ForEachSharp(binding, tex_buffers, info->texture_buffers, [](auto& spec, const auto& desc, AmdGpu::Buffer sharp) { spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); + spec.dst_select = sharp.DstSelect(); }); ForEachSharp(binding, images, info->images, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { spec.type = sharp.GetBoundType(); spec.is_integer = AmdGpu::IsInteger(sharp.GetNumberFmt()); + if (desc.is_storage) { + spec.dst_select = sharp.DstSelect(); + } }); ForEachSharp(binding, fmasks, info->fmasks, [](auto& spec, const auto& desc, AmdGpu::Image sharp) { diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index ba87425f2..5d7417559 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -52,6 +52,10 @@ struct Buffer { return std::memcmp(this, &other, sizeof(Buffer)) == 0; } + u32 DstSelect() const { + return dst_sel_x | (dst_sel_y << 3) | (dst_sel_z << 6) | (dst_sel_w << 9); + } + CompSwizzle GetSwizzle(u32 comp) const noexcept { const std::array select{dst_sel_x, dst_sel_y, dst_sel_z, dst_sel_w}; return static_cast(select[comp]); @@ -204,6 +208,11 @@ struct Image { return dst_sel_x | (dst_sel_y << 3) | (dst_sel_z << 6) | (dst_sel_w << 9); } + CompSwizzle GetSwizzle(u32 comp) const noexcept { + const std::array select{dst_sel_x, dst_sel_y, dst_sel_z, dst_sel_w}; + return static_cast(select[comp]); + } + static char SelectComp(u32 sel) { switch (sel) { case 0: diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index fa8d28ba0..ec0bb3bb7 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -699,15 +699,6 @@ vk::Format AdjustColorBufferFormat(vk::Format base_format, default: break; } - } else if (comp_swap_reverse) { - switch (base_format) { - case vk::Format::eR8G8B8A8Unorm: - return vk::Format::eA8B8G8R8UnormPack32; - case vk::Format::eR8G8B8A8Srgb: - return vk::Format::eA8B8G8R8SrgbPack32; - default: - break; - } } return base_format; } diff --git a/src/video_core/texture_cache/image_view.cpp b/src/video_core/texture_cache/image_view.cpp index 12ad201d1..cc467e9a4 100644 --- a/src/video_core/texture_cache/image_view.cpp +++ b/src/video_core/texture_cache/image_view.cpp @@ -50,34 +50,6 @@ vk::ComponentSwizzle ConvertComponentSwizzle(u32 dst_sel) { } } -bool IsIdentityMapping(u32 dst_sel, u32 num_components) { - return (num_components == 1 && dst_sel == 0b001'000'000'100) || - (num_components == 2 && dst_sel == 0b001'000'101'100) || - (num_components == 3 && dst_sel == 0b001'110'101'100) || - (num_components == 4 && dst_sel == 0b111'110'101'100); -} - -vk::Format TrySwizzleFormat(vk::Format format, u32 dst_sel) { - // BGRA - if (dst_sel == 0b111100101110) { - switch (format) { - case vk::Format::eR8G8B8A8Unorm: - return vk::Format::eB8G8R8A8Unorm; - case vk::Format::eR8G8B8A8Snorm: - return vk::Format::eB8G8R8A8Snorm; - case vk::Format::eR8G8B8A8Uint: - return vk::Format::eB8G8R8A8Uint; - case vk::Format::eR8G8B8A8Sint: - return vk::Format::eB8G8R8A8Sint; - case vk::Format::eR8G8B8A8Srgb: - return vk::Format::eB8G8R8A8Srgb; - default: - break; - } - } - return format; -} - ImageViewInfo::ImageViewInfo(const AmdGpu::Image& image, const Shader::ImageResource& desc) noexcept : is_storage{desc.is_storage} { const auto dfmt = image.GetDataFmt(); @@ -120,17 +92,6 @@ ImageViewInfo::ImageViewInfo(const AmdGpu::Image& image, const Shader::ImageReso mapping.b = ConvertComponentSwizzle(image.dst_sel_z); mapping.a = ConvertComponentSwizzle(image.dst_sel_w); } - // Check for unfortunate case of storage images being swizzled - const u32 num_comps = AmdGpu::NumComponents(image.GetDataFmt()); - const u32 dst_sel = image.DstSelect(); - if (is_storage && !IsIdentityMapping(dst_sel, num_comps)) { - if (auto new_format = TrySwizzleFormat(format, dst_sel); new_format != format) { - format = new_format; - return; - } - LOG_ERROR(Render_Vulkan, "Storage image (num_comps = {}) requires swizzling {}", num_comps, - image.DstSelectName()); - } } ImageViewInfo::ImageViewInfo(const AmdGpu::Liverpool::ColorBuffer& col_buffer) noexcept { From f1c23d514b204e0f90c8538743978706fabc30b8 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:51:39 -0800 Subject: [PATCH 88/89] shader_recompiler: Implement FREXP instructions. (#1766) --- externals/sirit | 2 +- .../spirv/emit_spirv_floating_point.cpp | 26 ++++++++++++- .../backend/spirv/emit_spirv_instructions.h | 7 +++- .../backend/spirv/spirv_emit_context.cpp | 4 ++ .../backend/spirv/spirv_emit_context.h | 2 + .../frontend/translate/translate.h | 5 +++ .../frontend/translate/vector_alu.cpp | 37 ++++++++++++++++++- src/shader_recompiler/ir/ir_emitter.cpp | 33 ++++++++++++++++- src/shader_recompiler/ir/ir_emitter.h | 4 +- src/shader_recompiler/ir/opcodes.inc | 7 +++- 10 files changed, 119 insertions(+), 8 deletions(-) diff --git a/externals/sirit b/externals/sirit index 6cecb95d6..e12b6b592 160000 --- a/externals/sirit +++ b/externals/sirit @@ -1 +1 @@ -Subproject commit 6cecb95d679c82c413d1f989e0b7ad9af130600d +Subproject commit e12b6b592ce9917a85303c555259488643c56f47 diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp index 1e8f31ddc..a63be87e2 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp @@ -225,10 +225,34 @@ Id EmitFPTrunc64(EmitContext& ctx, Id value) { return ctx.OpTrunc(ctx.F64[1], value); } -Id EmitFPFract(EmitContext& ctx, Id value) { +Id EmitFPFract32(EmitContext& ctx, Id value) { return ctx.OpFract(ctx.F32[1], value); } +Id EmitFPFract64(EmitContext& ctx, Id value) { + return ctx.OpFract(ctx.F64[1], value); +} + +Id EmitFPFrexpSig32(EmitContext& ctx, Id value) { + const auto frexp = ctx.OpFrexpStruct(ctx.frexp_result_f32, value); + return ctx.OpCompositeExtract(ctx.F32[1], frexp, 0); +} + +Id EmitFPFrexpSig64(EmitContext& ctx, Id value) { + const auto frexp = ctx.OpFrexpStruct(ctx.frexp_result_f64, value); + return ctx.OpCompositeExtract(ctx.F64[1], frexp, 0); +} + +Id EmitFPFrexpExp32(EmitContext& ctx, Id value) { + const auto frexp = ctx.OpFrexpStruct(ctx.frexp_result_f32, value); + return ctx.OpCompositeExtract(ctx.U32[1], frexp, 1); +} + +Id EmitFPFrexpExp64(EmitContext& ctx, Id value) { + const auto frexp = ctx.OpFrexpStruct(ctx.frexp_result_f64, value); + return ctx.OpCompositeExtract(ctx.U32[1], frexp, 1); +} + Id EmitFPOrdEqual16(EmitContext& ctx, Id lhs, Id rhs) { return ctx.OpFOrdEqual(ctx.U1[1], lhs, rhs); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 071b430d5..4ff53670e 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -222,7 +222,12 @@ Id EmitFPCeil64(EmitContext& ctx, Id value); Id EmitFPTrunc16(EmitContext& ctx, Id value); Id EmitFPTrunc32(EmitContext& ctx, Id value); Id EmitFPTrunc64(EmitContext& ctx, Id value); -Id EmitFPFract(EmitContext& ctx, Id value); +Id EmitFPFract32(EmitContext& ctx, Id value); +Id EmitFPFract64(EmitContext& ctx, Id value); +Id EmitFPFrexpSig32(EmitContext& ctx, Id value); +Id EmitFPFrexpSig64(EmitContext& ctx, Id value); +Id EmitFPFrexpExp32(EmitContext& ctx, Id value); +Id EmitFPFrexpExp64(EmitContext& ctx, Id value); Id EmitFPOrdEqual16(EmitContext& ctx, Id lhs, Id rhs); Id EmitFPOrdEqual32(EmitContext& ctx, Id lhs, Id rhs); Id EmitFPOrdEqual64(EmitContext& ctx, Id lhs, Id rhs); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 5c7278c6b..1ada2f1f9 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -147,6 +147,10 @@ void EmitContext::DefineArithmeticTypes() { full_result_i32x2 = Name(TypeStruct(S32[1], S32[1]), "full_result_i32x2"); full_result_u32x2 = Name(TypeStruct(U32[1], U32[1]), "full_result_u32x2"); + frexp_result_f32 = Name(TypeStruct(F32[1], U32[1]), "frexp_result_f32"); + if (info.uses_fp64) { + frexp_result_f64 = Name(TypeStruct(F64[1], U32[1]), "frexp_result_f64"); + } } void EmitContext::DefineInterfaces() { diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 4e5e7dd3b..cd1293328 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -148,6 +148,8 @@ public: Id full_result_i32x2; Id full_result_u32x2; + Id frexp_result_f32; + Id frexp_result_f64; Id pi_x2; diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 43f3ccef2..2f320a6c7 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -200,6 +200,11 @@ public: void V_BFREV_B32(const GcnInst& inst); void V_FFBH_U32(const GcnInst& inst); void V_FFBL_B32(const GcnInst& inst); + void V_FREXP_EXP_I32_F64(const GcnInst& inst); + void V_FREXP_MANT_F64(const GcnInst& inst); + void V_FRACT_F64(const GcnInst& inst); + void V_FREXP_EXP_I32_F32(const GcnInst& inst); + void V_FREXP_MANT_F32(const GcnInst& inst); void V_MOVRELD_B32(const GcnInst& inst); void V_MOVRELS_B32(const GcnInst& inst); void V_MOVRELSD_B32(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 8149230db..3e9e677a7 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -179,6 +179,16 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { return V_FFBH_U32(inst); case Opcode::V_FFBL_B32: return V_FFBL_B32(inst); + case Opcode::V_FREXP_EXP_I32_F64: + return V_FREXP_EXP_I32_F64(inst); + case Opcode::V_FREXP_MANT_F64: + return V_FREXP_MANT_F64(inst); + case Opcode::V_FRACT_F64: + return V_FRACT_F64(inst); + case Opcode::V_FREXP_EXP_I32_F32: + return V_FREXP_EXP_I32_F32(inst); + case Opcode::V_FREXP_MANT_F32: + return V_FREXP_MANT_F32(inst); case Opcode::V_MOVRELD_B32: return V_MOVRELD_B32(inst); case Opcode::V_MOVRELS_B32: @@ -733,7 +743,7 @@ void Translator::V_CVT_F32_UBYTE(u32 index, const GcnInst& inst) { void Translator::V_FRACT_F32(const GcnInst& inst) { const IR::F32 src0{GetSrc(inst.src[0])}; - SetDst(inst.dst[0], ir.Fract(src0)); + SetDst(inst.dst[0], ir.FPFract(src0)); } void Translator::V_TRUNC_F32(const GcnInst& inst) { @@ -822,6 +832,31 @@ void Translator::V_FFBL_B32(const GcnInst& inst) { SetDst(inst.dst[0], ir.FindILsb(src0)); } +void Translator::V_FREXP_EXP_I32_F64(const GcnInst& inst) { + const IR::F64 src0{GetSrc64(inst.src[0])}; + SetDst(inst.dst[0], ir.FPFrexpExp(src0)); +} + +void Translator::V_FREXP_MANT_F64(const GcnInst& inst) { + const IR::F64 src0{GetSrc64(inst.src[0])}; + SetDst64(inst.dst[0], ir.FPFrexpSig(src0)); +} + +void Translator::V_FRACT_F64(const GcnInst& inst) { + const IR::F32 src0{GetSrc64(inst.src[0])}; + SetDst64(inst.dst[0], ir.FPFract(src0)); +} + +void Translator::V_FREXP_EXP_I32_F32(const GcnInst& inst) { + const IR::F32 src0{GetSrc(inst.src[0])}; + SetDst(inst.dst[0], ir.FPFrexpExp(src0)); +} + +void Translator::V_FREXP_MANT_F32(const GcnInst& inst) { + const IR::F32 src0{GetSrc(inst.src[0])}; + SetDst(inst.dst[0], ir.FPFrexpSig(src0)); +} + void Translator::V_MOVRELD_B32(const GcnInst& inst) { const IR::U32 src_val{GetSrc(inst.src[0])}; u32 dst_vgprno = inst.dst[0].code - static_cast(IR::VectorReg::V0); diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 5fa20b744..29b406699 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -869,8 +869,37 @@ F32F64 IREmitter::FPTrunc(const F32F64& value) { } } -F32 IREmitter::Fract(const F32& value) { - return Inst(Opcode::FPFract, value); +F32F64 IREmitter::FPFract(const F32F64& value) { + switch (value.Type()) { + case Type::F32: + return Inst(Opcode::FPFract32, value); + case Type::F64: + return Inst(Opcode::FPFract64, value); + default: + ThrowInvalidType(value.Type()); + } +} + +F32F64 IREmitter::FPFrexpSig(const F32F64& value) { + switch (value.Type()) { + case Type::F32: + return Inst(Opcode::FPFrexpSig32, value); + case Type::F64: + return Inst(Opcode::FPFrexpSig64, value); + default: + ThrowInvalidType(value.Type()); + } +} + +U32 IREmitter::FPFrexpExp(const F32F64& value) { + switch (value.Type()) { + case Type::F32: + return Inst(Opcode::FPFrexpExp32, value); + case Type::F64: + return Inst(Opcode::FPFrexpExp64, value); + default: + ThrowInvalidType(value.Type()); + } } U1 IREmitter::FPEqual(const F32F64& lhs, const F32F64& rhs, bool ordered) { diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index e6608cba7..f77e22b82 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -180,7 +180,9 @@ public: [[nodiscard]] F32F64 FPFloor(const F32F64& value); [[nodiscard]] F32F64 FPCeil(const F32F64& value); [[nodiscard]] F32F64 FPTrunc(const F32F64& value); - [[nodiscard]] F32 Fract(const F32& value); + [[nodiscard]] F32F64 FPFract(const F32F64& value); + [[nodiscard]] F32F64 FPFrexpSig(const F32F64& value); + [[nodiscard]] U32 FPFrexpExp(const F32F64& value); [[nodiscard]] U1 FPEqual(const F32F64& lhs, const F32F64& rhs, bool ordered = true); [[nodiscard]] U1 FPNotEqual(const F32F64& lhs, const F32F64& rhs, bool ordered = true); diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 60232a3a1..8f40ed985 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -210,7 +210,12 @@ OPCODE(FPCeil32, F32, F32, OPCODE(FPCeil64, F64, F64, ) OPCODE(FPTrunc32, F32, F32, ) OPCODE(FPTrunc64, F64, F64, ) -OPCODE(FPFract, F32, F32, ) +OPCODE(FPFract32, F32, F32, ) +OPCODE(FPFract64, F64, F64, ) +OPCODE(FPFrexpSig32, F32, F32, ) +OPCODE(FPFrexpSig64, F64, F64, ) +OPCODE(FPFrexpExp32, U32, F32, ) +OPCODE(FPFrexpExp64, U32, F64, ) OPCODE(FPOrdEqual32, U1, F32, F32, ) OPCODE(FPOrdEqual64, U1, F64, F64, ) From 715ac8a2795be8a471e8ff9c02c716725215c43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Cea=20L=C3=B3pez?= Date: Fri, 13 Dec 2024 23:27:09 +0100 Subject: [PATCH 89/89] vk_shader_hle: Don't alter the order of the skipped copies. (#1757) * vk_shader_hle: Don't alter the order of the skipped copies. * Simplification. * Format. * More simplification. --- src/video_core/renderer_vulkan/vk_shader_hle.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_hle.cpp b/src/video_core/renderer_vulkan/vk_shader_hle.cpp index d1d4f9af3..b863dce21 100644 --- a/src/video_core/renderer_vulkan/vk_shader_hle.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_hle.cpp @@ -60,7 +60,7 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg static constexpr vk::DeviceSize MaxDistanceForMerge = 64_MB; u32 batch_start = 0; - u32 batch_end = copies.size() > 1 ? 1 : 0; + u32 batch_end = 0; while (batch_end < copies.size()) { // Place first copy into the current batch @@ -70,19 +70,19 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg auto dst_offset_min = copy.dstOffset; auto dst_offset_max = copy.dstOffset + copy.size; - for (int i = batch_start + 1; i < copies.size(); i++) { + for (++batch_end; batch_end < copies.size(); batch_end++) { // Compute new src and dst bounds if we were to batch this copy - const auto& [src_offset, dst_offset, size] = copies[i]; + const auto& [src_offset, dst_offset, size] = copies[batch_end]; auto new_src_offset_min = std::min(src_offset_min, src_offset); auto new_src_offset_max = std::max(src_offset_max, src_offset + size); if (new_src_offset_max - new_src_offset_min > MaxDistanceForMerge) { - continue; + break; } auto new_dst_offset_min = std::min(dst_offset_min, dst_offset); auto new_dst_offset_max = std::max(dst_offset_max, dst_offset + size); if (new_dst_offset_max - new_dst_offset_min > MaxDistanceForMerge) { - continue; + break; } // We can batch this copy @@ -90,10 +90,6 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg src_offset_max = new_src_offset_max; dst_offset_min = new_dst_offset_min; dst_offset_max = new_dst_offset_max; - if (i != batch_end) { - std::swap(copies[i], copies[batch_end]); - } - ++batch_end; } // Obtain buffers for the total source and destination ranges. @@ -116,7 +112,6 @@ bool ExecuteCopyShaderHLE(const Shader::Info& info, const AmdGpu::Liverpool::Reg src_offset_max - src_offset_min, dst_offset_max - dst_offset_min); scheduler.CommandBuffer().copyBuffer(src_buf->Handle(), dst_buf->Handle(), vk_copies); batch_start = batch_end; - ++batch_end; } scheduler.CommandBuffer().pipelineBarrier(