From d884fbc08e8a9388fcff96784b29fa0daa58a37a Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 20 Jun 2026 20:27:56 -0700 Subject: [PATCH] arm64/libmali GS backend: device gates, depth quantization, display rotation (fork-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handheld/libmali GS device adaptations — VK_KHR_display direct-to-monitor WSI, 3-image swapchain, optional VK_KHR_push_descriptor with per-frame pool fallback, ColorClip HDR fallback, present-time DisplayRotation, PS2 depth-quantization gate (no_ps2_z_quantization), and present-timing diagnostics. libmali-specific; kept on the fork branch. Co-Authored-By: Ryan Walklin Co-Authored-By: Brian Degenhardt Co-Authored-By: Claude Opus 4.8 --- common/WindowInfo.h | 7 +- pcsx2/Config.h | 16 + pcsx2/GS/Renderers/Common/GSDevice.cpp | 9 + pcsx2/GS/Renderers/Common/GSDevice.h | 22 ++ pcsx2/GS/Renderers/Common/GSRenderer.cpp | 6 +- pcsx2/GS/Renderers/HW/GSRendererHW.cpp | 6 +- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp | 452 +++++++++++++++++++--- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h | 13 + pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp | 289 +++++++++++++- pcsx2/GS/Renderers/Vulkan/VKSwapChain.h | 24 ++ pcsx2/ImGui/ImGuiManager.cpp | 16 +- pcsx2/Pcsx2Config.cpp | 12 + 12 files changed, 810 insertions(+), 62 deletions(-) diff --git a/common/WindowInfo.h b/common/WindowInfo.h index 4ccad5a4de..18c454ff88 100644 --- a/common/WindowInfo.h +++ b/common/WindowInfo.h @@ -15,7 +15,12 @@ struct WindowInfo Win32, X11, Wayland, - MacOS + MacOS, + // Vulkan VK_KHR_display direct-to-monitor (no compositor / no GBM + // intermediate). Frontend supplies no native window handle; the + // renderer enumerates displays itself. surface_width/surface_height + // carry the requested mode (0 = pick the display's preferred mode). + VulkanDirect }; /// The type of the surface. Surfaceless indicates it will not be displayed on screen at all. diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 5958aa9317..6c6d3ddd6f 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -241,6 +241,19 @@ enum class FMVAspectRatioSwitchType : u8 MaxCount }; +// Display rotation applied at present time. Useful for handhelds whose panel +// is mounted in one orientation but the user wants the game in another. +// Rotation is applied to the *final* swapchain blit only; internal GS +// coordinates and aspect-ratio math run in the unrotated frame. +enum class DisplayRotation : u8 +{ + Rot0, + Rot90, + Rot180, + Rot270, + MaxCount +}; + enum class MemoryCardType { Empty, @@ -709,6 +722,7 @@ struct Pcsx2Config { static const char* AspectRatioNames[]; static const char* FMVAspectRatioSwitchNames[]; + static const char* DisplayRotationNames[]; static const char* BlendingLevelNames[]; static const char* CaptureContainers[]; @@ -771,6 +785,7 @@ struct Pcsx2Config UseBlitSwapChain : 1, DisableShaderCache : 1, DisableFramebufferFetch : 1, + DisablePS2DepthQuantization : 1, DisableVertexShaderExpand : 1, SkipDuplicateFrames : 1, OsdShowSpeed : 1, @@ -855,6 +870,7 @@ struct Pcsx2Config AspectRatioType AspectRatio = DEFAULT_ASPECT_RATIO; FMVAspectRatioSwitchType FMVAspectRatioSwitch = DEFAULT_FMV_ASPECT_RATIO; + DisplayRotation Rotation = DisplayRotation::Rot0; GSInterlaceMode InterlaceMode = DEFAULT_INTERLACE_MODE; GSPostBilinearMode LinearPresent = DEFAULT_BILINEAR_FILTERING_MODE; diff --git a/pcsx2/GS/Renderers/Common/GSDevice.cpp b/pcsx2/GS/Renderers/Common/GSDevice.cpp index 9653b99301..e71e22c961 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.cpp +++ b/pcsx2/GS/Renderers/Common/GSDevice.cpp @@ -246,6 +246,15 @@ GSDevice::~GSDevice() pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas); } +GSVector2i GSDevice::GetPresentationSize() const +{ + const s32 w = GetWindowWidth(); + const s32 h = GetWindowHeight(); + return (GSConfig.Rotation == DisplayRotation::Rot90 || GSConfig.Rotation == DisplayRotation::Rot270) + ? GSVector2i(h, w) + : GSVector2i(w, h); +} + const char* GSDevice::RenderAPIToString(RenderAPI api) { switch (api) diff --git a/pcsx2/GS/Renderers/Common/GSDevice.h b/pcsx2/GS/Renderers/Common/GSDevice.h index f53f9f58cc..701209383c 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.h +++ b/pcsx2/GS/Renderers/Common/GSDevice.h @@ -14,6 +14,8 @@ #include "GS/GSExtra.h" #include #include +#include +#include enum class Filter { @@ -1387,6 +1389,7 @@ public: bool stencil_buffer : 1; ///< Supports stencil buffer, and can use for DATE. bool cas_sharpening : 1; ///< Supports sufficient functionality for contrast adaptive sharpening. bool test_and_sample_depth: 1; ///< Supports concurrently binding the depth-stencil buffer for sampling and depth testing. + bool no_ps2_z_quantization: 1; ///< Skip PS2 32-bit-fixed Z floor (saves SPIR-V DepthReplacing → re-enables early-ZS on tilers). bool depth_feedback : 1; ///< Depth feedback loops can be done with DS directly (otherwise need to copy to separate RT). Implies `feedback_loops`. bool aa1 : 1; ///< Supports the GS AA1 feature. bool rov : 1; ///< Supports rasterizer ordered views for both depth and color. @@ -1551,6 +1554,16 @@ public: __fi s32 GetWindowWidth() const { return static_cast(m_window_info.surface_width); } __fi s32 GetWindowHeight() const { return static_cast(m_window_info.surface_height); } __fi GSVector2i GetWindowSize() const { return GSVector2i(static_cast(m_window_info.surface_width), static_cast(m_window_info.surface_height)); } + // Logical window dimensions for layout: same as GetWindowSize for + // Rot0/Rot180, swapped for Rot90/Rot270 so callers compute the present + // rect against a portrait box that the rotation transform then maps onto + // the landscape swapchain. Use this in callers that produce coordinates + // later consumed by the rotation-aware Vulkan present path (game draw_rect, + // ImGui DisplaySize). Other callers (GS Resize, viewport setup) want the + // raw physical dims and should keep using GetWindowWidth/Height. + GSVector2i GetPresentationSize() const; + __fi s32 GetPresentationWidth() const { return GetPresentationSize().x; } + __fi s32 GetPresentationHeight() const { return GetPresentationSize().y; } __fi float GetWindowScale() const { return m_window_info.surface_scale; } __fi GSVSyncMode GetVSyncMode() const { return m_vsync_mode; } __fi bool IsPresentThrottleAllowed() const { return m_allow_present_throttle; } @@ -1606,6 +1619,15 @@ public: /// Returns the amount of GPU time utilized since the last time this method was called. virtual float GetAndResetAccumulatedGPUTime() = 0; + /// Enables backend-specific diagnostic counters (e.g. Vulkan acquire/present timing). + /// Off by default to surface WSI-layer timing in diagnostic tools without paying + /// the cost on the normal present hot path. + virtual void EnableExtendedStats(bool enabled) {} + + /// Returns backend-specific diagnostic lines (swapchain config, present/acquire timing, etc). + /// Each line is a fully-formatted string, ready to print as-is. Default: empty. + virtual std::vector GetExtendedStats() const { return {}; } + /// Returns true if not enough time has passed for present to not block. bool ShouldSkipPresentingFrame(); diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp index 71708ade6e..15414b7bf0 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp +++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp @@ -661,7 +661,8 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame) { src_rect = CalculateDrawSrcRect(current, m_real_size); src_uv = GSVector4(src_rect) / GSVector4(current->GetSize()).xyxy(); - draw_rect = CalculateDrawDstRect(g_gs_device->GetWindowWidth(), g_gs_device->GetWindowHeight(), + const GSVector2i pres_size = g_gs_device->GetPresentationSize(); + draw_rect = CalculateDrawDstRect(pres_size.x, pres_size.y, src_rect, current->GetSize(), s_display_alignment, g_gs_device->UsesLowerLeftOrigin(), GetVideoMode() == GSVideoMode::SDTV_480P); s_last_draw_rect = draw_rect; @@ -962,7 +963,8 @@ void GSRenderer::PresentCurrentFrame() { const GSVector4i src_rect(CalculateDrawSrcRect(current, m_real_size)); const GSVector4 src_uv(GSVector4(src_rect) / GSVector4(current->GetSize()).xyxy()); - const GSVector4 draw_rect(CalculateDrawDstRect(g_gs_device->GetWindowWidth(), g_gs_device->GetWindowHeight(), + const GSVector2i pres_size = g_gs_device->GetPresentationSize(); + const GSVector4 draw_rect(CalculateDrawDstRect(pres_size.x, pres_size.y, src_rect, current->GetSize(), s_display_alignment, g_gs_device->UsesLowerLeftOrigin(), GetVideoMode() == GSVideoMode::SDTV_480P)); s_last_draw_rect = draw_rect; diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index c70d17d58b..965499f635 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -5589,7 +5589,9 @@ void GSRendererHW::EmulateZbuffer(const GSTextureCache::Target* ds) // Even when Z is read-only, Z floor must be enabled with ZTST_GREATER since otherwise there // can be false passing if the incoming Z is not floored when the buffer value is floored. - m_conf.ps.zfloor = !flat_z && + // On tilers (Mali), the device can opt out: declaring gl_FragDepth disables early-ZS for + // the entire pipeline. zclamp (large_z) is independent and stays correct. + m_conf.ps.zfloor = !flat_z && !g_gs_device->Features().no_ps2_z_quantization && (m_cached_ctx.DepthWrite() || (m_cached_ctx.DepthRead() && m_cached_ctx.TEST.ZTST == ZTST_GREATER)); if (m_cached_ctx.DepthWrite() && large_z) @@ -6527,7 +6529,7 @@ __ri u32 GSRendererHW::EmulateChannelShuffle(GSTextureCache::Target* src, bool t const GSLocalMemory::psm_t& t_psm = GSLocalMemory::m_psm[m_cached_ctx.TEX0.PSM]; const GSLocalMemory::psm_t& f_psm = GSLocalMemory::m_psm[m_cached_ctx.FRAME.PSM]; GSVector4i block_offset = GSVector4i(min_uv.x / t_psm.bs.x, min_uv.y / t_psm.bs.y).xyxy(); - GSVector4i m_r_block_offset = GSVector4i((m_r.x & (f_psm.pgs.x - 1)) / f_psm.bs.x, (m_r.y & (f_psm.pgs.y - 1)) / f_psm.bs.y); + [[maybe_unused]] GSVector4i m_r_block_offset = GSVector4i((m_r.x & (f_psm.pgs.x - 1)) / f_psm.bs.x, (m_r.y & (f_psm.pgs.y - 1)) / f_psm.bs.y); // Adjust it back to the page boundary min_uv.x -= block_offset.x * t_psm.bs.x; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index fed43abe73..3a3c61cbfa 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -21,6 +21,7 @@ #include "common/HostSys.h" #include "common/Path.h" #include "common/ScopedGuard.h" +#include "common/Timer.h" #include "imgui.h" @@ -36,7 +37,12 @@ enum : u32 MAX_COMBINED_IMAGE_SAMPLER_DESCRIPTORS_PER_FRAME = 2 * MAX_DRAW_CALLS_PER_FRAME, MAX_SAMPLED_IMAGE_DESCRIPTORS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME, // assume at least half our draws aren't going to be shuffle/blending - MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME = 4, // Currently used by CAS only + // CAS uses one storage image per frame, but the TFX texture set also carries the + // two ROV storage-image bindings (TFX_TEXTURE_RT_ROV / _DEPTH_ROV), and every + // vkAllocateDescriptorSets of that layout reserves both whether written or not. + // On the ROV-without-push-descriptor path that is two per TFX draw, so + // size to match the draw budget rather than the old CAS-only value of 4. + MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME = 2 * MAX_DRAW_CALLS_PER_FRAME, MAX_INPUT_ATTACHMENT_IMAGE_DESCRIPTORS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME, MAX_DESCRIPTOR_SETS_PER_FRAME = MAX_DRAW_CALLS_PER_FRAME * 2, @@ -80,7 +86,6 @@ static std::mutex s_instance_mutex; // Device extensions that are required for PCSX2. static constexpr const char* s_required_device_extensions[] = { - VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, }; GSDeviceVK::GSDeviceVK() @@ -199,6 +204,16 @@ bool GSDeviceVK::SelectInstanceExtensions(ExtensionList* extension_list, const W return false; #endif + // VK_KHR_display direct-to-monitor surface (kmsdrm handhelds). + // VK_KHR_get_display_properties2 is optional but lets us read HDR / extended + // display info on ICDs that support it. + if (wi.type == WindowInfo::Type::VulkanDirect) + { + if (!SupportsExtension(VK_KHR_DISPLAY_EXTENSION_NAME, true)) + return false; + SupportsExtension(VK_KHR_GET_DISPLAY_PROPERTIES_2_EXTENSION_NAME, false); + } + // VK_EXT_debug_utils if (enable_debug_utils && !SupportsExtension(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, false)) Console.Warning("VK: Debug report requested, but extension is not available."); @@ -407,6 +422,7 @@ bool GSDeviceVK::SelectDeviceExtensions(ExtensionList* extension_list, bool enab return false; } + m_optional_extensions.vk_khr_push_descriptor = SupportsExtension(VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, false); m_optional_extensions.vk_ext_provoking_vertex = SupportsExtension(VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, false); m_optional_extensions.vk_ext_memory_budget = SupportsExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, false); m_optional_extensions.vk_ext_calibrated_timestamps = @@ -771,18 +787,24 @@ bool GSDeviceVK::ProcessDeviceExtensions() VkPhysicalDevicePushDescriptorPropertiesKHR push_descriptor_properties = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR}; - Vulkan::AddPointerToChain(&properties2, &push_descriptor_properties); + if (m_optional_extensions.vk_khr_push_descriptor) + Vulkan::AddPointerToChain(&properties2, &push_descriptor_properties); // query vkGetPhysicalDeviceProperties2(m_physical_device, &properties2); // confirm we actually support it - if (push_descriptor_properties.maxPushDescriptors < NUM_TFX_TEXTURES) + if (m_optional_extensions.vk_khr_push_descriptor) { - Console.Error("VK: maxPushDescriptors (%u) is below required (%u)", push_descriptor_properties.maxPushDescriptors, - NUM_TFX_TEXTURES); - return false; + if (push_descriptor_properties.maxPushDescriptors < NUM_TFX_TEXTURES) + { + Console.Warning("VK: maxPushDescriptors (%u) is below required (%u), disabling push descriptors", + push_descriptor_properties.maxPushDescriptors, NUM_TFX_TEXTURES); + m_optional_extensions.vk_khr_push_descriptor = false; + } } + if (!m_optional_extensions.vk_khr_push_descriptor) + Console.Warning("VK: VK_KHR_push_descriptor is not available, using per-frame descriptor pools instead."); if (m_optional_extensions.vk_ext_line_rasterization && !line_rasterization_feature.bresenhamLines) { @@ -957,6 +979,30 @@ bool GSDeviceVK::CreateCommandBuffers() return false; } Vulkan::SetObjectName(m_device, resources.fence, "Frame Fence %u", frame_index); + + // Create per-frame descriptor pool when push descriptors are not available. + if (!m_optional_extensions.vk_khr_push_descriptor) + { + // Pool sizes cover TFX textures, utility, and CAS descriptor sets per frame. + static constexpr const VkDescriptorPoolSize frame_pool_sizes[] = { + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, MAX_COMBINED_IMAGE_SAMPLER_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, MAX_SAMPLED_IMAGE_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, MAX_STORAGE_IMAGE_DESCRIPTORS_PER_FRAME}, + {VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, MAX_INPUT_ATTACHMENT_IMAGE_DESCRIPTORS_PER_FRAME}, + }; + + VkDescriptorPoolCreateInfo dp_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, nullptr, 0, + MAX_DESCRIPTOR_SETS_PER_FRAME, static_cast(std::size(frame_pool_sizes)), frame_pool_sizes}; + + res = vkCreateDescriptorPool(m_device, &dp_info, nullptr, &resources.descriptor_pool); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkCreateDescriptorPool (per-frame) failed: "); + return false; + } + Vulkan::SetObjectName(m_device, resources.descriptor_pool, "Frame Descriptor Pool %u", frame_index); + } + ++frame_index; } @@ -1085,6 +1131,28 @@ void GSDeviceVK::FreePersistentDescriptorSet(VkDescriptorSet set) vkFreeDescriptorSets(m_device, m_global_descriptor_pool, 1, &set); } +VkDescriptorSet GSDeviceVK::AllocateDescriptorSetFromFramePool(VkDescriptorSetLayout set_layout) +{ + VkDescriptorPool pool = m_frame_resources[m_current_frame].descriptor_pool; + pxAssert(pool != VK_NULL_HANDLE); + + VkDescriptorSetAllocateInfo allocate_info = { + VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1, &set_layout}; + + VkDescriptorSet descriptor_set; + VkResult res = vkAllocateDescriptorSets(m_device, &allocate_info, &descriptor_set); + if (res == VK_SUCCESS) + return descriptor_set; + + // Pool exhausted. Recovery (flush the command buffer to reset the frame pool, + // then restart the render pass and re-apply state) must be driven by the caller: + // callers capture their command buffer and emit binding state before calling us, + // so flushing here would leave them writing to a submitted command buffer with no + // active render pass. Signal exhaustion with a null set and let the caller flush, + // restart, and re-enter (mirroring the uniform-buffer overflow paths). + return VK_NULL_HANDLE; +} + void GSDeviceVK::WaitForFenceCounter(u64 fence_counter) { if (m_completed_fence_counter >= fence_counter) @@ -1122,6 +1190,55 @@ bool GSDeviceVK::SetGPUTimingEnabled(bool enabled) return (enabled == m_gpu_timing_enabled); } +void GSDeviceVK::EnableExtendedStats(bool enabled) +{ + VKSwapChain::SetPresentStatsEnabled(enabled); +} + +std::vector GSDeviceVK::GetExtendedStats() const +{ + std::vector lines; + if (m_swap_chain) + { + const WindowInfo& wi = m_swap_chain->GetWindowInfo(); + const char* wsi_name = "?"; + switch (wi.type) + { + case WindowInfo::Type::Surfaceless: wsi_name = "Surfaceless"; break; + case WindowInfo::Type::Win32: wsi_name = "Win32"; break; + case WindowInfo::Type::X11: wsi_name = "X11"; break; + case WindowInfo::Type::Wayland: wsi_name = "Wayland"; break; + case WindowInfo::Type::MacOS: wsi_name = "MacOS"; break; + case WindowInfo::Type::VulkanDirect: wsi_name = "VulkanDirect"; break; + } + const char* present_name = "?"; + switch (m_swap_chain->GetPresentMode()) + { + case VK_PRESENT_MODE_IMMEDIATE_KHR: present_name = "IMMEDIATE"; break; + case VK_PRESENT_MODE_MAILBOX_KHR: present_name = "MAILBOX"; break; + case VK_PRESENT_MODE_FIFO_KHR: present_name = "FIFO"; break; + case VK_PRESENT_MODE_FIFO_RELAXED_KHR: present_name = "FIFO_RELAXED"; break; + default: break; + } + lines.push_back(fmt::format( + "Swapchain: {}x{} (scale {:.2f}) fmt={} present={} images={} wsi={}", + m_swap_chain->GetWidth(), m_swap_chain->GetHeight(), wi.surface_scale, + static_cast(m_swap_chain->GetTextureFormat()), + present_name, m_swap_chain->GetImageCount(), wsi_name)); + } + + const VKSwapChain::PresentStats ps = VKSwapChain::GetPresentStats(); + const double acquire_avg_ms = ps.acquire_count ? (ps.acquire_total_ms / ps.acquire_count) : 0.0; + const double present_avg_ms = ps.present_count ? (ps.present_total_ms / ps.present_count) : 0.0; + lines.push_back(fmt::format( + "vkAcquireNextImage: avg {:.3f} ms, max {:.3f} ms, n={}", acquire_avg_ms, ps.acquire_max_ms, ps.acquire_count)); + lines.push_back(fmt::format( + "vkQueuePresent: avg {:.3f} ms, max {:.3f} ms, n={}", present_avg_ms, ps.present_max_ms, ps.present_count)); + lines.push_back(fmt::format( + "Suboptimal: {}, OutOfDate: {}", ps.suboptimal_count, ps.out_of_date_count)); + return lines; +} + void GSDeviceVK::ScanForCommandBufferCompletion() { for (u32 check_index = (m_current_frame + 1) % NUM_COMMAND_BUFFERS; check_index != m_current_frame; @@ -1297,7 +1414,15 @@ void GSDeviceVK::SubmitCommandBuffer(VKSwapChain* present_swap_chain) present_swap_chain->ResetImageAcquireResult(); + const bool stats = VKSwapChain::IsPresentStatsEnabled(); + const Common::Timer::Value t_present_start = stats ? Common::Timer::GetCurrentValue() : 0; res = vkQueuePresentKHR(m_present_queue, &present_info); + if (stats) + { + const double present_elapsed_ms = + Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - t_present_start); + VKSwapChain::NotePresent(present_elapsed_ms, res); + } if (res != VK_SUCCESS && res != VK_SUBOPTIMAL_KHR) { // VK_ERROR_OUT_OF_DATE_KHR is not fatal, just means we need to recreate our swap chain. @@ -1389,6 +1514,14 @@ void GSDeviceVK::ActivateCommandBuffer(u32 index) if (res != VK_SUCCESS) LOG_VULKAN_ERROR(res, "vkResetCommandPool failed: "); + // Reset per-frame descriptor pool when push descriptors are not available. + if (resources.descriptor_pool != VK_NULL_HANDLE) + { + res = vkResetDescriptorPool(m_device, resources.descriptor_pool, 0); + if (res != VK_SUCCESS) + LOG_VULKAN_ERROR(res, "vkResetDescriptorPool failed: "); + } + // Enable commands to be recorded to the two buffers again. VkCommandBufferBeginInfo begin_info = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, nullptr}; @@ -2717,6 +2850,11 @@ bool GSDeviceVK::CheckFeatures() // Use D32F depth instead of D32S8 when we have framebuffer fetch. m_features.stencil_buffer &= !m_features.framebuffer_fetch; + // On tiler GPUs, declaring gl_FragDepth (for PS2 32-bit Z quantization) emits + // SPIR-V ExecutionMode DepthReplacing, which disables early-ZS for the entire + // pipeline. Default-on for ARM GPUs; opt-out via INI for Z-precision-sensitive titles. + m_features.no_ps2_z_quantization = GSConfig.DisablePS2DepthQuantization || IsDeviceARM(); + // whether we can do point/line expand depends on the range of the device const float f_upscale = static_cast(GSConfig.UpscaleMultiplier); m_features.point_expand = (m_device_features.largePoints && limits.pointSizeRange[0] <= f_upscale && @@ -2727,9 +2865,10 @@ bool GSDeviceVK::CheckFeatures() m_features.depth_feedback = m_features.feedback_loops(); m_features.aa1 = GSConfig.HWAA1 && m_features.vs_expand && m_features.feedback_loops(); - DevCon.WriteLn("Optional features:%s%s%s%s%s", m_features.primitive_id ? " primitive_id" : "", + DevCon.WriteLn("Optional features:%s%s%s%s%s%s", m_features.primitive_id ? " primitive_id" : "", m_features.texture_barrier ? " texture_barrier" : "", m_features.framebuffer_fetch ? " framebuffer_fetch" : "", - m_features.provoking_vertex_last ? " provoking_vertex_last" : "", m_features.vs_expand ? " vs_expand" : ""); + m_features.provoking_vertex_last ? " provoking_vertex_last" : "", m_features.vs_expand ? " vs_expand" : "", + m_features.no_ps2_z_quantization ? " no_ps2_z_quantization" : ""); DevCon.WriteLn("Using %s for point expansion and %s for line expansion.", m_features.point_expand ? "hardware" : "vertex expanding", @@ -2748,6 +2887,16 @@ bool GSDeviceVK::CheckFeatures() vkGetPhysicalDeviceFormatProperties(m_physical_device, vkfmt, &props); if ((props.optimalTilingFeatures & bits) != bits) { + // ColorClip (R16G16B16A16_UNORM) may not be supported as a render target on some GPUs + // (e.g. Broadcom V3D). Fall back to ColorHDR (R16G16B16A16_SFLOAT) which provides + // equivalent precision for color clamping emulation. + if (static_cast(fmt) == GSTexture::Format::ColorClip) + { + Console.Warning("VK: ColorClip format (R16G16B16A16_UNORM) not supported as render target, falling back to ColorHDR (R16G16B16A16_SFLOAT)."); + m_colorclip_fallback_to_hdr = true; + continue; + } + Host::ReportFormattedErrorAsync("VK: Renderer Unavailable", "Required format %u is missing bits, you may need to update your driver. (vk:%u, has:0x%x, needs:0x%x)", fmt, static_cast(vkfmt), props.optimalTilingFeatures, bits); @@ -2848,6 +2997,9 @@ VkFormat GSDeviceVK::LookupNativeFormat(GSTexture::Format format) const VK_FORMAT_BC7_UNORM_BLOCK, // BC7 }}; + if (format == GSTexture::Format::ColorClip && m_colorclip_fallback_to_hdr) + return VK_FORMAT_R16G16B16A16_SFLOAT; + return (format != GSTexture::Format::DepthStencil || m_features.stencil_buffer) ? s_format_mapping[static_cast(format)] : VK_FORMAT_D32_SFLOAT; @@ -2971,7 +3123,7 @@ void GSDeviceVK::PresentRect(GSTexture* sTex, const GSVector4& sRect, GSTexture* { DisplayConstantBuffer cb; cb.SetSource(sRect, sTex->GetSize()); - cb.SetTarget(dRect, dTex ? dTex->GetSize() : GSVector2i(GetWindowWidth(), GetWindowHeight())); + cb.SetTarget(dRect, dTex ? dTex->GetSize() : GetPresentationSize()); cb.SetTime(shaderTime); SetUtilityPushConstants(&cb, sizeof(cb)); @@ -3155,7 +3307,7 @@ void GSDeviceVK::DoStretchRect(GSTextureVK* sTex, const GSVector4& sRect, GSText const bool is_present = (!dTex); const bool depth = (dTex && dTex->GetType() == GSTexture::Type::DepthStencil); - const GSVector2i size(is_present ? GSVector2i(GetWindowWidth(), GetWindowHeight()) : dTex->GetSize()); + const GSVector2i size(is_present ? GetPresentationSize() : dTex->GetSize()); const GSVector4i dtex_rc(0, 0, size.x, size.y); const GSVector4i dst_rc(GSVector4i(dRect).rintersect(dtex_rc)); @@ -3178,6 +3330,43 @@ void GSDeviceVK::DoStretchRect(GSTextureVK* sTex, const GSVector4& sRect, GSText DrawStretchRect(sRect, dRect, size); } +// Rotate a logical-NDC point (x, y) into physical-NDC by GSConfig.Rotation. +// Used by the present pass to map a quad laid out for a logically-rotated +// window onto the unrotated swapchain viewport. +// Rot0: (x, y) +// Rot90: (y, -x) (image rotates 90° CW on the panel) +// Rot180: (-x,-y) +// Rot270: (-y, x) (image rotates 90° CCW on the panel) +static void RotateNDCForPresent(float& x, float& y) +{ + switch (GSConfig.Rotation) + { + case DisplayRotation::Rot90: + { + const float nx = y; + const float ny = -x; + x = nx; + y = ny; + break; + } + case DisplayRotation::Rot180: + x = -x; + y = -y; + break; + case DisplayRotation::Rot270: + { + const float nx = -y; + const float ny = x; + x = nx; + y = ny; + break; + } + case DisplayRotation::Rot0: + default: + break; + } +} + void GSDeviceVK::DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const GSVector2i& ds) { g_perfmon.Put(GSPerfMon::TextureCopies, 1); @@ -3186,18 +3375,43 @@ void GSDeviceVK::DrawStretchRect(const GSVector4& sRect, const GSVector4& dRect, const float inv_x = 2.0f / ds.x; const float inv_y = 2.0f / ds.y; - const float left = dRect.x * inv_x - 1.0f; - const float right = dRect.z * inv_x - 1.0f; - const float top = 1.0f - dRect.y * inv_y; - const float bottom = 1.0f - dRect.w * inv_y; + float left = dRect.x * inv_x - 1.0f; + float right = dRect.z * inv_x - 1.0f; + float top = 1.0f - dRect.y * inv_y; + float bottom = 1.0f - dRect.w * inv_y; - const GSVertexPT1 vertices[] = { - {GSVector4(left, top, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, - {GSVector4(right, top, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, - {GSVector4(left, bottom, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, - {GSVector4(right, bottom, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, - }; - IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + // Present pass: map logical-NDC (computed against the rotated window) onto + // the unrotated physical swapchain viewport. Non-present passes pass the + // real dst-texture size in `ds` and must not rotate. + if (m_is_presenting && GSConfig.Rotation != DisplayRotation::Rot0) + { + float tlx = left, tly = top; + float trx = right, try_ = top; + float blx = left, bly = bottom; + float brx = right, bry = bottom; + RotateNDCForPresent(tlx, tly); + RotateNDCForPresent(trx, try_); + RotateNDCForPresent(blx, bly); + RotateNDCForPresent(brx, bry); + + const GSVertexPT1 vertices[] = { + {GSVector4(tlx, tly, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, + {GSVector4(trx, try_, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, + {GSVector4(blx, bly, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, + {GSVector4(brx, bry, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, + }; + IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + } + else + { + const GSVertexPT1 vertices[] = { + {GSVector4(left, top, 0.5f, 1.0f), GSVector2(sRect.x, sRect.y)}, + {GSVector4(right, top, 0.5f, 1.0f), GSVector2(sRect.z, sRect.y)}, + {GSVector4(left, bottom, 0.5f, 1.0f), GSVector2(sRect.x, sRect.w)}, + {GSVector4(right, bottom, 0.5f, 1.0f), GSVector2(sRect.z, sRect.w)}, + }; + IASetVertexBuffer(vertices, sizeof(vertices[0]), std::size(vertices)); + } if (ApplyUtilityState()) DrawPrimitive(); @@ -3902,7 +4116,8 @@ bool GSDeviceVK::CreatePipelineLayouts() // Convert Pipeline Layout ////////////////////////////////////////////////////////////////////////// - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + dslb.SetPushFlag(); dslb.AddBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, NUM_UTILITY_SAMPLERS, VK_SHADER_STAGE_FRAGMENT_BIT); if ((m_utility_ds_layout = dslb.Create(dev)) == VK_NULL_HANDLE) return false; @@ -3931,7 +4146,8 @@ bool GSDeviceVK::CreatePipelineLayouts() return false; Vulkan::SetObjectName(dev, m_tfx_ubo_ds_layout, "TFX UBO descriptor layout"); - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + dslb.SetPushFlag(); dslb.AddBinding(TFX_TEXTURE_TEXTURE, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT); dslb.AddBinding(TFX_TEXTURE_PALETTE, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_FRAGMENT_BIT); dslb.AddBinding(TFX_TEXTURE_RT, @@ -4460,7 +4676,8 @@ bool GSDeviceVK::CompileCASPipelines() Vulkan::DescriptorSetLayoutBuilder dslb; Vulkan::PipelineLayoutBuilder plb; - dslb.SetPushFlag(); + if (m_optional_extensions.vk_khr_push_descriptor) + dslb.SetPushFlag(); dslb.AddBinding(0, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); dslb.AddBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); if ((m_cas_ds_layout = dslb.Create(dev)) == VK_NULL_HANDLE) @@ -4562,11 +4779,15 @@ void GSDeviceVK::RenderImGui() UpdateImGuiTextures(); - const GSVector4 uniforms( - 2.0f / static_cast(m_window_info.surface_width), - 2.0f / static_cast(m_window_info.surface_height), - -1.0f, - -1.0f); + // ImGui's vertex Position and ClipRect both come in *logical* pixel coords + // (against io.DisplaySize, which we set to the rotated presentation size). + // uScale/uTranslate map pixel coords directly to NDC. Rotation transforms + // logical pixels into the physical pixel space of the swapchain viewport, + // so uScale must always be in physical units (not logical) for both the + // rotated and unrotated paths. + const float phys_w = static_cast(m_window_info.surface_width); + const float phys_h = static_cast(m_window_info.surface_height); + const GSVector4 uniforms(2.0f / phys_w, 2.0f / phys_h, -1.0f, -1.0f); SetUtilityPushConstants(&uniforms, sizeof(uniforms)); SetPipeline(m_imgui_pipeline); @@ -4580,6 +4801,40 @@ void GSDeviceVK::RenderImGui() // this is for presenting, we don't want to screw with the viewport/scissor set by display m_dirty_flags &= ~(DIRTY_FLAG_VIEWPORT | DIRTY_FLAG_SCISSOR); + // Logical/physical coords differ for Rot90/Rot270; the rotation transform + // maps a logical pixel (lx, ly) on a (lw, lh) logical surface to a + // physical pixel on the (phys_w, phys_h) swapchain. Rotation is around + // the geometric centre of each surface. + const bool rotate = (GSConfig.Rotation != DisplayRotation::Rot0); + const GSVector2i pres = GetPresentationSize(); + const float lw = static_cast(pres.x); + const float lh = static_cast(pres.y); + const auto rotate_pixel = [&](float lx, float ly, float& px, float& py) { + const float lcx = lx - lw * 0.5f; + const float lcy = ly - lh * 0.5f; + float pcx = lcx; + float pcy = lcy; + switch (GSConfig.Rotation) + { + case DisplayRotation::Rot90: + pcx = lcy; + pcy = -lcx; + break; + case DisplayRotation::Rot180: + pcx = -lcx; + pcy = -lcy; + break; + case DisplayRotation::Rot270: + pcx = -lcy; + pcy = lcx; + break; + default: + break; + } + px = pcx + phys_w * 0.5f; + py = pcy + phys_h * 0.5f; + }; + for (int n = 0; n < draw_data->CmdListsCount; n++) { const ImDrawList* cmd_list = draw_data->CmdLists[n]; @@ -4594,7 +4849,22 @@ void GSDeviceVK::RenderImGui() } vertex_offset = m_vertex_stream_buffer.GetCurrentOffset() / sizeof(ImDrawVert); - std::memcpy(m_vertex_stream_buffer.GetCurrentHostPointer(), cmd_list->VtxBuffer.Data, size); + if (!rotate) + { + std::memcpy(m_vertex_stream_buffer.GetCurrentHostPointer(), + cmd_list->VtxBuffer.Data, size); + } + else + { + ImDrawVert* dst = reinterpret_cast( + m_vertex_stream_buffer.GetCurrentHostPointer()); + const ImDrawVert* src = cmd_list->VtxBuffer.Data; + for (int i = 0; i < cmd_list->VtxBuffer.Size; i++) + { + dst[i] = src[i]; + rotate_pixel(src[i].pos.x, src[i].pos.y, dst[i].pos.x, dst[i].pos.y); + } + } m_vertex_stream_buffer.CommitMemory(size); } @@ -4606,10 +4876,28 @@ void GSDeviceVK::RenderImGui() const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; pxAssert(!pcmd->UserCallback); - const GSVector4 clip = GSVector4::load(&pcmd->ClipRect); + GSVector4 clip = GSVector4::load(&pcmd->ClipRect); if ((clip.zwzw() <= clip.xyxy()).mask() != 0) continue; + if (rotate) + { + // Rotate the four corners of the logical clip rect into + // physical space, then take their axis-aligned bounding box. + // (90/270 rotations preserve axis-alignment.) + float x0 = clip.x, y0 = clip.y, x1 = clip.z, y1 = clip.w; + float c0x, c0y, c1x, c1y, c2x, c2y, c3x, c3y; + rotate_pixel(x0, y0, c0x, c0y); + rotate_pixel(x1, y0, c1x, c1y); + rotate_pixel(x0, y1, c2x, c2y); + rotate_pixel(x1, y1, c3x, c3y); + const float xmin = std::min(std::min(c0x, c1x), std::min(c2x, c3x)); + const float xmax = std::max(std::max(c0x, c1x), std::max(c2x, c3x)); + const float ymin = std::min(std::min(c0y, c1y), std::min(c2y, c3y)); + const float ymax = std::max(std::max(c0y, c1y), std::max(c2y, c3y)); + clip = GSVector4(xmin, ymin, xmax, ymax); + } + SetScissor(GSVector4i(clip).max_i32(GSVector4i::zero())); // Since we don't have the GSTexture... @@ -4666,9 +4954,22 @@ bool GSDeviceVK::DoCAS( // only happening once a frame, so the update isn't a huge deal. Vulkan::DescriptorSetUpdateBuilder dsub; - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, 0, sTexVK->GetView(), sTexVK->GetVkLayout()); - dsub.AddStorageImageDescriptorWrite(VK_NULL_HANDLE, 1, dTexVK->GetView(), dTexVK->GetVkLayout()); - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, false); + if (m_optional_extensions.vk_khr_push_descriptor) + { + dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, 0, sTexVK->GetView(), sTexVK->GetVkLayout()); + dsub.AddStorageImageDescriptorWrite(VK_NULL_HANDLE, 1, dTexVK->GetView(), dTexVK->GetVkLayout()); + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, false); + } + else + { + VkDescriptorSet ds = AllocateDescriptorSetFromFramePool(m_cas_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + return false; // single alloc per frame after EndRenderPass — exhaustion implausible; skip the sharpen pass + dsub.AddImageDescriptorWrite(ds, 0, sTexVK->GetView(), sTexVK->GetVkLayout()); + dsub.AddStorageImageDescriptorWrite(ds, 1, dTexVK->GetView(), dTexVK->GetVkLayout()); + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_cas_pipeline_layout, 0, 1, &ds, 0, nullptr); + } // the actual meat and potatoes! only four commands. static const int threadGroupWorkRegionDim = 16; @@ -4803,6 +5104,8 @@ void GSDeviceVK::DestroyResources() vkFreeCommandBuffers(m_device, resources.command_pool, static_cast(resources.command_buffers.size()), resources.command_buffers.data()); } + if (resources.descriptor_pool != VK_NULL_HANDLE) + vkDestroyDescriptorPool(m_device, resources.descriptor_pool, nullptr); if (resources.command_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, resources.command_pool, nullptr); } @@ -5664,15 +5967,35 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (flags & DIRTY_FLAG_TFX_TEXTURES) { + VkDescriptorSet ds = VK_NULL_HANDLE; + // Without push descriptors, we must write all bindings to a fresh descriptor set. + if (!m_optional_extensions.vk_khr_push_descriptor) + { + ds = AllocateDescriptorSetFromFramePool(m_tfx_texture_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + { + if (already_execed) + { + Console.Error("VK: Failed to allocate TFX texture descriptor set"); + return false; + } + + // Frame descriptor pool exhausted — flush to reset it, then restart + // the render pass and re-apply all state on the fresh command buffer. + ExecuteCommandBufferAndRestartRenderPass(false, "Out of TFX texture descriptors"); + return ApplyTFXState(true); + } + } + if (flags & DIRTY_FLAG_TFX_TEXTURE_TEX) { - dsub.AddCombinedImageSamplerDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_TEXTURE, + dsub.AddCombinedImageSamplerDescriptorWrite(ds, TFX_TEXTURE_TEXTURE, m_tfx_textures[TFX_TEXTURE_TEXTURE]->GetView(), m_tfx_sampler, m_tfx_textures[TFX_TEXTURE_TEXTURE]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_PALETTE) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_PALETTE, + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_PALETTE, m_tfx_textures[TFX_TEXTURE_PALETTE]->GetView(), m_tfx_textures[TFX_TEXTURE_PALETTE]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_RT) @@ -5680,17 +6003,17 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (m_features.texture_barrier && !UseFeedbackLoopLayout()) { dsub.AddInputAttachmentDescriptorWrite( - VK_NULL_HANDLE, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), VK_IMAGE_LAYOUT_GENERAL); + ds, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), VK_IMAGE_LAYOUT_GENERAL); } else { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_RT, m_tfx_textures[TFX_TEXTURE_RT]->GetView(), m_tfx_textures[TFX_TEXTURE_RT]->GetVkLayout()); } } if (flags & DIRTY_FLAG_TFX_TEXTURE_PRIMID) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_PRIMID, + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_PRIMID, m_tfx_textures[TFX_TEXTURE_PRIMID]->GetView(), m_tfx_textures[TFX_TEXTURE_PRIMID]->GetVkLayout()); } if (flags & DIRTY_FLAG_TFX_TEXTURE_DEPTH) @@ -5698,26 +6021,35 @@ bool GSDeviceVK::ApplyTFXState(bool already_execed) if (m_features.texture_barrier && !UseFeedbackLoopLayout()) { dsub.AddInputAttachmentDescriptorWrite( - VK_NULL_HANDLE, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), VK_IMAGE_LAYOUT_GENERAL); + ds, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), VK_IMAGE_LAYOUT_GENERAL); } else { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_DEPTH, m_tfx_textures[TFX_TEXTURE_DEPTH]->GetView(), m_tfx_textures[TFX_TEXTURE_DEPTH]->GetVkLayout()); } } if (flags & DIRTY_FLAG_TFX_TEXTURE_RT_ROV) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_RT_ROV, m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_RT_ROV, m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetView(), m_tfx_textures[TFX_TEXTURE_RT_ROV]->GetVkLayout(), true); } if (flags & DIRTY_FLAG_TFX_TEXTURE_DEPTH_ROV) { - dsub.AddImageDescriptorWrite(VK_NULL_HANDLE, TFX_TEXTURE_DEPTH_ROV, m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetView(), + dsub.AddImageDescriptorWrite(ds, TFX_TEXTURE_DEPTH_ROV, m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetView(), m_tfx_textures[TFX_TEXTURE_DEPTH_ROV]->GetVkLayout(), true); } - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, TFX_DESCRIPTOR_SET_TEXTURES); + if (m_optional_extensions.vk_khr_push_descriptor) + { + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, TFX_DESCRIPTOR_SET_TEXTURES); + } + else + { + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_tfx_pipeline_layout, + TFX_DESCRIPTOR_SET_TEXTURES, 1, &ds, 0, nullptr); + } } ApplyBaseState(flags, cmdbuf); @@ -5738,9 +6070,33 @@ bool GSDeviceVK::ApplyUtilityState(bool already_execed) m_current_pipeline_layout = PipelineLayout::Utility; Vulkan::DescriptorSetUpdateBuilder dsub; - dsub.AddCombinedImageSamplerDescriptorWrite( - VK_NULL_HANDLE, 0, m_utility_texture->GetView(), m_utility_sampler, m_utility_texture->GetVkLayout()); - dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, false); + if (m_optional_extensions.vk_khr_push_descriptor) + { + dsub.AddCombinedImageSamplerDescriptorWrite( + VK_NULL_HANDLE, 0, m_utility_texture->GetView(), m_utility_sampler, m_utility_texture->GetVkLayout()); + dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, false); + } + else + { + VkDescriptorSet ds = AllocateDescriptorSetFromFramePool(m_utility_ds_layout); + if (ds == VK_NULL_HANDLE) [[unlikely]] + { + if (already_execed) + { + Console.Error("VK: Failed to allocate utility descriptor set"); + return false; + } + + // Frame descriptor pool exhausted — flush to reset it, then restart + // the render pass and re-apply all state on the fresh command buffer. + ExecuteCommandBufferAndRestartRenderPass(false, "Out of utility descriptors"); + return ApplyUtilityState(true); + } + dsub.AddCombinedImageSamplerDescriptorWrite( + ds, 0, m_utility_texture->GetView(), m_utility_sampler, m_utility_texture->GetVkLayout()); + dsub.Update(m_device); + vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, m_utility_pipeline_layout, 0, 1, &ds, 0, nullptr); + } } diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h index 647527f2e0..8d42264d8c 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h @@ -43,6 +43,7 @@ public: bool vk_ext_line_rasterization : 1; bool vk_swapchain_maintenance1 : 1; bool vk_swapchain_maintenance1_is_khr : 1; + bool vk_khr_push_descriptor : 1; bool vk_khr_driver_properties : 1; bool vk_khr_shader_non_semantic_info : 1; bool vk_ext_attachment_feedback_loop_layout : 1; @@ -82,6 +83,9 @@ public: /// Returns true if running on an AMD GPU. __fi bool IsDeviceAMD() const { return (m_device_properties.vendorID == 0x1002); } + /// Returns true if running on an ARM GPU (Mali). + __fi bool IsDeviceARM() const { return (m_device_properties.vendorID == 0x13B5); } + // Creates a simple render pass. VkRenderPass GetRenderPass(VkFormat color_format, VkFormat depth_format, VkAttachmentLoadOp color_load_op = VK_ATTACHMENT_LOAD_OP_LOAD, @@ -104,6 +108,10 @@ public: /// Allocates a descriptor set from the pool reserved for the current frame. VkDescriptorSet AllocatePersistentDescriptorSet(VkDescriptorSetLayout set_layout); + /// Allocates a descriptor set from the current frame's per-frame pool (push descriptor fallback). + /// Returns VK_NULL_HANDLE on pool exhaustion after flushing the command buffer. + VkDescriptorSet AllocateDescriptorSetFromFramePool(VkDescriptorSetLayout set_layout); + /// Frees a descriptor set allocated from the global pool. void FreePersistentDescriptorSet(VkDescriptorSet set); @@ -214,6 +222,7 @@ private: // [0] - Init (upload) command buffer, [1] - draw command buffer VkCommandPool command_pool = VK_NULL_HANDLE; std::array command_buffers{VK_NULL_HANDLE, VK_NULL_HANDLE}; + VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; // Per-frame pool, used when push descriptors are unavailable VkFence fence = VK_NULL_HANDLE; u64 fence_counter = 0; s32 spin_id = -1; @@ -292,6 +301,7 @@ private: VkPhysicalDeviceProperties m_device_properties = {}; VkPhysicalDeviceDriverPropertiesKHR m_device_driver_properties = {}; OptionalExtensions m_optional_extensions = {}; + bool m_colorclip_fallback_to_hdr = false; public: enum FeedbackLoopFlag : u8 @@ -562,6 +572,9 @@ public: bool SetGPUTimingEnabled(bool enabled) override; float GetAndResetAccumulatedGPUTime() override; + void EnableExtendedStats(bool enabled) override; + std::vector GetExtendedStats() const override; + void PushDebugGroup(const char* fmt, ...) override; void PopDebugGroup() override; void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override; diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp index f93b1673fc..83230b85bd 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.cpp @@ -9,9 +9,11 @@ #include "common/Assertions.h" #include "common/CocoaTools.h" #include "common/Console.h" +#include "common/Timer.h" #include #include +#include #include #if defined(VK_USE_PLATFORM_XLIB_KHR) @@ -20,6 +22,34 @@ static_assert(VKSwapChain::NUM_SEMAPHORES == (GSDeviceVK::NUM_COMMAND_BUFFERS + 1)); +namespace +{ + // Diagnostic counters for present/acquire stalls (esp. on tiler-class drivers). + // Atomics so MTGS-thread acquire/present writes are race-free under reads from any thread. + std::atomic s_stats_enabled{false}; + std::atomic s_acquire_count{0}; + std::atomic s_acquire_total_ns{0}; + std::atomic s_acquire_max_ns{0}; + std::atomic s_present_count{0}; + std::atomic s_present_total_ns{0}; + std::atomic s_present_max_ns{0}; + // Aggregate counts of SUBOPTIMAL / OUT_OF_DATE results across BOTH the + // acquire and the present path (NoteAcquire + NotePresent both tick these). + // A persistently-stale swapchain can therefore tick up to twice per frame — + // these are "results observed", not "frames affected". Intentional: it keeps + // both event sources visible in the overlay without a 4-counter schema. + std::atomic s_suboptimal_count{0}; + std::atomic s_out_of_date_count{0}; + + void UpdateMax(std::atomic& dst, u64 sample) + { + u64 prev = dst.load(std::memory_order_relaxed); + while (sample > prev && !dst.compare_exchange_weak(prev, sample, std::memory_order_relaxed)) + { + } + } +} // namespace + VKSwapChain::VKSwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR present_mode, std::optional exclusive_fullscreen_control) : m_window_info(wi) @@ -124,6 +154,144 @@ VkSurfaceKHR VKSwapChain::CreateVulkanSurface(VkInstance instance, VkPhysicalDev } #endif + // VK_KHR_display direct-to-monitor (kmsdrm handhelds). No compositor, + // no GBM, no native window handle from the frontend — the renderer + // enumerates displays itself. + if (wi->type == WindowInfo::Type::VulkanDirect) + { + u32 display_count = 0; + VkResult res = vkGetPhysicalDeviceDisplayPropertiesKHR(physical_device, &display_count, nullptr); + if (res != VK_SUCCESS || display_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPropertiesKHR (count) failed: "); + Console.Error("VK_KHR_display: no displays reported by ICD."); + return VK_NULL_HANDLE; + } + + std::vector displays(display_count); + res = vkGetPhysicalDeviceDisplayPropertiesKHR(physical_device, &display_count, displays.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + // Pick the first display. Multi-monitor handhelds are vanishingly + // rare; revisit if needed. + const VkDisplayKHR display = displays[0].display; + INFO_LOG("VK_KHR_display: using display '{}', physical {}x{} mm", + displays[0].displayName ? displays[0].displayName : "", + displays[0].physicalDimensions.width, displays[0].physicalDimensions.height); + + u32 mode_count = 0; + res = vkGetDisplayModePropertiesKHR(physical_device, display, &mode_count, nullptr); + if (res != VK_SUCCESS || mode_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetDisplayModePropertiesKHR (count) failed: "); + return VK_NULL_HANDLE; + } + + std::vector modes(mode_count); + res = vkGetDisplayModePropertiesKHR(physical_device, display, &mode_count, modes.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetDisplayModePropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + // Index 0 is the display's preferred (native) mode per spec. + // If the caller asked for a specific resolution, try to match it. + u32 best_mode_idx = 0; + if (wi->surface_width != 0 && wi->surface_height != 0) + { + for (u32 i = 0; i < mode_count; i++) + { + if (modes[i].parameters.visibleRegion.width == wi->surface_width && + modes[i].parameters.visibleRegion.height == wi->surface_height) + { + best_mode_idx = i; + break; + } + } + } + const VkDisplayModeKHR mode = modes[best_mode_idx].displayMode; + const VkExtent2D mode_extent = modes[best_mode_idx].parameters.visibleRegion; + INFO_LOG("VK_KHR_display: selected mode {}x{}@{}.{:03} Hz", + mode_extent.width, mode_extent.height, + modes[best_mode_idx].parameters.refreshRate / 1000, + modes[best_mode_idx].parameters.refreshRate % 1000); + + u32 plane_count = 0; + res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physical_device, &plane_count, nullptr); + if (res != VK_SUCCESS || plane_count == 0) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR (count) failed: "); + return VK_NULL_HANDLE; + } + + std::vector planes(plane_count); + res = vkGetPhysicalDeviceDisplayPlanePropertiesKHR(physical_device, &plane_count, planes.data()); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkGetPhysicalDeviceDisplayPlanePropertiesKHR (data) failed: "); + return VK_NULL_HANDLE; + } + + u32 selected_plane = UINT32_MAX; + for (u32 i = 0; i < plane_count; i++) + { + // Skip planes already bound to a different display. + if (planes[i].currentDisplay != VK_NULL_HANDLE && planes[i].currentDisplay != display) + continue; + + u32 supported_count = 0; + if (vkGetDisplayPlaneSupportedDisplaysKHR(physical_device, i, &supported_count, nullptr) != VK_SUCCESS || + supported_count == 0) + continue; + + std::vector supported(supported_count); + vkGetDisplayPlaneSupportedDisplaysKHR(physical_device, i, &supported_count, supported.data()); + if (std::find(supported.begin(), supported.end(), display) != supported.end()) + { + selected_plane = i; + break; + } + } + + if (selected_plane == UINT32_MAX) + { + Console.Error("VK_KHR_display: no compatible plane found for selected display."); + return VK_NULL_HANDLE; + } + + VkDisplaySurfaceCreateInfoKHR surface_create_info = {}; + surface_create_info.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR; + surface_create_info.displayMode = mode; + surface_create_info.planeIndex = selected_plane; + surface_create_info.planeStackIndex = planes[selected_plane].currentStackIndex; + surface_create_info.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + surface_create_info.globalAlpha = 1.0f; + surface_create_info.alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR; + surface_create_info.imageExtent = mode_extent; + + VkSurfaceKHR surface; + res = vkCreateDisplayPlaneSurfaceKHR(instance, &surface_create_info, nullptr, &surface); + if (res != VK_SUCCESS) + { + LOG_VULKAN_ERROR(res, "vkCreateDisplayPlaneSurfaceKHR failed: "); + return VK_NULL_HANDLE; + } + + // Reflect the actual selected mode back to the caller so the + // swapchain sizes correctly even when 0x0 was passed in. + wi->surface_width = mode_extent.width; + wi->surface_height = mode_extent.height; + wi->surface_refresh_rate = + static_cast(modes[best_mode_idx].parameters.refreshRate) / 1000.0f; + + return surface; + } + return VK_NULL_HANDLE; } @@ -202,6 +370,72 @@ std::optional VKSwapChain::SelectSurfaceFormat(VkSurfaceKHR return std::nullopt; } +VKSwapChain::PresentStats VKSwapChain::GetPresentStats() +{ + const u64 acquire_n = s_acquire_count.load(std::memory_order_relaxed); + const u64 present_n = s_present_count.load(std::memory_order_relaxed); + return PresentStats{ + acquire_n, + static_cast(s_acquire_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + static_cast(s_acquire_max_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + present_n, + static_cast(s_present_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + static_cast(s_present_max_ns.load(std::memory_order_relaxed)) / 1'000'000.0, + s_suboptimal_count.load(std::memory_order_relaxed), + s_out_of_date_count.load(std::memory_order_relaxed), + }; +} + +void VKSwapChain::ResetPresentStats() +{ + s_acquire_count.store(0, std::memory_order_relaxed); + s_acquire_total_ns.store(0, std::memory_order_relaxed); + s_acquire_max_ns.store(0, std::memory_order_relaxed); + s_present_count.store(0, std::memory_order_relaxed); + s_present_total_ns.store(0, std::memory_order_relaxed); + s_present_max_ns.store(0, std::memory_order_relaxed); + s_suboptimal_count.store(0, std::memory_order_relaxed); + s_out_of_date_count.store(0, std::memory_order_relaxed); +} + +void VKSwapChain::SetPresentStatsEnabled(bool enabled) +{ + s_stats_enabled.store(enabled, std::memory_order_relaxed); +} + +bool VKSwapChain::IsPresentStatsEnabled() +{ + return s_stats_enabled.load(std::memory_order_relaxed); +} + +void VKSwapChain::NoteAcquire(double ms, VkResult res) +{ + if (!s_stats_enabled.load(std::memory_order_relaxed)) + return; + const u64 ns = static_cast(ms * 1'000'000.0); + s_acquire_count.fetch_add(1, std::memory_order_relaxed); + s_acquire_total_ns.fetch_add(ns, std::memory_order_relaxed); + UpdateMax(s_acquire_max_ns, ns); + if (res == VK_SUBOPTIMAL_KHR) + s_suboptimal_count.fetch_add(1, std::memory_order_relaxed); + else if (res == VK_ERROR_OUT_OF_DATE_KHR) + s_out_of_date_count.fetch_add(1, std::memory_order_relaxed); +} + +void VKSwapChain::NotePresent(double ms, VkResult res) +{ + if (!s_stats_enabled.load(std::memory_order_relaxed)) + return; + const u64 ns = static_cast(ms * 1'000'000.0); + s_present_count.fetch_add(1, std::memory_order_relaxed); + s_present_total_ns.fetch_add(ns, std::memory_order_relaxed); + UpdateMax(s_present_max_ns, ns); + if (res == VK_SUBOPTIMAL_KHR) + s_suboptimal_count.fetch_add(1, std::memory_order_relaxed); + else if (res == VK_ERROR_OUT_OF_DATE_KHR) + s_out_of_date_count.fetch_add(1, std::memory_order_relaxed); +} + static const char* PresentModeToString(VkPresentModeKHR mode) { switch (mode) @@ -325,8 +559,18 @@ bool VKSwapChain::CreateSwapChain() // Select number of images in swap chain, we prefer one buffer in the background to work on in triple-buffered mode. // maxImageCount can be zero, in which case there isn't an upper limit on the number of buffers. + // VK_KHR_display (VulkanDirect) + FIFO + 2 images stalls vkAcquireNextImageKHR + // for ~1.5 vsync intervals per frame waiting for the display engine to release + // the previously-presented image (measured on some tiler-class drivers). A third + // image lets the GPU work on N+2 while N is on-screen and N+1 is queued, recovering + // ~33% throughput. Default to 3 images for VulkanDirect, and for MAILBOX + // present mode regardless of WSI. + const bool use_triple = + (m_window_info.type == WindowInfo::Type::VulkanDirect) || + (m_present_mode == VK_PRESENT_MODE_MAILBOX_KHR); + const u32 desired_image_count = use_triple ? 3 : 2; u32 image_count = std::clamp( - (m_present_mode == VK_PRESENT_MODE_MAILBOX_KHR) ? 3 : 2, surface_capabilities.minImageCount, + desired_image_count, surface_capabilities.minImageCount, (surface_capabilities.maxImageCount == 0) ? std::numeric_limits::max() : surface_capabilities.maxImageCount); DEV_LOG("Creating a swap chain with {} images in present mode {}", image_count, PresentModeToString(m_present_mode)); @@ -343,6 +587,28 @@ bool VKSwapChain::CreateSwapChain() size.height = std::clamp(size.height, surface_capabilities.minImageExtent.height, surface_capabilities.maxImageExtent.height); + // One-shot log of the resolved swapchain config — useful for WSI-path diagnosis + // (e.g. comparing VK_KHR_display vs a Wayland surface on the same device). + { + const char* wsi_name = "?"; + switch (m_window_info.type) + { + case WindowInfo::Type::Surfaceless: wsi_name = "Surfaceless"; break; + case WindowInfo::Type::Win32: wsi_name = "Win32"; break; + case WindowInfo::Type::X11: wsi_name = "X11"; break; + case WindowInfo::Type::Wayland: wsi_name = "Wayland"; break; + case WindowInfo::Type::MacOS: wsi_name = "MacOS"; break; + case WindowInfo::Type::VulkanDirect: wsi_name = "VulkanDirect"; break; + } + Console.WriteLnFmt( + "Vulkan: Swapchain {}x{} fmt={} colorspace={} present={} images={} (desired={} min={} max={}) wsi={}", + size.width, size.height, static_cast(surface_format->format), + static_cast(surface_format->colorSpace), + PresentModeToString(m_present_mode), image_count, + desired_image_count, surface_capabilities.minImageCount, surface_capabilities.maxImageCount, + wsi_name); + } + // Prefer identity transform if possible VkSurfaceTransformFlagBitsKHR transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; if (!(surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)) @@ -382,9 +648,18 @@ bool VKSwapChain::CreateSwapChain() // VK_EXT_swapchain_maintenance1 types/enums are aliases of VK_KHR_swapchain_maintenance1 types/enums. const VkSwapchainPresentModesCreateInfoKHR modes_info{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODES_CREATE_INFO_KHR, nullptr, 1u, &m_present_mode}; + // Some ARM Mali Vulkan drivers advertise VK_EXT_swapchain_maintenance1 but + // vkCreateSwapchainKHR errors VK_ERROR_INITIALIZATION_FAILED whenever this pNext is + // attached, regardless of present mode. Keep the extension enabled — the rest of its + // surface (present-fence-info, release-swapchain-images) works fine — and just skip + // the create-time pNext on ARM Mali. + const bool use_present_modes_pnext = + GSDeviceVK::GetInstance()->GetOptionalExtensions().vk_swapchain_maintenance1 && + !GSDeviceVK::GetInstance()->IsDeviceARM(); + // Now we can actually create the swap chain - VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, - GSDeviceVK::GetInstance()->GetOptionalExtensions().vk_swapchain_maintenance1 ? &modes_info : nullptr, 0, m_surface, + VkSwapchainCreateInfoKHR swap_chain_info = {VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + use_present_modes_pnext ? &modes_info : nullptr, 0, m_surface, image_count, surface_format->format, surface_format->colorSpace, size, 1u, image_usage, VK_SHARING_MODE_EXCLUSIVE, 0, nullptr, transform, alpha, m_present_mode, VK_TRUE, old_swap_chain}; std::array indices = {{ @@ -551,8 +826,16 @@ VkResult VKSwapChain::AcquireNextImage() // Use a different semaphore for each image. m_current_semaphore = (m_current_semaphore + 1) % static_cast(m_semaphores.size()); + const bool stats = s_stats_enabled.load(std::memory_order_relaxed); + const Common::Timer::Value t_start = stats ? Common::Timer::GetCurrentValue() : 0; const VkResult res = vkAcquireNextImageKHR(GSDeviceVK::GetInstance()->GetDevice(), m_swap_chain, UINT64_MAX, m_semaphores[m_current_semaphore].available_semaphore, VK_NULL_HANDLE, &m_current_image); + if (stats) + { + const double elapsed_ms = + Common::Timer::ConvertValueToMilliseconds(Common::Timer::GetCurrentValue() - t_start); + NoteAcquire(elapsed_ms, res); + } m_image_acquire_result = res; return res; } diff --git a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h index ce43ea689d..bca8d40022 100644 --- a/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h +++ b/pcsx2/GS/Renderers/Vulkan/VKSwapChain.h @@ -23,6 +23,29 @@ public: ~VKSwapChain(); + // Diagnostic counters for present/acquire timing, accumulated across all swapchains. + // Surfaces WSI-layer stalls (e.g. slow present/acquire on tiler-class drivers). + struct PresentStats + { + u64 acquire_count; + double acquire_total_ms; + double acquire_max_ms; + u64 present_count; + double present_total_ms; + double present_max_ms; + u64 suboptimal_count; + u64 out_of_date_count; + }; + static PresentStats GetPresentStats(); + static void ResetPresentStats(); + // Controls whether NoteAcquire/NotePresent record anything. Default off so + // the normal present path pays only one atomic-load + branch per call; + // diagnostic tools flip this on at startup. + static void SetPresentStatsEnabled(bool enabled); + static bool IsPresentStatsEnabled(); + static void NoteAcquire(double ms, VkResult res); + static void NotePresent(double ms, VkResult res); + // Creates a vulkan-renderable surface for the specified window handle. static VkSurfaceKHR CreateVulkanSurface(VkInstance instance, VkPhysicalDevice physical_device, WindowInfo* wi); @@ -46,6 +69,7 @@ public: __fi u32 GetCurrentImageIndex() const { return m_current_image; } __fi const u32* GetCurrentImageIndexPtr() const { return &m_current_image; } __fi u32 GetImageCount() const { return static_cast(m_images.size()); } + __fi VkPresentModeKHR GetPresentMode() const { return m_present_mode; } __fi const GSTextureVK* GetCurrentTexture() const { return m_images[m_current_image].get(); } __fi GSTextureVK* GetCurrentTexture() { return m_images[m_current_image].get(); } __fi VkSemaphore GetImageAvailableSemaphore() const diff --git a/pcsx2/ImGui/ImGuiManager.cpp b/pcsx2/ImGui/ImGuiManager.cpp index ae0d272bad..307abb4d57 100644 --- a/pcsx2/ImGui/ImGuiManager.cpp +++ b/pcsx2/ImGui/ImGuiManager.cpp @@ -183,8 +183,11 @@ bool ImGuiManager::Initialize() g.ConfigNavWindowingKeyPrev = ImGuiKey_None; g.ConfigNavWindowingWithGamepad = false; - s_window_width = static_cast(g_gs_device->GetWindowWidth()); - s_window_height = static_cast(g_gs_device->GetWindowHeight()); + { + const GSVector2i pres = g_gs_device->GetPresentationSize(); + s_window_width = static_cast(pres.x); + s_window_height = static_cast(pres.y); + } io.DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling io.DisplaySize = ImVec2(s_window_width, s_window_height); @@ -258,11 +261,12 @@ float ImGuiManager::GetWindowHeight() void ImGuiManager::WindowResized() { - const u32 new_width = g_gs_device ? g_gs_device->GetWindowWidth() : 0; - const u32 new_height = g_gs_device ? g_gs_device->GetWindowHeight() : 0; + GSVector2i new_size{}; + if (g_gs_device) + new_size = g_gs_device->GetPresentationSize(); - s_window_width = static_cast(new_width); - s_window_height = static_cast(new_height); + s_window_width = static_cast(new_size.x); + s_window_height = static_cast(new_size.y); ImGui::GetIO().DisplaySize = ImVec2(s_window_width, s_window_height); // Scale might have changed as a result of window resize. diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 8fa9ad8608..d70ea21c6f 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -658,6 +658,13 @@ const char* Pcsx2Config::GSOptions::FMVAspectRatioSwitchNames[(size_t)FMVAspectR "10:7", nullptr}; +const char* Pcsx2Config::GSOptions::DisplayRotationNames[(size_t)DisplayRotation::MaxCount + 1] = { + "0", + "90", + "180", + "270", + nullptr}; + const char* Pcsx2Config::GSOptions::BlendingLevelNames[] = { "Minimum", "Basic", @@ -727,6 +734,7 @@ Pcsx2Config::GSOptions::GSOptions() UseBlitSwapChain = false; DisableShaderCache = false; DisableFramebufferFetch = false; + DisablePS2DepthQuantization = false; DisableVertexShaderExpand = false; SkipDuplicateFrames = true; OsdMessagesPos = OsdOverlayPos::TopLeft; @@ -806,6 +814,7 @@ bool Pcsx2Config::GSOptions::operator==(const GSOptions& right) const OpEqu(AspectRatio) && OpEqu(FMVAspectRatioSwitch) && + OpEqu(Rotation) && OptionsAreEqual(right)); } @@ -916,6 +925,7 @@ bool Pcsx2Config::GSOptions::RestartOptionsAreEqual(const GSOptions& right) cons OpEqu(UseBlitSwapChain) && OpEqu(DisableShaderCache) && OpEqu(DisableFramebufferFetch) && + OpEqu(DisablePS2DepthQuantization) && OpEqu(DisableVertexShaderExpand) && OpEqu(OverrideTextureBarriers) && OpEqu(DepthFeedbackMode) && @@ -942,6 +952,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapEnumEx(AspectRatio, "AspectRatio", AspectRatioNames); SettingsWrapEnumEx(FMVAspectRatioSwitch, "FMVAspectRatioSwitch", FMVAspectRatioSwitchNames); + SettingsWrapEnumEx(Rotation, "DisplayRotation", DisplayRotationNames); SettingsWrapIntEnumEx(ScreenshotSize, "ScreenshotSize"); SettingsWrapIntEnumEx(ScreenshotFormat, "ScreenshotFormat"); SettingsWrapEntry(ScreenshotQuality); @@ -964,6 +975,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) SettingsWrapBitBool(UseBlitSwapChain); SettingsWrapBitBool(DisableShaderCache); SettingsWrapBitBool(DisableFramebufferFetch); + SettingsWrapBitBool(DisablePS2DepthQuantization); SettingsWrapBitBool(DisableVertexShaderExpand); SettingsWrapBitBool(SkipDuplicateFrames); SettingsWrapBitBool(OsdShowSpeed);