From 997ad60c5047dbb0c6ecd2a410c709a6328c0c10 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sun, 6 Sep 2026 08:34:58 +0400 Subject: [PATCH] system: keep error dialogs open until dismissed --- src/graphics/presentation/imeOverlay.h | 42 ---- src/graphics/presentation/presenter.h | 2 +- .../{imeOverlay.cpp => systemOverlay.cpp} | 228 +++++++++++++----- src/graphics/presentation/systemOverlay.h | 42 ++++ src/graphics/presentation/videoOut.cpp | 2 +- .../presentation/window/swapchain.cpp | 65 ++--- .../presentation/window/vulkanWindow.cpp | 4 +- src/graphics/presentation/window/window.cpp | 6 +- src/libs/dialog.cpp | 70 +++++- src/libs/dialog.h | 15 ++ src/libs/libSystemService.cpp | 4 +- tests/ShaderRecompilerComputeTests.cpp | 142 +++++++++++ 12 files changed, 466 insertions(+), 156 deletions(-) delete mode 100644 src/graphics/presentation/imeOverlay.h rename src/graphics/presentation/{imeOverlay.cpp => systemOverlay.cpp} (81%) create mode 100644 src/graphics/presentation/systemOverlay.h diff --git a/src/graphics/presentation/imeOverlay.h b/src/graphics/presentation/imeOverlay.h deleted file mode 100644 index 6ba71c8db..000000000 --- a/src/graphics/presentation/imeOverlay.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEOVERLAY_H_ -#define EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEOVERLAY_H_ - -#include "common/common.h" -#include "graphics/host_gpu/vulkanCommon.h" - -#include - -union SDL_Event; - -namespace Libs::Graphics { - -struct GraphicContext; - -struct ImeVisualState { - bool active; - uint64_t revision; -}; - -void InitializeImeInput(); -void ShutdownImeInput(); -bool ProcessImeInput(const SDL_Event& event); -ImeVisualState GetImeVisualState() noexcept; - -class ImeOverlay final { -public: - explicit ImeOverlay(GraphicContext& graphics); - ~ImeOverlay(); - KYTY_CLASS_NO_COPY(ImeOverlay); - - [[nodiscard]] bool PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count); - void Record(vk::CommandBuffer command, vk::ImageView target); - void ReleaseVulkan(); - -private: - struct Impl; - std::unique_ptr m_impl; -}; - -} // namespace Libs::Graphics - -#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_IMEOVERLAY_H_ diff --git a/src/graphics/presentation/presenter.h b/src/graphics/presentation/presenter.h index 451e1ad90..4399e58af 100644 --- a/src/graphics/presentation/presenter.h +++ b/src/graphics/presentation/presenter.h @@ -25,7 +25,7 @@ public: CommandBuffer* producer = nullptr); [[nodiscard]] Frame* PrepareLastFrame(); [[nodiscard]] bool IsGuestPaused() const noexcept; - [[nodiscard]] bool NeedsImeRefresh() const noexcept; + [[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept; [[nodiscard]] RenderContext& Renderer() const noexcept; void Present(Frame& frame, bool reuse = false); void Discard(Frame& frame); diff --git a/src/graphics/presentation/imeOverlay.cpp b/src/graphics/presentation/systemOverlay.cpp similarity index 81% rename from src/graphics/presentation/imeOverlay.cpp rename to src/graphics/presentation/systemOverlay.cpp index afb9903aa..adc2e2b7a 100644 --- a/src/graphics/presentation/imeOverlay.cpp +++ b/src/graphics/presentation/systemOverlay.cpp @@ -1,4 +1,4 @@ -#include "graphics/presentation/imeOverlay.h" +#include "graphics/presentation/systemOverlay.h" #include "SDL.h" #include "common/assert.h" @@ -7,6 +7,7 @@ #include "imgui.h" #include "imgui_impl_vulkan.h" #include "libs/controller.h" +#include "libs/dialog.h" #include "libs/ime.h" #include "libs/imeDialog.h" @@ -29,6 +30,7 @@ namespace { namespace CoreIme = Libs::Ime; namespace DialogIme = Libs::Dialog::ImeDialog; +namespace ErrorDialog = Libs::Dialog::ErrorDialog; namespace Ime { @@ -101,6 +103,34 @@ bool HostQueueExternalInput(uint64_t generation, ExternalInput input) { } // namespace Ime +enum class OverlayKind : uint8_t { None, Ime, Error }; + +struct OverlaySession { + OverlayKind kind = OverlayKind::None; + uint64_t generation = 0; + + bool operator==(const OverlaySession&) const = default; +}; + +struct OverlaySnapshot { + OverlaySession session; + Ime::HostSnapshot ime; + ErrorDialog::HostSnapshot error; +}; + +bool GetOverlaySnapshot(OverlaySnapshot* snapshot) { + if (ErrorDialog::GetHostSnapshot(&snapshot->error)) { + snapshot->session = {OverlayKind::Error, snapshot->error.generation}; + return true; + } + if (Ime::GetHostSnapshot(&snapshot->ime)) { + snapshot->session = {OverlayKind::Ime, snapshot->ime.generation}; + return true; + } + snapshot->session = {}; + return false; +} + constexpr size_t INPUT_QUEUE_CAPACITY = 128; enum class InputKind : uint8_t { @@ -113,19 +143,20 @@ enum class InputKind : uint8_t { }; struct InputEvent { - InputKind kind; - uint64_t generation; - int id; - float x; - float y; + InputKind kind; + OverlaySession session; + int id; + float x; + float y; }; struct VisibilityUpdate { - uint64_t generation; - bool visible; - bool capture_controller; - bool capture_keyboard; - bool multiline; + OverlaySession session; + bool visible; + bool capture_controller; + bool capture_keyboard; + bool text_input; + bool multiline; }; std::atomic g_visibility_event {static_cast(-1)}; @@ -137,15 +168,14 @@ size_t g_missing_visibility_wakeups = 0; bool g_input_reset_requested = false; uint16_t g_last_external_keycode = 0; uint32_t g_last_external_status = 0; -uint64_t g_input_generation = 0; +OverlaySession g_input_session; bool g_input_active = false; bool g_input_controller = false; bool g_input_keyboard = false; bool g_input_multiline = false; bool g_input_lifecycle_active = false; -bool g_active = false; bool g_controller_captured = false; -uint64_t g_generation = 0; +OverlaySession g_session; void ClearInputEvents() { std::scoped_lock lock(g_input_mutex); @@ -159,7 +189,7 @@ void QueueInput(InputEvent event) { event.kind == InputKind::Axis || event.kind == InputKind::MousePosition; if (replaceable && !g_input_events.empty()) { auto& last = g_input_events.back(); - if (last.generation == event.generation && last.kind == event.kind && last.id == event.id) { + if (last.session == event.session && last.kind == event.kind && last.id == event.id) { last = event; return; } @@ -192,34 +222,36 @@ void RefreshVisibility() { if (!g_input_lifecycle_active) { return; } - Ime::HostSnapshot snapshot; - const bool visible = Ime::GetHostSnapshot(&snapshot); - const uint64_t generation = visible ? snapshot.generation : g_generation; - if (visible == g_active && (!visible || generation == g_generation)) { + OverlaySnapshot snapshot; + const bool visible = GetOverlaySnapshot(&snapshot); + if (snapshot.session == g_session) { return; } - g_generation = generation; + g_session = snapshot.session; bool capture_controller = false; bool capture_keyboard = false; + bool text_input = false; bool multiline = false; - if (visible) { - capture_controller = (snapshot.disable_device & Ime::DISABLE_DEVICE_CONTROLLER) == 0; - capture_keyboard = (snapshot.disable_device & Ime::DISABLE_DEVICE_EXT_KEYBOARD) == 0; - multiline = (snapshot.option & Ime::OPTION_MULTILINE) != 0; + if (snapshot.session.kind == OverlayKind::Error) { + capture_controller = true; + capture_keyboard = true; + } else if (snapshot.session.kind == OverlayKind::Ime) { + capture_controller = (snapshot.ime.disable_device & Ime::DISABLE_DEVICE_CONTROLLER) == 0; + capture_keyboard = (snapshot.ime.disable_device & Ime::DISABLE_DEVICE_EXT_KEYBOARD) == 0; + text_input = capture_keyboard; + multiline = (snapshot.ime.option & Ime::OPTION_MULTILINE) != 0; } const bool was_controller = std::exchange(g_controller_captured, capture_controller); if (capture_controller || was_controller) { Controller::ResetInputState(); } - g_active = visible; - const Uint32 type = g_visibility_event.load(std::memory_order_acquire); if (type != static_cast(-1)) { SDL_Event event {}; event.type = type; std::scoped_lock input_lock(g_input_mutex); - g_visibility_updates.push_back( - {generation, visible, capture_controller, capture_keyboard, multiline}); + g_visibility_updates.push_back({snapshot.session, visible, capture_controller, + capture_keyboard, text_input, multiline}); if (g_missing_visibility_wakeups != 0 || SDL_PushEvent(&event) <= 0) { g_missing_visibility_wakeups++; } @@ -383,7 +415,7 @@ void CheckVulkanResult(VkResult result) { } // namespace -void InitializeImeInput() { +void InitializeSystemOverlayInput() { const Uint32 type = SDL_RegisterEvents(1); EXIT_IF(type == static_cast(-1)); { @@ -393,16 +425,18 @@ void InitializeImeInput() { } CoreIme::SetVisibilityCallback(OnCoreVisibilityChanged); DialogIme::SetVisibilityCallback(OnDialogVisibilityChanged); + ErrorDialog::SetVisibilityCallback(RefreshVisibility); RefreshVisibility(); } -void ShutdownImeInput() { +void ShutdownSystemOverlayInput() { CoreIme::SetVisibilityCallback(nullptr); DialogIme::SetVisibilityCallback(nullptr); + ErrorDialog::SetVisibilityCallback(nullptr); { std::scoped_lock lock(g_visibility_mutex); g_input_lifecycle_active = false; - g_active = false; + g_session = {}; if (std::exchange(g_controller_captured, false)) { Controller::ResetInputState(); } @@ -415,6 +449,7 @@ void ShutdownImeInput() { g_missing_visibility_wakeups = 0; } g_input_active = false; + g_input_session = {}; g_input_controller = false; g_input_keyboard = false; g_input_multiline = false; @@ -423,13 +458,15 @@ void ShutdownImeInput() { } } -ImeVisualState GetImeVisualState() noexcept { +SystemOverlayVisualState GetSystemOverlayVisualState() noexcept { const auto core = CoreIme::GetVisualState(); const auto dialog = DialogIme::GetVisualState(); - return {core.active || dialog.active, core.revision + dialog.revision}; + const auto error = ErrorDialog::GetVisualState(); + return {core.active || dialog.active || error.active, + core.revision + dialog.revision + error.revision}; } -bool ProcessImeInput(const SDL_Event& event) { +bool ProcessSystemOverlayInput(const SDL_Event& event) { RetryVisibilityWakeup(); if (event.type == g_visibility_event.load(std::memory_order_acquire)) { VisibilityUpdate update {}; @@ -444,12 +481,12 @@ bool ProcessImeInput(const SDL_Event& event) { ClearInputEvents(); g_last_external_keycode = 0; g_last_external_status = 0; - g_input_generation = update.generation; + g_input_session = update.session; g_input_active = update.visible; g_input_controller = update.capture_controller; g_input_keyboard = update.capture_keyboard; g_input_multiline = update.multiline; - if (g_input_keyboard) { + if (update.text_input) { SDL_StartTextInput(); } else if (SDL_IsTextInputActive() == SDL_TRUE) { SDL_StopTextInput(); @@ -458,7 +495,7 @@ bool ProcessImeInput(const SDL_Event& event) { } if (event.type == SDL_CONTROLLERDEVICEREMOVED) { if (g_input_active && g_input_controller) { - QueueInput({InputKind::ResetController, g_input_generation, 0, 0.0f, 0.0f}); + QueueInput({InputKind::ResetController, g_input_session, 0, 0.0f, 0.0f}); } return false; } @@ -466,7 +503,8 @@ bool ProcessImeInput(const SDL_Event& event) { return false; } - const uint64_t generation = g_input_generation; + const auto session = g_input_session; + const uint64_t generation = session.generation; const bool controller_event = event.type == SDL_CONTROLLERBUTTONDOWN || event.type == SDL_CONTROLLERBUTTONUP || event.type == SDL_CONTROLLERAXISMOTION; @@ -478,6 +516,13 @@ bool ProcessImeInput(const SDL_Event& event) { if (keyboard_event && !g_input_keyboard) { return false; } + if (keyboard_event && session.kind == OverlayKind::Error) { + if (event.type == SDL_KEYDOWN && event.key.repeat == 0 && + (event.key.keysym.sym == SDLK_RETURN || event.key.keysym.sym == SDLK_KP_ENTER)) { + ErrorDialog::HostAccept(generation); + } + return true; + } switch (event.type) { case SDL_TEXTINPUT: { const auto text = Utf8ToUtf16(event.text.text); @@ -524,31 +569,33 @@ bool ProcessImeInput(const SDL_Event& event) { case SDL_KEYUP: return true; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: - QueueInput({InputKind::Button, generation, event.cbutton.button, + QueueInput({InputKind::Button, session, event.cbutton.button, event.type == SDL_CONTROLLERBUTTONDOWN ? 1.0f : 0.0f, 0.0f}); return true; case SDL_CONTROLLERAXISMOTION: - QueueInput({InputKind::Axis, generation, event.caxis.axis, + QueueInput({InputKind::Axis, session, event.caxis.axis, static_cast(event.caxis.value), 0.0f}); return true; case SDL_MOUSEMOTION: - QueueInput({InputKind::MousePosition, generation, 0, static_cast(event.motion.x), + QueueInput({InputKind::MousePosition, session, 0, static_cast(event.motion.x), static_cast(event.motion.y)}); return true; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: - QueueInput({InputKind::MouseButton, generation, event.button.button, + QueueInput({InputKind::MousePosition, session, 0, static_cast(event.button.x), + static_cast(event.button.y)}); + QueueInput({InputKind::MouseButton, session, event.button.button, event.type == SDL_MOUSEBUTTONDOWN ? 1.0f : 0.0f, 0.0f}); return true; case SDL_MOUSEWHEEL: - QueueInput({InputKind::MouseWheel, generation, 0, static_cast(event.wheel.x), + QueueInput({InputKind::MouseWheel, session, 0, static_cast(event.wheel.x), static_cast(event.wheel.y)}); return true; default: return false; } } -struct ImeOverlay::Impl { +struct SystemOverlay::Impl { explicit Impl(GraphicContext& context): graphics(context) {} ~Impl() { @@ -571,7 +618,7 @@ struct ImeOverlay::Impl { io.LogFilename = nullptr; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; io.BackendFlags |= ImGuiBackendFlags_HasGamepad; - io.BackendPlatformName = "Kyty IME input"; + io.BackendPlatformName = "Kyty system overlay input"; ImGui::StyleColorsDark(); auto& style = ImGui::GetStyle(); style.WindowRounding = 10.0f; @@ -609,7 +656,7 @@ struct ImeOverlay::Impl { vulkan_initialized = true; } - void DrainInput(uint64_t generation) { + void DrainInput(OverlaySession session) { std::deque events; bool reset = false; { @@ -626,16 +673,18 @@ struct ImeOverlay::Impl { right_stick = {}; } for (const auto& event: events) { - if (event.generation != generation) { + if (event.session != session) { continue; } switch (event.kind) { case InputKind::Button: { const bool down = event.x != 0.0f; - if (down && event.id == SDL_CONTROLLER_BUTTON_B) { - Ime::HostCancel(generation); - } else if (down && event.id == SDL_CONTROLLER_BUTTON_Y) { - Ime::HostBackspace(generation); + if (session.kind == OverlayKind::Ime && down) { + if (event.id == SDL_CONTROLLER_BUTTON_B) { + Ime::HostCancel(session.generation); + } else if (event.id == SDL_CONTROLLER_BUTTON_Y) { + Ime::HostBackspace(session.generation); + } } const ImGuiKey key = ControllerButtonToKey(event.id); if (key != ImGuiKey_None) { @@ -732,7 +781,7 @@ struct ImeOverlay::Impl { focus_pending = false; } - void DrawDialog(const Ime::HostSnapshot& snapshot, vk::Extent2D extent) { + void DrawIme(const Ime::HostSnapshot& snapshot, vk::Extent2D extent) { const ImVec2 display(static_cast(extent.width), static_cast(extent.height)); const bool over_2k = (snapshot.option & Ime::OPTION_USE_OVER_2K) != 0; const float reference_width = over_2k ? 3840.0f : 1920.0f; @@ -851,17 +900,58 @@ struct ImeOverlay::Impl { ImGui::End(); } + void DrawError(const ErrorDialog::HostSnapshot& snapshot, vk::Extent2D extent) { + const ImVec2 display(static_cast(extent.width), static_cast(extent.height)); + const float scale = std::max(std::min(display.x / 1280.0f, display.y / 720.0f), 0.5f); + ImGui::GetBackgroundDrawList()->AddRectFilled({0.0f, 0.0f}, display, + IM_COL32(0, 0, 0, 160)); + ImGui::SetNextWindowPos({display.x * 0.5f, display.y * 0.5f}, ImGuiCond_Always, + {0.5f, 0.5f}); + ImGui::SetNextWindowSize( + {std::max(std::min(620.0f * scale, display.x - 32.0f), 1.0f), 0.0f}, ImGuiCond_Always); + if (focus_pending) { + ImGui::SetNextWindowFocus(); + } + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {24.0f * scale, 24.0f * scale}); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {12.0f * scale, 16.0f * scale}); + ImGui::PushFont(nullptr, 20.0f * scale); + constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize; + ImGui::Begin("##SystemError", nullptr, flags); + ImGui::TextUnformatted("Error"); + ImGui::Separator(); + const auto error_code = static_cast(snapshot.error_code); + ImGui::TextWrapped("%s", error_code == 0x80550006u + ? "You are not signed in to PlayStation Network." + : "An error has occurred."); + ImGui::TextDisabled("Error code: 0x%08X", error_code); + const float button_width = std::min(140.0f * scale, ImGui::GetContentRegionAvail().x); + ImGui::SetCursorPosX((ImGui::GetWindowSize().x - button_width) * 0.5f); + const bool accepted = ImGui::Button("OK", {button_width, 44.0f * scale}); + if (focus_pending) { + ImGui::SetItemDefaultFocus(); + focus_pending = false; + } + ImGui::End(); + ImGui::PopFont(); + ImGui::PopStyleVar(2); + if (accepted) { + ErrorDialog::HostAccept(snapshot.generation); + } + } + bool PrepareFrame(vk::Extent2D frame_extent, vk::Format format, uint32_t image_count) { - Ime::HostSnapshot snapshot; - if (!Ime::GetHostSnapshot(&snapshot)) { + OverlaySnapshot snapshot; + if (!GetOverlaySnapshot(&snapshot)) { return false; } - const uint64_t prepared_generation = snapshot.generation; + const auto prepared_session = snapshot.session; EnsureVulkan(format, image_count); - if (generation != snapshot.generation) { - generation = snapshot.generation; + if (session != snapshot.session) { + session = snapshot.session; focus_pending = true; - shift = (snapshot.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0; + shift = (snapshot.ime.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0; symbol_mode = false; panel_offset = {}; right_stick = {}; @@ -870,7 +960,7 @@ struct ImeOverlay::Impl { io.ClearInputKeys(); io.ClearInputMouse(); } - DrainInput(snapshot.generation); + DrainInput(snapshot.session); auto& io = ImGui::GetIO(); io.DisplaySize = {static_cast(frame_extent.width), @@ -883,11 +973,15 @@ struct ImeOverlay::Impl { last_frame = now; ImGui_ImplVulkan_NewFrame(); ImGui::NewFrame(); - if (!Ime::GetHostSnapshot(&snapshot) || snapshot.generation != prepared_generation) { + if (!GetOverlaySnapshot(&snapshot) || snapshot.session != prepared_session) { ImGui::EndFrame(); return false; } - DrawDialog(snapshot, frame_extent); + if (snapshot.session.kind == OverlayKind::Error) { + DrawError(snapshot.error, frame_extent); + } else { + DrawIme(snapshot.ime, frame_extent); + } ImGui::Render(); extent = frame_extent; return true; @@ -934,24 +1028,24 @@ struct ImeOverlay::Impl { float button_height = 42.0f; ImVec2 panel_offset {}; ImVec2 right_stick {}; - uint64_t generation = 0; + OverlaySession session; vk::Extent2D extent {}; std::chrono::steady_clock::time_point last_frame; }; -ImeOverlay::ImeOverlay(GraphicContext& graphics): m_impl(std::make_unique(graphics)) {} +SystemOverlay::SystemOverlay(GraphicContext& graphics): m_impl(std::make_unique(graphics)) {} -ImeOverlay::~ImeOverlay() = default; +SystemOverlay::~SystemOverlay() = default; -bool ImeOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) { +bool SystemOverlay::PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count) { return m_impl->PrepareFrame(extent, format, image_count); } -void ImeOverlay::Record(vk::CommandBuffer command, vk::ImageView target) { +void SystemOverlay::Record(vk::CommandBuffer command, vk::ImageView target) { m_impl->Record(command, target); } -void ImeOverlay::ReleaseVulkan() { +void SystemOverlay::ReleaseVulkan() { m_impl->ReleaseVulkan(); } diff --git a/src/graphics/presentation/systemOverlay.h b/src/graphics/presentation/systemOverlay.h new file mode 100644 index 000000000..4c6fe0618 --- /dev/null +++ b/src/graphics/presentation/systemOverlay.h @@ -0,0 +1,42 @@ +#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_SYSTEMOVERLAY_H_ +#define EMULATOR_SRC_GRAPHICS_PRESENTATION_SYSTEMOVERLAY_H_ + +#include "common/common.h" +#include "graphics/host_gpu/vulkanCommon.h" + +#include + +union SDL_Event; + +namespace Libs::Graphics { + +struct GraphicContext; + +struct SystemOverlayVisualState { + bool active; + uint64_t revision; +}; + +void InitializeSystemOverlayInput(); +void ShutdownSystemOverlayInput(); +bool ProcessSystemOverlayInput(const SDL_Event& event); +SystemOverlayVisualState GetSystemOverlayVisualState() noexcept; + +class SystemOverlay final { +public: + explicit SystemOverlay(GraphicContext& graphics); + ~SystemOverlay(); + KYTY_CLASS_NO_COPY(SystemOverlay); + + [[nodiscard]] bool PrepareFrame(vk::Extent2D extent, vk::Format format, uint32_t image_count); + void Record(vk::CommandBuffer command, vk::ImageView target); + void ReleaseVulkan(); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace Libs::Graphics + +#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_SYSTEMOVERLAY_H_ diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 0bdae5c86..000e61239 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -809,7 +809,7 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) { VblankBegin(); bool presented = m_flip_queue.Flip(0); - if (!presented && m_presenter.NeedsImeRefresh()) { + if (!presented && m_presenter.NeedsSystemOverlayRefresh()) { if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) { m_presenter.Present(*frame, true); presented = true; diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index ce54cf4f1..80feb306c 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -9,8 +9,8 @@ #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" -#include "graphics/presentation/imeOverlay.h" #include "graphics/presentation/presenter.h" +#include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/videoOut.h" #include "graphics/presentation/window/windowInternal.h" @@ -293,8 +293,9 @@ public: void Create(); void Recreate(bool surface_lost = false); [[nodiscard]] Status AcquireNextImage(); - [[nodiscard]] bool PrepareImeOverlay(); - void RecordPresentCommands(CommandBuffer& command, VulkanImage& source, bool draw_ime_overlay); + [[nodiscard]] bool PrepareSystemOverlay(); + void RecordPresentCommands(CommandBuffer& command, VulkanImage& source, + bool draw_system_overlay); uint64_t Submit(CommandScheduler& scheduler); [[nodiscard]] Status Present(); @@ -314,7 +315,7 @@ private: std::vector m_image_views; std::vector m_image_acquired; std::vector m_render_complete; - std::unique_ptr m_ime_overlay; + std::unique_ptr m_system_overlay; uint32_t m_image_index = static_cast(-1); uint32_t m_frame_index = 0; }; @@ -361,7 +362,7 @@ struct Presenter::Impl { Swapchain swapchain; CommandScheduler present_scheduler; FramePool frames; - std::atomic presented_ime_revision {0}; + std::atomic presented_overlay_revision {0}; }; void Swapchain::Create() { @@ -504,8 +505,8 @@ void Swapchain::Destroy() { Common::LockGuard queue_lock(graphics.queue_mutex); RequireVulkanSuccess(graphics.queue.waitIdle(), "wait for swapchain queue"); } - if (m_ime_overlay != nullptr) { - m_ime_overlay->ReleaseVulkan(); + if (m_system_overlay != nullptr) { + m_system_overlay->ReleaseVulkan(); } for (const auto semaphore: m_image_acquired) { @@ -579,15 +580,15 @@ Swapchain::Status Swapchain::AcquireNextImage() { return Status::Success; } -bool Swapchain::PrepareImeOverlay() { - if (m_ime_overlay == nullptr) { - m_ime_overlay = std::make_unique(m_window.graphic_ctx); +bool Swapchain::PrepareSystemOverlay() { + if (m_system_overlay == nullptr) { + m_system_overlay = std::make_unique(m_window.graphic_ctx); } - return m_ime_overlay->PrepareFrame(m_extent, m_format, ImageCount()); + return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount()); } void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source, - bool draw_ime_overlay) { + bool draw_system_overlay) { if (source.state.layout != vk::ImageLayout::eTransferSrcOptimal) { EXIT("invalid prepared presentation image, vk_image=%p layout=%d\n", static_cast(source.image), static_cast(source.state.layout)); @@ -635,12 +636,12 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc vk::ImageMemoryBarrier to_present {}; to_present.sType = vk::StructureType::eImageMemoryBarrier; to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - to_present.dstAccessMask = draw_ime_overlay ? vk::AccessFlagBits::eColorAttachmentRead | - vk::AccessFlagBits::eColorAttachmentWrite - : vk::AccessFlagBits::eMemoryRead; + to_present.dstAccessMask = draw_system_overlay ? vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite + : vk::AccessFlagBits::eMemoryRead; to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal; - to_present.newLayout = draw_ime_overlay ? vk::ImageLayout::eColorAttachmentOptimal - : vk::ImageLayout::ePresentSrcKHR; + to_present.newLayout = draw_system_overlay ? vk::ImageLayout::eColorAttachmentOptimal + : vk::ImageLayout::ePresentSrcKHR; to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_present.image = m_images[m_image_index]; @@ -649,13 +650,13 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc to_present.subresourceRange.levelCount = 1; to_present.subresourceRange.baseArrayLayer = 0; to_present.subresourceRange.layerCount = 1; - vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, - draw_ime_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput - : vk::PipelineStageFlagBits::eAllCommands, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, - &to_present); - if (draw_ime_overlay) { - m_ime_overlay->Record(vk_command, m_image_views[m_image_index]); + vk_command.pipelineBarrier( + vk::PipelineStageFlagBits::eTransfer, + draw_system_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput + : vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present); + if (draw_system_overlay) { + m_system_overlay->Record(vk_command, m_image_views[m_image_index]); to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead; to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; @@ -762,10 +763,10 @@ bool Presenter::IsGuestPaused() const noexcept { return m_impl->window.loop.paused.load(std::memory_order_acquire); } -bool Presenter::NeedsImeRefresh() const noexcept { - const auto visual = GetImeVisualState(); +bool Presenter::NeedsSystemOverlayRefresh() const noexcept { + const auto visual = GetSystemOverlayVisualState(); return visual.active || - visual.revision != m_impl->presented_ime_revision.load(std::memory_order_acquire); + visual.revision != m_impl->presented_overlay_revision.load(std::memory_order_acquire); } RenderContext& Presenter::Renderer() const noexcept { @@ -776,7 +777,7 @@ void Presenter::Present(Frame& frame, bool reuse) { KYTY_PROFILER_FUNCTION(); m_impl->frames.ValidateForPresent(&frame, reuse); - const auto ime_visual = GetImeVisualState(); + const auto overlay_visual = GetSystemOverlayVisualState(); auto& swapchain = m_impl->swapchain; for (uint32_t attempt = 0; attempt < 2; attempt++) { auto status = swapchain.AcquireNextImage(); @@ -787,8 +788,9 @@ void Presenter::Present(Frame& frame, bool reuse) { { Common::LockGuard render_lock(m_impl->renderer.GetMutex()); auto& command = m_impl->present_scheduler.BeginCommand(); - const bool draw_ime_overlay = ime_visual.active && swapchain.PrepareImeOverlay(); - swapchain.RecordPresentCommands(command, frame.image, draw_ime_overlay); + const bool draw_system_overlay = + overlay_visual.active && swapchain.PrepareSystemOverlay(); + swapchain.RecordPresentCommands(command, frame.image, draw_system_overlay); frame.present_tick = swapchain.Submit(m_impl->present_scheduler); } status = swapchain.Present(); @@ -797,7 +799,8 @@ void Presenter::Present(Frame& frame, bool reuse) { continue; } - m_impl->presented_ime_revision.store(ime_visual.revision, std::memory_order_release); + m_impl->presented_overlay_revision.store(overlay_visual.revision, + std::memory_order_release); m_impl->window.UpdateTitle(); m_impl->frames.Release(&frame, true); return; diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index b05b4c576..5dff4bdb8 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -29,8 +29,8 @@ #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" -#include "graphics/presentation/imeOverlay.h" #include "graphics/presentation/presenter.h" +#include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/videoOut.h" #include "graphics/presentation/window.h" #include "graphics/presentation/window/windowInternal.h" @@ -1139,7 +1139,7 @@ void WindowContext::RecreateSurface() { } WindowContext::~WindowContext() { - ShutdownImeInput(); + ShutdownSystemOverlayInput(); presenter.reset(); LibKernel::Memory::InstallGpuResources(nullptr); render_context.reset(); diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 1053d667b..d74257d99 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -32,8 +32,8 @@ #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vma.h" #include "graphics/host_gpu/vulkanCommon.h" -#include "graphics/presentation/imeOverlay.h" #include "graphics/presentation/renderDoc.h" +#include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/window/hostInput.h" #include "graphics/presentation/window/windowInternal.h" #include "kytyGitVersion.h" @@ -513,7 +513,7 @@ void WindowContext::ProcessEvent(double time_s) { } return; } - if (ProcessImeInput(*event)) { + if (ProcessSystemOverlayInput(*event)) { return; } @@ -824,7 +824,7 @@ static void WindowCreate(WindowContext& context) { EXIT("%s\n", SDL_GetError()); } HostInputInit(); - InitializeImeInput(); + InitializeSystemOverlayInput(); LOGF("WindowCreate(): width = %d, height = %d\n", width, height); diff --git a/src/libs/dialog.cpp b/src/libs/dialog.cpp index 39376c374..54ae845f1 100644 --- a/src/libs/dialog.cpp +++ b/src/libs/dialog.cpp @@ -8,6 +8,7 @@ #include #include +#include // NOLINTNEXTLINE(modernize-concat-nested-namespaces) namespace Libs::Dialog { @@ -643,11 +644,61 @@ struct ErrorDialogParam { int32_t reserved; }; -static int g_error_status = STATUS_NONE; +static_assert(sizeof(ErrorDialogParam) == 16); + +static std::mutex g_error_mutex; +static int g_error_status = STATUS_NONE; +static uint64_t g_error_generation = 0; +static uint64_t g_error_revision = 0; +static int32_t g_error_code = 0; +static void (*g_error_visibility_callback)() = nullptr; + +static void SetStatus(int status, std::unique_lock& lock) { + const bool visibility_changed = + (g_error_status == STATUS_RUNNING) != (status == STATUS_RUNNING); + g_error_status = status; + if (visibility_changed) { + g_error_revision++; + } + const auto callback = visibility_changed ? g_error_visibility_callback : nullptr; + lock.unlock(); + if (callback != nullptr) { + callback(); + } +} + +bool GetHostSnapshot(HostSnapshot* snapshot) { + std::scoped_lock lock(g_error_mutex); + if (g_error_status != STATUS_RUNNING) { + return false; + } + *snapshot = {g_error_generation, g_error_code}; + return true; +} + +VisualState GetVisualState() noexcept { + std::scoped_lock lock(g_error_mutex); + return {g_error_status == STATUS_RUNNING, g_error_revision}; +} + +void SetVisibilityCallback(void (*callback)()) { + std::scoped_lock lock(g_error_mutex); + g_error_visibility_callback = callback; +} + +bool HostAccept(uint64_t generation) { + std::unique_lock lock(g_error_mutex); + if (g_error_status != STATUS_RUNNING || generation != g_error_generation) { + return false; + } + SetStatus(STATUS_FINISHED, lock); + return true; +} int KYTY_SYSV_ABI ErrorDialogInitialize() { PRINT_NAME(); + std::scoped_lock lock(g_error_mutex); if (g_error_status != STATUS_NONE) { return ERROR_ALREADY_INITIALIZED; } @@ -660,6 +711,7 @@ int KYTY_SYSV_ABI ErrorDialogInitialize() { int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) { PRINT_NAME(); + std::unique_lock lock(g_error_mutex); if (g_error_status != STATUS_INITIALIZED && g_error_status != STATUS_FINISHED) { return ERROR_INVALID_STATE; } @@ -677,7 +729,9 @@ int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) { "\t user_id = %d\n", p->size, static_cast(p->error_code), p->user_id); - g_error_status = STATUS_RUNNING; + g_error_code = p->error_code; + g_error_generation++; + SetStatus(STATUS_RUNNING, lock); return OK; } @@ -685,11 +739,12 @@ int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) { int KYTY_SYSV_ABI ErrorDialogClose() { PRINT_NAME(); + std::unique_lock lock(g_error_mutex); if (g_error_status != STATUS_RUNNING) { return ERROR_INVALID_STATE; } - g_error_status = STATUS_FINISHED; + SetStatus(STATUS_FINISHED, lock); return OK; } @@ -697,11 +752,12 @@ int KYTY_SYSV_ABI ErrorDialogClose() { int KYTY_SYSV_ABI ErrorDialogTerminate() { PRINT_NAME(); + std::unique_lock lock(g_error_mutex); if (g_error_status == STATUS_NONE) { return ERROR_NOT_INITIALIZED; } - g_error_status = STATUS_NONE; + SetStatus(STATUS_NONE, lock); return OK; } @@ -709,16 +765,14 @@ int KYTY_SYSV_ABI ErrorDialogTerminate() { int KYTY_SYSV_ABI ErrorDialogUpdateStatus() { PRINT_NAME(); - if (g_error_status == STATUS_RUNNING) { - g_error_status = STATUS_FINISHED; - } - + std::scoped_lock lock(g_error_mutex); return g_error_status; } int KYTY_SYSV_ABI ErrorDialogGetStatus() { PRINT_NAME(); + std::scoped_lock lock(g_error_mutex); return g_error_status; } diff --git a/src/libs/dialog.h b/src/libs/dialog.h index d8b546634..2604f12a4 100644 --- a/src/libs/dialog.h +++ b/src/libs/dialog.h @@ -70,6 +70,21 @@ int KYTY_SYSV_ABI MsgDialogProgressBarSetMsg(int target, const char* msg); namespace ErrorDialog { +struct HostSnapshot { + uint64_t generation; + int32_t error_code; +}; + +struct VisualState { + bool active; + uint64_t revision; +}; + +bool GetHostSnapshot(HostSnapshot* snapshot); +VisualState GetVisualState() noexcept; +void SetVisibilityCallback(void (*callback)()); +bool HostAccept(uint64_t generation); + int KYTY_SYSV_ABI ErrorDialogInitialize(); int KYTY_SYSV_ABI ErrorDialogOpen(const void* param); int KYTY_SYSV_ABI ErrorDialogClose(); diff --git a/src/libs/libSystemService.cpp b/src/libs/libSystemService.cpp index cd205f6dc..13c769ce2 100644 --- a/src/libs/libSystemService.cpp +++ b/src/libs/libSystemService.cpp @@ -4,6 +4,7 @@ #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/stringUtils.h" +#include "libs/dialog.h" #include "libs/errno.h" #include "libs/libs.h" #include "loader/symbolDatabase.h" @@ -160,7 +161,8 @@ static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - *status = SystemServiceStatus(); + *status = SystemServiceStatus(); + status->is_system_ui_overlaid = Dialog::ErrorDialog::GetVisualState().active; return OK; } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index b1ac03d1f..86e7bb2d5 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -46,6 +46,7 @@ #include "kernel/eventQueue.h" #include "kernel/memory.h" #include "libs/agc.h" +#include "libs/dialog.h" #include "libs/errno.h" #include "spirv-tools/libspirv.hpp" @@ -935,6 +936,142 @@ void Require(const char *shader_name, const char *stage, bool value, } } +void CheckErrorDialogLifecycle() { + namespace ErrorDialog = Libs::Dialog::ErrorDialog; + struct Param { + int32_t size = 16; + int32_t error_code = static_cast(0x80550006u); + int32_t user_id = 1; + int32_t reserved = 0; + } param; + static_assert(sizeof(Param) == 16); + constexpr int invalid_state = static_cast(0x80ed0005u); + constexpr int invalid_param = static_cast(0x80ed0003u); + static std::vector notifications; + notifications.clear(); + ErrorDialog::SetVisibilityCallback([] { + const auto visual = ErrorDialog::GetVisualState(); + ErrorDialog::HostSnapshot snapshot{}; + Require("ErrorDialog", "callback reentry", + ErrorDialog::GetHostSnapshot(&snapshot) == visual.active && + (ErrorDialog::ErrorDialogGetStatus() == 2) == visual.active, + "visibility callback observed inconsistent dialog state"); + notifications.push_back(visual); + }); + const auto initial_revision = ErrorDialog::GetVisualState().revision; + const auto check_state = [&](const char *stage, int status, + size_t visibility_changes) { + const auto visual = ErrorDialog::GetVisualState(); + ErrorDialog::HostSnapshot snapshot{}; + Require("ErrorDialog", stage, + ErrorDialog::ErrorDialogGetStatus() == status && + ErrorDialog::ErrorDialogUpdateStatus() == status && + visual.active == (status == 2) && + ErrorDialog::GetHostSnapshot(&snapshot) == visual.active && + visual.revision == initial_revision + visibility_changes && + notifications.size() == visibility_changes && + (notifications.empty() || + (notifications.back().active == visual.active && + notifications.back().revision == visual.revision)), + "status, host visibility, or visibility notification changed " + "unexpectedly"); + }; + check_state("uninitialized", 0, 0); + Require("ErrorDialog", "uninitialized operations", + ErrorDialog::ErrorDialogTerminate() == + static_cast(0x80ed0001u) && + ErrorDialog::ErrorDialogClose() == invalid_state, + "uninitialized dialog accepted termination or close"); + Require("ErrorDialog", "initialize", + ErrorDialog::ErrorDialogInitialize() == 0 && + ErrorDialog::ErrorDialogInitialize() == + static_cast(0x80ed0002u), + "initialization did not reject a second initialization"); + Param invalid = param; + invalid.size--; + Require("ErrorDialog", "invalid open", + ErrorDialog::ErrorDialogOpen(nullptr) == invalid_param && + ErrorDialog::ErrorDialogOpen(&invalid) == invalid_param && + ErrorDialog::ErrorDialogClose() == invalid_state, + "invalid parameters or close were accepted before opening"); + check_state("failed open preserves initialized state", 1, 0); + Require("ErrorDialog", "open", ErrorDialog::ErrorDialogOpen(¶m) == 0, + "valid error dialog did not open"); + ErrorDialog::HostSnapshot first{}; + Require("ErrorDialog", "copied error code", + ErrorDialog::GetHostSnapshot(&first) && + first.error_code == param.error_code, + "host dialog did not expose the guest error code"); + param.error_code = static_cast(0x8055000au); + for (int poll = 0; poll < 8; ++poll) { + check_state("polling waits for acknowledgement", 2, 1); + } + Require( + "ErrorDialog", "running failures", + ErrorDialog::ErrorDialogOpen(¶m) == invalid_state && + !ErrorDialog::HostAccept(first.generation + 1), + "an invalid open or stale acknowledgement replaced the active dialog"); + ErrorDialog::HostSnapshot current{}; + Require("ErrorDialog", "failed open preserves active dialog", + ErrorDialog::GetHostSnapshot(¤t) && + current.generation == first.generation && + current.error_code == first.error_code, + "active error code or generation changed after a failed open"); + check_state("failed operations preserve running state", 2, 1); + Require("ErrorDialog", "acknowledge", + ErrorDialog::HostAccept(first.generation), + "current host acknowledgement did not finish the dialog"); + check_state("acknowledged", 3, 2); + Require("ErrorDialog", "finished operations", + !ErrorDialog::HostAccept(first.generation) && + ErrorDialog::ErrorDialogClose() == invalid_state && + ErrorDialog::ErrorDialogOpen(&invalid) == invalid_param, + "finished dialog accepted duplicate completion or an invalid open"); + check_state("failed operations preserve finished state", 3, 2); + Require("ErrorDialog", "reopen and stale acknowledgement", + ErrorDialog::ErrorDialogOpen(¶m) == 0 && + ErrorDialog::GetHostSnapshot(¤t) && + current.generation != first.generation && + current.error_code == param.error_code && + !ErrorDialog::HostAccept(first.generation), + "reopened dialog accepted acknowledgement from its predecessor"); + check_state("reopened", 2, 3); + Require("ErrorDialog", "guest close", ErrorDialog::ErrorDialogClose() == 0, + "guest close did not finish the running dialog"); + check_state("closed", 3, 4); + Require("ErrorDialog", "terminate running dialog", + ErrorDialog::ErrorDialogOpen(¶m) == 0 && + ErrorDialog::GetHostSnapshot(¤t) && + ErrorDialog::ErrorDialogTerminate() == 0 && + !ErrorDialog::HostAccept(current.generation), + "termination failed to remove the active dialog"); + check_state("terminated", 0, 6); + Require("ErrorDialog", "reinitialize and stale acknowledgement", + ErrorDialog::ErrorDialogInitialize() == 0 && + ErrorDialog::ErrorDialogOpen(¶m) == 0 && + !ErrorDialog::HostAccept(current.generation), + "reinitialization reused an old dialog generation"); + check_state("reinitialized", 2, 7); + Require("ErrorDialog", "final termination", + ErrorDialog::ErrorDialogTerminate() == 0 && + ErrorDialog::ErrorDialogTerminate() == + static_cast(0x80ed0001u), + "termination did not restore the uninitialized state"); + check_state("final state", 0, 8); + ErrorDialog::SetVisibilityCallback(nullptr); + Require("ErrorDialog", "lifecycle without a visibility listener", + ErrorDialog::ErrorDialogInitialize() == 0 && + ErrorDialog::ErrorDialogOpen(¶m) == 0 && + ErrorDialog::ErrorDialogClose() == 0 && + ErrorDialog::ErrorDialogTerminate() == 0 && + ErrorDialog::ErrorDialogGetStatus() == 0 && + !ErrorDialog::GetVisualState().active && + ErrorDialog::GetVisualState().revision == initial_revision + 10 && + notifications.size() == 8, + "guest lifecycle depended on a registered host visibility listener"); + std::printf("[host] %-32s ok\n", "ErrorDialogLifecycle"); +} + #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS template void ExpectFatal(const char *name, Function function) { @@ -27438,6 +27575,10 @@ int main(int argc, char **argv) { std::setvbuf(stdout, nullptr, _IONBF, 0); EnsureConfigInitialized(); CheckLeastRecentlyUsedCacheOrdering(); + if (argc == 2 && std::strcmp(argv[1], "--error-dialog-only") == 0) { + CheckErrorDialogLifecycle(); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--fmask-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, ImageLoadFmaskUsesNativeSampleMapping()); @@ -27768,6 +27909,7 @@ int main(int argc, char **argv) { CheckNativeImageDescriptorTypes(); CheckClipControlDepthClipState(); CheckReferenceClockScale(); + CheckErrorDialogLifecycle(); CheckVulkan13FeatureRequirements(); CheckPm4AcquireMemNoOp(vulkan.RuntimeRenderer()); CheckPm4SyntheticOcclusionCounterDump(vulkan.RuntimeRenderer());