mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
arm64/libmali GS backend: device gates, depth quantization, display rotation (fork-only)
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 <ryan@testtoast.com> Co-Authored-By: Brian Degenhardt <bmd@bmdhacks.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Ryan Walklin
Claude Opus 4.8
parent
cd9d9b4d11
commit
d884fbc08e
+6
-1
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "GS/GSExtra.h"
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<s32>(m_window_info.surface_width); }
|
||||
__fi s32 GetWindowHeight() const { return static_cast<s32>(m_window_info.surface_height); }
|
||||
__fi GSVector2i GetWindowSize() const { return GSVector2i(static_cast<s32>(m_window_info.surface_width), static_cast<s32>(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<std::string> GetExtendedStats() const { return {}; }
|
||||
|
||||
/// Returns true if not enough time has passed for present to not block.
|
||||
bool ShouldSkipPresentingFrame();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<VkCommandBuffer, 2> 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<std::string> GetExtendedStats() const override;
|
||||
|
||||
void PushDebugGroup(const char* fmt, ...) override;
|
||||
void PopDebugGroup() override;
|
||||
void InsertDebugMessage(DebugMessageCategory category, const char* fmt, ...) override;
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
#include "common/Assertions.h"
|
||||
#include "common/CocoaTools.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/Timer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
|
||||
#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<bool> s_stats_enabled{false};
|
||||
std::atomic<u64> s_acquire_count{0};
|
||||
std::atomic<u64> s_acquire_total_ns{0};
|
||||
std::atomic<u64> s_acquire_max_ns{0};
|
||||
std::atomic<u64> s_present_count{0};
|
||||
std::atomic<u64> s_present_total_ns{0};
|
||||
std::atomic<u64> 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<u64> s_suboptimal_count{0};
|
||||
std::atomic<u64> s_out_of_date_count{0};
|
||||
|
||||
void UpdateMax(std::atomic<u64>& 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<bool> 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<VkDisplayPropertiesKHR> 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 : "<unnamed>",
|
||||
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<VkDisplayModePropertiesKHR> 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<VkDisplayPlanePropertiesKHR> 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<VkDisplayKHR> 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<float>(modes[best_mode_idx].parameters.refreshRate) / 1000.0f;
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
@@ -202,6 +370,72 @@ std::optional<VkSurfaceFormatKHR> 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<double>(s_acquire_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0,
|
||||
static_cast<double>(s_acquire_max_ns.load(std::memory_order_relaxed)) / 1'000'000.0,
|
||||
present_n,
|
||||
static_cast<double>(s_present_total_ns.load(std::memory_order_relaxed)) / 1'000'000.0,
|
||||
static_cast<double>(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<u64>(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<u64>(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<u32>(
|
||||
(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<u32>::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<unsigned>(surface_format->format),
|
||||
static_cast<unsigned>(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<uint32_t, 2> indices = {{
|
||||
@@ -551,8 +826,16 @@ VkResult VKSwapChain::AcquireNextImage()
|
||||
// Use a different semaphore for each image.
|
||||
m_current_semaphore = (m_current_semaphore + 1) % static_cast<u32>(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;
|
||||
}
|
||||
|
||||
@@ -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<u32>(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
|
||||
|
||||
@@ -183,8 +183,11 @@ bool ImGuiManager::Initialize()
|
||||
g.ConfigNavWindowingKeyPrev = ImGuiKey_None;
|
||||
g.ConfigNavWindowingWithGamepad = false;
|
||||
|
||||
s_window_width = static_cast<float>(g_gs_device->GetWindowWidth());
|
||||
s_window_height = static_cast<float>(g_gs_device->GetWindowHeight());
|
||||
{
|
||||
const GSVector2i pres = g_gs_device->GetPresentationSize();
|
||||
s_window_width = static_cast<float>(pres.x);
|
||||
s_window_height = static_cast<float>(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<float>(new_width);
|
||||
s_window_height = static_cast<float>(new_height);
|
||||
s_window_width = static_cast<float>(new_size.x);
|
||||
s_window_height = static_cast<float>(new_size.y);
|
||||
ImGui::GetIO().DisplaySize = ImVec2(s_window_width, s_window_height);
|
||||
|
||||
// Scale might have changed as a result of window resize.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user