From 3184592404a70a4c1fcd07c8dd6f2494c4bd8b4f Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:13:35 +0100 Subject: [PATCH] add macOS platform support --- CMakeLists.txt | 46 +- cmake/rexglue_helpers.cmake | 83 +- cmake/rexglue_install.cmake | 1 + include/rex/chrono/chrono.h | 14 + include/rex/graphics/graphics_system.h | 1 + .../pipeline/shader/spirv_translator.h | 7 +- .../rex/graphics/vulkan/command_processor.h | 38 + .../graphics/vulkan/deferred_command_buffer.h | 16 + .../rex/graphics/vulkan/render_target_cache.h | 2 +- include/rex/input/sdl/sdl_input_driver.h | 1 + include/rex/perf/counter.h | 41 + include/rex/platform/dynlib.h | 8 + include/rex/platform/fpscr.h | 5 +- include/rex/ppc/context.h | 8 +- include/rex/rex_app.h | 7 + include/rex/std_compat.h | 76 ++ include/rex/system/kernel_state.h | 2 +- include/rex/system/xmemory.h | 15 +- include/rex/thread/fiber.h | 13 +- include/rex/thread/timer_queue.h | 2 + include/rex/ui/surface.h | 3 + include/rex/ui/surface_sdl.h | 31 + include/rex/ui/vulkan/api.h | 6 + include/rex/ui/vulkan/device.h | 2 + .../rex/ui/vulkan/functions/device_1_0.inc | 1 + .../functions/instance_ext_metal_surface.inc | 2 + include/rex/ui/vulkan/instance.h | 7 + include/rex/ui/window_sdl.h | 77 ++ include/rex/ui/windowed_app_context_sdl.h | 29 + resources/templates/codegen/init_h.inja | 9 + resources/templates/test/ppc_config_h.inja | 10 +- src/codegen/builders/vector.cpp | 7 +- src/core/CMakeLists.txt | 5 +- src/core/clock_posix.cpp | 16 +- src/core/cvar.cpp | 25 +- src/core/exception_handler_posix.cpp | 115 ++- src/core/fiber_posix.cpp | 45 +- src/core/filesystem_posix.cpp | 36 +- src/core/mapped_memory_posix.cpp | 7 + src/core/memory_posix.cpp | 40 + src/core/perf/counter.cpp | 88 ++ src/core/threading_posix.cpp | 70 +- src/graphics/graphics_system.cpp | 4 +- .../pipeline/shader/spirv_translator.cpp | 148 ++- src/graphics/vulkan/command_processor.cpp | 845 ++++++++++++++---- .../vulkan/deferred_command_buffer.cpp | 5 + src/graphics/vulkan/pipeline_cache.cpp | 8 + src/graphics/vulkan/render_target_cache.cpp | 72 +- src/graphics/vulkan/texture_cache.cpp | 33 +- src/input/input_system.cpp | 11 + src/input/sdl/sdl_input_driver.cpp | 25 +- src/kernel/crt/heap.cpp | 3 +- src/kernel/xam/xam_net.cpp | 3 +- src/system/kernel_state.cpp | 22 + src/system/runtime.cpp | 13 + src/system/xmemory.cpp | 73 +- src/system/xsocket.cpp | 10 +- src/ui/CMakeLists.txt | 8 + src/ui/overlay/simple_settings_overlay.cpp | 7 + src/ui/presenter.cpp | 12 +- src/ui/rex_app.cpp | 40 + src/ui/surface_sdl.cpp | 21 + src/ui/vulkan/vulkan_device.cpp | 10 +- src/ui/vulkan/vulkan_immediate_drawer.cpp | 5 + src/ui/vulkan/vulkan_instance.cpp | 34 +- src/ui/vulkan/vulkan_presenter.cpp | 122 ++- src/ui/window.cpp | 1 + src/ui/window_sdl.cpp | 743 +++++++++++++++ src/ui/windowed_app_context_sdl.cpp | 70 ++ src/ui/windowed_app_main_sdl.cpp | 61 ++ thirdparty/CMakeLists.txt | 31 +- thirdparty/crypto/sha256.cpp | 12 +- thirdparty/renderdoc/renderdoc_app.h | 2 +- 73 files changed, 3139 insertions(+), 332 deletions(-) create mode 100644 include/rex/std_compat.h create mode 100644 include/rex/ui/surface_sdl.h create mode 100644 include/rex/ui/vulkan/functions/instance_ext_metal_surface.inc create mode 100644 include/rex/ui/window_sdl.h create mode 100644 include/rex/ui/windowed_app_context_sdl.h create mode 100644 src/ui/surface_sdl.cpp create mode 100644 src/ui/window_sdl.cpp create mode 100644 src/ui/windowed_app_context_sdl.cpp create mode 100644 src/ui/windowed_app_main_sdl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5afee77..a8fbf62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,15 @@ set(REXGLUE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}") include(GNUInstallDirs) include(CMakePackageConfigHelpers) -if(UNIX AND NOT APPLE) +if(APPLE) + # Keep app bundles and command-line launches able to find dylibs staged + # beside the executable. + list(APPEND CMAKE_INSTALL_RPATH + "@loader_path" + "@loader_path/../${CMAKE_INSTALL_LIBDIR}" + ) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH ON) +elseif(UNIX AND NOT APPLE) # Keep installed binaries self-contained by resolving shared libs relative # to the install prefix (for example, bin -> ../lib). set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) @@ -86,15 +94,26 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_DEBUG_POSTFIX d) set(CMAKE_RELWITHDEBINFO_POSTFIX rd) -if(NOT WIN32) +set(REX_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}") +if(APPLE AND CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES REX_OSX_ARCH_COUNT) + if(REX_OSX_ARCH_COUNT EQUAL 1) + set(REX_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}") + endif() +endif() + +if(UNIX AND NOT APPLE) # Use appropriate code model based on architecture - if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") + if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64") # Large code model on x86_64 for linking with very large recompiled executables (35MB+) add_compile_options(-mcmodel=large) else() # Small code model for other architectures (ARM64, etc.) add_compile_options(-mcmodel=small) endif() +endif() + +if(NOT WIN32) # Position-independent code for all targets set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() @@ -112,25 +131,32 @@ add_compile_options( # Platform detection (supports AMD64 and ARM64) if(WIN32) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") + if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64") set(REX_PLATFORM "win-amd64") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") + elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64") set(REX_PLATFORM "win-arm64") else() - message(FATAL_ERROR "Unsupported Windows architecture: ${CMAKE_SYSTEM_PROCESSOR}") + message(FATAL_ERROR "Unsupported Windows architecture: ${REX_TARGET_PROCESSOR}") endif() add_compile_definitions(REX_PLATFORM_WINDOWS=1) +elseif(APPLE) + if(REX_TARGET_PROCESSOR MATCHES "aarch64|arm64|ARM64") + set(REX_PLATFORM "macos-arm64") + else() + message(FATAL_ERROR "Unsupported macOS architecture: ${REX_TARGET_PROCESSOR}. macOS builds are ARM64-only.") + endif() + add_compile_definitions(REX_PLATFORM_MAC=1) elseif(UNIX AND NOT APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") + if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64") set(REX_PLATFORM "linux-amd64") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") + elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64") set(REX_PLATFORM "linux-arm64") else() - set(REX_PLATFORM "linux-${CMAKE_SYSTEM_PROCESSOR}") + set(REX_PLATFORM "linux-${REX_TARGET_PROCESSOR}") endif() add_compile_definitions(REX_PLATFORM_LINUX=1) else() - message(FATAL_ERROR "Unsupported platform. ReXGlue supports Windows and Linux only.") + message(FATAL_ERROR "Unsupported platform. ReXGlue supports Windows, Linux and macOS only.") endif() # Build config as compile definition (multi-config generators don't know config at configure time) diff --git a/cmake/rexglue_helpers.cmake b/cmake/rexglue_helpers.cmake index 6f88084..0edffab 100644 --- a/cmake/rexglue_helpers.cmake +++ b/cmake/rexglue_helpers.cmake @@ -14,22 +14,30 @@ # runtime DLL staging is the host's job (see rexglue_configure_target). #========================================================== function(rexglue_apply_target_settings target_name) + set(_rexglue_target_processor "${CMAKE_SYSTEM_PROCESSOR}") + if(APPLE AND CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES _rexglue_osx_arch_count) + if(_rexglue_osx_arch_count EQUAL 1) + set(_rexglue_target_processor "${CMAKE_OSX_ARCHITECTURES}") + endif() + endif() + if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) target_include_directories(${target_name} PRIVATE ${GTK3_INCLUDE_DIRS}) target_link_libraries(${target_name} PRIVATE ${GTK3_LIBRARIES}) # Large executable support - if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + if(_rexglue_target_processor MATCHES "x86_64|AMD64") target_link_options(${target_name} PRIVATE -Wl,--no-relax) target_compile_options(${target_name} PRIVATE -mcmodel=large) - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") + elseif(_rexglue_target_processor MATCHES "aarch64|ARM64") target_compile_options(${target_name} PRIVATE -march=armv8-a) endif() endif() - if(NOT MSVC) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + if(NOT MSVC AND NOT APPLE) + if(_rexglue_target_processor MATCHES "x86_64|AMD64") target_compile_options(${target_name} PRIVATE -msse4.1) endif() endif() @@ -51,6 +59,9 @@ function(rexglue_configure_target target_name) if(WIN32) target_sources(${target_name} PRIVATE ${REXGLUE_SHARE_DIR}/windowed_app_main_win.cpp) + elseif(APPLE) + target_sources(${target_name} PRIVATE + ${REXGLUE_SHARE_DIR}/windowed_app_main_sdl.cpp) else() target_sources(${target_name} PRIVATE ${REXGLUE_SHARE_DIR}/windowed_app_main_posix.cpp) @@ -68,7 +79,12 @@ function(rexglue_configure_target target_name) target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/rexglue-sdk/include) - if(UNIX AND NOT APPLE) + if(APPLE) + set_target_properties(${target_name} PROPERTIES + INSTALL_RPATH "@loader_path" + BUILD_WITH_INSTALL_RPATH ON + ) + elseif(UNIX AND NOT APPLE) set_target_properties(${target_name} PROPERTIES INSTALL_RPATH "$ORIGIN" BUILD_WITH_INSTALL_RPATH ON @@ -97,6 +113,56 @@ function(rexglue_configure_target target_name) ) endif() endforeach() + elseif(APPLE) + set(_rexglue_macos_vulkan_lib_dirs) + set(_rexglue_macos_vulkan_roots) + if(DEFINED ENV{VULKAN_SDK}) + list(APPEND _rexglue_macos_vulkan_lib_dirs "$ENV{VULKAN_SDK}/lib") + list(APPEND _rexglue_macos_vulkan_roots "$ENV{VULKAN_SDK}") + endif() + list(APPEND _rexglue_macos_vulkan_lib_dirs /opt/homebrew/lib /usr/local/lib) + + foreach(_vk_lib libvulkan.1.dylib libvulkan.dylib libMoltenVK.dylib) + foreach(_vk_lib_dir ${_rexglue_macos_vulkan_lib_dirs}) + if(EXISTS "${_vk_lib_dir}/${_vk_lib}") + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_vk_lib_dir}/${_vk_lib}" + $ + VERBATIM + ) + break() + endif() + endforeach() + endforeach() + + foreach(_vk_lib_dir ${_rexglue_macos_vulkan_lib_dirs}) + if(EXISTS "${_vk_lib_dir}/libMoltenVK.dylib") + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/lib + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_vk_lib_dir}/libMoltenVK.dylib" + $/lib + VERBATIM + ) + break() + endif() + endforeach() + + foreach(_vk_root ${_rexglue_macos_vulkan_roots}) + if(EXISTS "${_vk_root}/share/vulkan/icd.d/MoltenVK_icd.json") + add_custom_command(TARGET ${target_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/share/vulkan/icd.d + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_vk_root}/share/vulkan/icd.d/MoltenVK_icd.json" + $/share/vulkan/icd.d + VERBATIM + ) + break() + endif() + endforeach() endif() endfunction() @@ -131,7 +197,12 @@ function(rexglue_configure_module_target target_name) ) endif() - if(UNIX AND NOT APPLE) + if(APPLE) + set_target_properties(${target_name} PROPERTIES + INSTALL_RPATH "@loader_path" + BUILD_WITH_INSTALL_RPATH ON + ) + elseif(UNIX AND NOT APPLE) set_target_properties(${target_name} PROPERTIES INSTALL_RPATH "$ORIGIN" BUILD_WITH_INSTALL_RPATH ON diff --git a/cmake/rexglue_install.cmake b/cmake/rexglue_install.cmake index e7d5135..b8ef90d 100644 --- a/cmake/rexglue_install.cmake +++ b/cmake/rexglue_install.cmake @@ -116,6 +116,7 @@ endif() install(FILES src/ui/windowed_app_main_win.cpp src/ui/windowed_app_main_posix.cpp + src/ui/windowed_app_main_sdl.cpp src/ui/rex_app.cpp DESTINATION ${CMAKE_INSTALL_DATADIR}/rexglue ) diff --git a/include/rex/chrono/chrono.h b/include/rex/chrono/chrono.h index 8761cff..386c635 100644 --- a/include/rex/chrono/chrono.h +++ b/include/rex/chrono/chrono.h @@ -17,6 +17,20 @@ #include +#if !defined(__cpp_lib_chrono) || __cpp_lib_chrono < 201907L +namespace std::chrono { + +template +struct clock_time_conversion; + +template +auto clock_cast(const std::chrono::time_point& t) { + return clock_time_conversion{}(t); +} + +} // namespace std::chrono +#endif + namespace rex { using hundrednano = std::ratio<1, 10000000>; diff --git a/include/rex/graphics/graphics_system.h b/include/rex/graphics/graphics_system.h index 8fe5ca1..dcf4203 100644 --- a/include/rex/graphics/graphics_system.h +++ b/include/rex/graphics/graphics_system.h @@ -122,6 +122,7 @@ class GraphicsSystem : public system::IGraphicsSystem { uint32_t interrupt_callback_ = 0; uint32_t interrupt_callback_data_ = 0; + std::atomic shutting_down_{false}; std::atomic vsync_worker_running_; system::object_ref vsync_worker_thread_; diff --git a/include/rex/graphics/pipeline/shader/spirv_translator.h b/include/rex/graphics/pipeline/shader/spirv_translator.h index c51ea54..6740b91 100644 --- a/include/rex/graphics/pipeline/shader/spirv_translator.h +++ b/include/rex/graphics/pipeline/shader/spirv_translator.h @@ -991,8 +991,11 @@ class SpirvShaderTranslator : public ShaderTranslator { spv::Id main_memexport_allowed_; // VS rectangle-list fallback only - uint. spv::Id var_main_rectangle_guest_vertex_index_; - // VS rectangle-list fallback only - gl_PerVertex[3]. - spv::Id var_main_rectangle_per_vertex_; + // VS rectangle-list fallback only - float4[3]. + spv::Id var_main_rectangle_position_; + // VS rectangle-list fallback only - float[3 * distance_count], when needed. + spv::Id var_main_rectangle_clip_distances_; + spv::Id var_main_rectangle_cull_distances_; // VS rectangle-list fallback only - float4[3] for each used interpolator. std::array var_main_rectangle_interpolators_; // VS only - float3 (special exports). diff --git a/include/rex/graphics/vulkan/command_processor.h b/include/rex/graphics/vulkan/command_processor.h index 4b76791..40317bd 100644 --- a/include/rex/graphics/vulkan/command_processor.h +++ b/include/rex/graphics/vulkan/command_processor.h @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -242,6 +243,9 @@ class VulkanCommandProcessor : public CommandProcessor { bool keep_dynamic_blend_constants = false, bool keep_dynamic_stencil_mask_ref = false); void BindExternalComputePipeline(VkPipeline pipeline); + uint32_t BeginGpuTimestampedRegion(rex::perf::DrawBucket bucket) { + return BeginGpuTimestampedDraw(bucket); + } void SetViewport(const VkViewport& viewport); void SetScissor(const VkRect2D& scissor); @@ -446,6 +450,14 @@ class VulkanCommandProcessor : public CommandProcessor { void DisableHostOcclusionQueries(); uint64_t NormalizeOcclusionSamples(uint64_t samples) const; void WriteGuestOcclusionResult(uint32_t sample_count_address, uint64_t samples); + bool InitializeGpuTimestampResources(); + void ShutdownGpuTimestampResources(); + void BeginGpuTimestampFrame(); + void EndGpuTimestampFrame(); + void ResolveGpuTimestampFrame(VkCommandBuffer command_buffer); + void ProcessGpuTimestampResults(); + uint32_t BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket); + void EndGpuTimestampedDraw(uint32_t start_query); void InvalidateAllVertexBufferResidency(); void InvalidateVertexBufferResidency(uint32_t vfetch_index); void InvalidateVertexBufferResidencyRange(uint32_t first_vfetch, uint32_t last_vfetch); @@ -747,6 +759,32 @@ class VulkanCommandProcessor : public CommandProcessor { uint32_t host_index = UINT32_MAX; bool valid = false; } active_occlusion_query_; + // MoltenVK maps timestamp queries to Metal counter sample buffers with a + // small per-pool limit, so keep the per-frame query budget modest. + static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 1024; + VkQueryPool gpu_timestamp_query_pool_ = VK_NULL_HANDLE; + VkBuffer gpu_timestamp_readback_buffer_ = VK_NULL_HANDLE; + VkDeviceMemory gpu_timestamp_readback_memory_ = VK_NULL_HANDLE; + uint32_t gpu_timestamp_readback_memory_type_ = UINT32_MAX; + VkDeviceSize gpu_timestamp_readback_memory_size_ = 0; + uint64_t* gpu_timestamp_readback_mapping_ = nullptr; + bool gpu_timestamp_resources_available_ = false; + struct GpuTimestampFrame { + uint64_t frame = 0; + uint64_t submission = 0; + uint32_t query_base = 0; + uint32_t query_count = 0; + uint32_t frame_start_query = UINT32_MAX; + uint32_t frame_end_query = UINT32_MAX; + bool pending = false; + bool active_bucket_valid = false; + rex::perf::DrawBucket active_bucket = rex::perf::DrawBucket::kMainColorDepth; + uint32_t active_bucket_index = UINT32_MAX; + std::vector buckets; + std::vector bucket_start_queries; + std::vector bucket_end_queries; + }; + std::array gpu_timestamp_frames_; struct VertexBufferState { uint32_t address = UINT32_MAX; uint32_t size = UINT32_MAX; diff --git a/include/rex/graphics/vulkan/deferred_command_buffer.h b/include/rex/graphics/vulkan/deferred_command_buffer.h index 9d2a848..f6fea06 100644 --- a/include/rex/graphics/vulkan/deferred_command_buffer.h +++ b/include/rex/graphics/vulkan/deferred_command_buffer.h @@ -351,6 +351,15 @@ class DeferredCommandBuffer { std::memcpy(args_ptr + header_size, viewports, sizeof(VkViewport) * viewport_count); } + void CmdVkWriteTimestamp(VkPipelineStageFlagBits pipeline_stage, VkQueryPool query_pool, + uint32_t query) { + auto& args = *reinterpret_cast( + WriteCommand(Command::kVkWriteTimestamp, sizeof(ArgsVkWriteTimestamp))); + args.pipeline_stage = pipeline_stage; + args.query_pool = query_pool; + args.query = query; + } + private: enum class Command { kVkBeginRenderPass, @@ -381,6 +390,7 @@ class DeferredCommandBuffer { kVkSetStencilReference, kVkSetStencilWriteMask, kVkSetViewport, + kVkWriteTimestamp, }; struct CommandHeader { @@ -526,6 +536,12 @@ class DeferredCommandBuffer { uint32_t query_count; }; + struct ArgsVkWriteTimestamp { + VkPipelineStageFlagBits pipeline_stage; + VkQueryPool query_pool; + uint32_t query; + }; + struct ArgsVkPipelineBarrier { VkPipelineStageFlags src_stage_mask; VkPipelineStageFlags dst_stage_mask; diff --git a/include/rex/graphics/vulkan/render_target_cache.h b/include/rex/graphics/vulkan/render_target_cache.h index 43ef7a2..0a49681 100644 --- a/include/rex/graphics/vulkan/render_target_cache.h +++ b/include/rex/graphics/vulkan/render_target_cache.h @@ -682,7 +682,7 @@ class VulkanRenderTargetCache final : public RenderTargetCache { // need to be dumped). Using 8x16 since that's 128 - the minimum required // group size on Vulkan, and the maximum number of lanes in a subgroup on // Vulkan. - static constexpr uint32_t kDumpSamplesPerGroupX = 8; + static constexpr uint32_t kDumpSamplesPerGroupX = 40; static constexpr uint32_t kDumpSamplesPerGroupY = 16; union DumpPitches { diff --git a/include/rex/input/sdl/sdl_input_driver.h b/include/rex/input/sdl/sdl_input_driver.h index 92bec81..34ed6d1 100644 --- a/include/rex/input/sdl/sdl_input_driver.h +++ b/include/rex/input/sdl/sdl_input_driver.h @@ -75,6 +75,7 @@ class SDLInputDriver final : public InputDriver, public rex::ui::WindowListener void HandleEvent(const SDL_Event& event); std::unique_lock DrainAndLock(); void ProcessEventLocked(const SDL_Event& event); + void StopRumbleLocked(ControllerState& state); void OnControllerDeviceAddedLocked(const SDL_Event& event); void OnControllerDeviceRemovedLocked(const SDL_Event& event); void OnControllerDeviceAxisMotionLocked(const SDL_Event& event); diff --git a/include/rex/perf/counter.h b/include/rex/perf/counter.h index 6f22f62..5307043 100644 --- a/include/rex/perf/counter.h +++ b/include/rex/perf/counter.h @@ -29,6 +29,11 @@ enum class DrawBucket : uint8_t { kCopyResolve, kMemexport, kNoPixelShader, + kCopyDump, + kCopyResolveShader, + kCopyResolveFast32, + kCopyResolveFull32, + kResolveDownscale, }; struct DrawFingerprint { @@ -91,6 +96,11 @@ enum class CounterId : uint16_t { kGpuCopyUs, kGpuMemexportUs, kGpuNoPixelShaderUs, + kGpuCopyDumpUs, + kGpuCopyResolveUs, + kGpuCopyResolveFast32Us, + kGpuCopyResolveFull32Us, + kGpuResolveDownscaleUs, kGpuTimestampedDraws, kGpuCommandProcessorFrameUs, kGpuTimestampedFrames, @@ -178,6 +188,37 @@ enum class CounterId : uint16_t { kRtResolveTransferClearUs, kRtResolveReadbackCalls, kRtResolveReadbackUs, + kRtDumpCalls, + kRtDumpRectangles, + kRtDumpDispatches, + kRtDumpGroups, + kRtResolveCopyDispatches, + kRtResolveCopyGroups, + kRtResolveScaledBytes, + kRtScaledResolveWriteBarriers, + kRtScaledResolveWriteBarrierOverlaps, + kRtScaledResolveWriteBarrierSkipped, + kRtScaledResolveWriteBarrierBytes, + kRtResolveCopyFast32bpp1x2xMsaaDispatches, + kRtResolveCopyFast32bpp1x2xMsaaGroups, + kRtResolveCopyFast32bpp4xMsaaDispatches, + kRtResolveCopyFast32bpp4xMsaaGroups, + kRtResolveCopyFast64bpp1x2xMsaaDispatches, + kRtResolveCopyFast64bpp1x2xMsaaGroups, + kRtResolveCopyFast64bpp4xMsaaDispatches, + kRtResolveCopyFast64bpp4xMsaaGroups, + kRtResolveCopyFull8bppDispatches, + kRtResolveCopyFull8bppGroups, + kRtResolveCopyFull16bppDispatches, + kRtResolveCopyFull16bppGroups, + kRtResolveCopyFull32bppDispatches, + kRtResolveCopyFull32bppGroups, + kRtResolveCopyFull64bppDispatches, + kRtResolveCopyFull64bppGroups, + kRtResolveCopyFull128bppDispatches, + kRtResolveCopyFull128bppGroups, + kDrawNoPixelDepthTestCalls, + kDrawNoPixelDepthWriteCalls, // Audio kXmaFramesDecoded, diff --git a/include/rex/platform/dynlib.h b/include/rex/platform/dynlib.h index c30ae1c..3e00730 100644 --- a/include/rex/platform/dynlib.h +++ b/include/rex/platform/dynlib.h @@ -56,6 +56,14 @@ inline constexpr const char* kVulkanLoader = "libvulkan.so.1"; inline constexpr const char* kRenderDoc = "librenderdoc.so"; inline constexpr const char* kSpirvToolsSdkPath = "bin/libSPIRV-Tools-shared.so"; +#elif REX_PLATFORM_MAC + +inline constexpr const char* kVulkanLoader = "libvulkan.1.dylib"; +inline constexpr const char* kVulkanLoaderFallback = "libvulkan.dylib"; +inline constexpr const char* kMoltenVK = "libMoltenVK.dylib"; +inline constexpr const char* kRenderDoc = "librenderdoc.dylib"; +inline constexpr const char* kSpirvToolsSdkPath = "bin/libSPIRV-Tools-shared.dylib"; + #else #error No library names provided for the target platform. #endif diff --git a/include/rex/platform/fpscr.h b/include/rex/platform/fpscr.h index c2c4fd4..b3fa018 100644 --- a/include/rex/platform/fpscr.h +++ b/include/rex/platform/fpscr.h @@ -75,7 +75,10 @@ struct FPSCRPlatform { return csr; } - static inline void setcsr(u32 csr) noexcept { __asm__ __volatile__("msr fpcr, %0" : : "r"(csr)); } + static inline void setcsr(u32 csr) noexcept { + u64 fpcr = csr; + __asm__ __volatile__("msr fpcr, %0" : : "r"(fpcr)); + } static inline void InitHostExceptions(u32& csr) noexcept { csr &= ~ExceptionMask; // Clear enable bits to disable exceptions diff --git a/include/rex/ppc/context.h b/include/rex/ppc/context.h index ce766b6..6b36b40 100644 --- a/include/rex/ppc/context.h +++ b/include/rex/ppc/context.h @@ -205,7 +205,8 @@ struct FPSCRRegister { Platform::InitHostExceptions(fpu_csr); fpu_csr &= ~FlushMask; if (vmx_csr == 0) { - vmx_csr = ExceptionMask | FlushMask | Platform::GuestToHost[kRoundNearest]; + vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest]; + Platform::InitHostExceptions(vmx_csr); } csr = fpu_csr; } @@ -216,7 +217,7 @@ struct FPSCRRegister { fpu_csr &= ~RoundMaskVal; fpu_csr |= Platform::GuestToHost[value & kRoundMask]; fpu_csr &= ~FlushMask; - fpu_csr |= ExceptionMask; + Platform::InitHostExceptions(fpu_csr); csr = fpu_csr; vmx_mode = false; setcsr(csr); @@ -254,7 +255,8 @@ struct FPSCRRegister { fpu_csr = getcsr(); Platform::InitHostExceptions(fpu_csr); fpu_csr &= ~FlushMask; - vmx_csr = ExceptionMask | FlushMask | Platform::GuestToHost[kRoundNearest]; + vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest]; + Platform::InitHostExceptions(vmx_csr); csr = fpu_csr; vmx_mode = false; setcsr(csr); diff --git a/include/rex/rex_app.h b/include/rex/rex_app.h index d3c1c2e..6021c5d 100644 --- a/include/rex/rex_app.h +++ b/include/rex/rex_app.h @@ -22,7 +22,11 @@ #include #include +#include #include +#if REX_PLATFORM_MAC +#include +#endif #include #include #include @@ -221,6 +225,9 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win std::filesystem::path cache_root_; std::unique_ptr runtime_; std::unique_ptr window_; +#if REX_PLATFORM_MAC + system::object_ref main_thread_; +#endif std::thread module_thread_; std::atomic shutting_down_{false}; std::unique_ptr immediate_drawer_; diff --git a/include/rex/std_compat.h b/include/rex/std_compat.h new file mode 100644 index 0000000..e85cdb7 --- /dev/null +++ b/include/rex/std_compat.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include + +#ifndef __cpp_lib_move_only_function +namespace std { + +template +class move_only_function; + +template +class move_only_function { + public: + move_only_function() = default; + move_only_function(nullptr_t) {} + + move_only_function(const move_only_function&) = delete; + move_only_function& operator=(const move_only_function&) = delete; + move_only_function(move_only_function&&) noexcept = default; + move_only_function& operator=(move_only_function&&) noexcept = default; + + template + requires(!std::is_same_v, move_only_function> && + !std::is_same_v, nullptr_t> && + std::is_invocable_r_v) + move_only_function(F&& f) + : callable_(std::make_unique>>(std::forward(f))) {} + + move_only_function& operator=(nullptr_t) { + callable_.reset(); + return *this; + } + + explicit operator bool() const noexcept { return callable_ != nullptr; } + + friend bool operator==(const move_only_function& f, nullptr_t) noexcept { + return !static_cast(f); + } + friend bool operator==(nullptr_t, const move_only_function& f) noexcept { + return !static_cast(f); + } + friend bool operator!=(const move_only_function& f, nullptr_t) noexcept { + return static_cast(f); + } + friend bool operator!=(nullptr_t, const move_only_function& f) noexcept { + return static_cast(f); + } + + R operator()(Args... args) { + return callable_->Call(std::forward(args)...); + } + + private: + struct CallableBase { + virtual ~CallableBase() = default; + virtual R Call(Args&&... args) = 0; + }; + + template + struct Callable final : CallableBase { + explicit Callable(F&& f) : f_(std::move(f)) {} + explicit Callable(const F& f) : f_(f) {} + + R Call(Args&&... args) override { return std::invoke(f_, std::forward(args)...); } + + F f_; + }; + + std::unique_ptr callable_; +}; + +} // namespace std +#endif diff --git a/include/rex/system/kernel_state.h b/include/rex/system/kernel_state.h index 18a328f..85994b4 100644 --- a/include/rex/system/kernel_state.h +++ b/include/rex/system/kernel_state.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -278,7 +279,6 @@ class KernelState { void OnThreadExecute(XThread* thread); void OnThreadExit(XThread* thread); object_ref GetThreadByID(uint32_t thread_id); - FiberInfo* LookupFiber(uint32_t guest_addr); void RegisterFiber(uint32_t guest_addr, const FiberInfo& info); void UnregisterFiber(uint32_t guest_addr); diff --git a/include/rex/system/xmemory.h b/include/rex/system/xmemory.h index 3c3969a..62a214c 100644 --- a/include/rex/system/xmemory.h +++ b/include/rex/system/xmemory.h @@ -28,15 +28,12 @@ class ByteStream; namespace rex::memory::detail { -/// Compensates for Windows 64KB allocation granularity on the 0xE0 physical heap. -/// The backing file maps the 0xE0 heap at a 0x1000-byte offset, but MapViewOfFileEx -/// rounds down to 64KB boundaries. Linux mmap handles 4KB offsets natively. -constexpr u32 PhysicalHostOffset([[maybe_unused]] u32 guest_addr) noexcept { -#if REX_PLATFORM_WIN32 - return (guest_addr >= 0xE0000000u) ? 0x1000u : 0u; -#else - return 0u; -#endif +/// Compensates for host allocation granularities larger than Xenon's 4 KB +/// pages on the 0xE0000000 physical heap. The memory views are rounded down to +/// the host granularity, while the heap tracks the requested 4 KB-aligned guest +/// range via host_address_offset. +inline u32 PhysicalHostOffset(u32 guest_addr) noexcept { + return (guest_addr >= 0xE0000000u && allocation_granularity() > 0x1000) ? 0x1000u : 0u; } } // namespace rex::memory::detail diff --git a/include/rex/thread/fiber.h b/include/rex/thread/fiber.h index 64e70a7..c80e029 100644 --- a/include/rex/thread/fiber.h +++ b/include/rex/thread/fiber.h @@ -14,10 +14,13 @@ #include #include -#if REX_PLATFORM_LINUX || REX_PLATFORM_MAC +#if REX_PLATFORM_LINUX #include #include #include +#elif REX_PLATFORM_MAC +#include +#include #endif namespace rex::thread { @@ -58,6 +61,14 @@ struct Fiber { void* arg_ = nullptr; bool is_thread_fiber_ = false; + static void Trampoline(); +#elif REX_PLATFORM_MAC + void* context_ = nullptr; + std::vector stack_; + void (*entry_)(void*) = nullptr; + void* arg_ = nullptr; + bool is_thread_fiber_ = false; + static void Trampoline(); #endif }; diff --git a/include/rex/thread/timer_queue.h b/include/rex/thread/timer_queue.h index cb2e84a..113331a 100644 --- a/include/rex/thread/timer_queue.h +++ b/include/rex/thread/timer_queue.h @@ -16,6 +16,8 @@ #include #include +#include + // This is a platform independent implementation of a timer queue similar to // Windows CreateTimerQueueTimer with WT_EXECUTEINTIMERTHREAD. diff --git a/include/rex/ui/surface.h b/include/rex/ui/surface.h index 34e1d65..0edf991 100644 --- a/include/rex/ui/surface.h +++ b/include/rex/ui/surface.h @@ -33,11 +33,14 @@ class Surface { kTypeIndex_XcbWindow, // Windows. kTypeIndex_Win32Hwnd, + // macOS. + kTypeIndex_SDLMetalView, }; using TypeFlags = uint32_t; enum : TypeFlags { kTypeFlag_AndroidNativeWindow = TypeFlags(1) << kTypeIndex_AndroidNativeWindow, kTypeFlag_XcbWindow = TypeFlags(1) << kTypeIndex_XcbWindow, + kTypeFlag_SDLMetalView = TypeFlags(1) << kTypeIndex_SDLMetalView, kTypeFlag_Win32Hwnd = TypeFlags(1) << kTypeIndex_Win32Hwnd, }; diff --git a/include/rex/ui/surface_sdl.h b/include/rex/ui/surface_sdl.h new file mode 100644 index 0000000..ccf7c10 --- /dev/null +++ b/include/rex/ui/surface_sdl.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include + +namespace rex { +namespace ui { + +class SDLMetalViewSurface final : public Surface { + public: + explicit SDLMetalViewSurface(SDL_Window* window, SDL_MetalView metal_view, + void* metal_layer) + : window_(window), metal_view_(metal_view), metal_layer_(metal_layer) {} + + TypeIndex GetType() const override { return kTypeIndex_SDLMetalView; } + SDL_MetalView metal_view() const { return metal_view_; } + void* metal_layer() const { return metal_layer_; } + + protected: + bool GetSizeImpl(uint32_t& width_out, uint32_t& height_out) const override; + + private: + SDL_Window* window_; + SDL_MetalView metal_view_; + void* metal_layer_; +}; + +} // namespace ui +} // namespace rex diff --git a/include/rex/ui/vulkan/api.h b/include/rex/ui/vulkan/api.h index 278e955..94ab7f2 100644 --- a/include/rex/ui/vulkan/api.h +++ b/include/rex/ui/vulkan/api.h @@ -38,6 +38,12 @@ #endif #endif +#if REX_PLATFORM_MAC +#ifndef VK_USE_PLATFORM_METAL_EXT +#define VK_USE_PLATFORM_METAL_EXT +#endif +#endif + #if REX_PLATFORM_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN diff --git a/include/rex/ui/vulkan/device.h b/include/rex/ui/vulkan/device.h index 947817f..5f55deb 100644 --- a/include/rex/ui/vulkan/device.h +++ b/include/rex/ui/vulkan/device.h @@ -93,6 +93,8 @@ class VulkanDevice { VkDeviceSize optimalBufferCopyOffsetAlignment = 1; VkDeviceSize optimalBufferCopyRowPitchAlignment = 1; VkDeviceSize nonCoherentAtomSize = 256; + bool timestampComputeAndGraphics = false; + float timestampPeriod = 0.0f; bool robustBufferAccess = false; bool fullDrawIndexUint32 = false; diff --git a/include/rex/ui/vulkan/functions/device_1_0.inc b/include/rex/ui/vulkan/functions/device_1_0.inc index aa55a4a..8034d58 100644 --- a/include/rex/ui/vulkan/functions/device_1_0.inc +++ b/include/rex/ui/vulkan/functions/device_1_0.inc @@ -32,6 +32,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdSetStencilCompareMask) XE_UI_VULKAN_FUNCTION(vkCmdSetStencilReference) XE_UI_VULKAN_FUNCTION(vkCmdSetStencilWriteMask) XE_UI_VULKAN_FUNCTION(vkCmdSetViewport) +XE_UI_VULKAN_FUNCTION(vkCmdWriteTimestamp) XE_UI_VULKAN_FUNCTION(vkCreateBuffer) XE_UI_VULKAN_FUNCTION(vkCreateBufferView) XE_UI_VULKAN_FUNCTION(vkCreateCommandPool) diff --git a/include/rex/ui/vulkan/functions/instance_ext_metal_surface.inc b/include/rex/ui/vulkan/functions/instance_ext_metal_surface.inc new file mode 100644 index 0000000..4f29ee8 --- /dev/null +++ b/include/rex/ui/vulkan/functions/instance_ext_metal_surface.inc @@ -0,0 +1,2 @@ +// VK_EXT_metal_surface functions used in ReXGlue. +XE_UI_VULKAN_FUNCTION(vkCreateMetalSurfaceEXT) diff --git a/include/rex/ui/vulkan/instance.h b/include/rex/ui/vulkan/instance.h index e672629..5da9073 100644 --- a/include/rex/ui/vulkan/instance.h +++ b/include/rex/ui/vulkan/instance.h @@ -66,6 +66,10 @@ class VulkanInstance { // VK_KHR_win32_surface (#10) #ifdef VK_USE_PLATFORM_WIN32_KHR #include +#endif + // VK_EXT_metal_surface (#218) +#ifdef VK_USE_PLATFORM_METAL_EXT +#include #endif // VK_KHR_get_physical_device_properties2 (#60, promoted to 1.1) #include @@ -91,6 +95,9 @@ class VulkanInstance { #endif #ifdef VK_USE_PLATFORM_WIN32_KHR bool ext_KHR_win32_surface = false; // #10 +#endif +#ifdef VK_USE_PLATFORM_METAL_EXT + bool ext_EXT_metal_surface = false; // #218 #endif bool ext_1_1_KHR_get_physical_device_properties2 = false; // #60 bool ext_EXT_debug_utils = false; // #129 diff --git a/include/rex/ui/window_sdl.h b/include/rex/ui/window_sdl.h new file mode 100644 index 0000000..ad98b21 --- /dev/null +++ b/include/rex/ui/window_sdl.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace rex { +namespace ui { + +class SDLWindow final : public Window { + using super = Window; + + public: + SDLWindow(WindowedAppContext& app_context, const std::string_view title, + uint32_t desired_logical_width, uint32_t desired_logical_height); + ~SDLWindow() override; + + static void HandleSDLEvent(const SDL_Event& event); + + protected: + uint32_t GetLatestDpiImpl() const override; + bool OpenImpl() override; + void RequestCloseImpl() override; + + void ApplyNewFullscreen() override; + void ApplyNewTitle() override; + void ApplyNewMouseCapture() override; + void ApplyNewMouseRelease() override; + void ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) override; + void FocusImpl() override; + + std::unique_ptr CreateSurfaceImpl(Surface::TypeFlags allowed_types) override; + void RequestPaintImpl() override; + + private: + void HandleEvent(const SDL_Event& event); + void HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver); + void HandleKey(const SDL_KeyboardEvent& event, bool down, + WindowDestructionReceiver& destruction_receiver); + void HandleMouseButton(const SDL_MouseButtonEvent& event, bool down, + WindowDestructionReceiver& destruction_receiver); + void HandleMouseMotion(const SDL_MouseMotionEvent& event, + WindowDestructionReceiver& destruction_receiver); + void HandleMouseWheel(const SDL_MouseWheelEvent& event, + WindowDestructionReceiver& destruction_receiver); + void RevealAutoHiddenCursor(); + void SetCursorAutoHideTimer(); + void RemoveCursorAutoHideTimer(); + void WindowPointToPhysical(float x, float y, int32_t& physical_x, int32_t& physical_y) const; + uint32_t QueryDpi() const; + + SDL_Window* window_ = nullptr; + SDL_MetalView metal_view_ = nullptr; + void* metal_layer_ = nullptr; + std::atomic paint_event_queued_{false}; + SDL_TimerID cursor_auto_hide_timer_ = 0; + bool cursor_currently_auto_hidden_ = false; + uint32_t dpi_ = 96; +}; + +class SDLMenuItem final : public MenuItem { + public: + SDLMenuItem(Type type, const std::string& text, const std::string& hotkey, + std::function callback) + : MenuItem(type, text, hotkey, std::move(callback)) {} +}; + +} // namespace ui +} // namespace rex diff --git a/include/rex/ui/windowed_app_context_sdl.h b/include/rex/ui/windowed_app_context_sdl.h new file mode 100644 index 0000000..2081ea0 --- /dev/null +++ b/include/rex/ui/windowed_app_context_sdl.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +#include + +namespace rex { +namespace ui { + +class SDLWindowedAppContext final : public WindowedAppContext { + public: + SDLWindowedAppContext() = default; + ~SDLWindowedAppContext() override = default; + + void NotifyUILoopOfPendingFunctions() override; + void PlatformQuitFromUIThread() override; + + int RunMainLoop(); + + private: + void DispatchEvent(const SDL_Event& event); + + std::atomic_bool pending_functions_event_queued_ = false; +}; + +} // namespace ui +} // namespace rex diff --git a/resources/templates/codegen/init_h.inja b/resources/templates/codegen/init_h.inja index c6c5ae6..17b78c9 100644 --- a/resources/templates/codegen/init_h.inja +++ b/resources/templates/codegen/init_h.inja @@ -60,9 +60,18 @@ extern PPCFuncMapping {{ project }}_PPCFuncMappings[]; REX_EXTERN(name); \ REX_EXTERN(__imp__##name) +#if defined(__APPLE__) +#define DEFINE_REX_FUNC(name) \ + REX_EXTERN(__imp__##name); \ + REX_WEAK_FUNC(name) { \ + __imp__##name(ctx, base); \ + } \ + REX_EXTERN(__imp__##name) +#else #define DEFINE_REX_FUNC(name) \ __attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \ REX_EXTERN(__imp__##name) +#endif //============================================================================= // Function Prologue diff --git a/resources/templates/test/ppc_config_h.inja b/resources/templates/test/ppc_config_h.inja index f2c2d62..671fe2b 100644 --- a/resources/templates/test/ppc_config_h.inja +++ b/resources/templates/test/ppc_config_h.inja @@ -55,10 +55,19 @@ #endif #ifndef DEFINE_REX_FUNC +#if defined(__APPLE__) +#define DEFINE_REX_FUNC(name) \ + REX_EXTERN(__imp__##name); \ + REX_WEAK_FUNC(name) { \ + __imp__##name(ctx, base); \ + } \ + REX_EXTERN(__imp__##name) +#else #define DEFINE_REX_FUNC(name) \ __attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \ REX_EXTERN(__imp__##name) #endif +#endif #if REX_PLATFORM_WIN32 #define REX_PHYS_HOST_OFFSET(addr) (((u32)(addr) >= 0xE0000000u) ? 0x1000u : 0u) @@ -177,4 +186,3 @@ inline std::atomic& ppc_global_lock_count_() { assert(old_count_ >= 1 && "LeaveGlobalLock called without matching EnterGlobalLock"); \ rex::thread::global_critical_region::mutex().unlock(); \ } while (0) - diff --git a/src/codegen/builders/vector.cpp b/src/codegen/builders/vector.cpp index 339f5e1..af95ce7 100644 --- a/src/codegen/builders/vector.cpp +++ b/src/codegen/builders/vector.cpp @@ -130,11 +130,10 @@ bool build_vrefp(BuilderContext& ctx) { } bool build_vrsqrtefp(BuilderContext& ctx) { - // Match Xenia's OPCODE_RSQRT lowering. The x86 estimate is not accurate - // enough for collision-sensitive code, so use sqrt + divide. + // Match the Xenon reciprocal square-root estimate semantics instead of the + // host's exact sqrt/divide behavior. ctx.emit_set_flush_mode(true); - ctx.emit_vec_fp_unary_expr( - "simde_mm_div_ps(simde_mm_set1_ps(1.0f), simde_mm_sqrt_ps(simde_mm_load_ps({vA}.f32)))"); + ctx.emit_vec_fp_unary_expr("rex::ppc::simde_mm_vrsqrtefp_ps(simde_mm_load_ps({vA}.f32))"); return true; } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4418490..06e8b7a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -76,7 +76,10 @@ target_include_directories(rexcore PRIVATE ) if(UNIX) - target_link_libraries(rexcore PRIVATE pthread rt dl) + target_link_libraries(rexcore PRIVATE pthread dl) + if(NOT APPLE) + target_link_libraries(rexcore PRIVATE rt) + endif() elseif(WIN32) target_link_libraries(rexcore PUBLIC ws2_32) endif() diff --git a/src/core/clock_posix.cpp b/src/core/clock_posix.cpp index 53b4462..5365c53 100644 --- a/src/core/clock_posix.cpp +++ b/src/core/clock_posix.cpp @@ -16,19 +16,27 @@ static_assert(REX_PLATFORM_LINUX || REX_PLATFORM_MAC, "This file is POSIX-only") namespace rex::chrono { +#if REX_PLATFORM_MAC +constexpr clockid_t kHostClock = CLOCK_MONOTONIC; +#else +constexpr clockid_t kHostClock = CLOCK_MONOTONIC_RAW; +#endif + uint64_t Clock::host_tick_frequency_platform() { timespec res; - int error = clock_getres(CLOCK_MONOTONIC_RAW, &res); + int error = clock_getres(kHostClock, &res); assert_zero(error); assert_zero(res.tv_sec); // Sub second resolution is required. + assert_true(res.tv_nsec > 0); - // Convert nano seconds to hertz. Resolution is 1ns on most systems. - return 1000000000ull / res.tv_nsec; + // host_tick_count_platform returns elapsed nanoseconds, not ticks in units of + // clock_getres. Report the frequency for those returned units. + return 1000000000ull; } uint64_t Clock::host_tick_count_platform() { timespec tp; - int error = clock_gettime(CLOCK_MONOTONIC_RAW, &tp); + int error = clock_gettime(kHostClock, &tp); assert_zero(error); return tp.tv_nsec + tp.tv_sec * 1000000000ull; diff --git a/src/core/cvar.cpp b/src/core/cvar.cpp index 2c8ac3b..67cd74c 100644 --- a/src/core/cvar.cpp +++ b/src/core/cvar.cpp @@ -49,6 +49,17 @@ std::unordered_map& GetRegistryIndex() { return index; } +std::vector>& GetCommandLineOverrides() { + static std::vector> overrides; + return overrides; +} + +void ApplyCommandLineOverrides() { + for (const auto& [name, value] : GetCommandLineOverrides()) { + SetFlagByName(name, value); + } +} + // Convert flag name to environment variable: gpu_vsync -> REX_GPU_VSYNC std::string FlagNameToEnvVar(std::string_view name) { std::string result = "REX_"; @@ -538,11 +549,19 @@ std::vector Init(int argc, char** argv) { if (entry.type == FlagType::Boolean) { app.add_flag_function( "--" + entry.name + ",!--no-" + entry.name, - [&entry](int64_t count) { entry.setter(count > 0 ? "true" : "false"); }, + [&entry](int64_t count) { + std::string value = count > 0 ? "true" : "false"; + GetCommandLineOverrides().emplace_back(entry.name, value); + entry.setter(value); + }, entry.description); } else { app.add_option_function( - "--" + entry.name, [&entry](const std::string& val) { entry.setter(val); }, + "--" + entry.name, + [&entry](const std::string& val) { + GetCommandLineOverrides().emplace_back(entry.name, val); + entry.setter(val); + }, entry.description); } } @@ -567,6 +586,8 @@ void LoadConfig(const std::filesystem::path& config_path) { try { auto config = toml::parse_file(config_path.string()); ApplyTomlTable(config, ""); + ApplyCommandLineOverrides(); + ApplyEnvironment(); REXLOG_INFO("Loaded config from {}", config_path.string()); } catch (const toml::parse_error& err) { REXLOG_ERROR("Failed to parse config {}: {}", config_path.string(), err.what()); diff --git a/src/core/exception_handler_posix.cpp b/src/core/exception_handler_posix.cpp index 8aced51..690fc8e 100644 --- a/src/core/exception_handler_posix.cpp +++ b/src/core/exception_handler_posix.cpp @@ -9,12 +9,17 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#if defined(__APPLE__) && !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 700 +#endif + #include #if REX_PLATFORM_LINUX || REX_PLATFORM_MAC #include +#include #include #include @@ -30,6 +35,7 @@ namespace rex::arch { bool signal_handlers_installed_ = false; struct sigaction original_sigill_handler_; struct sigaction original_sigsegv_handler_; +struct sigaction original_sigbus_handler_; // This can be as large as needed, but isn't often needed. // As we will be sometimes firing many exceptions we want to avoid having to @@ -40,9 +46,45 @@ constexpr size_t kMaxHandlerCount = 8; // Executed in order. std::pair handlers_[kMaxHandlerCount]; +static void DispatchUnhandledSignal(int signal_number, siginfo_t* signal_info, + void* signal_context) { + struct sigaction* original_handler = nullptr; + if (signal_number == SIGILL) { + original_handler = &original_sigill_handler_; + } else if (signal_number == SIGSEGV) { + original_handler = &original_sigsegv_handler_; + } else if (signal_number == SIGBUS) { + original_handler = &original_sigbus_handler_; + } + + if (original_handler) { + if ((original_handler->sa_flags & SA_SIGINFO) && original_handler->sa_sigaction) { + original_handler->sa_sigaction(signal_number, signal_info, signal_context); + return; + } + if (original_handler->sa_handler == SIG_IGN) { + return; + } + if (original_handler->sa_handler != SIG_DFL && original_handler->sa_handler) { + original_handler->sa_handler(signal_number); + return; + } + + sigaction(signal_number, original_handler, nullptr); + } else { + signal(signal_number, SIG_DFL); + } + raise(signal_number); + std::_Exit(128 + signal_number); +} + static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, void* signal_context) { +#if REX_PLATFORM_MAC + mcontext_t mcontext = reinterpret_cast(signal_context)->uc_mcontext; +#else mcontext_t& mcontext = reinterpret_cast(signal_context)->uc_mcontext; +#endif HostThreadContext thread_context; @@ -70,6 +112,19 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, std::memcpy(thread_context.xmm_registers, mcontext.fpregs->_xmm, sizeof(thread_context.xmm_registers)); #elif REX_ARCH_ARM64 +#if REX_PLATFORM_MAC + auto& thread_state = mcontext->__ss; + auto& neon_state = mcontext->__ns; + std::memcpy(thread_context.x, thread_state.__x, sizeof(thread_state.__x)); + thread_context.x[29] = thread_state.__fp; + thread_context.x[30] = thread_state.__lr; + thread_context.sp = thread_state.__sp; + thread_context.pc = thread_state.__pc; + thread_context.pstate = thread_state.__cpsr; + thread_context.fpsr = neon_state.__fpsr; + thread_context.fpcr = neon_state.__fpcr; + std::memcpy(thread_context.v, neon_state.__v, sizeof(thread_context.v)); +#else std::memcpy(thread_context.x, mcontext.regs, sizeof(thread_context.x)); thread_context.sp = mcontext.sp; thread_context.pc = mcontext.pc; @@ -98,6 +153,7 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, thread_context.fpcr = mcontext_fpsimd->fpcr; std::memcpy(thread_context.v, mcontext_fpsimd->vregs, sizeof(thread_context.v)); } +#endif // REX_PLATFORM_MAC #endif // REX_ARCH Exception ex; @@ -105,7 +161,8 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, case SIGILL: ex.InitializeIllegalInstruction(&thread_context); break; - case SIGSEGV: { + case SIGSEGV: + case SIGBUS: { Exception::AccessViolationOperation access_violation_operation; #if REX_ARCH_AMD64 // x86_pf_error_code::X86_PF_WRITE @@ -114,6 +171,24 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, ? Exception::AccessViolationOperation::kWrite : Exception::AccessViolationOperation::kRead; #elif REX_ARCH_ARM64 +#if REX_PLATFORM_MAC + const uint32_t esr = mcontext->__es.__esr; + if (((esr >> 26) & 0b111110) == 0b100100) { + access_violation_operation = (esr & (UINT64_C(1) << 6)) + ? Exception::AccessViolationOperation::kWrite + : Exception::AccessViolationOperation::kRead; + } else { + bool instruction_is_store; + if (IsArm64LoadPrefetchStore(*reinterpret_cast(thread_context.pc), + instruction_is_store)) { + access_violation_operation = instruction_is_store + ? Exception::AccessViolationOperation::kWrite + : Exception::AccessViolationOperation::kRead; + } else { + access_violation_operation = Exception::AccessViolationOperation::kUnknown; + } + } +#else // For a Data Abort (EC - ESR_EL1 bits 31:26 - 0b100100 from a lower // Exception Level, 0b100101 without a change in the Exception Level), // bit 6 is 0 for reading from a memory location, 1 for writing to a @@ -145,6 +220,7 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, access_violation_operation = Exception::AccessViolationOperation::kUnknown; } } +#endif // REX_PLATFORM_MAC #else access_violation_operation = Exception::AccessViolationOperation::kUnknown; #endif // REX_ARCH @@ -181,6 +257,34 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, &thread_context.xmm_registers[modified_register_index], sizeof(vec128_t)); } #elif REX_ARCH_ARM64 +#if REX_PLATFORM_MAC + auto& thread_state = mcontext->__ss; + auto& neon_state = mcontext->__ns; + uint32_t modified_register_index; + uint32_t modified_x_registers_remaining = ex.modified_x_registers(); + while (rex::bit_scan_forward(modified_x_registers_remaining, &modified_register_index)) { + modified_x_registers_remaining &= ~(UINT32_C(1) << modified_register_index); + if (modified_register_index < 29) { + thread_state.__x[modified_register_index] = + thread_context.x[modified_register_index]; + } else if (modified_register_index == 29) { + thread_state.__fp = thread_context.x[29]; + } else if (modified_register_index == 30) { + thread_state.__lr = thread_context.x[30]; + } + } + thread_state.__sp = thread_context.sp; + thread_state.__pc = thread_context.pc; + thread_state.__cpsr = uint32_t(thread_context.pstate); + neon_state.__fpsr = thread_context.fpsr; + neon_state.__fpcr = thread_context.fpcr; + uint32_t modified_v_registers_remaining = ex.modified_v_registers(); + while (rex::bit_scan_forward(modified_v_registers_remaining, &modified_register_index)) { + modified_v_registers_remaining &= ~(UINT32_C(1) << modified_register_index); + std::memcpy(&neon_state.__v[modified_register_index], + &thread_context.v[modified_register_index], sizeof(vec128_t)); + } +#else uint32_t modified_register_index; uint32_t modified_x_registers_remaining = ex.modified_x_registers(); while (rex::bit_scan_forward(modified_x_registers_remaining, &modified_register_index)) { @@ -201,10 +305,13 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info, mcontext.regs[modified_register_index] = thread_context.x[modified_register_index]; } } +#endif // REX_PLATFORM_MAC #endif // REX_ARCH return; } } + + DispatchUnhandledSignal(signal_number, signal_info, signal_context); } void ExceptionHandler::Install(Handler fn, void* data) { @@ -221,6 +328,9 @@ void ExceptionHandler::Install(Handler fn, void* data) { if (sigaction(SIGSEGV, &signal_handler, &original_sigsegv_handler_) != 0) { assert_always("Failed to install new SIGSEGV handler"); } + if (sigaction(SIGBUS, &signal_handler, &original_sigbus_handler_) != 0) { + assert_always("Failed to install new SIGBUS handler"); + } signal_handlers_installed_ = true; } @@ -261,6 +371,9 @@ void ExceptionHandler::Uninstall(Handler fn, void* data) { if (sigaction(SIGSEGV, &original_sigsegv_handler_, NULL) != 0) { assert_always("Failed to restore original SIGSEGV handler"); } + if (sigaction(SIGBUS, &original_sigbus_handler_, NULL) != 0) { + assert_always("Failed to restore original SIGBUS handler"); + } signal_handlers_installed_ = false; } } diff --git a/src/core/fiber_posix.cpp b/src/core/fiber_posix.cpp index e7c53fd..467c426 100644 --- a/src/core/fiber_posix.cpp +++ b/src/core/fiber_posix.cpp @@ -9,6 +9,10 @@ * See LICENSE file in the project root for full license text. */ +#if defined(__APPLE__) && !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 700 +#endif + #include #if REX_PLATFORM_LINUX || REX_PLATFORM_MAC @@ -21,9 +25,25 @@ namespace rex::thread { thread_local Fiber* Fiber::tls_current_ = nullptr; +namespace { + +#if REX_PLATFORM_MAC +ucontext_t* ToContext(void* context) { return static_cast(context); } +#else +ucontext_t* ToContext(ucontext_t& context) { return &context; } +#endif + +} // namespace + Fiber* Fiber::ConvertCurrentThread() { auto* f = new Fiber(); - if (getcontext(&f->context_) == -1) { +#if REX_PLATFORM_MAC + f->context_ = new ucontext_t{}; +#endif + if (getcontext(ToContext(f->context_)) == -1) { +#if REX_PLATFORM_MAC + delete ToContext(f->context_); +#endif delete f; return nullptr; } @@ -37,16 +57,23 @@ Fiber* Fiber::Create(size_t stack_size, void (*entry)(void*), void* arg) { f->entry_ = entry; f->arg_ = arg; f->stack_.resize(stack_size); +#if REX_PLATFORM_MAC + f->context_ = new ucontext_t{}; +#endif - if (getcontext(&f->context_) == -1) { + auto* context = ToContext(f->context_); + if (getcontext(context) == -1) { +#if REX_PLATFORM_MAC + delete context; +#endif delete f; return nullptr; } - f->context_.uc_stack.ss_sp = f->stack_.data(); - f->context_.uc_stack.ss_size = f->stack_.size(); - f->context_.uc_link = nullptr; + context->uc_stack.ss_sp = f->stack_.data(); + context->uc_stack.ss_size = f->stack_.size(); + context->uc_link = nullptr; // Trampoline reads entry_/arg_ from tls_current_ — no pointer splitting needed. - makecontext(&f->context_, &Fiber::Trampoline, 0); + makecontext(context, &Fiber::Trampoline, 0); return f; } @@ -59,7 +86,7 @@ Fiber* Fiber::Create(size_t stack_size, void (*entry)(void*), void* arg) { void Fiber::SwitchTo(Fiber* target) { Fiber* from = tls_current_; tls_current_ = target; - swapcontext(&from->context_, &target->context_); + swapcontext(ToContext(from->context_), ToContext(target->context_)); } void Fiber::Destroy() { @@ -70,6 +97,10 @@ void Fiber::Destroy() { assert(this != tls_current_ && "Destroy called on the currently running fiber"); } // No POSIX equivalent of ConvertFiberToThread; stack_ is freed by the vector destructor. +#if REX_PLATFORM_MAC + delete ToContext(context_); + context_ = nullptr; +#endif delete this; } diff --git a/src/core/filesystem_posix.cpp b/src/core/filesystem_posix.cpp index 00648e4..87f8e67 100644 --- a/src/core/filesystem_posix.cpp +++ b/src/core/filesystem_posix.cpp @@ -10,10 +10,13 @@ */ #include +#include #include +#include #include #include +#include #include #include @@ -23,6 +26,7 @@ #include #include #include +#include #include #include @@ -30,6 +34,15 @@ #include #include +#if REX_PLATFORM_MAC +#include + +using off64_t = off_t; +#define fseeko64 fseeko +#define ftello64 ftello +#define ftruncate64 ftruncate +#endif + namespace rex { std::string path_to_utf8(const std::filesystem::path& path) { @@ -51,10 +64,31 @@ std::filesystem::path to_path(const std::u16string_view source) { namespace filesystem { std::filesystem::path GetExecutablePath() { +#if REX_PLATFORM_MAC + uint32_t size = PATH_MAX; + std::vector path(size); + if (_NSGetExecutablePath(path.data(), &size) != 0) { + path.resize(size); + if (_NSGetExecutablePath(path.data(), &size) != 0) { + return {}; + } + } + + char resolved_path[PATH_MAX] = {}; + if (realpath(path.data(), resolved_path)) { + return resolved_path; + } + return path.data(); +#else char buff[FILENAME_MAX] = ""; - readlink("/proc/self/exe", buff, FILENAME_MAX); + ssize_t length = readlink("/proc/self/exe", buff, FILENAME_MAX - 1); + if (length < 0) { + return {}; + } + buff[length] = '\0'; std::string s(buff); return s; +#endif } std::filesystem::path GetExecutableFolder() { diff --git a/src/core/mapped_memory_posix.cpp b/src/core/mapped_memory_posix.cpp index 92c06c8..f6ca329 100644 --- a/src/core/mapped_memory_posix.cpp +++ b/src/core/mapped_memory_posix.cpp @@ -20,6 +20,13 @@ #include #include +#if REX_PLATFORM_MAC +using off64_t = off_t; +#define stat64 stat +#define fstat64 fstat +#define ftruncate64 ftruncate +#endif + namespace rex::memory { class PosixMappedMemory : public MappedMemory { diff --git a/src/core/memory_posix.cpp b/src/core/memory_posix.cpp index fce701e..df9fba3 100644 --- a/src/core/memory_posix.cpp +++ b/src/core/memory_posix.cpp @@ -38,9 +38,30 @@ // #include "xenia/base/main_android.h" #endif +#if REX_PLATFORM_MAC +#define ftruncate64 ftruncate +#define mmap64 mmap +#endif + namespace rex { namespace memory { +namespace { + +#if REX_PLATFORM_MAC +void AlignHostPageRange(void*& base_address, size_t& length) { + const uintptr_t page_mask = uintptr_t(page_size() - 1); + const uintptr_t start = reinterpret_cast(base_address); + const uintptr_t end = start + length; + const uintptr_t aligned_start = start & ~page_mask; + const uintptr_t aligned_end = (end + page_mask) & ~page_mask; + base_address = reinterpret_cast(aligned_start); + length = static_cast(aligned_end - aligned_start); +} +#endif + +} // namespace + // Convert filesystem path to valid shm_open name (must start with /, no other slashes) static std::string MakeShmName(const std::filesystem::path& path) { std::string name = path.string(); @@ -222,6 +243,19 @@ void* AllocFixed(void* base_address, size_t length, AllocationType allocation_ty break; } +#if REX_PLATFORM_MAC + if (base_address && + (allocation_type == AllocationType::kCommit || + allocation_type == AllocationType::kReserveCommit)) { + void* protect_base = base_address; + size_t protect_length = length; + AlignHostPageRange(protect_base, protect_length); + if (mprotect(protect_base, protect_length, static_cast(prot_requested)) == 0) { + return base_address; + } + } +#endif + // Build flags - always use MAP_FIXED_NOREPLACE for fixed addresses int flags = MAP_PRIVATE | MAP_ANONYMOUS; #if defined(MAP_FIXED_NOREPLACE) @@ -260,6 +294,9 @@ bool DeallocFixed(void* base_address, size_t length, DeallocationType deallocati switch (deallocation_type) { case DeallocationType::kDecommit: { // Decommit: remove access first, then release physical pages +#if REX_PLATFORM_MAC + AlignHostPageRange(base_address, length); +#endif if (mprotect(base_address, length, PROT_NONE) != 0) { return false; } @@ -298,6 +335,9 @@ bool Protect(void* base_address, size_t length, PageAccess access, PageAccess* o #endif uint32_t prot = ToPosixProtectFlags(access); +#if REX_PLATFORM_MAC + AlignHostPageRange(base_address, length); +#endif return mprotect(base_address, length, prot) == 0; } diff --git a/src/core/perf/counter.cpp b/src/core/perf/counter.cpp index 1eceed1..d5701c2 100644 --- a/src/core/perf/counter.cpp +++ b/src/core/perf/counter.cpp @@ -112,6 +112,12 @@ bool IsDelayedGpuTimingCounter(size_t counter_index) { return id == CounterId::kGpuMainUs || id == CounterId::kGpuDepthUs || id == CounterId::kGpuCopyUs || id == CounterId::kGpuMemexportUs || id == CounterId::kGpuNoPixelShaderUs || + id == CounterId::kGpuCopyDumpUs || + id == CounterId::kGpuCopyResolveUs || + id == CounterId::kGpuCopyResolveFast32Us || + id == CounterId::kGpuCopyResolveFull32Us || + id == CounterId::kGpuResolveDownscaleUs || + id == CounterId::kGpuTimestampedDraws || id == CounterId::kGpuCommandProcessorFrameUs || id == CounterId::kGpuTimestampedFrames || id == CounterId::kGpuBarrierUs || id == CounterId::kGpuClearUs || @@ -158,6 +164,11 @@ constexpr const char* kCounterNames[] = { "gpu_copy_us", "gpu_memexport_us", "gpu_no_pixel_shader_us", + "gpu_copy_dump_us", + "gpu_copy_resolve_us", + "gpu_copy_resolve_fast32_us", + "gpu_copy_resolve_full32_us", + "gpu_resolve_downscale_us", "gpu_timestamped_draws", "gpu_command_processor_frame_us", "gpu_timestamped_frames", @@ -245,6 +256,37 @@ constexpr const char* kCounterNames[] = { "rt_resolve_transfer_clear_us", "rt_resolve_readback_calls", "rt_resolve_readback_us", + "rt_dump_calls", + "rt_dump_rectangles", + "rt_dump_dispatches", + "rt_dump_groups", + "rt_resolve_copy_dispatches", + "rt_resolve_copy_groups", + "rt_resolve_scaled_bytes", + "rt_scaled_resolve_write_barriers", + "rt_scaled_resolve_write_barrier_overlaps", + "rt_scaled_resolve_write_barrier_skipped", + "rt_scaled_resolve_write_barrier_bytes", + "rt_resolve_copy_fast32_1x2x_dispatches", + "rt_resolve_copy_fast32_1x2x_groups", + "rt_resolve_copy_fast32_4x_dispatches", + "rt_resolve_copy_fast32_4x_groups", + "rt_resolve_copy_fast64_1x2x_dispatches", + "rt_resolve_copy_fast64_1x2x_groups", + "rt_resolve_copy_fast64_4x_dispatches", + "rt_resolve_copy_fast64_4x_groups", + "rt_resolve_copy_full8_dispatches", + "rt_resolve_copy_full8_groups", + "rt_resolve_copy_full16_dispatches", + "rt_resolve_copy_full16_groups", + "rt_resolve_copy_full32_dispatches", + "rt_resolve_copy_full32_groups", + "rt_resolve_copy_full64_dispatches", + "rt_resolve_copy_full64_groups", + "rt_resolve_copy_full128_dispatches", + "rt_resolve_copy_full128_groups", + "draw_no_pixel_depth_test_calls", + "draw_no_pixel_depth_write_calls", "xma_frames_decoded", "audio_frame_latency_us", "buffer_queue_depth", @@ -300,6 +342,11 @@ constexpr bool kIsGauge[] = { false, // kGpuCopyUs false, // kGpuMemexportUs false, // kGpuNoPixelShaderUs + false, // kGpuCopyDumpUs + false, // kGpuCopyResolveUs + false, // kGpuCopyResolveFast32Us + false, // kGpuCopyResolveFull32Us + false, // kGpuResolveDownscaleUs false, // kGpuTimestampedDraws false, // kGpuCommandProcessorFrameUs false, // kGpuTimestampedFrames @@ -387,6 +434,37 @@ constexpr bool kIsGauge[] = { false, // kRtResolveTransferClearUs false, // kRtResolveReadbackCalls false, // kRtResolveReadbackUs + false, // kRtDumpCalls + false, // kRtDumpRectangles + false, // kRtDumpDispatches + false, // kRtDumpGroups + false, // kRtResolveCopyDispatches + false, // kRtResolveCopyGroups + false, // kRtResolveScaledBytes + false, // kRtScaledResolveWriteBarriers + false, // kRtScaledResolveWriteBarrierOverlaps + false, // kRtScaledResolveWriteBarrierSkipped + false, // kRtScaledResolveWriteBarrierBytes + false, // kRtResolveCopyFast32bpp1x2xMsaaDispatches + false, // kRtResolveCopyFast32bpp1x2xMsaaGroups + false, // kRtResolveCopyFast32bpp4xMsaaDispatches + false, // kRtResolveCopyFast32bpp4xMsaaGroups + false, // kRtResolveCopyFast64bpp1x2xMsaaDispatches + false, // kRtResolveCopyFast64bpp1x2xMsaaGroups + false, // kRtResolveCopyFast64bpp4xMsaaDispatches + false, // kRtResolveCopyFast64bpp4xMsaaGroups + false, // kRtResolveCopyFull8bppDispatches + false, // kRtResolveCopyFull8bppGroups + false, // kRtResolveCopyFull16bppDispatches + false, // kRtResolveCopyFull16bppGroups + false, // kRtResolveCopyFull32bppDispatches + false, // kRtResolveCopyFull32bppGroups + false, // kRtResolveCopyFull64bppDispatches + false, // kRtResolveCopyFull64bppGroups + false, // kRtResolveCopyFull128bppDispatches + false, // kRtResolveCopyFull128bppGroups + false, // kDrawNoPixelDepthTestCalls + false, // kDrawNoPixelDepthWriteCalls false, // kXmaFramesDecoded false, // kAudioFrameLatencyUs false, // kBufferQueueDepth (set each frame) @@ -421,6 +499,16 @@ const char* DrawBucketName(DrawBucket bucket) { return "MemExp"; case DrawBucket::kNoPixelShader: return "NoPS"; + case DrawBucket::kCopyDump: + return "CopyDump"; + case DrawBucket::kCopyResolveShader: + return "CopyResolve"; + case DrawBucket::kCopyResolveFast32: + return "CopyResolveFast32"; + case DrawBucket::kCopyResolveFull32: + return "CopyResolveFull32"; + case DrawBucket::kResolveDownscale: + return "ResolveDownscale"; default: return "Unknown"; } diff --git a/src/core/threading_posix.cpp b/src/core/threading_posix.cpp index e10e30b..2f528e6 100644 --- a/src/core/threading_posix.cpp +++ b/src/core/threading_posix.cpp @@ -24,12 +24,15 @@ static_assert(REX_PLATFORM_LINUX || REX_PLATFORM_MAC, "This file is POSIX-only") #include #include -#include -#include #include #include #include +#if REX_PLATFORM_LINUX +#include +#include +#endif + #include #include #include @@ -110,13 +113,37 @@ enum class SignalType { }; int GetSystemSignal(SignalType num) { +#if REX_PLATFORM_MAC + switch (num) { + case SignalType::kThreadSuspend: + return SIGUSR1; + case SignalType::kThreadUserCallback: + return SIGUSR2; + default: + assert_unhandled_case(num); + return SIGUSR2; + } +#else auto result = SIGRTMIN + static_cast(num); assert_true(result < SIGRTMAX); return result; +#endif } SignalType GetSystemSignalType(int num) { +#if REX_PLATFORM_MAC + switch (num) { + case SIGUSR1: + return SignalType::kThreadSuspend; + case SIGUSR2: + return SignalType::kThreadUserCallback; + default: + assert_unhandled_case(num); + return SignalType::kThreadUserCallback; + } +#else return static_cast(num - SIGRTMIN); +#endif } std::array, static_cast(SignalType::k_Count)> signal_handler_installed = @@ -145,7 +172,13 @@ void EnableAffinityConfiguration() {} // uint64_t ticks() { return mach_absolute_time(); } uint32_t current_thread_system_id() { +#if REX_PLATFORM_MAC + uint64_t thread_id; + pthread_threadid_np(nullptr, &thread_id); + return static_cast(thread_id); +#else return static_cast(syscall(SYS_gettid)); +#endif } void MaybeYield() { @@ -669,7 +702,13 @@ class PosixCondition : public PosixConditionBase { WaitStarted(); std::unique_lock lock(state_mutex_); if (state_ != State::kUninitialized && state_ != State::kFinished) { +#if REX_PLATFORM_MAC + if (pthread_equal(thread_, pthread_self())) { + pthread_setname_np(std::string(name).c_str()); + } +#else pthread_setname_np(thread_, std::string(name).c_str()); +#endif #if REX_PLATFORM_ANDROID SetAndroidPreApi26Name(name); #endif @@ -687,9 +726,22 @@ class PosixCondition : public PosixConditionBase { } #endif - uint32_t system_id() const { return static_cast(thread_); } + uint32_t system_id() const { +#if REX_PLATFORM_MAC + uint64_t thread_id; + if (pthread_threadid_np(thread_, &thread_id) != 0) { + return 0; + } + return static_cast(thread_id); +#else + return static_cast(thread_); +#endif + } uint64_t affinity_mask() { +#if REX_PLATFORM_MAC + return 0; +#else WaitStarted(); cpu_set_t cpu_set; #if REX_PLATFORM_ANDROID @@ -708,9 +760,13 @@ class PosixCondition : public PosixConditionBase { result |= set << i; } return result; +#endif } void set_affinity_mask(uint64_t mask) { +#if REX_PLATFORM_MAC + (void)mask; +#else WaitStarted(); cpu_set_t cpu_set; CPU_ZERO(&cpu_set); @@ -727,6 +783,7 @@ class PosixCondition : public PosixConditionBase { if (pthread_setaffinity_np(thread_, sizeof(cpu_set_t), &cpu_set) != 0) { assert_always(); } +#endif #endif } @@ -784,6 +841,9 @@ class PosixCondition : public PosixConditionBase { #if REX_PLATFORM_ANDROID int result = sigqueue(pthread_gettid_np(thread_), GetSystemSignal(SignalType::kThreadUserCallback), value); +#elif REX_PLATFORM_MAC + (void)value; + int result = pthread_kill(thread_, GetSystemSignal(SignalType::kThreadUserCallback)); #else int result = pthread_sigqueue(thread_, GetSystemSignal(SignalType::kThreadUserCallback), value); #endif @@ -1394,7 +1454,11 @@ void Thread::Exit(int exit_code) { } void set_current_thread_name(const std::string_view name) { +#if REX_PLATFORM_MAC + pthread_setname_np(std::string(name).c_str()); +#else pthread_setname_np(pthread_self(), std::string(name).c_str()); +#endif #if REX_PLATFORM_ANDROID if (!android_pthread_getname_np_ && current_thread_) { current_thread_->condition().SetAndroidPreApi26Name(name); diff --git a/src/graphics/graphics_system.cpp b/src/graphics/graphics_system.cpp index 763f78d..2101fec 100644 --- a/src/graphics/graphics_system.cpp +++ b/src/graphics/graphics_system.cpp @@ -330,6 +330,8 @@ X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dis } void GraphicsSystem::Shutdown() { + shutting_down_.store(true, std::memory_order_release); + if (command_processor_) { EndTracing(); command_processor_->Shutdown(); @@ -447,7 +449,7 @@ void GraphicsSystem::SetInterruptCallback(uint32_t callback, uint32_t user_data) } void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) { - if (!interrupt_callback_) { + if (shutting_down_.load(std::memory_order_acquire) || !interrupt_callback_) { return; } diff --git a/src/graphics/pipeline/shader/spirv_translator.cpp b/src/graphics/pipeline/shader/spirv_translator.cpp index c709705..dae1752 100644 --- a/src/graphics/pipeline/shader/spirv_translator.cpp +++ b/src/graphics/pipeline/shader/spirv_translator.cpp @@ -145,7 +145,9 @@ void SpirvShaderTranslator::Reset() { var_main_memexport_data_written_ = spv::NoResult; main_memexport_allowed_ = spv::NoResult; var_main_rectangle_guest_vertex_index_ = spv::NoResult; - var_main_rectangle_per_vertex_ = spv::NoResult; + var_main_rectangle_position_ = spv::NoResult; + var_main_rectangle_clip_distances_ = spv::NoResult; + var_main_rectangle_cull_distances_ = spv::NoResult; std::fill(var_main_rectangle_interpolators_.begin(), var_main_rectangle_interpolators_.end(), spv::NoResult); var_main_point_size_edge_flag_kill_vertex_ = spv::NoResult; @@ -514,11 +516,34 @@ void SpirvShaderTranslator::StartTranslation() { var_main_rectangle_guest_vertex_index_ = builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, type_uint_, "xe_var_rectangle_guest_vertex_index", const_uint_0_); - spv::Id type_rectangle_per_vertex_array = - builder_->makeArrayType(type_output_per_vertex_, builder_->makeUintConstant(3), 0); - var_main_rectangle_per_vertex_ = - builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, - type_rectangle_per_vertex_array, "xe_var_rectangle_per_vertex"); + spv::Id type_rectangle_position_array = + builder_->makeArrayType(type_float4_, builder_->makeUintConstant(3), sizeof(float) * 4); + builder_->addDecoration(type_rectangle_position_array, spv::DecorationArrayStride, + sizeof(float) * 4); + var_main_rectangle_position_ = builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_rectangle_position_array, + "xe_var_rectangle_position"); + Modification shader_modification = GetSpirvShaderModification(); + uint32_t clip_distance_count = shader_modification.GetVertexClipDistanceCount(); + if (clip_distance_count) { + spv::Id type_rectangle_clip_distance_array = builder_->makeArrayType( + type_float_, builder_->makeUintConstant(3 * clip_distance_count), sizeof(float)); + builder_->addDecoration(type_rectangle_clip_distance_array, spv::DecorationArrayStride, + sizeof(float)); + var_main_rectangle_clip_distances_ = builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_rectangle_clip_distance_array, + "xe_var_rectangle_clip_distances"); + } + uint32_t cull_distance_count = shader_modification.GetVertexCullDistanceCount(); + if (cull_distance_count) { + spv::Id type_rectangle_cull_distance_array = builder_->makeArrayType( + type_float_, builder_->makeUintConstant(3 * cull_distance_count), sizeof(float)); + builder_->addDecoration(type_rectangle_cull_distance_array, spv::DecorationArrayStride, + sizeof(float)); + var_main_rectangle_cull_distances_ = builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_rectangle_cull_distance_array, + "xe_var_rectangle_cull_distances"); + } uint32_t interpolators_remaining = GetModificationInterpolatorMask(); uint32_t interpolator_index; while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) { @@ -693,13 +718,51 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { spv::Id rectangle_guest_vertex_index_int = builder_->createUnaryOp(spv::OpBitcast, type_int_, rectangle_guest_vertex_index); - // Store current iteration outputs. + // Store current iteration outputs. Keep system values in plain function + // arrays rather than copying the gl_PerVertex block because SPIRV-Cross + // emits invalid MSL for gl_PerVertex used as a local function type. + id_vector_temp_.clear(); + id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition)); + spv::Id rectangle_output_position = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassOutput, output_per_vertex_, id_vector_temp_), + spv::NoPrecision); id_vector_temp_.clear(); id_vector_temp_.push_back(rectangle_guest_vertex_index_int); - builder_->createStore( - builder_->createLoad(output_per_vertex_, spv::NoPrecision), - builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_, - id_vector_temp_)); + builder_->createStore(rectangle_output_position, + builder_->createAccessChain(spv::StorageClassFunction, + var_main_rectangle_position_, + id_vector_temp_)); + auto store_rectangle_distance_member = + [this, rectangle_guest_vertex_index_int](uint32_t output_member_index, + uint32_t component_count, spv::Id scratch_var) { + for (uint32_t i = 0; i < component_count; ++i) { + id_vector_temp_.clear(); + id_vector_temp_.push_back(builder_->makeIntConstant(int(output_member_index))); + id_vector_temp_.push_back(builder_->makeIntConstant(int(i))); + spv::Id output_value = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassOutput, output_per_vertex_, + id_vector_temp_), + spv::NoPrecision); + spv::Id scratch_index = builder_->createBinOp( + spv::OpIAdd, type_int_, + builder_->createBinOp(spv::OpIMul, type_int_, rectangle_guest_vertex_index_int, + builder_->makeIntConstant(int(component_count))), + builder_->makeIntConstant(int(i))); + id_vector_temp_.clear(); + id_vector_temp_.push_back(scratch_index); + builder_->createStore(output_value, + builder_->createAccessChain(spv::StorageClassFunction, + scratch_var, id_vector_temp_)); + } + }; + if (clip_distance_count && var_main_rectangle_clip_distances_ != spv::NoResult) { + store_rectangle_distance_member(output_per_vertex_member_clip_distance_, + clip_distance_count, var_main_rectangle_clip_distances_); + } + if (cull_distance_count && var_main_rectangle_cull_distances_ != spv::NoResult) { + store_rectangle_distance_member(output_per_vertex_member_cull_distance_, + cull_distance_count, var_main_rectangle_cull_distances_); + } uint32_t interpolators_remaining = GetModificationInterpolatorMask(); uint32_t interpolator_index; while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) { @@ -884,7 +947,7 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { } builder_->createBranch(main_loop_continue_); } - if_not_last_guest_vertex.makeBeginElse(); + if_not_last_guest_vertex.makeBeginElse(false); { spv::Id rectangle_host_vertex_in_primitive = builder_->createBinOp( spv::OpBitwiseAnd, type_uint_, @@ -903,10 +966,9 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { auto load_rectangle_position_xy = [this](spv::Id vertex_index, uint32_t component) { id_vector_temp_.clear(); id_vector_temp_.push_back(vertex_index); - id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition)); id_vector_temp_.push_back(builder_->makeIntConstant(int(component))); return builder_->createLoad( - builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_, + builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_, id_vector_temp_), spv::NoPrecision); }; @@ -1002,17 +1064,27 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { input_output_interpolators_[interpolator_index]); } - auto load_rectangle_per_vertex_component = - [this](spv::Id vertex_index, uint32_t member_index, spv::Id component_index) { + auto load_rectangle_position = [this](spv::Id vertex_index) { + id_vector_temp_.clear(); + id_vector_temp_.push_back(vertex_index); + return builder_->createLoad( + builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_, + id_vector_temp_), + spv::NoPrecision); + }; + auto load_rectangle_distance_component = + [this](spv::Id vertex_index, uint32_t component_count, spv::Id component_index, + spv::Id scratch_var) { + spv::Id scratch_index = builder_->createBinOp( + spv::OpIAdd, type_int_, + builder_->createBinOp(spv::OpIMul, type_int_, vertex_index, + builder_->makeIntConstant(int(component_count))), + component_index); id_vector_temp_.clear(); - id_vector_temp_.push_back(vertex_index); - id_vector_temp_.push_back(builder_->makeIntConstant(int(member_index))); - if (component_index != spv::NoResult) { - id_vector_temp_.push_back(component_index); - } + id_vector_temp_.push_back(scratch_index); return builder_->createLoad( - builder_->createAccessChain(spv::StorageClassFunction, - var_main_rectangle_per_vertex_, id_vector_temp_), + builder_->createAccessChain(spv::StorageClassFunction, scratch_var, + id_vector_temp_), spv::NoPrecision); }; @@ -1020,12 +1092,9 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition)); spv::Id rectangle_position_ptr = builder_->createAccessChain( spv::StorageClassOutput, output_per_vertex_, id_vector_temp_); - spv::Id position_0 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[0], kOutputPerVertexMemberPosition, spv::NoResult); - spv::Id position_1 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[1], kOutputPerVertexMemberPosition, spv::NoResult); - spv::Id position_2 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[2], kOutputPerVertexMemberPosition, spv::NoResult); + spv::Id position_0 = load_rectangle_position(rectangle_vertex_indices[0]); + spv::Id position_1 = load_rectangle_position(rectangle_vertex_indices[1]); + spv::Id position_2 = load_rectangle_position(rectangle_vertex_indices[2]); spv::Id position_original = select_rectangle_vertex_component(position_0, position_1, position_2, type_float4_); spv::Id position_synthetic = builder_->createNoContractionBinOp( @@ -1039,16 +1108,17 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { auto write_rectangle_distance_member = [this, &select_rectangle_vertex_component, rectangle_host_vertex_is_synthetic, - &load_rectangle_per_vertex_component, - rectangle_vertex_indices](uint32_t member_index, uint32_t component_count) { + &load_rectangle_distance_component, + rectangle_vertex_indices](uint32_t member_index, uint32_t component_count, + spv::Id scratch_var) { for (uint32_t i = 0; i < component_count; ++i) { spv::Id component_index = builder_->makeIntConstant(int(i)); - spv::Id value_0 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[0], member_index, component_index); - spv::Id value_1 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[1], member_index, component_index); - spv::Id value_2 = load_rectangle_per_vertex_component( - rectangle_vertex_indices[2], member_index, component_index); + spv::Id value_0 = load_rectangle_distance_component( + rectangle_vertex_indices[0], component_count, component_index, scratch_var); + spv::Id value_1 = load_rectangle_distance_component( + rectangle_vertex_indices[1], component_count, component_index, scratch_var); + spv::Id value_2 = load_rectangle_distance_component( + rectangle_vertex_indices[2], component_count, component_index, scratch_var); spv::Id value_original = select_rectangle_vertex_component(value_0, value_1, value_2, type_float_); spv::Id value_synthetic = builder_->createNoContractionBinOp( @@ -1069,11 +1139,11 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { if (clip_distance_count && output_per_vertex_member_clip_distance_ != UINT32_MAX) { write_rectangle_distance_member(output_per_vertex_member_clip_distance_, - clip_distance_count); + clip_distance_count, var_main_rectangle_clip_distances_); } if (cull_distance_count && output_per_vertex_member_cull_distance_ != UINT32_MAX) { write_rectangle_distance_member(output_per_vertex_member_cull_distance_, - cull_distance_count); + cull_distance_count, var_main_rectangle_cull_distances_); } } if_not_last_guest_vertex.makeEndIf(); diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index afacf68..195c9ef 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,10 @@ REXCVAR_DEFINE_BOOL(vulkan_dynamic_rendering, true, "GPU/Vulkan", "Use VK_KHR_dynamic_rendering for Vulkan GPU emulation when supported by the " "device (falls back to render passes otherwise)") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(vulkan_skip_inert_no_pixel_draws, false, "GPU/Vulkan", + "Skip Vulkan no-pixel-shader draws that have no color, depth, stencil or " + "occlusion side effects") + .lifecycle(rex::cvar::Lifecycle::kHotReload); namespace rex::graphics::vulkan { @@ -1903,6 +1908,7 @@ bool VulkanCommandProcessor::SetupContext() { } occlusion_query_resources_available_ = InitializeOcclusionQueryResources(); + gpu_timestamp_resources_available_ = InitializeGpuTimestampResources(); // Just not to expose uninitialized memory. std::memset(&system_constants_, 0, sizeof(system_constants_)); @@ -1913,6 +1919,7 @@ bool VulkanCommandProcessor::SetupContext() { void VulkanCommandProcessor::ShutdownContext() { AwaitAllQueueOperationsCompletion(); InvalidateAllVertexBufferResidency(); + ShutdownGpuTimestampResources(); ShutdownOcclusionQueryResources(); const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); @@ -3607,6 +3614,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t #if XE_GPU_FINE_GRAINED_DRAW_SCOPES SCOPE_profile_cpu_f("gpu"); #endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES + rex::perf::ScopedCounterTimer draw_stage_total_timer( + rex::perf::CounterId::kDrawStageTotalUs); const RegisterFile& regs = *register_file_; (void)index_buffer_info; @@ -3627,6 +3636,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t xenos::EdramMode edram_mode = regs.Get().edram_mode; if (edram_mode == xenos::EdramMode::kCopy) { // Special copy handling. + rex::perf::ScopedCounterTimer resolve_timer(rex::perf::CounterId::kRtResolveUs); const uint32_t primitive_count = draw_util::EstimatePrimitiveCount(prim_type, index_count); PROFILE_DRAW_BUCKET(rex::perf::DrawBucket::kCopyResolve, index_count, primitive_count); if (rex::perf::ShouldCollectDrawFingerprints()) { @@ -3641,7 +3651,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t fingerprint.primitive_count = primitive_count; PROFILE_DRAW_FINGERPRINT(fingerprint); } - return IssueCopy(); + uint32_t gpu_timestamp_start = + BeginGpuTimestampedDraw(rex::perf::DrawBucket::kCopyResolve); + bool copy_result = IssueCopy(); + EndGpuTimestampedDraw(gpu_timestamp_start); + return copy_result; } bool surface_pitch_is_zero = regs.Get().surface_pitch == 0; @@ -3736,8 +3750,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } // Process primitives. - if (!primitive_processor_->Process(primitive_processing_result)) { - return draw_fail("primitive_processing"); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStagePrimitiveUs); + if (!primitive_processor_->Process(primitive_processing_result)) { + return draw_fail("primitive_processing"); + } } if (!primitive_processing_result.host_draw_vertex_count) { // Nothing to draw. @@ -3856,6 +3873,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t uint32_t normalized_color_mask = pixel_shader ? draw_util::GetNormalizedColorMask(regs, pixel_shader->writes_color_targets()) : 0; + if (REXCVAR_GET(vulkan_skip_inert_no_pixel_draws) && pixel_shader == nullptr && + normalized_color_mask == 0 && !normalized_depth_control.z_enable && + !normalized_depth_control.stencil_enable && !active_occlusion_query_.valid) { + return true; + } // Update the textures before most other work in the submission because // samplers depend on this (and in case of sampler overflow in a submission, @@ -3863,13 +3885,19 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t uint32_t used_texture_mask = vertex_shader->GetUsedTextureMaskAfterTranslation() | (pixel_shader != nullptr ? pixel_shader->GetUsedTextureMaskAfterTranslation() : 0); - texture_cache_->RequestTextures(used_texture_mask); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageTextureUs); + texture_cache_->RequestTextures(used_texture_mask); + } const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider; // Set up the render targets - this may perform dispatches and draws. - if (!render_target_cache_->Update(is_rasterization_done, normalized_depth_control, - normalized_color_mask, *vertex_shader)) { - return draw_fail("render_target_update"); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageRenderTargetUs); + if (!render_target_cache_->Update(is_rasterization_done, normalized_depth_control, + normalized_color_mask, *vertex_shader)) { + return draw_fail("render_target_update"); + } } // Create the pipeline (for this, need the render pass from the render target @@ -3877,24 +3905,28 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t // textures. VkPipeline pipeline; void* pipeline_handle = nullptr; - if (!pipeline_cache_->ConfigurePipeline(vertex_shader_translation, pixel_shader_translation, - primitive_processing_result, normalized_depth_control, - normalized_color_mask, - render_target_cache_->last_update_render_pass_key(), - pipeline, pipeline_layout_provider, &pipeline_handle)) { - return draw_fail("configure_pipeline"); - } - bool pipeline_is_placeholder = false; - // Reload the current handle state to observe async hot-swap completions that - // may happen between pipeline configuration and binding. - pipeline_cache_->GetPipelineAndLayoutByHandle(pipeline_handle, pipeline, pipeline_layout_provider, - &pipeline_is_placeholder); - if (REXCVAR_GET(async_shader_compilation) && pipeline_is_placeholder) { - frame_used_async_placeholder_pipeline_ = true; - return true; - } - if (pipeline == VK_NULL_HANDLE || pipeline_layout_provider == nullptr) { - return draw_fail("pipeline_lookup"); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStagePipelineUs); + if (!pipeline_cache_->ConfigurePipeline(vertex_shader_translation, pixel_shader_translation, + primitive_processing_result, normalized_depth_control, + normalized_color_mask, + render_target_cache_->last_update_render_pass_key(), + pipeline, pipeline_layout_provider, &pipeline_handle)) { + return draw_fail("configure_pipeline"); + } + bool pipeline_is_placeholder = false; + // Reload the current handle state to observe async hot-swap completions that + // may happen between pipeline configuration and binding. + pipeline_cache_->GetPipelineAndLayoutByHandle(pipeline_handle, pipeline, + pipeline_layout_provider, + &pipeline_is_placeholder); + if (REXCVAR_GET(async_shader_compilation) && pipeline_is_placeholder) { + frame_used_async_placeholder_pipeline_ = true; + return true; + } + if (pipeline == VK_NULL_HANDLE || pipeline_layout_provider == nullptr) { + return draw_fail("pipeline_lookup"); + } } if (current_guest_graphics_pipeline_ != pipeline) { deferred_command_buffer_.CmdVkBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); @@ -3937,31 +3969,34 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t // Get dynamic rasterizer state. draw_util::ViewportInfo viewport_info; - // Just handling maxViewportDimensions is enough - viewportBoundsRange[1] must - // be at least 2 * max(maxViewportDimensions[0...1]) - 1, and - // maxViewportDimensions must be greater than or equal to the size of the - // largest possible framebuffer attachment (if the viewport has positive - // offset and is between maxViewportDimensions and viewportBoundsRange[1], - // GetHostViewportInfo will adjust ndc_scale/ndc_offset to clamp it, and the - // clamped range will be outside the largest possible framebuffer anyway. - // FIXME(Triang3l): Possibly handle maxViewportDimensions and - // viewportBoundsRange separately because when using fragment shader - // interlocks, framebuffers are not used, while the range may be wider than - // dimensions? Though viewport bigger than 4096 - the smallest possible - // maximum dimension (which is below the 8192 texture size limit on the Xbox - // 360) - and with offset, is probably a situation that never happens in real - // life. Or even disregard the viewport bounds range in the fragment shader - // interlocks case completely - apply the viewport and the scissor offset - // directly to pixel address and to things like ps_param_gen. - draw_util::GetHostViewportInfo( - regs, draw_resolution_scale_x, draw_resolution_scale_y, false, - device_properties.maxViewportDimensions[0], device_properties.maxViewportDimensions[1], true, - normalized_depth_control, - host_render_targets_used && render_target_cache_->depth_float24_convert_in_pixel_shader(), - host_render_targets_used, pixel_shader && pixel_shader->writes_depth(), viewport_info); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageFixedFunctionUs); + // Just handling maxViewportDimensions is enough - viewportBoundsRange[1] must + // be at least 2 * max(maxViewportDimensions[0...1]) - 1, and + // maxViewportDimensions must be greater than or equal to the size of the + // largest possible framebuffer attachment (if the viewport has positive + // offset and is between maxViewportDimensions and viewportBoundsRange[1], + // GetHostViewportInfo will adjust ndc_scale/ndc_offset to clamp it, and the + // clamped range will be outside the largest possible framebuffer anyway. + // FIXME(Triang3l): Possibly handle maxViewportDimensions and + // viewportBoundsRange separately because when using fragment shader + // interlocks, framebuffers are not used, while the range may be wider than + // dimensions? Though viewport bigger than 4096 - the smallest possible + // maximum dimension (which is below the 8192 texture size limit on the Xbox + // 360) - and with offset, is probably a situation that never happens in real + // life. Or even disregard the viewport bounds range in the fragment shader + // interlocks case completely - apply the viewport and the scissor offset + // directly to pixel address and to things like ps_param_gen. + draw_util::GetHostViewportInfo( + regs, draw_resolution_scale_x, draw_resolution_scale_y, false, + device_properties.maxViewportDimensions[0], device_properties.maxViewportDimensions[1], + true, normalized_depth_control, + host_render_targets_used && render_target_cache_->depth_float24_convert_in_pixel_shader(), + host_render_targets_used, pixel_shader && pixel_shader->writes_depth(), viewport_info); - // Update dynamic graphics pipeline state. - UpdateDynamicState(viewport_info, primitive_polygonal, normalized_depth_control); + // Update dynamic graphics pipeline state. + UpdateDynamicState(viewport_info, primitive_polygonal, normalized_depth_control); + } auto vgt_draw_initiator = regs.Get(); @@ -4001,147 +4036,157 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t // Update uniform buffers and descriptor sets after binding the pipeline with // the new layout. - if (!UpdateBindings(vertex_shader, pixel_shader)) { - return draw_fail("update_bindings"); - } - - // Ensure vertex buffers are resident. - const Shader::ConstantRegisterMap& constant_map_vertex = vertex_shader->constant_register_map(); - for (uint32_t i = 0; i < rex::countof(constant_map_vertex.vertex_fetch_bitmap); ++i) { - uint32_t vfetch_bits_remaining = constant_map_vertex.vertex_fetch_bitmap[i]; - uint32_t j; - while (rex::bit_scan_forward(vfetch_bits_remaining, &j)) { - vfetch_bits_remaining &= ~(uint32_t(1) << j); - uint32_t vfetch_index = i * 32 + j; - uint64_t vfetch_bit = uint64_t(1) << (vfetch_index & 63); - if (vertex_buffers_in_sync_[vfetch_index >> 6] & vfetch_bit) { - continue; - } - xenos::xe_gpu_vertex_fetch_t vfetch_constant = regs.GetVertexFetch(vfetch_index); - switch (vfetch_constant.type) { - case xenos::FetchConstantType::kVertex: - break; - case xenos::FetchConstantType::kInvalidVertex: - if (REXCVAR_GET(gpu_allow_invalid_fetch_constants)) { - break; - } - REXGPU_WARN( - "Vertex fetch constant {} ({:08X} {:08X}) has \"invalid\" type! " - "This " - "is incorrect behavior, but you can try bypassing this by " - "launching Xenia with --gpu_allow_invalid_fetch_constants=true.", - vfetch_index, vfetch_constant.dword_0, vfetch_constant.dword_1); - return false; - default: - REXGPU_WARN("Vertex fetch constant {} ({:08X} {:08X}) is completely invalid!", - vfetch_index, vfetch_constant.dword_0, vfetch_constant.dword_1); - return false; - } - VertexBufferState& state = vertex_buffer_states_[vfetch_index]; - if (state.address == vfetch_constant.address && state.size == vfetch_constant.size) { - vertex_buffers_in_sync_[vfetch_index >> 6] |= vfetch_bit; - continue; - } - if (!shared_memory_->RequestRange(vfetch_constant.address << 2, vfetch_constant.size << 2)) { - REXGPU_ERROR( - "Failed to request vertex buffer at 0x{:08X} (size {}) in the shared " - "memory", - vfetch_constant.address << 2, vfetch_constant.size << 2); - return false; - } - state.address = vfetch_constant.address; - state.size = vfetch_constant.size; - vertex_buffers_in_sync_[vfetch_index >> 6] |= vfetch_bit; - } - } - - // Synchronize the memory pages backing memory scatter export streams, and - // calculate the range that includes the streams for the buffer barrier. - uint32_t memexport_extent_start = UINT32_MAX, memexport_extent_end = 0; - for (const draw_util::MemExportRange& memexport_range : memexport_ranges_) { - uint32_t memexport_range_base_bytes = memexport_range.base_address_dwords << 2; - if (!shared_memory_->RequestRange(memexport_range_base_bytes, memexport_range.size_bytes)) { - REXGPU_ERROR( - "Failed to request memexport stream at 0x{:08X} (size {}) in the " - "shared memory", - memexport_range_base_bytes, memexport_range.size_bytes); - return false; - } - memexport_extent_start = std::min(memexport_extent_start, memexport_range_base_bytes); - memexport_extent_end = - std::max(memexport_extent_end, memexport_range_base_bytes + memexport_range.size_bytes); - } - if (memexport_writes_possible && memexport_ranges_.empty()) { - if (!shared_memory_->RequestRange(0, SharedMemory::kBufferSize)) { - REXGPU_ERROR( - "Failed to request full shared memory residency for unresolved " - "memexport destinations"); - return false; + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageBindingsUs); + if (!UpdateBindings(vertex_shader, pixel_shader)) { + return draw_fail("update_bindings"); } } ScratchBufferAcquisition guest_dma_index_scratch_buffer; - if (primitive_processing_result.index_buffer_type == - PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA && - !shader_32bit_index_dma && memexport_writes_possible) { - // Align with D3D12 behavior where memexporting draws using guest DMA indices - // read a snapshot of indices to avoid read/write aliasing through shared - // memory. - VkDeviceSize guest_dma_index_size = - VkDeviceSize(primitive_processing_result.host_draw_vertex_count) * - (primitive_processing_result.host_index_format == xenos::IndexFormat::kInt16 - ? sizeof(uint16_t) - : sizeof(uint32_t)); - if (guest_dma_index_size) { - guest_dma_index_scratch_buffer = AcquireScratchGpuBuffer( - guest_dma_index_size, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); - if (guest_dma_index_scratch_buffer.buffer() == VK_NULL_HANDLE) { - return draw_fail("guest_dma_index_scratch_buffer"); + + // Ensure vertex buffers are resident. + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageVertexBuffersUs); + const Shader::ConstantRegisterMap& constant_map_vertex = vertex_shader->constant_register_map(); + for (uint32_t i = 0; i < rex::countof(constant_map_vertex.vertex_fetch_bitmap); ++i) { + uint32_t vfetch_bits_remaining = constant_map_vertex.vertex_fetch_bitmap[i]; + uint32_t j; + while (rex::bit_scan_forward(vfetch_bits_remaining, &j)) { + vfetch_bits_remaining &= ~(uint32_t(1) << j); + uint32_t vfetch_index = i * 32 + j; + uint64_t vfetch_bit = uint64_t(1) << (vfetch_index & 63); + if (vertex_buffers_in_sync_[vfetch_index >> 6] & vfetch_bit) { + continue; + } + xenos::xe_gpu_vertex_fetch_t vfetch_constant = regs.GetVertexFetch(vfetch_index); + switch (vfetch_constant.type) { + case xenos::FetchConstantType::kVertex: + break; + case xenos::FetchConstantType::kInvalidVertex: + if (REXCVAR_GET(gpu_allow_invalid_fetch_constants)) { + break; + } + REXGPU_WARN( + "Vertex fetch constant {} ({:08X} {:08X}) has \"invalid\" type! " + "This " + "is incorrect behavior, but you can try bypassing this by " + "launching Xenia with --gpu_allow_invalid_fetch_constants=true.", + vfetch_index, vfetch_constant.dword_0, vfetch_constant.dword_1); + return false; + default: + REXGPU_WARN("Vertex fetch constant {} ({:08X} {:08X}) is completely invalid!", + vfetch_index, vfetch_constant.dword_0, vfetch_constant.dword_1); + return false; + } + VertexBufferState& state = vertex_buffer_states_[vfetch_index]; + if (state.address == vfetch_constant.address && state.size == vfetch_constant.size) { + vertex_buffers_in_sync_[vfetch_index >> 6] |= vfetch_bit; + continue; + } + if (!shared_memory_->RequestRange(vfetch_constant.address << 2, vfetch_constant.size << 2)) { + REXGPU_ERROR( + "Failed to request vertex buffer at 0x{:08X} (size {}) in the shared " + "memory", + vfetch_constant.address << 2, vfetch_constant.size << 2); + return false; + } + state.address = vfetch_constant.address; + state.size = vfetch_constant.size; + vertex_buffers_in_sync_[vfetch_index >> 6] |= vfetch_bit; } - - shared_memory_->Use(VulkanSharedMemory::Usage::kRead); - SubmitBarriers(true); - - VkBufferCopy guest_dma_index_copy_region = {}; - guest_dma_index_copy_region.srcOffset = primitive_processing_result.guest_index_base; - guest_dma_index_copy_region.dstOffset = 0; - guest_dma_index_copy_region.size = guest_dma_index_size; - deferred_command_buffer_.CmdVkCopyBuffer(shared_memory_->buffer(), - guest_dma_index_scratch_buffer.buffer(), 1, - &guest_dma_index_copy_region); - PushBufferMemoryBarrier( - guest_dma_index_scratch_buffer.buffer(), 0, guest_dma_index_size, - guest_dma_index_scratch_buffer.GetStageMask(), VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, - guest_dma_index_scratch_buffer.GetAccessMask(), VK_ACCESS_INDEX_READ_BIT); - guest_dma_index_scratch_buffer.SetStageMask(VK_PIPELINE_STAGE_VERTEX_INPUT_BIT); - guest_dma_index_scratch_buffer.SetAccessMask(VK_ACCESS_INDEX_READ_BIT); } - } - // Insert the shared memory barrier if needed. - // TODO(Triang3l): Find some PM4 command that can be used for indication of - // when memexports should be awaited instead of inserting the barrier in Use - // every time if memory export was done in the previous draw? - if (memexport_extent_start < memexport_extent_end) { - shared_memory_->Use( - VulkanSharedMemory::Usage::kGuestDrawReadWrite, - std::make_pair(memexport_extent_start, memexport_extent_end - memexport_extent_start)); - } else if (memexport_writes_possible) { - // Stream constants can be invalid or dynamic, making the exact destination - // unknown. Keep synchronization conservative similarly to D3D12 UAV - // handling for memexport-capable draws. - shared_memory_->Use(VulkanSharedMemory::Usage::kGuestDrawReadWrite, - std::make_pair(0u, SharedMemory::kBufferSize)); - } else { - shared_memory_->Use(VulkanSharedMemory::Usage::kRead); + // Synchronize the memory pages backing memory scatter export streams, and + // calculate the range that includes the streams for the buffer barrier. + uint32_t memexport_extent_start = UINT32_MAX, memexport_extent_end = 0; + for (const draw_util::MemExportRange& memexport_range : memexport_ranges_) { + uint32_t memexport_range_base_bytes = memexport_range.base_address_dwords << 2; + if (!shared_memory_->RequestRange(memexport_range_base_bytes, memexport_range.size_bytes)) { + REXGPU_ERROR( + "Failed to request memexport stream at 0x{:08X} (size {}) in the " + "shared memory", + memexport_range_base_bytes, memexport_range.size_bytes); + return false; + } + memexport_extent_start = std::min(memexport_extent_start, memexport_range_base_bytes); + memexport_extent_end = + std::max(memexport_extent_end, memexport_range_base_bytes + memexport_range.size_bytes); + } + if (memexport_writes_possible && memexport_ranges_.empty()) { + if (!shared_memory_->RequestRange(0, SharedMemory::kBufferSize)) { + REXGPU_ERROR( + "Failed to request full shared memory residency for unresolved " + "memexport destinations"); + return false; + } + } + + if (primitive_processing_result.index_buffer_type == + PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA && + !shader_32bit_index_dma && memexport_writes_possible) { + // Align with D3D12 behavior where memexporting draws using guest DMA indices + // read a snapshot of indices to avoid read/write aliasing through shared + // memory. + VkDeviceSize guest_dma_index_size = + VkDeviceSize(primitive_processing_result.host_draw_vertex_count) * + (primitive_processing_result.host_index_format == xenos::IndexFormat::kInt16 + ? sizeof(uint16_t) + : sizeof(uint32_t)); + if (guest_dma_index_size) { + guest_dma_index_scratch_buffer = AcquireScratchGpuBuffer( + guest_dma_index_size, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + if (guest_dma_index_scratch_buffer.buffer() == VK_NULL_HANDLE) { + return draw_fail("guest_dma_index_scratch_buffer"); + } + + shared_memory_->Use(VulkanSharedMemory::Usage::kRead); + SubmitBarriers(true); + + VkBufferCopy guest_dma_index_copy_region = {}; + guest_dma_index_copy_region.srcOffset = primitive_processing_result.guest_index_base; + guest_dma_index_copy_region.dstOffset = 0; + guest_dma_index_copy_region.size = guest_dma_index_size; + deferred_command_buffer_.CmdVkCopyBuffer(shared_memory_->buffer(), + guest_dma_index_scratch_buffer.buffer(), 1, + &guest_dma_index_copy_region); + PushBufferMemoryBarrier( + guest_dma_index_scratch_buffer.buffer(), 0, guest_dma_index_size, + guest_dma_index_scratch_buffer.GetStageMask(), VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, + guest_dma_index_scratch_buffer.GetAccessMask(), VK_ACCESS_INDEX_READ_BIT); + guest_dma_index_scratch_buffer.SetStageMask(VK_PIPELINE_STAGE_VERTEX_INPUT_BIT); + guest_dma_index_scratch_buffer.SetAccessMask(VK_ACCESS_INDEX_READ_BIT); + } + } + + // Insert the shared memory barrier if needed. + // TODO(Triang3l): Find some PM4 command that can be used for indication of + // when memexports should be awaited instead of inserting the barrier in Use + // every time if memory export was done in the previous draw? + if (memexport_extent_start < memexport_extent_end) { + shared_memory_->Use( + VulkanSharedMemory::Usage::kGuestDrawReadWrite, + std::make_pair(memexport_extent_start, memexport_extent_end - memexport_extent_start)); + } else if (memexport_writes_possible) { + // Stream constants can be invalid or dynamic, making the exact destination + // unknown. Keep synchronization conservative similarly to D3D12 UAV + // handling for memexport-capable draws. + shared_memory_->Use(VulkanSharedMemory::Usage::kGuestDrawReadWrite, + std::make_pair(0u, SharedMemory::kBufferSize)); + } else { + shared_memory_->Use(VulkanSharedMemory::Usage::kRead); + } } // After all commands that may dispatch, copy or insert barriers, submit // the barriers (may end the render pass), and (re)enter the render pass // before drawing. - SubmitBarriersAndEnterRenderTargetCacheRenderPass( - render_target_cache_->last_update_render_pass(), - render_target_cache_->last_update_framebuffer()); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageBarriersUs); + SubmitBarriersAndEnterRenderTargetCacheRenderPass( + render_target_cache_->last_update_render_pass(), + render_target_cache_->last_update_framebuffer()); + } const uint32_t host_draw_vertex_count = primitive_processing_result.host_draw_vertex_count; const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount( @@ -4153,6 +4198,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t ? rex::perf::DrawBucket::kDepthOnly : (pixel_shader == nullptr ? rex::perf::DrawBucket::kNoPixelShader : rex::perf::DrawBucket::kMainColorDepth)); + if (draw_bucket == rex::perf::DrawBucket::kNoPixelShader) { + if (normalized_depth_control.z_enable || normalized_depth_control.stencil_enable) { + PERF_counter_inc(kDrawNoPixelDepthTestCalls); + } + if (normalized_depth_control.z_write_enable) { + PERF_counter_inc(kDrawNoPixelDepthWriteCalls); + } + } const bool collect_draw_fingerprints = rex::perf::ShouldCollectDrawFingerprints(); rex::perf::DrawFingerprint draw_fingerprint; if (collect_draw_fingerprints) { @@ -4183,9 +4236,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } // Draw. + uint32_t gpu_timestamp_start = BeginGpuTimestampedDraw(draw_bucket); if (primitive_processing_result.index_buffer_type == PrimitiveProcessor::ProcessedIndexBufferType::kNone || shader_32bit_index_dma) { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSubmitUs); PROFILE_DRAW_CALL(); PROFILE_VERTICES(host_draw_vertex_count); PROFILE_PRIMITIVES(host_draw_primitive_count); @@ -4195,6 +4250,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } deferred_command_buffer_.CmdVkDraw(host_draw_vertex_count, 1, 0, 0); } else { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSubmitUs); std::pair index_buffer; switch (primitive_processing_result.index_buffer_type) { case PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA: @@ -4233,6 +4289,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } deferred_command_buffer_.CmdVkDrawIndexed(host_draw_vertex_count, 1, 0, 0, 0); } + EndGpuTimestampedDraw(gpu_timestamp_start); // Invalidate textures in memexported memory and watch for changes. if (!memexport_ranges_.empty()) { @@ -4661,6 +4718,9 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { texture_cache_->UseScaledResolveBufferForRead(); SubmitBarriers(true); + uint32_t resolve_downscale_timestamp_start = + BeginGpuTimestampedRegion(rex::perf::DrawBucket::kResolveDownscale); + VkBufferMemoryBarrier pre_barrier = {}; pre_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; pre_barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; @@ -4713,6 +4773,7 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { readback_region.size = written_length; deferred_command_buffer_.CmdVkCopyBuffer(resolve_downscale_buffer_, readback.buffers[write_index], 1, &readback_region); + EndGpuTimestampedDraw(resolve_downscale_timestamp_start); } else { shared_memory_->Use(VulkanSharedMemory::Usage::kRead); SubmitBarriers(true); @@ -4810,6 +4871,334 @@ void VulkanCommandProcessor::EvictOldReadbackBuffers( } } +bool VulkanCommandProcessor::InitializeGpuTimestampResources() { + gpu_timestamp_query_pool_ = VK_NULL_HANDLE; + gpu_timestamp_readback_buffer_ = VK_NULL_HANDLE; + gpu_timestamp_readback_memory_ = VK_NULL_HANDLE; + gpu_timestamp_readback_memory_type_ = UINT32_MAX; + gpu_timestamp_readback_memory_size_ = 0; + gpu_timestamp_readback_mapping_ = nullptr; + gpu_timestamp_resources_available_ = false; + for (GpuTimestampFrame& frame : gpu_timestamp_frames_) { + frame = {}; + } + + const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Properties& properties = vulkan_device->properties(); + if (!properties.timestampComputeAndGraphics || properties.timestampPeriod <= 0.0f) { + REXGPU_WARN("VulkanCommandProcessor: GPU timestamps are unavailable"); + return false; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + VkDevice device = vulkan_device->device(); + + VkQueryPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + pool_info.queryType = VK_QUERY_TYPE_TIMESTAMP; + pool_info.queryCount = kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight; + if (dfn.vkCreateQueryPool(device, &pool_info, nullptr, &gpu_timestamp_query_pool_) != + VK_SUCCESS) { + REXGPU_WARN("VulkanCommandProcessor: Failed to create GPU timestamp query pool"); + return false; + } + + if (!ui::vulkan::util::CreateDedicatedAllocationBuffer( + vulkan_device, + sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight, + VK_BUFFER_USAGE_TRANSFER_DST_BIT, ui::vulkan::util::MemoryPurpose::kReadback, + gpu_timestamp_readback_buffer_, gpu_timestamp_readback_memory_, + &gpu_timestamp_readback_memory_type_, &gpu_timestamp_readback_memory_size_)) { + REXGPU_WARN("VulkanCommandProcessor: Failed to create GPU timestamp readback buffer"); + ShutdownGpuTimestampResources(); + return false; + } + + if (dfn.vkMapMemory(device, gpu_timestamp_readback_memory_, 0, VK_WHOLE_SIZE, 0, + reinterpret_cast(&gpu_timestamp_readback_mapping_)) != VK_SUCCESS) { + REXGPU_WARN("VulkanCommandProcessor: Failed to map GPU timestamp readback buffer"); + ShutdownGpuTimestampResources(); + return false; + } + + gpu_timestamp_resources_available_ = true; + return true; +} + +void VulkanCommandProcessor::ShutdownGpuTimestampResources() { + const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + VkDevice device = vulkan_device->device(); + + if (gpu_timestamp_readback_mapping_ && + gpu_timestamp_readback_memory_ != VK_NULL_HANDLE) { + dfn.vkUnmapMemory(device, gpu_timestamp_readback_memory_); + } + gpu_timestamp_readback_mapping_ = nullptr; + ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyBuffer, device, + gpu_timestamp_readback_buffer_); + ui::vulkan::util::DestroyAndNullHandle(dfn.vkFreeMemory, device, + gpu_timestamp_readback_memory_); + ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyQueryPool, device, + gpu_timestamp_query_pool_); + gpu_timestamp_readback_memory_type_ = UINT32_MAX; + gpu_timestamp_readback_memory_size_ = 0; + gpu_timestamp_resources_available_ = false; + for (GpuTimestampFrame& frame : gpu_timestamp_frames_) { + frame = {}; + } +} + +void VulkanCommandProcessor::BeginGpuTimestampFrame() { + if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE) { + return; + } + + GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; + frame.frame = frame_current_; + frame.submission = 0; + frame.query_base = + uint32_t((frame_current_ % kMaxFramesInFlight) * kMaxGpuTimestampQueriesPerFrame); + frame.query_count = 0; + frame.frame_start_query = UINT32_MAX; + frame.frame_end_query = UINT32_MAX; + frame.pending = false; + frame.active_bucket_valid = false; + frame.active_bucket = rex::perf::DrawBucket::kMainColorDepth; + frame.active_bucket_index = UINT32_MAX; + frame.buckets.clear(); + frame.bucket_start_queries.clear(); + frame.bucket_end_queries.clear(); + + if (!rex::perf::IsCaptureRecording()) { + return; + } + + deferred_command_buffer_.CmdVkResetQueryPool(gpu_timestamp_query_pool_, frame.query_base, + kMaxGpuTimestampQueriesPerFrame); + frame.frame_start_query = frame.query_count++; + deferred_command_buffer_.CmdVkWriteTimestamp(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + gpu_timestamp_query_pool_, + frame.query_base + frame.frame_start_query); +} + +void VulkanCommandProcessor::EndGpuTimestampFrame() { + if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE) { + return; + } + + GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; + if (frame.frame != frame_current_ || !frame.query_count || frame.pending) { + return; + } + + if (frame.frame_start_query != UINT32_MAX) { + if (frame.active_bucket_valid && + frame.query_count + 1 < kMaxGpuTimestampQueriesPerFrame) { + frame.bucket_end_queries[frame.active_bucket_index] = frame.query_count++; + deferred_command_buffer_.CmdVkWriteTimestamp( + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, gpu_timestamp_query_pool_, + frame.query_base + frame.bucket_end_queries[frame.active_bucket_index]); + frame.active_bucket_valid = false; + frame.active_bucket_index = UINT32_MAX; + } + if (frame.query_count < kMaxGpuTimestampQueriesPerFrame) { + frame.frame_end_query = frame.query_count++; + deferred_command_buffer_.CmdVkWriteTimestamp(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + gpu_timestamp_query_pool_, + frame.query_base + frame.frame_end_query); + } + } + frame.pending = true; +} + +void VulkanCommandProcessor::ResolveGpuTimestampFrame(VkCommandBuffer command_buffer) { + if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE || + gpu_timestamp_readback_buffer_ == VK_NULL_HANDLE || command_buffer == VK_NULL_HANDLE) { + return; + } + + GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; + if (frame.frame != frame_current_ || !frame.pending || !frame.query_count) { + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = GetVulkanDevice()->functions(); + dfn.vkCmdCopyQueryPoolResults(command_buffer, gpu_timestamp_query_pool_, frame.query_base, + frame.query_count, gpu_timestamp_readback_buffer_, + sizeof(uint64_t) * frame.query_base, sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT); +} + +void VulkanCommandProcessor::ProcessGpuTimestampResults() { + if (!gpu_timestamp_resources_available_ || gpu_timestamp_readback_mapping_ == nullptr) { + return; + } + + const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Properties& properties = vulkan_device->properties(); + if (properties.timestampPeriod <= 0.0f) { + return; + } + + bool invalidate_needed = false; + for (const GpuTimestampFrame& frame : gpu_timestamp_frames_) { + if (frame.pending && frame.submission != 0 && frame.submission <= submission_completed_) { + invalidate_needed = true; + break; + } + } + if (!invalidate_needed) { + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + if (!(vulkan_device->memory_types().host_coherent & + (uint32_t(1) << gpu_timestamp_readback_memory_type_))) { + VkMappedMemoryRange memory_range = {}; + memory_range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + memory_range.memory = gpu_timestamp_readback_memory_; + memory_range.offset = 0; + memory_range.size = std::min( + rex::round_up(VkDeviceSize(sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame * + kMaxFramesInFlight), + properties.nonCoherentAtomSize), + gpu_timestamp_readback_memory_size_); + dfn.vkInvalidateMappedMemoryRanges(device, 1, &memory_range); + } + + const double timestamp_period_ns = double(properties.timestampPeriod); + for (GpuTimestampFrame& frame : gpu_timestamp_frames_) { + if (!frame.pending || frame.submission == 0 || frame.submission > submission_completed_) { + continue; + } + const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + frame.query_base; + auto timestamp_delta_us = [&](uint32_t start_query, uint32_t end_query) -> int64_t { + if (start_query >= frame.query_count || end_query >= frame.query_count) { + return 0; + } + uint64_t start = timestamps[start_query]; + uint64_t end = timestamps[end_query]; + if (end <= start) { + return 0; + } + return int64_t(double(end - start) * timestamp_period_ns / 1000.0); + }; + + if (frame.frame_start_query != UINT32_MAX && frame.frame_end_query != UINT32_MAX) { + int64_t elapsed_us = timestamp_delta_us(frame.frame_start_query, frame.frame_end_query); + if (elapsed_us > 0) { + rex::perf::IncrementCounter(rex::perf::CounterId::kGpuCommandProcessorFrameUs, + elapsed_us); + rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedFrames); + } + } + + for (uint32_t i = 0; i < frame.buckets.size() && i < frame.bucket_start_queries.size() && + i < frame.bucket_end_queries.size(); + ++i) { + uint32_t start_query = frame.bucket_start_queries[i]; + uint32_t end_query = frame.bucket_end_queries[i]; + if (end_query == UINT32_MAX) { + continue; + } + int64_t elapsed_us = timestamp_delta_us(start_query, end_query); + if (elapsed_us <= 0) { + continue; + } + rex::perf::CounterId counter_id; + switch (frame.buckets[i]) { + case rex::perf::DrawBucket::kMainColorDepth: + counter_id = rex::perf::CounterId::kGpuMainUs; + break; + case rex::perf::DrawBucket::kDepthOnly: + counter_id = rex::perf::CounterId::kGpuDepthUs; + break; + case rex::perf::DrawBucket::kCopyResolve: + counter_id = rex::perf::CounterId::kGpuCopyUs; + break; + case rex::perf::DrawBucket::kMemexport: + counter_id = rex::perf::CounterId::kGpuMemexportUs; + break; + case rex::perf::DrawBucket::kNoPixelShader: + counter_id = rex::perf::CounterId::kGpuNoPixelShaderUs; + break; + case rex::perf::DrawBucket::kCopyDump: + counter_id = rex::perf::CounterId::kGpuCopyDumpUs; + break; + case rex::perf::DrawBucket::kCopyResolveShader: + counter_id = rex::perf::CounterId::kGpuCopyResolveUs; + break; + case rex::perf::DrawBucket::kCopyResolveFast32: + counter_id = rex::perf::CounterId::kGpuCopyResolveFast32Us; + break; + case rex::perf::DrawBucket::kCopyResolveFull32: + counter_id = rex::perf::CounterId::kGpuCopyResolveFull32Us; + break; + case rex::perf::DrawBucket::kResolveDownscale: + counter_id = rex::perf::CounterId::kGpuResolveDownscaleUs; + break; + default: + continue; + } + rex::perf::IncrementCounter(counter_id, elapsed_us); + rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedDraws); + } + + frame.pending = false; + frame.active_bucket_valid = false; + frame.active_bucket_index = UINT32_MAX; + frame.buckets.clear(); + frame.bucket_start_queries.clear(); + frame.bucket_end_queries.clear(); + } +} + +uint32_t VulkanCommandProcessor::BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket) { + if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE || + !rex::perf::IsCaptureRecording() || !frame_open_) { + return UINT32_MAX; + } + + GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; + if (frame.frame != frame_current_) { + return UINT32_MAX; + } + if (frame.active_bucket_valid && frame.active_bucket == bucket) { + return UINT32_MAX; + } + if (frame.query_count + (frame.active_bucket_valid ? 2 : 1) > + kMaxGpuTimestampQueriesPerFrame) { + return UINT32_MAX; + } + + if (frame.active_bucket_valid) { + frame.bucket_end_queries[frame.active_bucket_index] = frame.query_count++; + deferred_command_buffer_.CmdVkWriteTimestamp( + VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, gpu_timestamp_query_pool_, + frame.query_base + frame.bucket_end_queries[frame.active_bucket_index]); + frame.active_bucket_valid = false; + frame.active_bucket_index = UINT32_MAX; + } + + uint32_t start_query = frame.query_count++; + frame.active_bucket_valid = true; + frame.active_bucket = bucket; + frame.active_bucket_index = uint32_t(frame.buckets.size()); + frame.buckets.push_back(bucket); + frame.bucket_start_queries.push_back(start_query); + frame.bucket_end_queries.push_back(UINT32_MAX); + deferred_command_buffer_.CmdVkWriteTimestamp(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + gpu_timestamp_query_pool_, + frame.query_base + start_query); + return start_query; +} + +void VulkanCommandProcessor::EndGpuTimestampedDraw(uint32_t start_query) { + (void)start_query; +} + bool VulkanCommandProcessor::InitializeOcclusionQueryResources() { active_occlusion_query_ = {}; occlusion_query_cursor_ = 0; @@ -5044,6 +5433,9 @@ void VulkanCommandProcessor::InitializeTrace() { } void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_submission) { + rex::perf::ScopedCounterTimer check_fence_timer( + rex::perf::CounterId::kCpuD3D12CheckFenceUs); + // Only report once, no need to retry a wait that won't succeed anyway. if (device_lost_) { return; @@ -5070,9 +5462,14 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su // defined by vkQueueSubmit additionally include in the first // synchronization scope all commands that occur earlier in submission // order." - VkResult wait_result = - dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_), - submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX); + VkResult wait_result; + { + rex::perf::ScopedCounterTimer fence_wait_timer( + rex::perf::CounterId::kCpuD3D12FenceWaitUs); + wait_result = + dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_), + submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX); + } if (wait_result == VK_SUCCESS) { fences_awaited += await_submission - submission_completed_; } else { @@ -5106,6 +5503,10 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su // Not updated - no need to reclaim or download things. return; } + + rex::perf::ScopedCounterTimer fence_reclaim_timer( + rex::perf::CounterId::kCpuD3D12FenceReclaimUs); + // Reclaim fences. fences_free_.reserve(fences_free_.size() + fences_awaited); auto submissions_in_flight_fences_awaited_end = submissions_in_flight_fences_.cbegin(); @@ -5144,6 +5545,8 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su texture_cache_->CompletedSubmissionUpdated(submission_completed_); + ProcessGpuTimestampResults(); + // Destroy objects scheduled for destruction. while (!destroy_framebuffers_.empty()) { const auto& destroy_pair = destroy_framebuffers_.front(); @@ -5195,6 +5598,8 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { #if XE_GPU_FINE_GRAINED_DRAW_SCOPES SCOPE_profile_cpu_f("gpu"); #endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES + rex::perf::ScopedCounterTimer begin_submission_timer( + rex::perf::CounterId::kCpuD3D12BeginSubmissionUs); if (device_lost_) { return false; @@ -5211,12 +5616,19 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { // is still available, and whether the await was successful. uint64_t await_submission = is_opening_frame ? closed_frame_submissions_[frame_current_ % kMaxFramesInFlight] : 0; - CheckSubmissionFenceAndDeviceLoss(await_submission); + { + rex::perf::ScopedCounterTimer begin_submission_fence_timer( + rex::perf::CounterId::kCpuD3D12BeginSubmissionFenceUs); + CheckSubmissionFenceAndDeviceLoss(await_submission); + } if (device_lost_ || submission_completed_ < await_submission) { return false; } if (is_opening_frame) { + rex::perf::ScopedCounterTimer begin_submission_frame_open_timer( + rex::perf::CounterId::kCpuD3D12BeginSubmissionFrameOpenUs); + // Update the completed frame index, also obtaining the actual completed // frame number (since the CPU may be actually less than 3 frames behind) // before reclaiming resources tracked with the frame number. @@ -5230,6 +5642,9 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { } if (!submission_open_) { + rex::perf::ScopedCounterTimer begin_submission_open_timer( + rex::perf::CounterId::kCpuD3D12BeginSubmissionOpenUs); + submission_open_ = true; // Start a new deferred command buffer - will submit it to the real one in @@ -5324,12 +5739,17 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { primitive_processor_->BeginFrame(); texture_cache_->BeginFrame(); + + BeginGpuTimestampFrame(); } return true; } bool VulkanCommandProcessor::EndSubmission(bool is_swap) { + rex::perf::ScopedCounterTimer end_submission_timer( + rex::perf::CounterId::kCpuD3D12EndSubmissionUs); + const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); const VkDevice device = vulkan_device->device(); @@ -5394,6 +5814,8 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { bool is_closing_frame = is_swap && frame_open_; if (is_closing_frame) { + rex::perf::ScopedCounterTimer end_frame_timer(rex::perf::CounterId::kCpuD3D12EndFrameUs); + texture_cache_->EndFrame(); primitive_processor_->EndFrame(); @@ -5410,15 +5832,20 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { EndRenderPass(); - pipeline_cache_->EndSubmission(); + { + rex::perf::ScopedCounterTimer pipeline_end_timer( + rex::perf::CounterId::kCpuD3D12PipelineEndUs); - render_target_cache_->EndSubmission(); + pipeline_cache_->EndSubmission(); - primitive_processor_->EndSubmission(); + render_target_cache_->EndSubmission(); - shared_memory_->EndSubmission(); + primitive_processor_->EndSubmission(); - uniform_buffer_pool_->FlushWrites(); + shared_memory_->EndSubmission(); + + uniform_buffer_pool_->FlushWrites(); + } // Submit sparse binds earlier, before executing the deferred command // buffer, to reduce latency. @@ -5468,7 +5895,14 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { sparse_memory_binds_.clear(); } - SubmitBarriers(true); + { + rex::perf::ScopedCounterTimer submit_barriers_timer( + rex::perf::CounterId::kCpuD3D12SubmitBarriersUs); + SubmitBarriers(true); + } + if (is_closing_frame) { + EndGpuTimestampFrame(); + } assert_false(command_buffers_writable_.empty()); CommandBuffer command_buffer = command_buffers_writable_.back(); @@ -5481,14 +5915,26 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { command_buffer_begin_info.pNext = nullptr; command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; command_buffer_begin_info.pInheritanceInfo = nullptr; - if (dfn.vkBeginCommandBuffer(command_buffer.buffer, &command_buffer_begin_info) != VK_SUCCESS) { - REXGPU_ERROR("Failed to begin a Vulkan command buffer"); - return false; + { + rex::perf::ScopedCounterTimer deferred_execute_timer( + rex::perf::CounterId::kCpuD3D12DeferredExecuteUs); + if (dfn.vkBeginCommandBuffer(command_buffer.buffer, &command_buffer_begin_info) != + VK_SUCCESS) { + REXGPU_ERROR("Failed to begin a Vulkan command buffer"); + return false; + } + deferred_command_buffer_.Execute(command_buffer.buffer); + if (is_closing_frame) { + ResolveGpuTimestampFrame(command_buffer.buffer); + } } - deferred_command_buffer_.Execute(command_buffer.buffer); - if (dfn.vkEndCommandBuffer(command_buffer.buffer) != VK_SUCCESS) { - REXGPU_ERROR("Failed to end a Vulkan command buffer"); - return false; + { + rex::perf::ScopedCounterTimer command_list_close_timer( + rex::perf::CounterId::kCpuD3D12CommandListCloseUs); + if (dfn.vkEndCommandBuffer(command_buffer.buffer) != VK_SUCCESS) { + REXGPU_ERROR("Failed to end a Vulkan command buffer"); + return false; + } } VkSubmitInfo submit_info; @@ -5515,6 +5961,8 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { } VkResult submit_result; { + rex::perf::ScopedCounterTimer execute_command_lists_timer( + rex::perf::CounterId::kCpuD3D12ExecuteCommandListsUs); ui::vulkan::VulkanDevice::Queue::Acquisition queue_acquisition = vulkan_device->AcquireQueue(vulkan_device->queue_family_graphics_compute(), 0); submit_result = dfn.vkQueueSubmit(queue_acquisition.queue(), 1, &submit_info, fence); @@ -5530,6 +5978,13 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { return false; } uint64_t submission_current = GetCurrentSubmission(); + if (is_closing_frame) { + GpuTimestampFrame& gpu_timestamp_frame = + gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; + if (gpu_timestamp_frame.frame == frame_current_ && gpu_timestamp_frame.pending) { + gpu_timestamp_frame.submission = submission_current; + } + } current_submission_wait_stage_masks_.clear(); for (VkSemaphore semaphore : current_submission_wait_semaphores_) { submissions_in_flight_semaphores_.emplace_back(submission_current, semaphore); diff --git a/src/graphics/vulkan/deferred_command_buffer.cpp b/src/graphics/vulkan/deferred_command_buffer.cpp index b49e033..51215f2 100644 --- a/src/graphics/vulkan/deferred_command_buffer.cpp +++ b/src/graphics/vulkan/deferred_command_buffer.cpp @@ -314,6 +314,11 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { rex::align(sizeof(ArgsVkSetViewport), alignof(VkViewport)))); } break; + case Command::kVkWriteTimestamp: { + auto& args = *reinterpret_cast(stream); + dfn.vkCmdWriteTimestamp(command_buffer, args.pipeline_stage, args.query_pool, args.query); + } break; + default: assert_unhandled_case(header.command); break; diff --git a/src/graphics/vulkan/pipeline_cache.cpp b/src/graphics/vulkan/pipeline_cache.cpp index 91ac08f..c7086cc 100644 --- a/src/graphics/vulkan/pipeline_cache.cpp +++ b/src/graphics/vulkan/pipeline_cache.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -3213,7 +3214,14 @@ bool VulkanPipelineCache::EnsurePipelineCreated(const PipelineCreationArguments& assert_unhandled_case(description.primitive_topology); return false; } +#if REX_PLATFORM_MAC + // Metal always has primitive restart enabled in the paths MoltenVK exposes. + // Requesting it disabled produces VK_ERROR_FEATURE_NOT_PRESENT pipeline + // creation warnings and repeated failed/retried pipeline setup on macOS. + input_assembly_state.primitiveRestartEnable = VK_TRUE; +#else input_assembly_state.primitiveRestartEnable = description.primitive_restart ? VK_TRUE : VK_FALSE; +#endif VkPipelineViewportStateCreateInfo viewport_state; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; diff --git a/src/graphics/vulkan/render_target_cache.cpp b/src/graphics/vulkan/render_target_cache.cpp index 05d1e5c..9103252 100644 --- a/src/graphics/vulkan/render_target_cache.cpp +++ b/src/graphics/vulkan/render_target_cache.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include REXCVAR_DEFINE_STRING(render_target_path_vulkan, "", "GPU/Vulkan", @@ -387,6 +389,12 @@ bool VulkanRenderTargetCache::Initialize(uint32_t shared_memory_binding_count) { msaa_2x_attachments_supported_ = false; msaa_2x_no_attachments_supported_ = false; } + REXGPU_INFO( + "VulkanRenderTargetCache: Using {} render target path, native 2x MSAA attachments {}, " + "native 2x MSAA no-attachments {}", + path_ == Path::kHostRenderTargets ? "host/FBO" : "fragment shader interlock/FSI", + msaa_2x_attachments_supported_ ? "available" : "unavailable", + msaa_2x_no_attachments_supported_ ? "available" : "unavailable"); bool integer_transfer_sample_1x_supported = (device_properties.framebufferColorSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0 && (device_properties.sampledImageIntegerSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0; @@ -1466,6 +1474,14 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, UseEdramBuffer(EdramBufferUsage::kComputeRead); command_processor_.BindExternalComputePipeline( resolve_copy_pipelines_[size_t(copy_shader)]); + rex::perf::DrawBucket resolve_timing_bucket = + rex::perf::DrawBucket::kCopyResolveShader; + if (copy_shader == draw_util::ResolveCopyShaderIndex::kFast32bpp1x2xMSAA) { + resolve_timing_bucket = rex::perf::DrawBucket::kCopyResolveFast32; + } else if (copy_shader == draw_util::ResolveCopyShaderIndex::kFull32bpp) { + resolve_timing_bucket = rex::perf::DrawBucket::kCopyResolveFull32; + } + command_processor_.BeginGpuTimestampedRegion(resolve_timing_bucket); VkDescriptorSet descriptor_sets[kResolveCopyDescriptorSetCount] = {}; descriptor_sets[kResolveCopyDescriptorSetEdram] = edram_storage_buffer_descriptor_set_; descriptor_sets[kResolveCopyDescriptorSetDest] = descriptor_set_dest; @@ -1487,6 +1503,44 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, sizeof(copy_shader_constants), ©_shader_constants); } command_processor_.SubmitBarriers(true); + PERF_counter_inc(kRtResolveCopyDispatches); + PERF_counter_add(kRtResolveCopyGroups, + uint64_t(copy_group_count_x) * copy_group_count_y); +#ifdef REXGLUE_ENABLE_PERF_COUNTERS + static constexpr rex::perf::CounterId kResolveCopyDispatchCounters[] = { + rex::perf::CounterId::kRtResolveCopyFast32bpp1x2xMsaaDispatches, + rex::perf::CounterId::kRtResolveCopyFast32bpp4xMsaaDispatches, + rex::perf::CounterId::kRtResolveCopyFast64bpp1x2xMsaaDispatches, + rex::perf::CounterId::kRtResolveCopyFast64bpp4xMsaaDispatches, + rex::perf::CounterId::kRtResolveCopyFull8bppDispatches, + rex::perf::CounterId::kRtResolveCopyFull16bppDispatches, + rex::perf::CounterId::kRtResolveCopyFull32bppDispatches, + rex::perf::CounterId::kRtResolveCopyFull64bppDispatches, + rex::perf::CounterId::kRtResolveCopyFull128bppDispatches, + }; + static constexpr rex::perf::CounterId kResolveCopyGroupCounters[] = { + rex::perf::CounterId::kRtResolveCopyFast32bpp1x2xMsaaGroups, + rex::perf::CounterId::kRtResolveCopyFast32bpp4xMsaaGroups, + rex::perf::CounterId::kRtResolveCopyFast64bpp1x2xMsaaGroups, + rex::perf::CounterId::kRtResolveCopyFast64bpp4xMsaaGroups, + rex::perf::CounterId::kRtResolveCopyFull8bppGroups, + rex::perf::CounterId::kRtResolveCopyFull16bppGroups, + rex::perf::CounterId::kRtResolveCopyFull32bppGroups, + rex::perf::CounterId::kRtResolveCopyFull64bppGroups, + rex::perf::CounterId::kRtResolveCopyFull128bppGroups, + }; + static_assert(std::size(kResolveCopyDispatchCounters) == + size_t(draw_util::ResolveCopyShaderIndex::kCount)); + static_assert(std::size(kResolveCopyGroupCounters) == + size_t(draw_util::ResolveCopyShaderIndex::kCount)); + size_t copy_shader_index = size_t(copy_shader); + rex::perf::IncrementCounter(kResolveCopyDispatchCounters[copy_shader_index]); + rex::perf::IncrementCounter(kResolveCopyGroupCounters[copy_shader_index], + uint64_t(copy_group_count_x) * copy_group_count_y); +#endif + if (draw_resolution_scaled) { + PERF_counter_add(kRtResolveScaledBytes, copy_dest_use_length); + } command_buffer.CmdVkDispatch(copy_group_count_x, copy_group_count_y, 1); // Invalidate textures and mark the range as scaled if needed. @@ -4558,7 +4612,11 @@ VkPipeline const* VulkanRenderTargetCache::GetTransferPipelines(TransferPipeline input_assembly_state.pNext = nullptr; input_assembly_state.flags = 0; input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +#if REX_PLATFORM_MAC + input_assembly_state.primitiveRestartEnable = VK_TRUE; +#else input_assembly_state.primitiveRestartEnable = VK_FALSE; +#endif // Dynamic, to stay within maxViewportDimensions while preferring a // power-of-two factor for converting from pixel coordinates to NDC for exact @@ -6226,9 +6284,11 @@ bool VulkanRenderTargetCache::TryResolveCopyDirectly(const draw_util::ResolveInf bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dump_row_length_used, uint32_t dump_rows, uint32_t dump_pitch) { assert_true(GetPath() == Path::kHostRenderTargets); + PERF_counter_inc(kRtDumpCalls); GetResolveCopyRectanglesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch, dump_rectangles_); + PERF_counter_add(kRtDumpRectangles, dump_rectangles_.size()); if (dump_rectangles_.empty()) { return true; } @@ -6270,6 +6330,7 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum // Dump the render targets. DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kCopyDump); bool edram_buffer_bound = false; VkDescriptorSet last_source_descriptor_set = VK_NULL_HANDLE; DumpPitches last_pitches; @@ -6340,16 +6401,19 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum sizeof(last_offsets), &last_offsets); } command_processor_.SubmitBarriers(true); - command_buffer.CmdVkDispatch( + const uint32_t group_count_x = (draw_resolution_scale_x() * (xenos::kEdramTileWidthSamples >> uint32_t(rt_key.Is64bpp())) * dispatch.width_tiles + (kDumpSamplesPerGroupX - 1)) / - kDumpSamplesPerGroupX, + kDumpSamplesPerGroupX; + const uint32_t group_count_y = (draw_resolution_scale_y() * xenos::kEdramTileHeightSamples * dispatch.height_tiles + (kDumpSamplesPerGroupY - 1)) / - kDumpSamplesPerGroupY, - 1); + kDumpSamplesPerGroupY; + PERF_counter_inc(kRtDumpDispatches); + PERF_counter_add(kRtDumpGroups, uint64_t(group_count_x) * group_count_y); + command_buffer.CmdVkDispatch(group_count_x, group_count_y, 1); } MarkEdramBufferModified(); } diff --git a/src/graphics/vulkan/texture_cache.cpp b/src/graphics/vulkan/texture_cache.cpp index fc46bf8..371feab 100644 --- a/src/graphics/vulkan/texture_cache.cpp +++ b/src/graphics/vulkan/texture_cache.cpp @@ -29,11 +29,16 @@ #include #include #include +#include #include #include #include REXCVAR_DEFINE_BOOL(non_seamless_cube_map, false, "GPU", "Use non-seamless cube map sampling"); +REXCVAR_DEFINE_BOOL(vulkan_scaled_resolve_write_barrier_overlap_only, false, "GPU/Vulkan", + "Skip consecutive scaled resolve write barriers when destination ranges do " + "not overlap") + .lifecycle(rex::cvar::Lifecycle::kHotReload); namespace rex::graphics::vulkan { @@ -2293,13 +2298,27 @@ void VulkanTextureCache::UseScaledResolveBufferForWrite(uint64_t written_start_s std::min(written_length_scaled, scaled_resolve_buffer_size_ - written_start_scaled); if (scaled_resolve_last_usage_write_ && scaled_resolve_last_written_range_.second) { - VkPipelineStageFlags stage_mask; - VkAccessFlags access_mask; - GetScaledResolveUsageMasks(stage_mask, access_mask, true); - command_processor_.PushBufferMemoryBarrier( - scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first), - VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask, - access_mask, access_mask, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, false); + uint64_t previous_start = scaled_resolve_last_written_range_.first; + uint64_t previous_end = previous_start + scaled_resolve_last_written_range_.second; + uint64_t written_end = written_start_scaled + written_length_scaled; + bool ranges_overlap = previous_start < written_end && written_start_scaled < previous_end; + if (ranges_overlap) { + PERF_counter_inc(kRtScaledResolveWriteBarrierOverlaps); + } + if (ranges_overlap || !REXCVAR_GET(vulkan_scaled_resolve_write_barrier_overlap_only)) { + VkPipelineStageFlags stage_mask; + VkAccessFlags access_mask; + GetScaledResolveUsageMasks(stage_mask, access_mask, true); + command_processor_.PushBufferMemoryBarrier( + scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first), + VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask, + access_mask, access_mask, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, false); + PERF_counter_inc(kRtScaledResolveWriteBarriers); + PERF_counter_add(kRtScaledResolveWriteBarrierBytes, + scaled_resolve_last_written_range_.second); + } else { + PERF_counter_inc(kRtScaledResolveWriteBarrierSkipped); + } } else if (!scaled_resolve_last_usage_write_) { VkPipelineStageFlags src_stage_mask, dst_stage_mask; VkAccessFlags src_access_mask, dst_access_mask; diff --git a/src/input/input_system.cpp b/src/input/input_system.cpp index 426e022..15513a7 100644 --- a/src/input/input_system.cpp +++ b/src/input/input_system.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include namespace { @@ -50,6 +52,15 @@ X_STATUS InputSystem::Setup() { } void InputSystem::Shutdown() { + if (window_) { + rex::ui::UIEvent closing_event(window_); + for (auto& driver : drivers_) { + if (auto* window_listener = dynamic_cast(driver.get())) { + window_listener->OnClosing(closing_event); + } + } + window_ = nullptr; + } drivers_.clear(); } diff --git a/src/input/sdl/sdl_input_driver.cpp b/src/input/sdl/sdl_input_driver.cpp index 5c81807..2868d42 100644 --- a/src/input/sdl/sdl_input_driver.cpp +++ b/src/input/sdl/sdl_input_driver.cpp @@ -9,6 +9,7 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#include #include #include @@ -22,6 +23,8 @@ REXCVAR_DEFINE_STRING(hid_mappings_file, "gamecontrollerdb.txt", "Input", "Path to SDL gamecontroller mappings file"); +REXCVAR_DEFINE_UINT32(hid_sdl_rumble_duration_ms, 100, "Input", + "SDL rumble pulse duration for nonzero XInput vibration"); namespace rex::input::sdl { @@ -124,6 +127,7 @@ void SDLInputDriver::OnClosing(rex::ui::UIEvent&) { } for (size_t i = 0; i < controllers_.size(); i++) { if (controllers_.at(i).sdl) { + StopRumbleLocked(controllers_.at(i)); SDL_CloseGamepad(controllers_.at(i).sdl); controllers_.at(i) = {}; } @@ -220,16 +224,16 @@ X_RESULT SDLInputDriver::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibrat return X_ERROR_DEVICE_NOT_CONNECTED; } -#if SDL_VERSION_ATLEAST(2, 0, 9) - if (SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed, vibration->right_motor_speed, - 0)) { + const bool has_rumble = vibration->left_motor_speed || vibration->right_motor_speed; + const uint32_t duration_ms = + has_rumble ? std::clamp(REXCVAR_GET(hid_sdl_rumble_duration_ms), uint32_t(1), + uint32_t(1000)) + : 0; + if (!SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed, + vibration->right_motor_speed, duration_ms)) { return X_ERROR_FUNCTION_FAILED; - } else { - return X_ERROR_SUCCESS; } -#else return X_ERROR_SUCCESS; -#endif } X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags, @@ -437,6 +441,12 @@ void SDLInputDriver::ProcessEventLocked(const SDL_Event& event) { } } +void SDLInputDriver::StopRumbleLocked(ControllerState& state) { + if (state.sdl) { + SDL_RumbleGamepad(state.sdl, 0, 0, 0); + } +} + void SDLInputDriver::OnControllerDeviceAddedLocked(const SDL_Event& event) { // Open the controller. const auto controller = SDL_OpenGamepad(event.cdevice.which); @@ -493,6 +503,7 @@ void SDLInputDriver::OnControllerDeviceRemovedLocked(const SDL_Event& event) { // Find the disconnected gamecontroller and close it. auto idx = GetControllerIndexFromInstanceID(event.cdevice.which); if (idx) { + StopRumbleLocked(controllers_.at(*idx)); SDL_CloseGamepad(controllers_.at(*idx).sdl); controllers_.at(*idx) = {}; keystroke_states_.at(*idx) = {}; diff --git a/src/kernel/crt/heap.cpp b/src/kernel/crt/heap.cpp index 045fa8b..84c7c64 100644 --- a/src/kernel/crt/heap.cpp +++ b/src/kernel/crt/heap.cpp @@ -318,7 +318,8 @@ HeapDiagnostics ReXHeap::GetDiagnosticsLocked() const { diagnostics.capacity += d.capacity; diagnostics.allocated += d.allocated; diagnostics.peak_allocated += d.peak_allocated; - diagnostics.peak_request_size = std::max(diagnostics.peak_request_size, d.peak_request_size); + diagnostics.peak_request_size = + std::max(diagnostics.peak_request_size, static_cast(d.peak_request_size)); diagnostics.oom_count += d.oom_count; } return diagnostics; diff --git a/src/kernel/xam/xam_net.cpp b/src/kernel/xam/xam_net.cpp index 845b15c..01cbfc2 100644 --- a/src/kernel/xam/xam_net.cpp +++ b/src/kernel/xam/xam_net.cpp @@ -33,10 +33,11 @@ // NOTE: must be included last as it expects windows.h to already be included. #define _WINSOCK_DEPRECATED_NO_WARNINGS // inet_addr #include // NOLINT(build/include_order) -#elif REX_PLATFORM_LINUX +#elif REX_PLATFORM_LINUX || REX_PLATFORM_MAC #include #include #include +#include #include #endif diff --git a/src/system/kernel_state.cpp b/src/system/kernel_state.cpp index 5dc922d..465e4b8 100644 --- a/src/system/kernel_state.cpp +++ b/src/system/kernel_state.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -841,6 +842,26 @@ void KernelState::TerminateTitle() { REXSYS_DEBUG("KernelState::TerminateTitle"); auto global_lock = global_critical_region_.Acquire(); +#if REX_PLATFORM_MAC + // pthread_cancel is asynchronous on macOS in the host thread wrapper, but + // suspending a guest thread first can strand it in the suspend wait during + // title teardown. Terminate directly on macOS and let the thread wrapper + // signal waiters before requesting cancellation. + std::vector> threads_to_terminate; + for (auto it = threads_by_id_.begin(); it != threads_by_id_.end(); ++it) { + if (!XThread::IsInThread(it->second) && it->second->is_guest_thread() && + it->second->is_running()) { + threads_to_terminate.push_back(retain_object(it->second)); + } + } + + global_lock.unlock(); + for (auto& thread : threads_to_terminate) { + thread->Terminate(0); + } + threads_to_terminate.clear(); + global_lock.lock(); +#else // Suspend all running guest threads so they stop touching shared state. std::vector suspended_threads; for (auto it = threads_by_id_.begin(); it != threads_by_id_.end(); ++it) { @@ -857,6 +878,7 @@ void KernelState::TerminateTitle() { thread->Terminate(0); } global_lock.lock(); +#endif // Remove all guest threads from the map. for (auto it = threads_by_id_.begin(); it != threads_by_id_.end();) { diff --git a/src/system/runtime.cpp b/src/system/runtime.cpp index 5d66ca3..3d7005f 100644 --- a/src/system/runtime.cpp +++ b/src/system/runtime.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include // PPCFuncMapping #include // SEH exception support #include @@ -244,10 +245,22 @@ void Runtime::Shutdown() { input_system_.reset(); } kernel_state_.reset(); +#if REX_PLATFORM_MAC + // macOS tears down guest threads asynchronously. During process exit, a late + // guest callback can still hold references into the dispatcher or mapped + // guest memory after the owning systems have stopped. Keep those mappings + // alive until the process exits; the OS reclaims them immediately afterward. + function_dispatcher_.release(); +#else function_dispatcher_.reset(); +#endif export_resolver_.reset(); file_system_.reset(); +#if REX_PLATFORM_MAC + memory_.release(); +#else memory_.reset(); +#endif rex::perf::Profiler::Shutdown(); setup_complete_ = false; diff --git a/src/system/xmemory.cpp b/src/system/xmemory.cpp index f834673..6dbec66 100644 --- a/src/system/xmemory.cpp +++ b/src/system/xmemory.cpp @@ -29,6 +29,10 @@ // TODO(benvanik): move xbox.h out #include +#if REX_PLATFORM_MAC +#include +#endif + REXCVAR_DEFINE_BOOL(protect_zero, false, "Memory", "Protect the zero page from reads and writes") .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); @@ -77,6 +81,22 @@ uint32_t get_page_count(uint32_t value, uint32_t page_size, uint32_t page_size_s static memory::Memory* active_memory_ = nullptr; +namespace { + +constexpr size_t kGuestBackingLength = 0x120001000ull; + +bool ShouldSkipHostCommit(const BaseHeap& heap) { +#if REX_PLATFORM_MAC + return heap.heap_type() == HeapType::kGuestPhysical && heap.heap_base() == 0x00000000 && + rex::memory::page_size() > 0x1000; +#else + (void)heap; + return false; +#endif +} + +} // namespace + void CrashDump() { static std::atomic in_crash_dump(0); if (in_crash_dump.fetch_add(1)) { @@ -135,7 +155,8 @@ bool Memory::Initialize() { mapping_ = rex::memory::CreateFileMappingHandle(file_name_, // entire 4gb space + 512mb physical: - 0x11FFFFFFF, rex::memory::PageAccess::kReadWrite, false); + kGuestBackingLength, + rex::memory::PageAccess::kReadWrite, false); if (mapping_ == rex::memory::kFileMappingHandleInvalid) { REXSYS_ERROR("Unable to reserve the 4gb guest address space."); assert_always(); @@ -148,10 +169,17 @@ bool Memory::Initialize() { for (size_t n = 32; n < 64; n++) { auto mapping_base = reinterpret_cast(1ull << n); if (!MapViews(mapping_base)) { - mapping_base_ = mapping_base; + mapping_base_ = views_.all_views[0]; break; } } +#if REX_PLATFORM_MAC + if (!mapping_base_) { + if (!MapViews(nullptr)) { + mapping_base_ = views_.all_views[0]; + } + } +#endif if (!mapping_base_) { REXSYS_ERROR("Unable to find a continuous block in the 64bit address space."); assert_always(); @@ -209,6 +237,12 @@ bool Memory::Initialize() { rex::memory::AllocFixed(heaps_.physical.TranslateRelative(0), heaps_.physical.heap_size(), rex::memory::AllocationType::kCommit, rex::memory::PageAccess::kReadWrite); +#if REX_PLATFORM_MAC + // The 0x7F000000-0x7FFFFFFF view aliases the start of physical memory. Touch + // it explicitly on macOS so raw generated loads through this alias don't SIGBUS + // before any guest write has materialized the shared-memory pages. + std::memset(virtual_membase_ + 0x7F000000, 0, 0x01000000); +#endif // Install MMIO handler for physical address translation and MMIO ranges mmio_handler_ = runtime::MMIOHandler::Install( @@ -300,8 +334,34 @@ static const struct { 0x0000000100000000ull, }, }; + +namespace { + +constexpr size_t kGuestMappingLength = 0x120000000ull; + +} // namespace + int Memory::MapViews(uint8_t* mapping_base) { assert_true(rex::countof(map_info) == rex::countof(views_.all_views)); + +#if REX_PLATFORM_MAC + // macOS doesn't provide MAP_FIXED_NOREPLACE. Direct MAP_FIXED probing can + // overwrite unrelated host mappings, so first reserve the entire range as a + // hint and only proceed with a fixed base we own. + void* reservation = + mmap(mapping_base, kGuestMappingLength, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (reservation == MAP_FAILED) { + return 1; + } + if (mapping_base && reservation != mapping_base) { + if (reservation != MAP_FAILED) { + munmap(reservation, kGuestMappingLength); + } + return 1; + } + mapping_base = reinterpret_cast(reservation); +#endif + // 0xE0000000 4 KB offset is emulated via host_address_offset and on the CPU // side if system allocation granularity is bigger than 4 KB. uint64_t granularity_mask = ~uint64_t(system_allocation_granularity_ - 1); @@ -312,7 +372,12 @@ int Memory::MapViews(uint8_t* mapping_base) { rex::memory::PageAccess::kReadWrite, map_info[n].target_address & granularity_mask)); if (!views_.all_views[n]) { // Failed, so bail and try again. +#if REX_PLATFORM_MAC + munmap(mapping_base, kGuestMappingLength); + std::fill(std::begin(views_.all_views), std::end(views_.all_views), nullptr); +#else UnmapViews(); +#endif return 1; } } @@ -1111,7 +1176,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, uint32_t alignme // Allocate from host. if (allocation_type == memory::kMemoryAllocationReserve) { // Reserve is not needed, as we are mapped already. - } else { + } else if (!ShouldSkipHostCommit(*this)) { auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit) ? rex::memory::AllocationType::kCommit : rex::memory::AllocationType::kReserve; @@ -1265,7 +1330,7 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, uint32_t // Allocate from host. if (allocation_type == memory::kMemoryAllocationReserve) { // Reserve is not needed, as we are mapped already. - } else { + } else if (!ShouldSkipHostCommit(*this)) { auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit) ? rex::memory::AllocationType::kCommit : rex::memory::AllocationType::kReserve; diff --git a/src/system/xsocket.cpp b/src/system/xsocket.cpp index 0b44e06..67753bf 100644 --- a/src/system/xsocket.cpp +++ b/src/system/xsocket.cpp @@ -29,6 +29,14 @@ #include #include #include +#if REX_PLATFORM_MAC +#ifdef IPPROTO_TCP +#undef IPPROTO_TCP +#endif +#ifdef IPPROTO_UDP +#undef IPPROTO_UDP +#endif +#endif #endif namespace rex::system { @@ -247,4 +255,4 @@ bool XSocket::QueuePacket(uint32_t src_ip, uint16_t src_port, const uint8_t* buf return true; } -} // namespace rex::system \ No newline at end of file +} // namespace rex::system diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 550d966..cc6f113 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -63,6 +63,12 @@ if(WIN32) window_win.cpp windowed_app_context_win.cpp ) +elseif(APPLE) + set(REXUI_PLATFORM_SOURCES + surface_sdl.cpp + window_sdl.cpp + windowed_app_context_sdl.cpp + ) else() set(REXUI_PLATFORM_SOURCES surface_gnulinux.cpp @@ -138,6 +144,8 @@ if(WIN32) dwmapi Shcore ) +elseif(APPLE) + target_link_libraries(rexui PUBLIC SDL3::SDL3) else() find_package(PkgConfig REQUIRED) pkg_check_modules(GTK3 REQUIRED gtk+-3.0) diff --git a/src/ui/overlay/simple_settings_overlay.cpp b/src/ui/overlay/simple_settings_overlay.cpp index 4f0661f..bbe16f6 100644 --- a/src/ui/overlay/simple_settings_overlay.cpp +++ b/src/ui/overlay/simple_settings_overlay.cpp @@ -364,8 +364,15 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) { ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.08f, 0.07f, 1.00f)); if (ImGui::Button("Close Game", ImVec2(170.0f, 40.0f))) { if (close_game_) { + Hide(); close_game_(); } + ImGui::PopStyleColor(3); + ImGui::End(); + ImGui::PopStyleColor(17); + ImGui::PopStyleVar(7); + ImGui::PopFont(); + return; } ImGui::PopStyleColor(3); ImGui::Spacing(); diff --git a/src/ui/presenter.cpp b/src/ui/presenter.cpp index 39dea9d..f760f81 100644 --- a/src/ui/presenter.cpp +++ b/src/ui/presenter.cpp @@ -425,8 +425,10 @@ void Presenter::OnSurfaceResizeFromUIThread() { bool request_repaint; UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true); - // Request to repaint as soon as possible in the UI thread if needed. - if (request_repaint) { + // Request to repaint as soon as possible in the UI thread if needed. Active + // UI drawers need a repaint after every resize even if the connection reports + // unchanged, because UI-only presentation has no guest frame to kick it. + if (request_repaint || !ui_drawers_.empty()) { RequestPaintOrConnectionRecoveryViaWindow(true); } } @@ -1453,13 +1455,13 @@ bool Presenter::RequestPaintOrConnectionRecoveryViaWindow(bool force_ui_thread_p // theoretically. For safety, check whether the window exists unconditionally. assert_not_null(window_); assert_not_null(surface_); + if (force_ui_thread_paint_tick) { + ForceUIThreadPaintTick(); + } if (ui_thread_paint_requested_.exchange(true, std::memory_order_relaxed)) { // Invalidation pending already, no need to do it twice. return false; } - if (force_ui_thread_paint_tick) { - ForceUIThreadPaintTick(); - } window_->RequestPaint(); return true; } diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index 56c7634..57fcec7 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -48,6 +48,7 @@ #include #include #include +#include namespace rex { @@ -98,9 +99,24 @@ bool ReXApp::OnInitialize() { return true; } +#if REX_PLATFORM_MAC + // Let the platform event loop run once before runtime setup. SDL creates the + // Cocoa window synchronously, but the app may not visibly activate if guest + // initialization starts before the first event-loop pass. + app_context().CallInUIThreadDeferred([this, paths = std::move(*paths)]() mutable { + if (shutting_down_.load(std::memory_order_acquire)) + return; + if (!ConstructRuntime(paths)) { + app_context().QuitFromUIThread(); + return; + } + LaunchModule(); + }); +#else if (!ConstructRuntime(*paths)) return false; LaunchModule(); +#endif return true; } @@ -432,6 +448,9 @@ void ReXApp::LaunchModule() { app_context().QuitFromUIThread(); return; } +#if REX_PLATFORM_MAC + main_thread_ = main_thread; +#endif auto* graphics_system = static_cast(runtime_->graphics_system()); @@ -478,6 +497,11 @@ void ReXApp::OnClosing(ui::UIEvent& e) { (void)e; REXLOG_INFO("Window closing, shutting down..."); shutting_down_.store(true, std::memory_order_release); +#if REX_PLATFORM_MAC + if (main_thread_ && main_thread_->is_running()) { + main_thread_->Terminate(0); + } +#endif if (runtime_ && runtime_->kernel_state()) { runtime_->kernel_state()->TerminateTitle(); } @@ -488,6 +512,13 @@ void ReXApp::OnDestroy() { // Notify subclass before cleanup OnShutdown(); +#if REX_PLATFORM_MAC + shutting_down_.store(true, std::memory_order_release); + if (main_thread_ && main_thread_->is_running()) { + main_thread_->Terminate(0); + } +#endif + // Unregister overlay keybinds before destroying dialogs rex::ui::UnregisterBind("bind_debug_overlay"); rex::ui::UnregisterBind("bind_console"); @@ -516,6 +547,15 @@ void ReXApp::OnDestroy() { if (module_thread_.joinable()) { module_thread_.join(); } +#if REX_PLATFORM_MAC + main_thread_ = nullptr; +#endif + // Input drivers may still be registered as window listeners. Detach them + // before destroying the SDL window so their backend state is released in + // listener order. + if (runtime_ && runtime_->input_system()) { + static_cast(runtime_->input_system())->Shutdown(); + } if (window_) { window_->RemoveInputListener(this); window_->RemoveListener(this); diff --git a/src/ui/surface_sdl.cpp b/src/ui/surface_sdl.cpp new file mode 100644 index 0000000..744e075 --- /dev/null +++ b/src/ui/surface_sdl.cpp @@ -0,0 +1,21 @@ +#include + +namespace rex { +namespace ui { + +bool SDLMetalViewSurface::GetSizeImpl(uint32_t& width_out, uint32_t& height_out) const { + int width = 0; + int height = 0; + if (!window_ || !SDL_GetWindowSizeInPixels(window_, &width, &height) || width <= 0 || + height <= 0) { + width_out = 0; + height_out = 0; + return false; + } + width_out = uint32_t(width); + height_out = uint32_t(height); + return true; +} + +} // namespace ui +} // namespace rex diff --git a/src/ui/vulkan/vulkan_device.cpp b/src/ui/vulkan/vulkan_device.cpp index c6f48fd..fba470a 100644 --- a/src/ui/vulkan/vulkan_device.cpp +++ b/src/ui/vulkan/vulkan_device.cpp @@ -29,7 +29,13 @@ REXCVAR_DEFINE_BOOL(vulkan_require_vertex_pipeline_stores_and_atomics, true, "UI "Deprecated and ignored for parity; vertexPipelineStoresAndAtomics is always " "required for Vulkan GPU emulation") .lifecycle(rex::cvar::Lifecycle::kInitOnly); -REXCVAR_DEFINE_BOOL(vulkan_require_geometry_shader, true, "UI/Vulkan", +REXCVAR_DEFINE_BOOL(vulkan_require_geometry_shader, +#if REX_PLATFORM_MAC + false, +#else + true, +#endif + "UI/Vulkan", "Require geometryShader support for Vulkan GPU emulation (disable to allow " "fallback primitive emulation paths)") .lifecycle(rex::cvar::Lifecycle::kInitOnly); @@ -621,6 +627,8 @@ std::unique_ptr VulkanDevice::CreateIfSupported( XE_UI_VULKAN_LIMIT(optimalBufferCopyOffsetAlignment) XE_UI_VULKAN_LIMIT(optimalBufferCopyRowPitchAlignment) XE_UI_VULKAN_LIMIT(nonCoherentAtomSize) + XE_UI_VULKAN_LIMIT(timestampComputeAndGraphics) + XE_UI_VULKAN_LIMIT(timestampPeriod) if (with_gpu_emulation) { XE_UI_VULKAN_FEATURE(robustBufferAccess) diff --git a/src/ui/vulkan/vulkan_immediate_drawer.cpp b/src/ui/vulkan/vulkan_immediate_drawer.cpp index fa574c4..322c061 100644 --- a/src/ui/vulkan/vulkan_immediate_drawer.cpp +++ b/src/ui/vulkan/vulkan_immediate_drawer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -603,7 +604,11 @@ bool VulkanImmediateDrawer::EnsurePipelinesCreatedForCurrentRenderPass() { input_assembly_state.pNext = nullptr; input_assembly_state.flags = 0; input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +#if REX_PLATFORM_MAC + input_assembly_state.primitiveRestartEnable = VK_TRUE; +#else input_assembly_state.primitiveRestartEnable = VK_FALSE; +#endif VkPipelineViewportStateCreateInfo viewport_state; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; diff --git a/src/ui/vulkan/vulkan_instance.cpp b/src/ui/vulkan/vulkan_instance.cpp index 0d2ded3..c79e8ca 100644 --- a/src/ui/vulkan/vulkan_instance.cpp +++ b/src/ui/vulkan/vulkan_instance.cpp @@ -9,6 +9,7 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#include #include #include #include @@ -16,6 +17,7 @@ #include #include +#include #include #include #include @@ -31,6 +33,21 @@ std::unique_ptr VulkanInstance::Create(const bool with_surface, const bool try_enable_validation) { std::unique_ptr vulkan_instance(new VulkanInstance()); +#if REX_PLATFORM_MAC + // The Vulkan loader does not discover MoltenVK from an executable-local SDK + // layout by default. Prefer user-provided loader settings, but make local + // command-line builds run without requiring VK_ICD_FILENAMES in the shell. + if (!std::getenv("VK_ICD_FILENAMES") && !std::getenv("VK_DRIVER_FILES")) { + auto local_icd_path = + rex::filesystem::GetExecutableFolder() / "share" / "vulkan" / "icd.d" / "MoltenVK_icd.json"; + if (std::filesystem::is_regular_file(local_icd_path)) { + std::string local_icd_path_string = local_icd_path.string(); + setenv("VK_ICD_FILENAMES", local_icd_path_string.c_str(), 0); + setenv("VK_DRIVER_FILES", local_icd_path_string.c_str(), 0); + } + } +#endif + // Load the RenderDoc API if connected. vulkan_instance->renderdoc_api_ = RenderDocAPI::CreateIfConnected(); @@ -40,7 +57,12 @@ std::unique_ptr VulkanInstance::Create(const bool with_surface, Functions& ifn = vulkan_instance->functions_; bool functions_loaded = true; - if (!vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoader)) { + if (!vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoader) +#if REX_PLATFORM_MAC + && !vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoaderFallback) + && !vulkan_instance->loader_.Load(platform::lib_names::kMoltenVK) +#endif + ) { REXLOG_ERROR("Failed to load {}", platform::lib_names::kVulkanLoader); return nullptr; } @@ -116,6 +138,11 @@ std::unique_ptr VulkanInstance::Create(const bool with_surface, // #10. requested_extensions.emplace("VK_KHR_win32_surface", &vulkan_instance->extensions_.ext_KHR_win32_surface); +#endif +#ifdef VK_USE_PLATFORM_METAL_EXT + // #218. + requested_extensions.emplace("VK_EXT_metal_surface", + &vulkan_instance->extensions_.ext_EXT_metal_surface); #endif } @@ -379,6 +406,11 @@ std::unique_ptr VulkanInstance::Create(const bool with_surface, if (vulkan_instance->extensions_.ext_KHR_win32_surface) { #include } +#endif +#ifdef VK_USE_PLATFORM_METAL_EXT + if (vulkan_instance->extensions_.ext_EXT_metal_surface) { +#include + } #endif if (vulkan_instance->extensions_.ext_KHR_surface) { #include diff --git a/src/ui/vulkan/vulkan_presenter.cpp b/src/ui/vulkan/vulkan_presenter.cpp index 30bafba..afc9a02 100644 --- a/src/ui/vulkan/vulkan_presenter.cpp +++ b/src/ui/vulkan/vulkan_presenter.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -39,6 +40,9 @@ #if REX_PLATFORM_GNU_LINUX #include #endif +#if REX_PLATFORM_MAC +#include +#endif #if REX_PLATFORM_WIN32 #include #endif @@ -55,10 +59,77 @@ REXCVAR_DEFINE_BOOL(vulkan_allow_present_mode_mailbox, true, "UI/Vulkan", REXCVAR_DEFINE_BOOL(vulkan_allow_present_mode_fifo_relaxed, true, "UI/Vulkan", "Allow FIFO relaxed present mode"); +REXCVAR_DEFINE_BOOL(vulkan_present_timing_log, false, "UI/Vulkan", + "Log averaged Vulkan host presentation timing") + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_UINT32(vulkan_present_timing_interval, 120, "UI/Vulkan", + "Number of presented frames per Vulkan timing log entry") + .range(1, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + namespace rex { namespace ui { namespace vulkan { +namespace { + +struct VulkanPresentTimingStats { + uint64_t frames = 0; + uint64_t ui_thread_frames = 0; + uint64_t acquire_us = 0; + uint64_t consume_and_record_us = 0; + uint64_t end_command_buffer_us = 0; + uint64_t submit_us = 0; + uint64_t present_us = 0; + uint64_t total_us = 0; +}; + +uint64_t ElapsedUs(std::chrono::steady_clock::time_point start, + std::chrono::steady_clock::time_point end) { + return uint64_t(std::chrono::duration_cast(end - start).count()); +} + +void AccumulateAndMaybeLogPresentTiming(bool execute_ui_drawers, uint64_t acquire_us, + uint64_t consume_and_record_us, + uint64_t end_command_buffer_us, uint64_t submit_us, + uint64_t present_us, uint64_t total_us) { + static VulkanPresentTimingStats stats; + if (!REXCVAR_GET(vulkan_present_timing_log)) { + stats = {}; + return; + } + + ++stats.frames; + if (execute_ui_drawers) { + ++stats.ui_thread_frames; + } + stats.acquire_us += acquire_us; + stats.consume_and_record_us += consume_and_record_us; + stats.end_command_buffer_us += end_command_buffer_us; + stats.submit_us += submit_us; + stats.present_us += present_us; + stats.total_us += total_us; + + const uint32_t interval = std::max(uint32_t(1), REXCVAR_GET(vulkan_present_timing_interval)); + if (stats.frames < interval) { + return; + } + + const uint64_t frames = stats.frames; + REXLOG_INFO( + "VulkanPresenter timing: frames={}, ui_thread={}, avg_total_us={}, avg_acquire_us={}, " + "avg_record_us={}, avg_end_cmd_us={}, avg_submit_us={}, avg_present_us={}", + frames, stats.ui_thread_frames, stats.total_us / frames, stats.acquire_us / frames, + stats.consume_and_record_us / frames, stats.end_command_buffer_us / frames, + stats.submit_us / frames, stats.present_us / frames); + stats = {}; +} + +} // namespace + #if defined(REX_HAS_FIDELITYFX_RUNTIME) && REX_HAS_FIDELITYFX_RUNTIME namespace { @@ -430,6 +501,11 @@ Surface::TypeFlags VulkanPresenter::GetSurfaceTypesSupportedByInstance( type_flags |= Surface::kTypeFlag_XcbWindow; } #endif +#if REX_PLATFORM_MAC + if (instance_extensions.ext_EXT_metal_surface) { + type_flags |= Surface::kTypeFlag_SDLMetalView; + } +#endif #if REX_PLATFORM_WIN32 if (instance_extensions.ext_KHR_win32_surface) { type_flags |= Surface::kTypeFlag_Win32Hwnd; @@ -752,6 +828,14 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread(Surface& new_su // The retirement or destruction of the swapchain here will also cause // awaiting completion of the usage of the swapchain and the surface on the // GPU. +#if REX_PLATFORM_MAC + // Native macOS fullscreen transitions can reconfigure the CAMetalLayer behind + // MoltenVK without making the existing VkSurface visibly recoverable. Rebind + // from the layer instead of preserving the old surface through the transition. + if (paint_context_.vulkan_surface != VK_NULL_HANDLE) { + paint_context_.DestroySwapchainAndVulkanSurface(); + } +#endif if (paint_context_.vulkan_surface != VK_NULL_HANDLE) { VkSwapchainKHR old_swapchain = paint_context_.PrepareForSwapchainRetirement(); bool surface_unusable; @@ -823,6 +907,18 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread(Surface& new_su vulkan_surface_create_result = ifn.vkCreateWin32SurfaceKHR( instance, &surface_create_info, nullptr, &paint_context_.vulkan_surface); } break; +#endif +#if REX_PLATFORM_MAC + case Surface::kTypeIndex_SDLMetalView: { + auto& sdl_metal_surface = static_cast(new_surface); + VkMetalSurfaceCreateInfoEXT surface_create_info; + surface_create_info.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; + surface_create_info.pNext = nullptr; + surface_create_info.flags = 0; + surface_create_info.pLayer = sdl_metal_surface.metal_layer(); + vulkan_surface_create_result = ifn.vkCreateMetalSurfaceEXT( + instance, &surface_create_info, nullptr, &paint_context_.vulkan_surface); + } break; #endif default: assert_unhandled_case(surface_type); @@ -1347,7 +1443,8 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface( // interfering with GPU command processing, and also to allow tearing so // variable refresh rate may be used where it's available. // Note: If the priorities here are changes, update the cvar descriptions. - if (REXCVAR_GET(vulkan_allow_present_mode_immediate) && + constexpr bool allow_immediate_present_mode = true; + if (allow_immediate_present_mode && REXCVAR_GET(vulkan_allow_present_mode_immediate) && std::find(present_modes.cbegin(), present_modes.cend(), VK_PRESENT_MODE_IMMEDIATE_KHR) != present_modes.cend()) { // Allowing tearing to reduce latency, and possibly variable refresh rate @@ -1495,6 +1592,7 @@ bool VulkanPresenter::GuestOutputImage::Initialize() { } Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_drawers) { + const auto timing_start = std::chrono::steady_clock::now(); // Begin the submission in place of the one not currently potentially used on // the GPU. uint64_t current_paint_submission_index = @@ -1535,9 +1633,11 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw VkSemaphore acquire_semaphore = paint_submission.acquire_semaphore(); uint32_t swapchain_image_index; + const auto timing_acquire_start = std::chrono::steady_clock::now(); VkResult acquire_result = dfn.vkAcquireNextImageKHR(device, paint_context_.swapchain, UINT64_MAX, acquire_semaphore, VK_NULL_HANDLE, &swapchain_image_index); + const auto timing_acquire_end = std::chrono::steady_clock::now(); switch (acquire_result) { case VK_SUCCESS: case VK_SUBOPTIMAL_KHR: @@ -1798,6 +1898,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw paint_context_.guest_output_image_paint_last_submission); util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device, swapchain_effect_pipeline.swapchain_pipeline); + swapchain_effect_pipeline.swapchain_format = VK_FORMAT_UNDEFINED; } if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) { assert_true(CanGuestOutputPaintEffectBeFinal(swapchain_effect)); @@ -1806,6 +1907,9 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw swapchain_effect, paint_context_.swapchain_render_pass); if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) { guest_output_flow.effect_count = 0; + } else { + swapchain_effect_pipeline.swapchain_format = + paint_context_.swapchain_render_pass_format; } } } @@ -2119,7 +2223,9 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw dfn.vkCmdEndRenderPass(draw_command_buffer); + const auto timing_end_command_buffer_start = std::chrono::steady_clock::now(); dfn.vkEndCommandBuffer(draw_command_buffer); + const auto timing_end_command_buffer_end = std::chrono::steady_clock::now(); VkPipelineStageFlags acquire_semaphore_wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkCommandBuffer command_buffers[2]; @@ -2151,6 +2257,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw submit_info.pCommandBuffers = command_buffers; submit_info.signalSemaphoreCount = 1; submit_info.pSignalSemaphores = &present_semaphore; + const auto timing_submit_start = std::chrono::steady_clock::now(); { VulkanSubmissionTracker::FenceAcquisition fence_acqusition( paint_context_.submission_tracker.AcquireFenceToAdvanceSubmission()); @@ -2192,6 +2299,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw return PaintResult::kNotPresentedConnectionOutdated; } } + const auto timing_submit_end = std::chrono::steady_clock::now(); VkPresentInfoKHR present_info; present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; @@ -2203,11 +2311,20 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw present_info.pImageIndices = &swapchain_image_index; present_info.pResults = nullptr; VkResult present_result; + const auto timing_present_start = std::chrono::steady_clock::now(); { const VulkanDevice::Queue::Acquisition queue_acquisition = vulkan_device_->AcquireQueue(paint_context_.present_queue_family, 0); present_result = dfn.vkQueuePresentKHR(queue_acquisition.queue(), &present_info); } + const auto timing_present_end = std::chrono::steady_clock::now(); + AccumulateAndMaybeLogPresentTiming( + execute_ui_drawers, ElapsedUs(timing_acquire_start, timing_acquire_end), + ElapsedUs(timing_acquire_end, timing_submit_start), + ElapsedUs(timing_end_command_buffer_start, timing_end_command_buffer_end), + ElapsedUs(timing_submit_start, timing_submit_end), + ElapsedUs(timing_present_start, timing_present_end), + ElapsedUs(timing_start, timing_present_end)); switch (present_result) { case VK_SUCCESS: return PaintResult::kPresented; @@ -2555,6 +2672,9 @@ VkPipeline VulkanPresenter::CreateGuestOutputPaintPipeline(GuestOutputPaintEffec VkPipelineInputAssemblyStateCreateInfo input_assembly_state = {}; input_assembly_state.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; +#if REX_PLATFORM_MAC + input_assembly_state.primitiveRestartEnable = VK_TRUE; +#endif VkPipelineViewportStateCreateInfo viewport_state = {}; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; diff --git a/src/ui/window.cpp b/src/ui/window.cpp index d8dfee5..f45891f 100644 --- a/src/ui/window.cpp +++ b/src/ui/window.cpp @@ -150,6 +150,7 @@ void Window::AddInputListener(WindowInputListener* listener, size_t z_order) { // indices will result in the skipping being cancelled in the outer loop if // an inner one is done, a "greater than" comparison will cause the inner // loop to effectively terminate all outer ones). + break; } auto it_new = std::prev(std::make_reverse_iterator(input_listeners_.emplace(z_order, listener))); // If adding to layers in between the currently being processed ones (from diff --git a/src/ui/window_sdl.cpp b/src/ui/window_sdl.cpp new file mode 100644 index 0000000..0cb7202 --- /dev/null +++ b/src/ui/window_sdl.cpp @@ -0,0 +1,743 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t kDefaultDpi = 96; +constexpr uint32_t kDefaultCursorAutoHideMilliseconds = 1000; +constexpr uint32_t kCursorAutoHideEvent = SDL_EVENT_USER + 1; + +REXCVAR_DEFINE_BOOL(sdl_high_pixel_density, !REX_PLATFORM_MAC, "UI/SDL", + "Request high-DPI backing pixels for SDL windows") + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + +std::unordered_map& WindowMap() { + static std::unordered_map windows; + return windows; +} + +Uint32 SDLCALL AutoHideCursorTimerCallback(void* userdata, SDL_TimerID timer_id, + Uint32 interval) { + (void)interval; + SDL_Event event = {}; + event.type = kCursorAutoHideEvent; + event.user.data1 = userdata; + event.user.data2 = reinterpret_cast(uintptr_t(timer_id)); + SDL_PushEvent(&event); + return 0; +} + +uint32_t ResolveWindowWidth(uint32_t requested_width) { + if (REXCVAR_GET(window_width) > 0) { + return uint32_t(REXCVAR_GET(window_width)); + } + if (!rex::cvar::HasNonDefaultValue("window_width")) { + if (rex::cvar::HasNonDefaultValue("video_mode_width") && REXCVAR_GET(video_mode_width) > 0) { + return uint32_t(std::clamp(REXCVAR_GET(video_mode_width), 1, 8192)); + } + int32_t preset_width = 0; + int32_t preset_height = 0; + if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, + preset_height)) { + return uint32_t(std::clamp(preset_width, 1, 8192)); + } + } + return requested_width; +} + +uint32_t ResolveWindowHeight(uint32_t requested_height) { + if (REXCVAR_GET(window_height) > 0) { + return uint32_t(REXCVAR_GET(window_height)); + } + if (!rex::cvar::HasNonDefaultValue("window_height")) { + if (rex::cvar::HasNonDefaultValue("video_mode_height") && REXCVAR_GET(video_mode_height) > 0) { + return uint32_t(std::clamp(REXCVAR_GET(video_mode_height), 1, 8192)); + } + int32_t preset_width = 0; + int32_t preset_height = 0; + if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, + preset_height)) { + return uint32_t(std::clamp(preset_height, 1, 8192)); + } + } + return requested_height; +} + +using rex::ui::VirtualKey; + +VirtualKey TranslateSDLScancode(SDL_Scancode scancode) { + switch (scancode) { + case SDL_SCANCODE_F1: + return VirtualKey::kF1; + case SDL_SCANCODE_F2: + return VirtualKey::kF2; + case SDL_SCANCODE_F3: + return VirtualKey::kF3; + case SDL_SCANCODE_F4: + return VirtualKey::kF4; + case SDL_SCANCODE_F5: + return VirtualKey::kF5; + case SDL_SCANCODE_F6: + return VirtualKey::kF6; + case SDL_SCANCODE_F7: + return VirtualKey::kF7; + case SDL_SCANCODE_F8: + return VirtualKey::kF8; + case SDL_SCANCODE_F9: + return VirtualKey::kF9; + case SDL_SCANCODE_F10: + return VirtualKey::kF10; + case SDL_SCANCODE_F11: + return VirtualKey::kF11; + case SDL_SCANCODE_F12: + return VirtualKey::kF12; + case SDL_SCANCODE_ESCAPE: + return VirtualKey::kEscape; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + return VirtualKey::kReturn; + case SDL_SCANCODE_SPACE: + return VirtualKey::kSpace; + case SDL_SCANCODE_TAB: + return VirtualKey::kTab; + case SDL_SCANCODE_BACKSPACE: + return VirtualKey::kBack; + case SDL_SCANCODE_DELETE: + return VirtualKey::kDelete; + case SDL_SCANCODE_INSERT: + return VirtualKey::kInsert; + case SDL_SCANCODE_HOME: + return VirtualKey::kHome; + case SDL_SCANCODE_END: + return VirtualKey::kEnd; + case SDL_SCANCODE_PAGEUP: + return VirtualKey::kPrior; + case SDL_SCANCODE_PAGEDOWN: + return VirtualKey::kNext; + case SDL_SCANCODE_LEFT: + return VirtualKey::kLeft; + case SDL_SCANCODE_RIGHT: + return VirtualKey::kRight; + case SDL_SCANCODE_UP: + return VirtualKey::kUp; + case SDL_SCANCODE_DOWN: + return VirtualKey::kDown; + case SDL_SCANCODE_LSHIFT: + return VirtualKey::kLShift; + case SDL_SCANCODE_RSHIFT: + return VirtualKey::kRShift; + case SDL_SCANCODE_LCTRL: + return VirtualKey::kLControl; + case SDL_SCANCODE_RCTRL: + return VirtualKey::kRControl; + case SDL_SCANCODE_LALT: + return VirtualKey::kLMenu; + case SDL_SCANCODE_RALT: + return VirtualKey::kRMenu; + case SDL_SCANCODE_LGUI: + return VirtualKey::kLWin; + case SDL_SCANCODE_RGUI: + return VirtualKey::kRWin; + case SDL_SCANCODE_APPLICATION: + return VirtualKey::kApps; + case SDL_SCANCODE_CAPSLOCK: + return VirtualKey::kCapital; + case SDL_SCANCODE_NUMLOCKCLEAR: + return VirtualKey::kNumLock; + case SDL_SCANCODE_SCROLLLOCK: + return VirtualKey::kScroll; + case SDL_SCANCODE_PRINTSCREEN: + return VirtualKey::kSnapshot; + case SDL_SCANCODE_PAUSE: + return VirtualKey::kPause; + case SDL_SCANCODE_0: + return VirtualKey::k0; + case SDL_SCANCODE_1: + return VirtualKey::k1; + case SDL_SCANCODE_2: + return VirtualKey::k2; + case SDL_SCANCODE_3: + return VirtualKey::k3; + case SDL_SCANCODE_4: + return VirtualKey::k4; + case SDL_SCANCODE_5: + return VirtualKey::k5; + case SDL_SCANCODE_6: + return VirtualKey::k6; + case SDL_SCANCODE_7: + return VirtualKey::k7; + case SDL_SCANCODE_8: + return VirtualKey::k8; + case SDL_SCANCODE_9: + return VirtualKey::k9; + case SDL_SCANCODE_A: + return VirtualKey::kA; + case SDL_SCANCODE_B: + return VirtualKey::kB; + case SDL_SCANCODE_C: + return VirtualKey::kC; + case SDL_SCANCODE_D: + return VirtualKey::kD; + case SDL_SCANCODE_E: + return VirtualKey::kE; + case SDL_SCANCODE_F: + return VirtualKey::kF; + case SDL_SCANCODE_G: + return VirtualKey::kG; + case SDL_SCANCODE_H: + return VirtualKey::kH; + case SDL_SCANCODE_I: + return VirtualKey::kI; + case SDL_SCANCODE_J: + return VirtualKey::kJ; + case SDL_SCANCODE_K: + return VirtualKey::kK; + case SDL_SCANCODE_L: + return VirtualKey::kL; + case SDL_SCANCODE_M: + return VirtualKey::kM; + case SDL_SCANCODE_N: + return VirtualKey::kN; + case SDL_SCANCODE_O: + return VirtualKey::kO; + case SDL_SCANCODE_P: + return VirtualKey::kP; + case SDL_SCANCODE_Q: + return VirtualKey::kQ; + case SDL_SCANCODE_R: + return VirtualKey::kR; + case SDL_SCANCODE_S: + return VirtualKey::kS; + case SDL_SCANCODE_T: + return VirtualKey::kT; + case SDL_SCANCODE_U: + return VirtualKey::kU; + case SDL_SCANCODE_V: + return VirtualKey::kV; + case SDL_SCANCODE_W: + return VirtualKey::kW; + case SDL_SCANCODE_X: + return VirtualKey::kX; + case SDL_SCANCODE_Y: + return VirtualKey::kY; + case SDL_SCANCODE_Z: + return VirtualKey::kZ; + case SDL_SCANCODE_KP_0: + return VirtualKey::kNumpad0; + case SDL_SCANCODE_KP_1: + return VirtualKey::kNumpad1; + case SDL_SCANCODE_KP_2: + return VirtualKey::kNumpad2; + case SDL_SCANCODE_KP_3: + return VirtualKey::kNumpad3; + case SDL_SCANCODE_KP_4: + return VirtualKey::kNumpad4; + case SDL_SCANCODE_KP_5: + return VirtualKey::kNumpad5; + case SDL_SCANCODE_KP_6: + return VirtualKey::kNumpad6; + case SDL_SCANCODE_KP_7: + return VirtualKey::kNumpad7; + case SDL_SCANCODE_KP_8: + return VirtualKey::kNumpad8; + case SDL_SCANCODE_KP_9: + return VirtualKey::kNumpad9; + case SDL_SCANCODE_KP_PLUS: + return VirtualKey::kAdd; + case SDL_SCANCODE_KP_MINUS: + return VirtualKey::kSubtract; + case SDL_SCANCODE_KP_MULTIPLY: + return VirtualKey::kMultiply; + case SDL_SCANCODE_KP_DIVIDE: + return VirtualKey::kDivide; + case SDL_SCANCODE_KP_DECIMAL: + return VirtualKey::kDecimal; + case SDL_SCANCODE_GRAVE: + return VirtualKey::kOem3; + case SDL_SCANCODE_MINUS: + return VirtualKey::kOemMinus; + case SDL_SCANCODE_EQUALS: + return VirtualKey::kOemPlus; + case SDL_SCANCODE_COMMA: + return VirtualKey::kOemComma; + case SDL_SCANCODE_PERIOD: + return VirtualKey::kOemPeriod; + case SDL_SCANCODE_SEMICOLON: + return VirtualKey::kOem1; + case SDL_SCANCODE_SLASH: + return VirtualKey::kOem2; + case SDL_SCANCODE_BACKSLASH: + return VirtualKey::kOem5; + case SDL_SCANCODE_LEFTBRACKET: + return VirtualKey::kOem4; + case SDL_SCANCODE_RIGHTBRACKET: + return VirtualKey::kOem6; + case SDL_SCANCODE_APOSTROPHE: + return VirtualKey::kOem7; + default: + return VirtualKey::kNone; + } +} + +rex::ui::MouseEvent::Button TranslateMouseButton(uint8_t button) { + switch (button) { + case SDL_BUTTON_LEFT: + return rex::ui::MouseEvent::Button::kLeft; + case SDL_BUTTON_RIGHT: + return rex::ui::MouseEvent::Button::kRight; + case SDL_BUTTON_MIDDLE: + return rex::ui::MouseEvent::Button::kMiddle; + case SDL_BUTTON_X1: + return rex::ui::MouseEvent::Button::kX1; + case SDL_BUTTON_X2: + return rex::ui::MouseEvent::Button::kX2; + default: + return rex::ui::MouseEvent::Button::kNone; + } +} + +} // namespace + +namespace rex { +namespace ui { + +std::unique_ptr Window::Create(WindowedAppContext& app_context, + const std::string_view title, uint32_t desired_logical_width, + uint32_t desired_logical_height) { + desired_logical_width = ResolveWindowWidth(desired_logical_width); + desired_logical_height = ResolveWindowHeight(desired_logical_height); + return std::make_unique(app_context, title, desired_logical_width, + desired_logical_height); +} + +SDLWindow::SDLWindow(WindowedAppContext& app_context, const std::string_view title, + uint32_t desired_logical_width, uint32_t desired_logical_height) + : Window(app_context, title, desired_logical_width, desired_logical_height) {} + +SDLWindow::~SDLWindow() { + EnterDestructor(); + RemoveCursorAutoHideTimer(); + if (window_) { + WindowMap().erase(SDL_GetWindowID(window_)); + } + if (metal_view_) { + SDL_Metal_DestroyView(metal_view_); + metal_view_ = nullptr; + metal_layer_ = nullptr; + } + if (window_) { + SDL_DestroyWindow(window_); + window_ = nullptr; + } +} + +void SDLWindow::HandleSDLEvent(const SDL_Event& event) { + SDL_WindowID window_id = 0; + switch (event.type) { + case SDL_EVENT_WINDOW_SHOWN: + case SDL_EVENT_WINDOW_HIDDEN: + case SDL_EVENT_WINDOW_EXPOSED: + case SDL_EVENT_WINDOW_MOVED: + case SDL_EVENT_WINDOW_RESIZED: + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: + case SDL_EVENT_WINDOW_FOCUS_GAINED: + case SDL_EVENT_WINDOW_FOCUS_LOST: + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: + case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: + window_id = event.window.windowID; + break; + case kCursorAutoHideEvent: + window_id = SDL_WindowID(reinterpret_cast(event.user.data1)); + break; + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + window_id = event.key.windowID; + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + window_id = event.button.windowID; + break; + case SDL_EVENT_MOUSE_MOTION: + window_id = event.motion.windowID; + break; + case SDL_EVENT_MOUSE_WHEEL: + window_id = event.wheel.windowID; + break; + default: + return; + } + + auto it = WindowMap().find(window_id); + if (it != WindowMap().end()) { + it->second->HandleEvent(event); + } +} + +uint32_t SDLWindow::GetLatestDpiImpl() const { return dpi_ ? dpi_ : kDefaultDpi; } + +bool SDLWindow::OpenImpl() { + SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); + SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; + if (REXCVAR_GET(sdl_high_pixel_density)) { + flags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; + } + if (IsFullscreen()) { + flags |= SDL_WINDOW_FULLSCREEN; + } + + window_ = SDL_CreateWindow(GetTitle().c_str(), int(GetDesiredLogicalWidth()), + int(GetDesiredLogicalHeight()), flags); + if (!window_) { + REXLOG_ERROR("SDLWindow: Failed to create window: {}", SDL_GetError()); + return false; + } + + WindowMap().emplace(SDL_GetWindowID(window_), this); + + metal_view_ = SDL_Metal_CreateView(window_); + if (!metal_view_) { + REXLOG_ERROR("SDLWindow: Failed to create Metal view: {}", SDL_GetError()); + WindowMap().erase(SDL_GetWindowID(window_)); + SDL_DestroyWindow(window_); + window_ = nullptr; + return false; + } + metal_layer_ = SDL_Metal_GetLayer(metal_view_); + if (!metal_layer_) { + REXLOG_ERROR("SDLWindow: Failed to get Metal layer: {}", SDL_GetError()); + SDL_Metal_DestroyView(metal_view_); + metal_view_ = nullptr; + WindowMap().erase(SDL_GetWindowID(window_)); + SDL_DestroyWindow(window_); + window_ = nullptr; + return false; + } + + dpi_ = QueryDpi(); + SDL_ShowWindow(window_); + if (IsMouseCaptureRequested()) { + SDL_SetWindowMouseGrab(window_, true); + } + ApplyNewCursorVisibility(CursorVisibility::kVisible); + + WindowDestructionReceiver destruction_receiver(this); + HandleSizeUpdate(destruction_receiver); + if (destruction_receiver.IsWindowDestroyedOrClosed()) { + return true; + } + OnFocusUpdate((SDL_GetWindowFlags(window_) & SDL_WINDOW_INPUT_FOCUS) != 0, destruction_receiver); + return true; +} + +void SDLWindow::RequestCloseImpl() { + WindowDestructionReceiver destruction_receiver(this); + OnBeforeClose(destruction_receiver); + if (!destruction_receiver.IsWindowDestroyed()) { + RemoveCursorAutoHideTimer(); + if (metal_view_) { + SDL_Metal_DestroyView(metal_view_); + metal_view_ = nullptr; + metal_layer_ = nullptr; + } + if (window_) { + WindowMap().erase(SDL_GetWindowID(window_)); + SDL_DestroyWindow(window_); + window_ = nullptr; + } + OnAfterClose(); + } +} + +void SDLWindow::ApplyNewFullscreen() { + if (window_) { + SDL_SetWindowFullscreen(window_, IsFullscreen()); + } +} + +void SDLWindow::ApplyNewTitle() { + if (window_) { + SDL_SetWindowTitle(window_, GetTitle().c_str()); + } +} + +void SDLWindow::ApplyNewMouseCapture() { + if (window_) { + SDL_SetWindowMouseGrab(window_, true); + } +} + +void SDLWindow::ApplyNewMouseRelease() { + if (window_) { + SDL_SetWindowMouseGrab(window_, false); + } +} + +void SDLWindow::ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) { + (void)old_cursor_visibility; + RemoveCursorAutoHideTimer(); + switch (GetCursorVisibility()) { + case CursorVisibility::kVisible: + cursor_currently_auto_hidden_ = false; + SDL_ShowCursor(); + break; + case CursorVisibility::kAutoHidden: + cursor_currently_auto_hidden_ = true; + SDL_HideCursor(); + break; + case CursorVisibility::kHidden: + cursor_currently_auto_hidden_ = true; + SDL_HideCursor(); + break; + } +} + +void SDLWindow::FocusImpl() { + if (window_) { + SDL_RaiseWindow(window_); + } +} + +std::unique_ptr SDLWindow::CreateSurfaceImpl(Surface::TypeFlags allowed_types) { + if (!(allowed_types & Surface::kTypeFlag_SDLMetalView)) { + return nullptr; + } + return std::make_unique(window_, metal_view_, metal_layer_); +} + +void SDLWindow::RequestPaintImpl() { + bool expected = false; + if (!paint_event_queued_.compare_exchange_strong(expected, true, std::memory_order_acq_rel, + std::memory_order_acquire)) { + return; + } + + SDL_Event event = {}; + event.type = SDL_EVENT_WINDOW_EXPOSED; + event.window.windowID = SDL_GetWindowID(window_); + if (!SDL_PushEvent(&event)) { + paint_event_queued_.store(false, std::memory_order_release); + } +} + +void SDLWindow::HandleEvent(const SDL_Event& event) { + WindowDestructionReceiver destruction_receiver(this); + switch (event.type) { + case SDL_EVENT_WINDOW_EXPOSED: + paint_event_queued_.store(false, std::memory_order_release); + OnPaint(); + break; + case SDL_EVENT_WINDOW_RESIZED: + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + HandleSizeUpdate(destruction_receiver); + break; + case SDL_EVENT_WINDOW_FOCUS_GAINED: + OnFocusUpdate(true, destruction_receiver); + break; + case SDL_EVENT_WINDOW_FOCUS_LOST: + OnFocusUpdate(false, destruction_receiver); + break; + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: + OnDesiredFullscreenUpdate(true); + break; + case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: + OnDesiredFullscreenUpdate(false); + break; + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: +#if REX_PLATFORM_MAC + app_context().QuitFromUIThread(); +#else + RequestCloseImpl(); + app_context().RequestDeferredQuit(); +#endif + break; + case kCursorAutoHideEvent: + if (GetCursorVisibility() == CursorVisibility::kAutoHidden && + SDL_TimerID(reinterpret_cast(event.user.data2)) == cursor_auto_hide_timer_) { + cursor_auto_hide_timer_ = 0; + cursor_currently_auto_hidden_ = true; + SDL_HideCursor(); + } + break; + case SDL_EVENT_KEY_DOWN: + HandleKey(event.key, true, destruction_receiver); + break; + case SDL_EVENT_KEY_UP: + HandleKey(event.key, false, destruction_receiver); + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + HandleMouseButton(event.button, true, destruction_receiver); + break; + case SDL_EVENT_MOUSE_BUTTON_UP: + HandleMouseButton(event.button, false, destruction_receiver); + break; + case SDL_EVENT_MOUSE_MOTION: + HandleMouseMotion(event.motion, destruction_receiver); + break; + case SDL_EVENT_MOUSE_WHEEL: + HandleMouseWheel(event.wheel, destruction_receiver); + break; + default: + break; + } +} + +void SDLWindow::HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver) { + dpi_ = QueryDpi(); + int width = 0; + int height = 0; + if (!SDL_GetWindowSizeInPixels(window_, &width, &height)) { + return; + } + if (!IsFullscreen()) { + int logical_width = 0; + int logical_height = 0; + if (SDL_GetWindowSize(window_, &logical_width, &logical_height)) { + OnDesiredLogicalSizeUpdate(uint32_t(std::max(logical_width, 0)), + uint32_t(std::max(logical_height, 0))); + } + } + OnActualSizeUpdate(uint32_t(std::max(width, 0)), uint32_t(std::max(height, 0)), + destruction_receiver); +} + +void SDLWindow::HandleKey(const SDL_KeyboardEvent& event, bool down, + WindowDestructionReceiver& destruction_receiver) { + SDL_Keymod mod = SDL_GetModState(); + KeyEvent e(this, TranslateSDLScancode(event.scancode), event.repeat ? 2 : 1, !down, + (mod & SDL_KMOD_SHIFT) != 0, (mod & SDL_KMOD_CTRL) != 0, + (mod & SDL_KMOD_ALT) != 0, (mod & SDL_KMOD_GUI) != 0); + if (down) { + OnKeyDown(e, destruction_receiver); + } else { + OnKeyUp(e, destruction_receiver); + } +} + +void SDLWindow::HandleMouseButton(const SDL_MouseButtonEvent& event, bool down, + WindowDestructionReceiver& destruction_receiver) { + RevealAutoHiddenCursor(); + int32_t physical_x = 0; + int32_t physical_y = 0; + WindowPointToPhysical(event.x, event.y, physical_x, physical_y); + MouseEvent e(this, TranslateMouseButton(event.button), physical_x, physical_y); + if (down) { + OnMouseDown(e, destruction_receiver); + } else { + OnMouseUp(e, destruction_receiver); + } +} + +void SDLWindow::HandleMouseMotion(const SDL_MouseMotionEvent& event, + WindowDestructionReceiver& destruction_receiver) { + RevealAutoHiddenCursor(); + int32_t physical_x = 0; + int32_t physical_y = 0; + WindowPointToPhysical(event.x, event.y, physical_x, physical_y); + MouseEvent e(this, MouseEvent::Button::kNone, physical_x, physical_y); + OnMouseMove(e, destruction_receiver); +} + +void SDLWindow::HandleMouseWheel(const SDL_MouseWheelEvent& event, + WindowDestructionReceiver& destruction_receiver) { + RevealAutoHiddenCursor(); + int32_t physical_x = 0; + int32_t physical_y = 0; + WindowPointToPhysical(event.mouse_x, event.mouse_y, physical_x, physical_y); + MouseEvent e(this, MouseEvent::Button::kNone, physical_x, physical_y, + int32_t(std::lround(event.x * double(MouseEvent::kScrollPerDetent))), + int32_t(std::lround(event.y * double(MouseEvent::kScrollPerDetent)))); + OnMouseWheel(e, destruction_receiver); +} + +void SDLWindow::RevealAutoHiddenCursor() { + if (GetCursorVisibility() != CursorVisibility::kAutoHidden) { + return; + } + if (cursor_currently_auto_hidden_) { + cursor_currently_auto_hidden_ = false; + SDL_ShowCursor(); + } + SetCursorAutoHideTimer(); +} + +void SDLWindow::SetCursorAutoHideTimer() { + RemoveCursorAutoHideTimer(); + if (!window_) { + return; + } + cursor_auto_hide_timer_ = + SDL_AddTimer(kDefaultCursorAutoHideMilliseconds, AutoHideCursorTimerCallback, + reinterpret_cast(uintptr_t(SDL_GetWindowID(window_)))); +} + +void SDLWindow::RemoveCursorAutoHideTimer() { + if (!cursor_auto_hide_timer_) { + return; + } + SDL_RemoveTimer(cursor_auto_hide_timer_); + cursor_auto_hide_timer_ = 0; +} + +void SDLWindow::WindowPointToPhysical(float x, float y, int32_t& physical_x, + int32_t& physical_y) const { + int logical_width = 0; + int logical_height = 0; + int physical_width = 0; + int physical_height = 0; + if (window_ && SDL_GetWindowSize(window_, &logical_width, &logical_height) && + SDL_GetWindowSizeInPixels(window_, &physical_width, &physical_height) && + logical_width > 0 && logical_height > 0 && physical_width > 0 && physical_height > 0) { + physical_x = int32_t(std::lround(double(x) * double(physical_width) / double(logical_width))); + physical_y = int32_t(std::lround(double(y) * double(physical_height) / double(logical_height))); + return; + } + + physical_x = int32_t(std::lround(x)); + physical_y = int32_t(std::lround(y)); +} + +uint32_t SDLWindow::QueryDpi() const { + if (!window_) { + return kDefaultDpi; + } + float scale = SDL_GetWindowDisplayScale(window_); + if (scale <= 0.0f) { + scale = 1.0f; + } + return std::clamp(uint32_t(std::lround(double(kDefaultDpi) * scale)), uint32_t(48), + uint32_t(384)); +} + +std::unique_ptr MenuItem::Create(Type type, const std::string& text, + const std::string& hotkey, + std::function callback) { + return std::make_unique(type, text, hotkey, std::move(callback)); +} + +} // namespace ui +} // namespace rex diff --git a/src/ui/windowed_app_context_sdl.cpp b/src/ui/windowed_app_context_sdl.cpp new file mode 100644 index 0000000..74af109 --- /dev/null +++ b/src/ui/windowed_app_context_sdl.cpp @@ -0,0 +1,70 @@ +#include +#include + +#include + +#include + +namespace rex { +namespace ui { + +namespace { + +constexpr uint32_t kPendingFunctionsEvent = SDL_EVENT_USER; + +} // namespace + +void SDLWindowedAppContext::NotifyUILoopOfPendingFunctions() { + bool expected = false; + if (!pending_functions_event_queued_.compare_exchange_strong(expected, true)) { + return; + } + + SDL_Event event = {}; + event.type = kPendingFunctionsEvent; + if (!SDL_PushEvent(&event)) { + pending_functions_event_queued_ = false; + } +} + +void SDLWindowedAppContext::PlatformQuitFromUIThread() { + SDL_Event event = {}; + event.type = SDL_EVENT_QUIT; + SDL_PushEvent(&event); +} + +int SDLWindowedAppContext::RunMainLoop() { + if (HasQuitFromUIThread()) { + return EXIT_SUCCESS; + } + + SDL_Event event; + while (!HasQuitFromUIThread() && SDL_WaitEvent(&event)) { + DispatchEvent(event); + } + + if (!HasQuitFromUIThread()) { + REXLOG_WARN("SDL event loop exited unexpectedly: {}", SDL_GetError()); + QuitFromUIThread(); + } + return EXIT_SUCCESS; +} + +void SDLWindowedAppContext::DispatchEvent(const SDL_Event& event) { + if (event.type == kPendingFunctionsEvent) { + pending_functions_event_queued_ = false; + ExecutePendingFunctionsFromUIThread(); + return; + } + + if (event.type == SDL_EVENT_QUIT) { + REXLOG_INFO("SDL quit event received"); + QuitFromUIThread(); + return; + } + + SDLWindow::HandleSDLEvent(event); +} + +} // namespace ui +} // namespace rex diff --git a/src/ui/windowed_app_main_sdl.cpp b/src/ui/windowed_app_main_sdl.cpp new file mode 100644 index 0000000..c4fc966 --- /dev/null +++ b/src/ui/windowed_app_main_sdl.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +int main(int argc, char** argv) { + auto remaining = rex::cvar::Init(argc, argv); + rex::cvar::ApplyEnvironment(); + rex::InitLoggingEarly(); + +#if REX_PLATFORM_MAC + // Use native macOS fullscreen so the menu bar and app switching behave like + // other Mac apps. MoltenVK presentation is paced separately on macOS. + SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, "1"); + SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_MENU_VISIBILITY, "1"); +#endif + + if (!SDL_Init(SDL_INIT_VIDEO)) { + std::fprintf(stderr, "Failed to initialize SDL video: %s\n", SDL_GetError()); + return EXIT_FAILURE; + } + + int result = EXIT_FAILURE; + { + rex::ui::SDLWindowedAppContext app_context; + std::unique_ptr app = rex::ui::GetWindowedAppCreator()(app_context); + + const auto& option_names = app->GetPositionalOptions(); + std::map parsed; + size_t count = std::min(remaining.size(), option_names.size()); + for (size_t i = 0; i < count; ++i) { + parsed[option_names[i]] = remaining[i]; + } + app->SetParsedArguments(std::move(parsed)); + + bool initialized = app->OnInitialize(); + result = initialized ? app_context.RunMainLoop() : EXIT_FAILURE; + app->InvokeOnDestroy(); + } + + rex::ShutdownLogging(); +#if REX_PLATFORM_MAC + // After explicit app teardown and log flushing, avoid macOS/SDL/global static + // teardown paths that can change a clean Close Game exit into status 1. + std::_Exit(result); +#else + SDL_Quit(); +#endif + return result; +} diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index af6607f..93e2180 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -5,10 +5,22 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) #============================================================================= # Architecture detection #============================================================================= -if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$") +set(REX_THIRDPARTY_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}") +if(APPLE AND CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES REX_THIRDPARTY_OSX_ARCH_COUNT) + if(REX_THIRDPARTY_OSX_ARCH_COUNT EQUAL 1) + set(REX_THIRDPARTY_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}") + endif() +endif() + +if(APPLE AND NOT REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") + message(FATAL_ERROR "Unsupported macOS architecture: ${REX_THIRDPARTY_TARGET_PROCESSOR}. macOS builds are ARM64-only.") +endif() + +if(REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(x86_64|AMD64)$") set(IS_ARM64 FALSE) set(IS_X86_64 TRUE) -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|ARM64)$") +elseif(REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") set(IS_ARM64 TRUE) set(IS_X86_64 FALSE) else() @@ -197,6 +209,9 @@ if(REXGLUE_USE_VULKAN) add_subdirectory(vulkan-memory-allocator) # volk + if(APPLE) + set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_METAL_EXT CACHE STRING "" FORCE) + endif() set(VOLK_INSTALL ON CACHE BOOL "" FORCE) set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE) add_subdirectory(volk) @@ -237,7 +252,11 @@ set(SDL_TEST OFF CACHE BOOL "" FORCE) set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "" FORCE) # Disable unused subsystems -set(SDL_VIDEO OFF CACHE BOOL "" FORCE) +if(APPLE) + set(SDL_VIDEO ON CACHE BOOL "" FORCE) +else() + set(SDL_VIDEO OFF CACHE BOOL "" FORCE) +endif() set(SDL_RENDER OFF CACHE BOOL "" FORCE) set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) set(SDL_SENSOR OFF CACHE BOOL "" FORCE) @@ -254,7 +273,11 @@ set(SDL_CAMERA OFF CACHE BOOL "" FORCE) set(SDL_OPENGL OFF CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_VULKAN OFF CACHE BOOL "" FORCE) -set(SDL_METAL OFF CACHE BOOL "" FORCE) +if(APPLE) + set(SDL_METAL ON CACHE BOOL "" FORCE) +else() + set(SDL_METAL OFF CACHE BOOL "" FORCE) +endif() # Disable dummy drivers set(SDL_DISKAUDIO OFF CACHE BOOL "" FORCE) diff --git a/thirdparty/crypto/sha256.cpp b/thirdparty/crypto/sha256.cpp index f63fb2a..0ecbf41 100644 --- a/thirdparty/crypto/sha256.cpp +++ b/thirdparty/crypto/sha256.cpp @@ -9,8 +9,18 @@ // big endian architectures need #define __BYTE_ORDER __BIG_ENDIAN #ifndef _MSC_VER +#ifdef __APPLE__ +#include +#ifndef __BYTE_ORDER +#define __BYTE_ORDER BYTE_ORDER +#endif +#ifndef __BIG_ENDIAN +#define __BIG_ENDIAN BIG_ENDIAN +#endif +#else #include #endif +#endif namespace sha256 { @@ -573,4 +583,4 @@ std::string SHA256::operator()(const std::string& text) { return getHash(); } -} \ No newline at end of file +} diff --git a/thirdparty/renderdoc/renderdoc_app.h b/thirdparty/renderdoc/renderdoc_app.h index da323e9..1c48bba 100644 --- a/thirdparty/renderdoc/renderdoc_app.h +++ b/thirdparty/renderdoc/renderdoc_app.h @@ -30,7 +30,7 @@ #if defined(WIN32) || defined(_WIN32) #define RENDERDOC_CC __cdecl -#elif defined(__linux__) +#elif defined(__linux__) || defined(__APPLE__) #define RENDERDOC_CC #else #error "Unknown platform"