add macOS platform support

This commit is contained in:
mchughalex 2026-06-06 15:13:35 +01:00
parent b845372efa
commit 3184592404
73 changed files with 3139 additions and 332 deletions

View file

@ -54,7 +54,15 @@ set(REXGLUE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}")
include(GNUInstallDirs) include(GNUInstallDirs)
include(CMakePackageConfigHelpers) 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 # Keep installed binaries self-contained by resolving shared libs relative
# to the install prefix (for example, bin -> ../lib). # to the install prefix (for example, bin -> ../lib).
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
@ -86,15 +94,26 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_DEBUG_POSTFIX d) set(CMAKE_DEBUG_POSTFIX d)
set(CMAKE_RELWITHDEBINFO_POSTFIX rd) 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 # 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+) # Large code model on x86_64 for linking with very large recompiled executables (35MB+)
add_compile_options(-mcmodel=large) add_compile_options(-mcmodel=large)
else() else()
# Small code model for other architectures (ARM64, etc.) # Small code model for other architectures (ARM64, etc.)
add_compile_options(-mcmodel=small) add_compile_options(-mcmodel=small)
endif() endif()
endif()
if(NOT WIN32)
# Position-independent code for all targets # Position-independent code for all targets
set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif() endif()
@ -112,25 +131,32 @@ add_compile_options(
# Platform detection (supports AMD64 and ARM64) # Platform detection (supports AMD64 and ARM64)
if(WIN32) if(WIN32)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64") if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64")
set(REX_PLATFORM "win-amd64") set(REX_PLATFORM "win-amd64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64")
set(REX_PLATFORM "win-arm64") set(REX_PLATFORM "win-arm64")
else() else()
message(FATAL_ERROR "Unsupported Windows architecture: ${CMAKE_SYSTEM_PROCESSOR}") message(FATAL_ERROR "Unsupported Windows architecture: ${REX_TARGET_PROCESSOR}")
endif() endif()
add_compile_definitions(REX_PLATFORM_WINDOWS=1) 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) 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") set(REX_PLATFORM "linux-amd64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64") elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64")
set(REX_PLATFORM "linux-arm64") set(REX_PLATFORM "linux-arm64")
else() else()
set(REX_PLATFORM "linux-${CMAKE_SYSTEM_PROCESSOR}") set(REX_PLATFORM "linux-${REX_TARGET_PROCESSOR}")
endif() endif()
add_compile_definitions(REX_PLATFORM_LINUX=1) add_compile_definitions(REX_PLATFORM_LINUX=1)
else() 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() endif()
# Build config as compile definition (multi-config generators don't know config at configure time) # Build config as compile definition (multi-config generators don't know config at configure time)

View file

@ -14,22 +14,30 @@
# runtime DLL staging is the host's job (see rexglue_configure_target). # runtime DLL staging is the host's job (see rexglue_configure_target).
#========================================================== #==========================================================
function(rexglue_apply_target_settings target_name) 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) if(UNIX AND NOT APPLE)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0) pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
target_include_directories(${target_name} PRIVATE ${GTK3_INCLUDE_DIRS}) target_include_directories(${target_name} PRIVATE ${GTK3_INCLUDE_DIRS})
target_link_libraries(${target_name} PRIVATE ${GTK3_LIBRARIES}) target_link_libraries(${target_name} PRIVATE ${GTK3_LIBRARIES})
# Large executable support # 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_link_options(${target_name} PRIVATE -Wl,--no-relax)
target_compile_options(${target_name} PRIVATE -mcmodel=large) 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) target_compile_options(${target_name} PRIVATE -march=armv8-a)
endif() endif()
endif() endif()
if(NOT MSVC) if(NOT MSVC AND NOT APPLE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") if(_rexglue_target_processor MATCHES "x86_64|AMD64")
target_compile_options(${target_name} PRIVATE -msse4.1) target_compile_options(${target_name} PRIVATE -msse4.1)
endif() endif()
endif() endif()
@ -51,6 +59,9 @@ function(rexglue_configure_target target_name)
if(WIN32) if(WIN32)
target_sources(${target_name} PRIVATE target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_win.cpp) ${REXGLUE_SHARE_DIR}/windowed_app_main_win.cpp)
elseif(APPLE)
target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_sdl.cpp)
else() else()
target_sources(${target_name} PRIVATE target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_posix.cpp) ${REXGLUE_SHARE_DIR}/windowed_app_main_posix.cpp)
@ -68,7 +79,12 @@ function(rexglue_configure_target target_name)
target_include_directories(${target_name} PRIVATE target_include_directories(${target_name} PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/rexglue-sdk/include) ${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 set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH ON BUILD_WITH_INSTALL_RPATH ON
@ -97,6 +113,56 @@ function(rexglue_configure_target target_name)
) )
endif() endif()
endforeach() 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}"
$<TARGET_FILE_DIR:${target_name}>
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
$<TARGET_FILE_DIR:${target_name}>/lib
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_vk_lib_dir}/libMoltenVK.dylib"
$<TARGET_FILE_DIR:${target_name}>/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
$<TARGET_FILE_DIR:${target_name}>/share/vulkan/icd.d
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_vk_root}/share/vulkan/icd.d/MoltenVK_icd.json"
$<TARGET_FILE_DIR:${target_name}>/share/vulkan/icd.d
VERBATIM
)
break()
endif()
endforeach()
endif() endif()
endfunction() endfunction()
@ -131,7 +197,12 @@ function(rexglue_configure_module_target target_name)
) )
endif() 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 set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH ON BUILD_WITH_INSTALL_RPATH ON

View file

@ -116,6 +116,7 @@ endif()
install(FILES install(FILES
src/ui/windowed_app_main_win.cpp src/ui/windowed_app_main_win.cpp
src/ui/windowed_app_main_posix.cpp src/ui/windowed_app_main_posix.cpp
src/ui/windowed_app_main_sdl.cpp
src/ui/rex_app.cpp src/ui/rex_app.cpp
DESTINATION ${CMAKE_INSTALL_DATADIR}/rexglue DESTINATION ${CMAKE_INSTALL_DATADIR}/rexglue
) )

View file

@ -17,6 +17,20 @@
#include <rex/chrono/clock.h> #include <rex/chrono/clock.h>
#if !defined(__cpp_lib_chrono) || __cpp_lib_chrono < 201907L
namespace std::chrono {
template <typename DestClock, typename SourceClock>
struct clock_time_conversion;
template <typename DestClock, typename SourceClock, typename Duration>
auto clock_cast(const std::chrono::time_point<SourceClock, Duration>& t) {
return clock_time_conversion<DestClock, SourceClock>{}(t);
}
} // namespace std::chrono
#endif
namespace rex { namespace rex {
using hundrednano = std::ratio<1, 10000000>; using hundrednano = std::ratio<1, 10000000>;

View file

@ -122,6 +122,7 @@ class GraphicsSystem : public system::IGraphicsSystem {
uint32_t interrupt_callback_ = 0; uint32_t interrupt_callback_ = 0;
uint32_t interrupt_callback_data_ = 0; uint32_t interrupt_callback_data_ = 0;
std::atomic<bool> shutting_down_{false};
std::atomic<bool> vsync_worker_running_; std::atomic<bool> vsync_worker_running_;
system::object_ref<system::XHostThread> vsync_worker_thread_; system::object_ref<system::XHostThread> vsync_worker_thread_;

View file

@ -991,8 +991,11 @@ class SpirvShaderTranslator : public ShaderTranslator {
spv::Id main_memexport_allowed_; spv::Id main_memexport_allowed_;
// VS rectangle-list fallback only - uint. // VS rectangle-list fallback only - uint.
spv::Id var_main_rectangle_guest_vertex_index_; spv::Id var_main_rectangle_guest_vertex_index_;
// VS rectangle-list fallback only - gl_PerVertex[3]. // VS rectangle-list fallback only - float4[3].
spv::Id var_main_rectangle_per_vertex_; 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. // VS rectangle-list fallback only - float4[3] for each used interpolator.
std::array<spv::Id, xenos::kMaxInterpolators> var_main_rectangle_interpolators_; std::array<spv::Id, xenos::kMaxInterpolators> var_main_rectangle_interpolators_;
// VS only - float3 (special exports). // VS only - float3 (special exports).

View file

@ -37,6 +37,7 @@
#include <rex/graphics/vulkan/texture_cache.h> #include <rex/graphics/vulkan/texture_cache.h>
#include <rex/graphics/xenos.h> #include <rex/graphics/xenos.h>
#include <rex/hash.h> #include <rex/hash.h>
#include <rex/perf/counter.h>
#include <rex/system/kernel_state.h> #include <rex/system/kernel_state.h>
#include <rex/ui/vulkan/linked_type_descriptor_set_allocator.h> #include <rex/ui/vulkan/linked_type_descriptor_set_allocator.h>
#include <rex/ui/vulkan/presenter.h> #include <rex/ui/vulkan/presenter.h>
@ -242,6 +243,9 @@ class VulkanCommandProcessor : public CommandProcessor {
bool keep_dynamic_blend_constants = false, bool keep_dynamic_blend_constants = false,
bool keep_dynamic_stencil_mask_ref = false); bool keep_dynamic_stencil_mask_ref = false);
void BindExternalComputePipeline(VkPipeline pipeline); void BindExternalComputePipeline(VkPipeline pipeline);
uint32_t BeginGpuTimestampedRegion(rex::perf::DrawBucket bucket) {
return BeginGpuTimestampedDraw(bucket);
}
void SetViewport(const VkViewport& viewport); void SetViewport(const VkViewport& viewport);
void SetScissor(const VkRect2D& scissor); void SetScissor(const VkRect2D& scissor);
@ -446,6 +450,14 @@ class VulkanCommandProcessor : public CommandProcessor {
void DisableHostOcclusionQueries(); void DisableHostOcclusionQueries();
uint64_t NormalizeOcclusionSamples(uint64_t samples) const; uint64_t NormalizeOcclusionSamples(uint64_t samples) const;
void WriteGuestOcclusionResult(uint32_t sample_count_address, uint64_t samples); 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 InvalidateAllVertexBufferResidency();
void InvalidateVertexBufferResidency(uint32_t vfetch_index); void InvalidateVertexBufferResidency(uint32_t vfetch_index);
void InvalidateVertexBufferResidencyRange(uint32_t first_vfetch, uint32_t last_vfetch); void InvalidateVertexBufferResidencyRange(uint32_t first_vfetch, uint32_t last_vfetch);
@ -747,6 +759,32 @@ class VulkanCommandProcessor : public CommandProcessor {
uint32_t host_index = UINT32_MAX; uint32_t host_index = UINT32_MAX;
bool valid = false; bool valid = false;
} active_occlusion_query_; } 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<rex::perf::DrawBucket> buckets;
std::vector<uint32_t> bucket_start_queries;
std::vector<uint32_t> bucket_end_queries;
};
std::array<GpuTimestampFrame, kMaxFramesInFlight> gpu_timestamp_frames_;
struct VertexBufferState { struct VertexBufferState {
uint32_t address = UINT32_MAX; uint32_t address = UINT32_MAX;
uint32_t size = UINT32_MAX; uint32_t size = UINT32_MAX;

View file

@ -351,6 +351,15 @@ class DeferredCommandBuffer {
std::memcpy(args_ptr + header_size, viewports, sizeof(VkViewport) * viewport_count); 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<ArgsVkWriteTimestamp*>(
WriteCommand(Command::kVkWriteTimestamp, sizeof(ArgsVkWriteTimestamp)));
args.pipeline_stage = pipeline_stage;
args.query_pool = query_pool;
args.query = query;
}
private: private:
enum class Command { enum class Command {
kVkBeginRenderPass, kVkBeginRenderPass,
@ -381,6 +390,7 @@ class DeferredCommandBuffer {
kVkSetStencilReference, kVkSetStencilReference,
kVkSetStencilWriteMask, kVkSetStencilWriteMask,
kVkSetViewport, kVkSetViewport,
kVkWriteTimestamp,
}; };
struct CommandHeader { struct CommandHeader {
@ -526,6 +536,12 @@ class DeferredCommandBuffer {
uint32_t query_count; uint32_t query_count;
}; };
struct ArgsVkWriteTimestamp {
VkPipelineStageFlagBits pipeline_stage;
VkQueryPool query_pool;
uint32_t query;
};
struct ArgsVkPipelineBarrier { struct ArgsVkPipelineBarrier {
VkPipelineStageFlags src_stage_mask; VkPipelineStageFlags src_stage_mask;
VkPipelineStageFlags dst_stage_mask; VkPipelineStageFlags dst_stage_mask;

View file

@ -682,7 +682,7 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
// need to be dumped). Using 8x16 since that's 128 - the minimum required // 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 // group size on Vulkan, and the maximum number of lanes in a subgroup on
// Vulkan. // Vulkan.
static constexpr uint32_t kDumpSamplesPerGroupX = 8; static constexpr uint32_t kDumpSamplesPerGroupX = 40;
static constexpr uint32_t kDumpSamplesPerGroupY = 16; static constexpr uint32_t kDumpSamplesPerGroupY = 16;
union DumpPitches { union DumpPitches {

View file

@ -75,6 +75,7 @@ class SDLInputDriver final : public InputDriver, public rex::ui::WindowListener
void HandleEvent(const SDL_Event& event); void HandleEvent(const SDL_Event& event);
std::unique_lock<std::mutex> DrainAndLock(); std::unique_lock<std::mutex> DrainAndLock();
void ProcessEventLocked(const SDL_Event& event); void ProcessEventLocked(const SDL_Event& event);
void StopRumbleLocked(ControllerState& state);
void OnControllerDeviceAddedLocked(const SDL_Event& event); void OnControllerDeviceAddedLocked(const SDL_Event& event);
void OnControllerDeviceRemovedLocked(const SDL_Event& event); void OnControllerDeviceRemovedLocked(const SDL_Event& event);
void OnControllerDeviceAxisMotionLocked(const SDL_Event& event); void OnControllerDeviceAxisMotionLocked(const SDL_Event& event);

View file

@ -29,6 +29,11 @@ enum class DrawBucket : uint8_t {
kCopyResolve, kCopyResolve,
kMemexport, kMemexport,
kNoPixelShader, kNoPixelShader,
kCopyDump,
kCopyResolveShader,
kCopyResolveFast32,
kCopyResolveFull32,
kResolveDownscale,
}; };
struct DrawFingerprint { struct DrawFingerprint {
@ -91,6 +96,11 @@ enum class CounterId : uint16_t {
kGpuCopyUs, kGpuCopyUs,
kGpuMemexportUs, kGpuMemexportUs,
kGpuNoPixelShaderUs, kGpuNoPixelShaderUs,
kGpuCopyDumpUs,
kGpuCopyResolveUs,
kGpuCopyResolveFast32Us,
kGpuCopyResolveFull32Us,
kGpuResolveDownscaleUs,
kGpuTimestampedDraws, kGpuTimestampedDraws,
kGpuCommandProcessorFrameUs, kGpuCommandProcessorFrameUs,
kGpuTimestampedFrames, kGpuTimestampedFrames,
@ -178,6 +188,37 @@ enum class CounterId : uint16_t {
kRtResolveTransferClearUs, kRtResolveTransferClearUs,
kRtResolveReadbackCalls, kRtResolveReadbackCalls,
kRtResolveReadbackUs, 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 // Audio
kXmaFramesDecoded, kXmaFramesDecoded,

View file

@ -56,6 +56,14 @@ inline constexpr const char* kVulkanLoader = "libvulkan.so.1";
inline constexpr const char* kRenderDoc = "librenderdoc.so"; inline constexpr const char* kRenderDoc = "librenderdoc.so";
inline constexpr const char* kSpirvToolsSdkPath = "bin/libSPIRV-Tools-shared.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 #else
#error No library names provided for the target platform. #error No library names provided for the target platform.
#endif #endif

View file

@ -75,7 +75,10 @@ struct FPSCRPlatform {
return csr; 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 { static inline void InitHostExceptions(u32& csr) noexcept {
csr &= ~ExceptionMask; // Clear enable bits to disable exceptions csr &= ~ExceptionMask; // Clear enable bits to disable exceptions

View file

@ -205,7 +205,8 @@ struct FPSCRRegister {
Platform::InitHostExceptions(fpu_csr); Platform::InitHostExceptions(fpu_csr);
fpu_csr &= ~FlushMask; fpu_csr &= ~FlushMask;
if (vmx_csr == 0) { if (vmx_csr == 0) {
vmx_csr = ExceptionMask | FlushMask | Platform::GuestToHost[kRoundNearest]; vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
Platform::InitHostExceptions(vmx_csr);
} }
csr = fpu_csr; csr = fpu_csr;
} }
@ -216,7 +217,7 @@ struct FPSCRRegister {
fpu_csr &= ~RoundMaskVal; fpu_csr &= ~RoundMaskVal;
fpu_csr |= Platform::GuestToHost[value & kRoundMask]; fpu_csr |= Platform::GuestToHost[value & kRoundMask];
fpu_csr &= ~FlushMask; fpu_csr &= ~FlushMask;
fpu_csr |= ExceptionMask; Platform::InitHostExceptions(fpu_csr);
csr = fpu_csr; csr = fpu_csr;
vmx_mode = false; vmx_mode = false;
setcsr(csr); setcsr(csr);
@ -254,7 +255,8 @@ struct FPSCRRegister {
fpu_csr = getcsr(); fpu_csr = getcsr();
Platform::InitHostExceptions(fpu_csr); Platform::InitHostExceptions(fpu_csr);
fpu_csr &= ~FlushMask; fpu_csr &= ~FlushMask;
vmx_csr = ExceptionMask | FlushMask | Platform::GuestToHost[kRoundNearest]; vmx_csr = FlushMask | Platform::GuestToHost[kRoundNearest];
Platform::InitHostExceptions(vmx_csr);
csr = fpu_csr; csr = fpu_csr;
vmx_mode = false; vmx_mode = false;
setcsr(csr); setcsr(csr);

View file

@ -22,7 +22,11 @@
#include <thread> #include <thread>
#include <rex/image_info.h> #include <rex/image_info.h>
#include <rex/platform.h>
#include <rex/runtime.h> #include <rex/runtime.h>
#if REX_PLATFORM_MAC
#include <rex/system/xthread.h>
#endif
#include <rex/ui/imgui_dialog.h> #include <rex/ui/imgui_dialog.h>
#include <rex/ui/imgui_drawer.h> #include <rex/ui/imgui_drawer.h>
#include <rex/ui/immediate_drawer.h> #include <rex/ui/immediate_drawer.h>
@ -221,6 +225,9 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win
std::filesystem::path cache_root_; std::filesystem::path cache_root_;
std::unique_ptr<Runtime> runtime_; std::unique_ptr<Runtime> runtime_;
std::unique_ptr<ui::Window> window_; std::unique_ptr<ui::Window> window_;
#if REX_PLATFORM_MAC
system::object_ref<system::XThread> main_thread_;
#endif
std::thread module_thread_; std::thread module_thread_;
std::atomic<bool> shutting_down_{false}; std::atomic<bool> shutting_down_{false};
std::unique_ptr<ui::ImmediateDrawer> immediate_drawer_; std::unique_ptr<ui::ImmediateDrawer> immediate_drawer_;

76
include/rex/std_compat.h Normal file
View file

@ -0,0 +1,76 @@
#pragma once
#include <functional>
#include <memory>
#include <type_traits>
#include <utility>
#ifndef __cpp_lib_move_only_function
namespace std {
template <typename>
class move_only_function;
template <typename R, typename... Args>
class move_only_function<R(Args...)> {
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 <typename F>
requires(!std::is_same_v<std::remove_cvref_t<F>, move_only_function> &&
!std::is_same_v<std::remove_cvref_t<F>, nullptr_t> &&
std::is_invocable_r_v<R, F&, Args...>)
move_only_function(F&& f)
: callable_(std::make_unique<Callable<std::remove_cvref_t<F>>>(std::forward<F>(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<bool>(f);
}
friend bool operator==(nullptr_t, const move_only_function& f) noexcept {
return !static_cast<bool>(f);
}
friend bool operator!=(const move_only_function& f, nullptr_t) noexcept {
return static_cast<bool>(f);
}
friend bool operator!=(nullptr_t, const move_only_function& f) noexcept {
return static_cast<bool>(f);
}
R operator()(Args... args) {
return callable_->Call(std::forward<Args>(args)...);
}
private:
struct CallableBase {
virtual ~CallableBase() = default;
virtual R Call(Args&&... args) = 0;
};
template <typename F>
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>(args)...); }
F f_;
};
std::unique_ptr<CallableBase> callable_;
};
} // namespace std
#endif

View file

@ -25,6 +25,7 @@
#include <rex/filesystem/vfs.h> #include <rex/filesystem/vfs.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/std_compat.h>
#include <rex/system/thread_state.h> #include <rex/system/thread_state.h>
#include <rex/thread/fiber.h> #include <rex/thread/fiber.h>
#include <rex/system/util/native_list.h> #include <rex/system/util/native_list.h>
@ -278,7 +279,6 @@ class KernelState {
void OnThreadExecute(XThread* thread); void OnThreadExecute(XThread* thread);
void OnThreadExit(XThread* thread); void OnThreadExit(XThread* thread);
object_ref<XThread> GetThreadByID(uint32_t thread_id); object_ref<XThread> GetThreadByID(uint32_t thread_id);
FiberInfo* LookupFiber(uint32_t guest_addr); FiberInfo* LookupFiber(uint32_t guest_addr);
void RegisterFiber(uint32_t guest_addr, const FiberInfo& info); void RegisterFiber(uint32_t guest_addr, const FiberInfo& info);
void UnregisterFiber(uint32_t guest_addr); void UnregisterFiber(uint32_t guest_addr);

View file

@ -28,15 +28,12 @@ class ByteStream;
namespace rex::memory::detail { namespace rex::memory::detail {
/// Compensates for Windows 64KB allocation granularity on the 0xE0 physical heap. /// Compensates for host allocation granularities larger than Xenon's 4 KB
/// The backing file maps the 0xE0 heap at a 0x1000-byte offset, but MapViewOfFileEx /// pages on the 0xE0000000 physical heap. The memory views are rounded down to
/// rounds down to 64KB boundaries. Linux mmap handles 4KB offsets natively. /// the host granularity, while the heap tracks the requested 4 KB-aligned guest
constexpr u32 PhysicalHostOffset([[maybe_unused]] u32 guest_addr) noexcept { /// range via host_address_offset.
#if REX_PLATFORM_WIN32 inline u32 PhysicalHostOffset(u32 guest_addr) noexcept {
return (guest_addr >= 0xE0000000u) ? 0x1000u : 0u; return (guest_addr >= 0xE0000000u && allocation_granularity() > 0x1000) ? 0x1000u : 0u;
#else
return 0u;
#endif
} }
} // namespace rex::memory::detail } // namespace rex::memory::detail

View file

@ -14,10 +14,13 @@
#include <rex/platform.h> #include <rex/platform.h>
#include <cstddef> #include <cstddef>
#if REX_PLATFORM_LINUX || REX_PLATFORM_MAC #if REX_PLATFORM_LINUX
#include <ucontext.h> #include <ucontext.h>
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#elif REX_PLATFORM_MAC
#include <cstdint>
#include <vector>
#endif #endif
namespace rex::thread { namespace rex::thread {
@ -58,6 +61,14 @@ struct Fiber {
void* arg_ = nullptr; void* arg_ = nullptr;
bool is_thread_fiber_ = false; bool is_thread_fiber_ = false;
static void Trampoline();
#elif REX_PLATFORM_MAC
void* context_ = nullptr;
std::vector<uint8_t> stack_;
void (*entry_)(void*) = nullptr;
void* arg_ = nullptr;
bool is_thread_fiber_ = false;
static void Trampoline(); static void Trampoline();
#endif #endif
}; };

View file

@ -16,6 +16,8 @@
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <rex/std_compat.h>
// This is a platform independent implementation of a timer queue similar to // This is a platform independent implementation of a timer queue similar to
// Windows CreateTimerQueueTimer with WT_EXECUTEINTIMERTHREAD. // Windows CreateTimerQueueTimer with WT_EXECUTEINTIMERTHREAD.

View file

@ -33,11 +33,14 @@ class Surface {
kTypeIndex_XcbWindow, kTypeIndex_XcbWindow,
// Windows. // Windows.
kTypeIndex_Win32Hwnd, kTypeIndex_Win32Hwnd,
// macOS.
kTypeIndex_SDLMetalView,
}; };
using TypeFlags = uint32_t; using TypeFlags = uint32_t;
enum : TypeFlags { enum : TypeFlags {
kTypeFlag_AndroidNativeWindow = TypeFlags(1) << kTypeIndex_AndroidNativeWindow, kTypeFlag_AndroidNativeWindow = TypeFlags(1) << kTypeIndex_AndroidNativeWindow,
kTypeFlag_XcbWindow = TypeFlags(1) << kTypeIndex_XcbWindow, kTypeFlag_XcbWindow = TypeFlags(1) << kTypeIndex_XcbWindow,
kTypeFlag_SDLMetalView = TypeFlags(1) << kTypeIndex_SDLMetalView,
kTypeFlag_Win32Hwnd = TypeFlags(1) << kTypeIndex_Win32Hwnd, kTypeFlag_Win32Hwnd = TypeFlags(1) << kTypeIndex_Win32Hwnd,
}; };

View file

@ -0,0 +1,31 @@
#pragma once
#include <rex/ui/surface.h>
#include <SDL3/SDL_metal.h>
#include <SDL3/SDL_video.h>
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

View file

@ -38,6 +38,12 @@
#endif #endif
#endif #endif
#if REX_PLATFORM_MAC
#ifndef VK_USE_PLATFORM_METAL_EXT
#define VK_USE_PLATFORM_METAL_EXT
#endif
#endif
#if REX_PLATFORM_WIN32 #if REX_PLATFORM_WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN

View file

@ -93,6 +93,8 @@ class VulkanDevice {
VkDeviceSize optimalBufferCopyOffsetAlignment = 1; VkDeviceSize optimalBufferCopyOffsetAlignment = 1;
VkDeviceSize optimalBufferCopyRowPitchAlignment = 1; VkDeviceSize optimalBufferCopyRowPitchAlignment = 1;
VkDeviceSize nonCoherentAtomSize = 256; VkDeviceSize nonCoherentAtomSize = 256;
bool timestampComputeAndGraphics = false;
float timestampPeriod = 0.0f;
bool robustBufferAccess = false; bool robustBufferAccess = false;
bool fullDrawIndexUint32 = false; bool fullDrawIndexUint32 = false;

View file

@ -32,6 +32,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdSetStencilCompareMask)
XE_UI_VULKAN_FUNCTION(vkCmdSetStencilReference) XE_UI_VULKAN_FUNCTION(vkCmdSetStencilReference)
XE_UI_VULKAN_FUNCTION(vkCmdSetStencilWriteMask) XE_UI_VULKAN_FUNCTION(vkCmdSetStencilWriteMask)
XE_UI_VULKAN_FUNCTION(vkCmdSetViewport) XE_UI_VULKAN_FUNCTION(vkCmdSetViewport)
XE_UI_VULKAN_FUNCTION(vkCmdWriteTimestamp)
XE_UI_VULKAN_FUNCTION(vkCreateBuffer) XE_UI_VULKAN_FUNCTION(vkCreateBuffer)
XE_UI_VULKAN_FUNCTION(vkCreateBufferView) XE_UI_VULKAN_FUNCTION(vkCreateBufferView)
XE_UI_VULKAN_FUNCTION(vkCreateCommandPool) XE_UI_VULKAN_FUNCTION(vkCreateCommandPool)

View file

@ -0,0 +1,2 @@
// VK_EXT_metal_surface functions used in ReXGlue.
XE_UI_VULKAN_FUNCTION(vkCreateMetalSurfaceEXT)

View file

@ -66,6 +66,10 @@ class VulkanInstance {
// VK_KHR_win32_surface (#10) // VK_KHR_win32_surface (#10)
#ifdef VK_USE_PLATFORM_WIN32_KHR #ifdef VK_USE_PLATFORM_WIN32_KHR
#include <rex/ui/vulkan/functions/instance_khr_win32_surface.inc> #include <rex/ui/vulkan/functions/instance_khr_win32_surface.inc>
#endif
// VK_EXT_metal_surface (#218)
#ifdef VK_USE_PLATFORM_METAL_EXT
#include <rex/ui/vulkan/functions/instance_ext_metal_surface.inc>
#endif #endif
// VK_KHR_get_physical_device_properties2 (#60, promoted to 1.1) // VK_KHR_get_physical_device_properties2 (#60, promoted to 1.1)
#include <rex/ui/vulkan/functions/instance_1_1_khr_get_physical_device_properties2.inc> #include <rex/ui/vulkan/functions/instance_1_1_khr_get_physical_device_properties2.inc>
@ -91,6 +95,9 @@ class VulkanInstance {
#endif #endif
#ifdef VK_USE_PLATFORM_WIN32_KHR #ifdef VK_USE_PLATFORM_WIN32_KHR
bool ext_KHR_win32_surface = false; // #10 bool ext_KHR_win32_surface = false; // #10
#endif
#ifdef VK_USE_PLATFORM_METAL_EXT
bool ext_EXT_metal_surface = false; // #218
#endif #endif
bool ext_1_1_KHR_get_physical_device_properties2 = false; // #60 bool ext_1_1_KHR_get_physical_device_properties2 = false; // #60
bool ext_EXT_debug_utils = false; // #129 bool ext_EXT_debug_utils = false; // #129

View file

@ -0,0 +1,77 @@
#pragma once
#include <memory>
#include <string>
#include <atomic>
#include <rex/ui/menu_item.h>
#include <rex/ui/window.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_metal.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_video.h>
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<Surface> 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<bool> 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<void()> callback)
: MenuItem(type, text, hotkey, std::move(callback)) {}
};
} // namespace ui
} // namespace rex

View file

@ -0,0 +1,29 @@
#pragma once
#include <atomic>
#include <rex/ui/windowed_app_context.h>
#include <SDL3/SDL_events.h>
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

View file

@ -60,9 +60,18 @@ extern PPCFuncMapping {{ project }}_PPCFuncMappings[];
REX_EXTERN(name); \ REX_EXTERN(name); \
REX_EXTERN(__imp__##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) \ #define DEFINE_REX_FUNC(name) \
__attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \ __attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \
REX_EXTERN(__imp__##name) REX_EXTERN(__imp__##name)
#endif
//============================================================================= //=============================================================================
// Function Prologue // Function Prologue

View file

@ -55,10 +55,19 @@
#endif #endif
#ifndef DEFINE_REX_FUNC #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) \ #define DEFINE_REX_FUNC(name) \
__attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \ __attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \
REX_EXTERN(__imp__##name) REX_EXTERN(__imp__##name)
#endif #endif
#endif
#if REX_PLATFORM_WIN32 #if REX_PLATFORM_WIN32
#define REX_PHYS_HOST_OFFSET(addr) (((u32)(addr) >= 0xE0000000u) ? 0x1000u : 0u) #define REX_PHYS_HOST_OFFSET(addr) (((u32)(addr) >= 0xE0000000u) ? 0x1000u : 0u)
@ -177,4 +186,3 @@ inline std::atomic<i32>& ppc_global_lock_count_() {
assert(old_count_ >= 1 && "LeaveGlobalLock called without matching EnterGlobalLock"); \ assert(old_count_ >= 1 && "LeaveGlobalLock called without matching EnterGlobalLock"); \
rex::thread::global_critical_region::mutex().unlock(); \ rex::thread::global_critical_region::mutex().unlock(); \
} while (0) } while (0)

View file

@ -130,11 +130,10 @@ bool build_vrefp(BuilderContext& ctx) {
} }
bool build_vrsqrtefp(BuilderContext& ctx) { bool build_vrsqrtefp(BuilderContext& ctx) {
// Match Xenia's OPCODE_RSQRT lowering. The x86 estimate is not accurate // Match the Xenon reciprocal square-root estimate semantics instead of the
// enough for collision-sensitive code, so use sqrt + divide. // host's exact sqrt/divide behavior.
ctx.emit_set_flush_mode(true); ctx.emit_set_flush_mode(true);
ctx.emit_vec_fp_unary_expr( ctx.emit_vec_fp_unary_expr("rex::ppc::simde_mm_vrsqrtefp_ps(simde_mm_load_ps({vA}.f32))");
"simde_mm_div_ps(simde_mm_set1_ps(1.0f), simde_mm_sqrt_ps(simde_mm_load_ps({vA}.f32)))");
return true; return true;
} }

View file

@ -76,7 +76,10 @@ target_include_directories(rexcore PRIVATE
) )
if(UNIX) 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) elseif(WIN32)
target_link_libraries(rexcore PUBLIC ws2_32) target_link_libraries(rexcore PUBLIC ws2_32)
endif() endif()

View file

@ -16,19 +16,27 @@ static_assert(REX_PLATFORM_LINUX || REX_PLATFORM_MAC, "This file is POSIX-only")
namespace rex::chrono { 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() { uint64_t Clock::host_tick_frequency_platform() {
timespec res; timespec res;
int error = clock_getres(CLOCK_MONOTONIC_RAW, &res); int error = clock_getres(kHostClock, &res);
assert_zero(error); assert_zero(error);
assert_zero(res.tv_sec); // Sub second resolution is required. 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. // host_tick_count_platform returns elapsed nanoseconds, not ticks in units of
return 1000000000ull / res.tv_nsec; // clock_getres. Report the frequency for those returned units.
return 1000000000ull;
} }
uint64_t Clock::host_tick_count_platform() { uint64_t Clock::host_tick_count_platform() {
timespec tp; timespec tp;
int error = clock_gettime(CLOCK_MONOTONIC_RAW, &tp); int error = clock_gettime(kHostClock, &tp);
assert_zero(error); assert_zero(error);
return tp.tv_nsec + tp.tv_sec * 1000000000ull; return tp.tv_nsec + tp.tv_sec * 1000000000ull;

View file

@ -49,6 +49,17 @@ std::unordered_map<std::string, size_t>& GetRegistryIndex() {
return index; return index;
} }
std::vector<std::pair<std::string, std::string>>& GetCommandLineOverrides() {
static std::vector<std::pair<std::string, std::string>> 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 // Convert flag name to environment variable: gpu_vsync -> REX_GPU_VSYNC
std::string FlagNameToEnvVar(std::string_view name) { std::string FlagNameToEnvVar(std::string_view name) {
std::string result = "REX_"; std::string result = "REX_";
@ -538,11 +549,19 @@ std::vector<std::string> Init(int argc, char** argv) {
if (entry.type == FlagType::Boolean) { if (entry.type == FlagType::Boolean) {
app.add_flag_function( app.add_flag_function(
"--" + entry.name + ",!--no-" + entry.name, "--" + 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); entry.description);
} else { } else {
app.add_option_function<std::string>( app.add_option_function<std::string>(
"--" + 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); entry.description);
} }
} }
@ -567,6 +586,8 @@ void LoadConfig(const std::filesystem::path& config_path) {
try { try {
auto config = toml::parse_file(config_path.string()); auto config = toml::parse_file(config_path.string());
ApplyTomlTable(config, ""); ApplyTomlTable(config, "");
ApplyCommandLineOverrides();
ApplyEnvironment();
REXLOG_INFO("Loaded config from {}", config_path.string()); REXLOG_INFO("Loaded config from {}", config_path.string());
} catch (const toml::parse_error& err) { } catch (const toml::parse_error& err) {
REXLOG_ERROR("Failed to parse config {}: {}", config_path.string(), err.what()); REXLOG_ERROR("Failed to parse config {}: {}", config_path.string(), err.what());

View file

@ -9,12 +9,17 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/ */
#if defined(__APPLE__) && !defined(_XOPEN_SOURCE)
#define _XOPEN_SOURCE 700
#endif
#include <rex/exception_handler.h> #include <rex/exception_handler.h>
#if REX_PLATFORM_LINUX || REX_PLATFORM_MAC #if REX_PLATFORM_LINUX || REX_PLATFORM_MAC
#include <signal.h> #include <signal.h>
#include <cstdlib>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
@ -30,6 +35,7 @@ namespace rex::arch {
bool signal_handlers_installed_ = false; bool signal_handlers_installed_ = false;
struct sigaction original_sigill_handler_; struct sigaction original_sigill_handler_;
struct sigaction original_sigsegv_handler_; struct sigaction original_sigsegv_handler_;
struct sigaction original_sigbus_handler_;
// This can be as large as needed, but isn't often needed. // 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 // 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. // Executed in order.
std::pair<ExceptionHandler::Handler, void*> handlers_[kMaxHandlerCount]; std::pair<ExceptionHandler::Handler, void*> 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, static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
void* signal_context) { void* signal_context) {
#if REX_PLATFORM_MAC
mcontext_t mcontext = reinterpret_cast<ucontext_t*>(signal_context)->uc_mcontext;
#else
mcontext_t& mcontext = reinterpret_cast<ucontext_t*>(signal_context)->uc_mcontext; mcontext_t& mcontext = reinterpret_cast<ucontext_t*>(signal_context)->uc_mcontext;
#endif
HostThreadContext thread_context; 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, std::memcpy(thread_context.xmm_registers, mcontext.fpregs->_xmm,
sizeof(thread_context.xmm_registers)); sizeof(thread_context.xmm_registers));
#elif REX_ARCH_ARM64 #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)); std::memcpy(thread_context.x, mcontext.regs, sizeof(thread_context.x));
thread_context.sp = mcontext.sp; thread_context.sp = mcontext.sp;
thread_context.pc = mcontext.pc; 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; thread_context.fpcr = mcontext_fpsimd->fpcr;
std::memcpy(thread_context.v, mcontext_fpsimd->vregs, sizeof(thread_context.v)); std::memcpy(thread_context.v, mcontext_fpsimd->vregs, sizeof(thread_context.v));
} }
#endif // REX_PLATFORM_MAC
#endif // REX_ARCH #endif // REX_ARCH
Exception ex; Exception ex;
@ -105,7 +161,8 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
case SIGILL: case SIGILL:
ex.InitializeIllegalInstruction(&thread_context); ex.InitializeIllegalInstruction(&thread_context);
break; break;
case SIGSEGV: { case SIGSEGV:
case SIGBUS: {
Exception::AccessViolationOperation access_violation_operation; Exception::AccessViolationOperation access_violation_operation;
#if REX_ARCH_AMD64 #if REX_ARCH_AMD64
// x86_pf_error_code::X86_PF_WRITE // 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::kWrite
: Exception::AccessViolationOperation::kRead; : Exception::AccessViolationOperation::kRead;
#elif REX_ARCH_ARM64 #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<const uint32_t*>(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 // For a Data Abort (EC - ESR_EL1 bits 31:26 - 0b100100 from a lower
// Exception Level, 0b100101 without a change in the Exception Level), // 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 // 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; access_violation_operation = Exception::AccessViolationOperation::kUnknown;
} }
} }
#endif // REX_PLATFORM_MAC
#else #else
access_violation_operation = Exception::AccessViolationOperation::kUnknown; access_violation_operation = Exception::AccessViolationOperation::kUnknown;
#endif // REX_ARCH #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)); &thread_context.xmm_registers[modified_register_index], sizeof(vec128_t));
} }
#elif REX_ARCH_ARM64 #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_register_index;
uint32_t modified_x_registers_remaining = ex.modified_x_registers(); uint32_t modified_x_registers_remaining = ex.modified_x_registers();
while (rex::bit_scan_forward(modified_x_registers_remaining, &modified_register_index)) { 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]; mcontext.regs[modified_register_index] = thread_context.x[modified_register_index];
} }
} }
#endif // REX_PLATFORM_MAC
#endif // REX_ARCH #endif // REX_ARCH
return; return;
} }
} }
DispatchUnhandledSignal(signal_number, signal_info, signal_context);
} }
void ExceptionHandler::Install(Handler fn, void* data) { 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) { if (sigaction(SIGSEGV, &signal_handler, &original_sigsegv_handler_) != 0) {
assert_always("Failed to install new SIGSEGV handler"); 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; signal_handlers_installed_ = true;
} }
@ -261,6 +371,9 @@ void ExceptionHandler::Uninstall(Handler fn, void* data) {
if (sigaction(SIGSEGV, &original_sigsegv_handler_, NULL) != 0) { if (sigaction(SIGSEGV, &original_sigsegv_handler_, NULL) != 0) {
assert_always("Failed to restore original SIGSEGV handler"); 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; signal_handlers_installed_ = false;
} }
} }

View file

@ -9,6 +9,10 @@
* See LICENSE file in the project root for full license text. * See LICENSE file in the project root for full license text.
*/ */
#if defined(__APPLE__) && !defined(_XOPEN_SOURCE)
#define _XOPEN_SOURCE 700
#endif
#include <rex/platform.h> #include <rex/platform.h>
#if REX_PLATFORM_LINUX || REX_PLATFORM_MAC #if REX_PLATFORM_LINUX || REX_PLATFORM_MAC
@ -21,9 +25,25 @@ namespace rex::thread {
thread_local Fiber* Fiber::tls_current_ = nullptr; thread_local Fiber* Fiber::tls_current_ = nullptr;
namespace {
#if REX_PLATFORM_MAC
ucontext_t* ToContext(void* context) { return static_cast<ucontext_t*>(context); }
#else
ucontext_t* ToContext(ucontext_t& context) { return &context; }
#endif
} // namespace
Fiber* Fiber::ConvertCurrentThread() { Fiber* Fiber::ConvertCurrentThread() {
auto* f = new Fiber(); 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; delete f;
return nullptr; return nullptr;
} }
@ -37,16 +57,23 @@ Fiber* Fiber::Create(size_t stack_size, void (*entry)(void*), void* arg) {
f->entry_ = entry; f->entry_ = entry;
f->arg_ = arg; f->arg_ = arg;
f->stack_.resize(stack_size); 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; delete f;
return nullptr; return nullptr;
} }
f->context_.uc_stack.ss_sp = f->stack_.data(); context->uc_stack.ss_sp = f->stack_.data();
f->context_.uc_stack.ss_size = f->stack_.size(); context->uc_stack.ss_size = f->stack_.size();
f->context_.uc_link = nullptr; context->uc_link = nullptr;
// Trampoline reads entry_/arg_ from tls_current_ — no pointer splitting needed. // Trampoline reads entry_/arg_ from tls_current_ — no pointer splitting needed.
makecontext(&f->context_, &Fiber::Trampoline, 0); makecontext(context, &Fiber::Trampoline, 0);
return f; return f;
} }
@ -59,7 +86,7 @@ Fiber* Fiber::Create(size_t stack_size, void (*entry)(void*), void* arg) {
void Fiber::SwitchTo(Fiber* target) { void Fiber::SwitchTo(Fiber* target) {
Fiber* from = tls_current_; Fiber* from = tls_current_;
tls_current_ = target; tls_current_ = target;
swapcontext(&from->context_, &target->context_); swapcontext(ToContext(from->context_), ToContext(target->context_));
} }
void Fiber::Destroy() { void Fiber::Destroy() {
@ -70,6 +97,10 @@ void Fiber::Destroy() {
assert(this != tls_current_ && "Destroy called on the currently running fiber"); assert(this != tls_current_ && "Destroy called on the currently running fiber");
} }
// No POSIX equivalent of ConvertFiberToThread; stack_ is freed by the vector destructor. // No POSIX equivalent of ConvertFiberToThread; stack_ is freed by the vector destructor.
#if REX_PLATFORM_MAC
delete ToContext(context_);
context_ = nullptr;
#endif
delete this; delete this;
} }

View file

@ -10,10 +10,13 @@
*/ */
#include <assert.h> #include <assert.h>
#include <limits.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <iostream> #include <iostream>
#include <vector>
#include <fcntl.h> #include <fcntl.h>
#include <sys/stat.h> #include <sys/stat.h>
@ -23,6 +26,7 @@
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/filesystem.h> #include <rex/filesystem.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/platform.h>
#include <rex/string.h> #include <rex/string.h>
#include <dirent.h> #include <dirent.h>
@ -30,6 +34,15 @@
#include <libgen.h> #include <libgen.h>
#include <pwd.h> #include <pwd.h>
#if REX_PLATFORM_MAC
#include <mach-o/dyld.h>
using off64_t = off_t;
#define fseeko64 fseeko
#define ftello64 ftello
#define ftruncate64 ftruncate
#endif
namespace rex { namespace rex {
std::string path_to_utf8(const std::filesystem::path& path) { 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 { namespace filesystem {
std::filesystem::path GetExecutablePath() { std::filesystem::path GetExecutablePath() {
#if REX_PLATFORM_MAC
uint32_t size = PATH_MAX;
std::vector<char> 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] = ""; 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); std::string s(buff);
return s; return s;
#endif
} }
std::filesystem::path GetExecutableFolder() { std::filesystem::path GetExecutableFolder() {

View file

@ -20,6 +20,13 @@
#include <rex/memory/mapped_memory.h> #include <rex/memory/mapped_memory.h>
#include <rex/platform.h> #include <rex/platform.h>
#if REX_PLATFORM_MAC
using off64_t = off_t;
#define stat64 stat
#define fstat64 fstat
#define ftruncate64 ftruncate
#endif
namespace rex::memory { namespace rex::memory {
class PosixMappedMemory : public MappedMemory { class PosixMappedMemory : public MappedMemory {

View file

@ -38,9 +38,30 @@
// #include "xenia/base/main_android.h" // #include "xenia/base/main_android.h"
#endif #endif
#if REX_PLATFORM_MAC
#define ftruncate64 ftruncate
#define mmap64 mmap
#endif
namespace rex { namespace rex {
namespace memory { 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<uintptr_t>(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<void*>(aligned_start);
length = static_cast<size_t>(aligned_end - aligned_start);
}
#endif
} // namespace
// Convert filesystem path to valid shm_open name (must start with /, no other slashes) // Convert filesystem path to valid shm_open name (must start with /, no other slashes)
static std::string MakeShmName(const std::filesystem::path& path) { static std::string MakeShmName(const std::filesystem::path& path) {
std::string name = path.string(); std::string name = path.string();
@ -222,6 +243,19 @@ void* AllocFixed(void* base_address, size_t length, AllocationType allocation_ty
break; 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<int>(prot_requested)) == 0) {
return base_address;
}
}
#endif
// Build flags - always use MAP_FIXED_NOREPLACE for fixed addresses // Build flags - always use MAP_FIXED_NOREPLACE for fixed addresses
int flags = MAP_PRIVATE | MAP_ANONYMOUS; int flags = MAP_PRIVATE | MAP_ANONYMOUS;
#if defined(MAP_FIXED_NOREPLACE) #if defined(MAP_FIXED_NOREPLACE)
@ -260,6 +294,9 @@ bool DeallocFixed(void* base_address, size_t length, DeallocationType deallocati
switch (deallocation_type) { switch (deallocation_type) {
case DeallocationType::kDecommit: { case DeallocationType::kDecommit: {
// Decommit: remove access first, then release physical pages // 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) { if (mprotect(base_address, length, PROT_NONE) != 0) {
return false; return false;
} }
@ -298,6 +335,9 @@ bool Protect(void* base_address, size_t length, PageAccess access, PageAccess* o
#endif #endif
uint32_t prot = ToPosixProtectFlags(access); uint32_t prot = ToPosixProtectFlags(access);
#if REX_PLATFORM_MAC
AlignHostPageRange(base_address, length);
#endif
return mprotect(base_address, length, prot) == 0; return mprotect(base_address, length, prot) == 0;
} }

View file

@ -112,6 +112,12 @@ bool IsDelayedGpuTimingCounter(size_t counter_index) {
return id == CounterId::kGpuMainUs || id == CounterId::kGpuDepthUs || return id == CounterId::kGpuMainUs || id == CounterId::kGpuDepthUs ||
id == CounterId::kGpuCopyUs || id == CounterId::kGpuMemexportUs || id == CounterId::kGpuCopyUs || id == CounterId::kGpuMemexportUs ||
id == CounterId::kGpuNoPixelShaderUs || 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::kGpuCommandProcessorFrameUs ||
id == CounterId::kGpuTimestampedFrames || id == CounterId::kGpuTimestampedFrames ||
id == CounterId::kGpuBarrierUs || id == CounterId::kGpuClearUs || id == CounterId::kGpuBarrierUs || id == CounterId::kGpuClearUs ||
@ -158,6 +164,11 @@ constexpr const char* kCounterNames[] = {
"gpu_copy_us", "gpu_copy_us",
"gpu_memexport_us", "gpu_memexport_us",
"gpu_no_pixel_shader_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_timestamped_draws",
"gpu_command_processor_frame_us", "gpu_command_processor_frame_us",
"gpu_timestamped_frames", "gpu_timestamped_frames",
@ -245,6 +256,37 @@ constexpr const char* kCounterNames[] = {
"rt_resolve_transfer_clear_us", "rt_resolve_transfer_clear_us",
"rt_resolve_readback_calls", "rt_resolve_readback_calls",
"rt_resolve_readback_us", "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", "xma_frames_decoded",
"audio_frame_latency_us", "audio_frame_latency_us",
"buffer_queue_depth", "buffer_queue_depth",
@ -300,6 +342,11 @@ constexpr bool kIsGauge[] = {
false, // kGpuCopyUs false, // kGpuCopyUs
false, // kGpuMemexportUs false, // kGpuMemexportUs
false, // kGpuNoPixelShaderUs false, // kGpuNoPixelShaderUs
false, // kGpuCopyDumpUs
false, // kGpuCopyResolveUs
false, // kGpuCopyResolveFast32Us
false, // kGpuCopyResolveFull32Us
false, // kGpuResolveDownscaleUs
false, // kGpuTimestampedDraws false, // kGpuTimestampedDraws
false, // kGpuCommandProcessorFrameUs false, // kGpuCommandProcessorFrameUs
false, // kGpuTimestampedFrames false, // kGpuTimestampedFrames
@ -387,6 +434,37 @@ constexpr bool kIsGauge[] = {
false, // kRtResolveTransferClearUs false, // kRtResolveTransferClearUs
false, // kRtResolveReadbackCalls false, // kRtResolveReadbackCalls
false, // kRtResolveReadbackUs 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, // kXmaFramesDecoded
false, // kAudioFrameLatencyUs false, // kAudioFrameLatencyUs
false, // kBufferQueueDepth (set each frame) false, // kBufferQueueDepth (set each frame)
@ -421,6 +499,16 @@ const char* DrawBucketName(DrawBucket bucket) {
return "MemExp"; return "MemExp";
case DrawBucket::kNoPixelShader: case DrawBucket::kNoPixelShader:
return "NoPS"; 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: default:
return "Unknown"; return "Unknown";
} }

View file

@ -24,12 +24,15 @@ static_assert(REX_PLATFORM_LINUX || REX_PLATFORM_MAC, "This file is POSIX-only")
#include <pthread.h> #include <pthread.h>
#include <semaphore.h> #include <semaphore.h>
#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <sys/time.h> #include <sys/time.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#if REX_PLATFORM_LINUX
#include <sys/eventfd.h>
#include <sys/syscall.h>
#endif
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/chrono/chrono_steady_cast.h> #include <rex/chrono/chrono_steady_cast.h>
#include <rex/logging.h> #include <rex/logging.h>
@ -110,13 +113,37 @@ enum class SignalType {
}; };
int GetSystemSignal(SignalType num) { 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<int>(num); auto result = SIGRTMIN + static_cast<int>(num);
assert_true(result < SIGRTMAX); assert_true(result < SIGRTMAX);
return result; return result;
#endif
} }
SignalType GetSystemSignalType(int num) { 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<SignalType>(num - SIGRTMIN); return static_cast<SignalType>(num - SIGRTMIN);
#endif
} }
std::array<std::atomic<bool>, static_cast<size_t>(SignalType::k_Count)> signal_handler_installed = std::array<std::atomic<bool>, static_cast<size_t>(SignalType::k_Count)> signal_handler_installed =
@ -145,7 +172,13 @@ void EnableAffinityConfiguration() {}
// uint64_t ticks() { return mach_absolute_time(); } // uint64_t ticks() { return mach_absolute_time(); }
uint32_t current_thread_system_id() { uint32_t current_thread_system_id() {
#if REX_PLATFORM_MAC
uint64_t thread_id;
pthread_threadid_np(nullptr, &thread_id);
return static_cast<uint32_t>(thread_id);
#else
return static_cast<uint32_t>(syscall(SYS_gettid)); return static_cast<uint32_t>(syscall(SYS_gettid));
#endif
} }
void MaybeYield() { void MaybeYield() {
@ -669,7 +702,13 @@ class PosixCondition<Thread> : public PosixConditionBase {
WaitStarted(); WaitStarted();
std::unique_lock<std::mutex> lock(state_mutex_); std::unique_lock<std::mutex> lock(state_mutex_);
if (state_ != State::kUninitialized && state_ != State::kFinished) { 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()); pthread_setname_np(thread_, std::string(name).c_str());
#endif
#if REX_PLATFORM_ANDROID #if REX_PLATFORM_ANDROID
SetAndroidPreApi26Name(name); SetAndroidPreApi26Name(name);
#endif #endif
@ -687,9 +726,22 @@ class PosixCondition<Thread> : public PosixConditionBase {
} }
#endif #endif
uint32_t system_id() const { return static_cast<uint32_t>(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<uint32_t>(thread_id);
#else
return static_cast<uint32_t>(thread_);
#endif
}
uint64_t affinity_mask() { uint64_t affinity_mask() {
#if REX_PLATFORM_MAC
return 0;
#else
WaitStarted(); WaitStarted();
cpu_set_t cpu_set; cpu_set_t cpu_set;
#if REX_PLATFORM_ANDROID #if REX_PLATFORM_ANDROID
@ -708,9 +760,13 @@ class PosixCondition<Thread> : public PosixConditionBase {
result |= set << i; result |= set << i;
} }
return result; return result;
#endif
} }
void set_affinity_mask(uint64_t mask) { void set_affinity_mask(uint64_t mask) {
#if REX_PLATFORM_MAC
(void)mask;
#else
WaitStarted(); WaitStarted();
cpu_set_t cpu_set; cpu_set_t cpu_set;
CPU_ZERO(&cpu_set); CPU_ZERO(&cpu_set);
@ -727,6 +783,7 @@ class PosixCondition<Thread> : public PosixConditionBase {
if (pthread_setaffinity_np(thread_, sizeof(cpu_set_t), &cpu_set) != 0) { if (pthread_setaffinity_np(thread_, sizeof(cpu_set_t), &cpu_set) != 0) {
assert_always(); assert_always();
} }
#endif
#endif #endif
} }
@ -784,6 +841,9 @@ class PosixCondition<Thread> : public PosixConditionBase {
#if REX_PLATFORM_ANDROID #if REX_PLATFORM_ANDROID
int result = sigqueue(pthread_gettid_np(thread_), int result = sigqueue(pthread_gettid_np(thread_),
GetSystemSignal(SignalType::kThreadUserCallback), value); GetSystemSignal(SignalType::kThreadUserCallback), value);
#elif REX_PLATFORM_MAC
(void)value;
int result = pthread_kill(thread_, GetSystemSignal(SignalType::kThreadUserCallback));
#else #else
int result = pthread_sigqueue(thread_, GetSystemSignal(SignalType::kThreadUserCallback), value); int result = pthread_sigqueue(thread_, GetSystemSignal(SignalType::kThreadUserCallback), value);
#endif #endif
@ -1394,7 +1454,11 @@ void Thread::Exit(int exit_code) {
} }
void set_current_thread_name(const std::string_view name) { 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()); pthread_setname_np(pthread_self(), std::string(name).c_str());
#endif
#if REX_PLATFORM_ANDROID #if REX_PLATFORM_ANDROID
if (!android_pthread_getname_np_ && current_thread_) { if (!android_pthread_getname_np_ && current_thread_) {
current_thread_->condition().SetAndroidPreApi26Name(name); current_thread_->condition().SetAndroidPreApi26Name(name);

View file

@ -330,6 +330,8 @@ X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dis
} }
void GraphicsSystem::Shutdown() { void GraphicsSystem::Shutdown() {
shutting_down_.store(true, std::memory_order_release);
if (command_processor_) { if (command_processor_) {
EndTracing(); EndTracing();
command_processor_->Shutdown(); 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) { void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
if (!interrupt_callback_) { if (shutting_down_.load(std::memory_order_acquire) || !interrupt_callback_) {
return; return;
} }

View file

@ -145,7 +145,9 @@ void SpirvShaderTranslator::Reset() {
var_main_memexport_data_written_ = spv::NoResult; var_main_memexport_data_written_ = spv::NoResult;
main_memexport_allowed_ = spv::NoResult; main_memexport_allowed_ = spv::NoResult;
var_main_rectangle_guest_vertex_index_ = 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(), std::fill(var_main_rectangle_interpolators_.begin(), var_main_rectangle_interpolators_.end(),
spv::NoResult); spv::NoResult);
var_main_point_size_edge_flag_kill_vertex_ = 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_ = var_main_rectangle_guest_vertex_index_ =
builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, type_uint_, builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, type_uint_,
"xe_var_rectangle_guest_vertex_index", const_uint_0_); "xe_var_rectangle_guest_vertex_index", const_uint_0_);
spv::Id type_rectangle_per_vertex_array = spv::Id type_rectangle_position_array =
builder_->makeArrayType(type_output_per_vertex_, builder_->makeUintConstant(3), 0); builder_->makeArrayType(type_float4_, builder_->makeUintConstant(3), sizeof(float) * 4);
var_main_rectangle_per_vertex_ = builder_->addDecoration(type_rectangle_position_array, spv::DecorationArrayStride,
builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, sizeof(float) * 4);
type_rectangle_per_vertex_array, "xe_var_rectangle_per_vertex"); 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 interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index; uint32_t interpolator_index;
while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) { while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) {
@ -693,13 +718,51 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
spv::Id rectangle_guest_vertex_index_int = spv::Id rectangle_guest_vertex_index_int =
builder_->createUnaryOp(spv::OpBitcast, type_int_, rectangle_guest_vertex_index); 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_.clear();
id_vector_temp_.push_back(rectangle_guest_vertex_index_int); id_vector_temp_.push_back(rectangle_guest_vertex_index_int);
builder_->createStore( builder_->createStore(rectangle_output_position,
builder_->createLoad(output_per_vertex_, spv::NoPrecision), builder_->createAccessChain(spv::StorageClassFunction,
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_, var_main_rectangle_position_,
id_vector_temp_)); 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 interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index; uint32_t interpolator_index;
while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) { while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) {
@ -884,7 +947,7 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
} }
builder_->createBranch(main_loop_continue_); 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::Id rectangle_host_vertex_in_primitive = builder_->createBinOp(
spv::OpBitwiseAnd, type_uint_, spv::OpBitwiseAnd, type_uint_,
@ -903,10 +966,9 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
auto load_rectangle_position_xy = [this](spv::Id vertex_index, uint32_t component) { auto load_rectangle_position_xy = [this](spv::Id vertex_index, uint32_t component) {
id_vector_temp_.clear(); id_vector_temp_.clear();
id_vector_temp_.push_back(vertex_index); id_vector_temp_.push_back(vertex_index);
id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition));
id_vector_temp_.push_back(builder_->makeIntConstant(int(component))); id_vector_temp_.push_back(builder_->makeIntConstant(int(component)));
return builder_->createLoad( return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_, builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_,
id_vector_temp_), id_vector_temp_),
spv::NoPrecision); spv::NoPrecision);
}; };
@ -1002,17 +1064,27 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
input_output_interpolators_[interpolator_index]); input_output_interpolators_[interpolator_index]);
} }
auto load_rectangle_per_vertex_component = auto load_rectangle_position = [this](spv::Id vertex_index) {
[this](spv::Id vertex_index, uint32_t member_index, spv::Id component_index) {
id_vector_temp_.clear(); id_vector_temp_.clear();
id_vector_temp_.push_back(vertex_index); 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);
}
return builder_->createLoad( return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction, builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_,
var_main_rectangle_per_vertex_, id_vector_temp_), 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(scratch_index);
return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction, scratch_var,
id_vector_temp_),
spv::NoPrecision); spv::NoPrecision);
}; };
@ -1020,12 +1092,9 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition)); id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition));
spv::Id rectangle_position_ptr = builder_->createAccessChain( spv::Id rectangle_position_ptr = builder_->createAccessChain(
spv::StorageClassOutput, output_per_vertex_, id_vector_temp_); spv::StorageClassOutput, output_per_vertex_, id_vector_temp_);
spv::Id position_0 = load_rectangle_per_vertex_component( spv::Id position_0 = load_rectangle_position(rectangle_vertex_indices[0]);
rectangle_vertex_indices[0], kOutputPerVertexMemberPosition, spv::NoResult); spv::Id position_1 = load_rectangle_position(rectangle_vertex_indices[1]);
spv::Id position_1 = load_rectangle_per_vertex_component( spv::Id position_2 = load_rectangle_position(rectangle_vertex_indices[2]);
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_original = spv::Id position_original =
select_rectangle_vertex_component(position_0, position_1, position_2, type_float4_); select_rectangle_vertex_component(position_0, position_1, position_2, type_float4_);
spv::Id position_synthetic = builder_->createNoContractionBinOp( spv::Id position_synthetic = builder_->createNoContractionBinOp(
@ -1039,16 +1108,17 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
auto write_rectangle_distance_member = auto write_rectangle_distance_member =
[this, &select_rectangle_vertex_component, rectangle_host_vertex_is_synthetic, [this, &select_rectangle_vertex_component, rectangle_host_vertex_is_synthetic,
&load_rectangle_per_vertex_component, &load_rectangle_distance_component,
rectangle_vertex_indices](uint32_t member_index, uint32_t component_count) { rectangle_vertex_indices](uint32_t member_index, uint32_t component_count,
spv::Id scratch_var) {
for (uint32_t i = 0; i < component_count; ++i) { for (uint32_t i = 0; i < component_count; ++i) {
spv::Id component_index = builder_->makeIntConstant(int(i)); spv::Id component_index = builder_->makeIntConstant(int(i));
spv::Id value_0 = load_rectangle_per_vertex_component( spv::Id value_0 = load_rectangle_distance_component(
rectangle_vertex_indices[0], member_index, component_index); rectangle_vertex_indices[0], component_count, component_index, scratch_var);
spv::Id value_1 = load_rectangle_per_vertex_component( spv::Id value_1 = load_rectangle_distance_component(
rectangle_vertex_indices[1], member_index, component_index); rectangle_vertex_indices[1], component_count, component_index, scratch_var);
spv::Id value_2 = load_rectangle_per_vertex_component( spv::Id value_2 = load_rectangle_distance_component(
rectangle_vertex_indices[2], member_index, component_index); rectangle_vertex_indices[2], component_count, component_index, scratch_var);
spv::Id value_original = spv::Id value_original =
select_rectangle_vertex_component(value_0, value_1, value_2, type_float_); select_rectangle_vertex_component(value_0, value_1, value_2, type_float_);
spv::Id value_synthetic = builder_->createNoContractionBinOp( spv::Id value_synthetic = builder_->createNoContractionBinOp(
@ -1069,11 +1139,11 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
if (clip_distance_count && output_per_vertex_member_clip_distance_ != UINT32_MAX) { if (clip_distance_count && output_per_vertex_member_clip_distance_ != UINT32_MAX) {
write_rectangle_distance_member(output_per_vertex_member_clip_distance_, 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) { if (cull_distance_count && output_per_vertex_member_cull_distance_ != UINT32_MAX) {
write_rectangle_distance_member(output_per_vertex_member_cull_distance_, 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(); if_not_last_guest_vertex.makeEndIf();

View file

@ -29,6 +29,7 @@
#include <rex/perf/counter.h> #include <rex/perf/counter.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/platform.h>
#include <rex/graphics/util/draw.h> #include <rex/graphics/util/draw.h>
#include <rex/graphics/flags.h> #include <rex/graphics/flags.h>
#include <rex/graphics/pipeline/shader/shader.h> #include <rex/graphics/pipeline/shader/shader.h>
@ -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 " "Use VK_KHR_dynamic_rendering for Vulkan GPU emulation when supported by the "
"device (falls back to render passes otherwise)") "device (falls back to render passes otherwise)")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .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 { namespace rex::graphics::vulkan {
@ -1903,6 +1908,7 @@ bool VulkanCommandProcessor::SetupContext() {
} }
occlusion_query_resources_available_ = InitializeOcclusionQueryResources(); occlusion_query_resources_available_ = InitializeOcclusionQueryResources();
gpu_timestamp_resources_available_ = InitializeGpuTimestampResources();
// Just not to expose uninitialized memory. // Just not to expose uninitialized memory.
std::memset(&system_constants_, 0, sizeof(system_constants_)); std::memset(&system_constants_, 0, sizeof(system_constants_));
@ -1913,6 +1919,7 @@ bool VulkanCommandProcessor::SetupContext() {
void VulkanCommandProcessor::ShutdownContext() { void VulkanCommandProcessor::ShutdownContext() {
AwaitAllQueueOperationsCompletion(); AwaitAllQueueOperationsCompletion();
InvalidateAllVertexBufferResidency(); InvalidateAllVertexBufferResidency();
ShutdownGpuTimestampResources();
ShutdownOcclusionQueryResources(); ShutdownOcclusionQueryResources();
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); 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 #if XE_GPU_FINE_GRAINED_DRAW_SCOPES
SCOPE_profile_cpu_f("gpu"); SCOPE_profile_cpu_f("gpu");
#endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES #endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES
rex::perf::ScopedCounterTimer draw_stage_total_timer(
rex::perf::CounterId::kDrawStageTotalUs);
const RegisterFile& regs = *register_file_; const RegisterFile& regs = *register_file_;
(void)index_buffer_info; (void)index_buffer_info;
@ -3627,6 +3636,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
xenos::EdramMode edram_mode = regs.Get<reg::RB_MODECONTROL>().edram_mode; xenos::EdramMode edram_mode = regs.Get<reg::RB_MODECONTROL>().edram_mode;
if (edram_mode == xenos::EdramMode::kCopy) { if (edram_mode == xenos::EdramMode::kCopy) {
// Special copy handling. // Special copy handling.
rex::perf::ScopedCounterTimer resolve_timer(rex::perf::CounterId::kRtResolveUs);
const uint32_t primitive_count = draw_util::EstimatePrimitiveCount(prim_type, index_count); const uint32_t primitive_count = draw_util::EstimatePrimitiveCount(prim_type, index_count);
PROFILE_DRAW_BUCKET(rex::perf::DrawBucket::kCopyResolve, index_count, primitive_count); PROFILE_DRAW_BUCKET(rex::perf::DrawBucket::kCopyResolve, index_count, primitive_count);
if (rex::perf::ShouldCollectDrawFingerprints()) { if (rex::perf::ShouldCollectDrawFingerprints()) {
@ -3641,7 +3651,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
fingerprint.primitive_count = primitive_count; fingerprint.primitive_count = primitive_count;
PROFILE_DRAW_FINGERPRINT(fingerprint); 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<reg::RB_SURFACE_INFO>().surface_pitch == 0; bool surface_pitch_is_zero = regs.Get<reg::RB_SURFACE_INFO>().surface_pitch == 0;
@ -3736,9 +3750,12 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
} }
// Process primitives. // Process primitives.
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStagePrimitiveUs);
if (!primitive_processor_->Process(primitive_processing_result)) { if (!primitive_processor_->Process(primitive_processing_result)) {
return draw_fail("primitive_processing"); return draw_fail("primitive_processing");
} }
}
if (!primitive_processing_result.host_draw_vertex_count) { if (!primitive_processing_result.host_draw_vertex_count) {
// Nothing to draw. // Nothing to draw.
return true; return true;
@ -3856,6 +3873,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
uint32_t normalized_color_mask = uint32_t normalized_color_mask =
pixel_shader ? draw_util::GetNormalizedColorMask(regs, pixel_shader->writes_color_targets()) pixel_shader ? draw_util::GetNormalizedColorMask(regs, pixel_shader->writes_color_targets())
: 0; : 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 // Update the textures before most other work in the submission because
// samplers depend on this (and in case of sampler overflow in a submission, // samplers depend on this (and in case of sampler overflow in a submission,
@ -3863,20 +3885,28 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
uint32_t used_texture_mask = uint32_t used_texture_mask =
vertex_shader->GetUsedTextureMaskAfterTranslation() | vertex_shader->GetUsedTextureMaskAfterTranslation() |
(pixel_shader != nullptr ? pixel_shader->GetUsedTextureMaskAfterTranslation() : 0); (pixel_shader != nullptr ? pixel_shader->GetUsedTextureMaskAfterTranslation() : 0);
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageTextureUs);
texture_cache_->RequestTextures(used_texture_mask); texture_cache_->RequestTextures(used_texture_mask);
}
const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider; const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider;
// Set up the render targets - this may perform dispatches and draws. // Set up the render targets - this may perform dispatches and draws.
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageRenderTargetUs);
if (!render_target_cache_->Update(is_rasterization_done, normalized_depth_control, if (!render_target_cache_->Update(is_rasterization_done, normalized_depth_control,
normalized_color_mask, *vertex_shader)) { normalized_color_mask, *vertex_shader)) {
return draw_fail("render_target_update"); return draw_fail("render_target_update");
} }
}
// Create the pipeline (for this, need the render pass from the render target // Create the pipeline (for this, need the render pass from the render target
// cache), translating the shaders - doing this now to obtain the used // cache), translating the shaders - doing this now to obtain the used
// textures. // textures.
VkPipeline pipeline; VkPipeline pipeline;
void* pipeline_handle = nullptr; void* pipeline_handle = nullptr;
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStagePipelineUs);
if (!pipeline_cache_->ConfigurePipeline(vertex_shader_translation, pixel_shader_translation, if (!pipeline_cache_->ConfigurePipeline(vertex_shader_translation, pixel_shader_translation,
primitive_processing_result, normalized_depth_control, primitive_processing_result, normalized_depth_control,
normalized_color_mask, normalized_color_mask,
@ -3887,7 +3917,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
bool pipeline_is_placeholder = false; bool pipeline_is_placeholder = false;
// Reload the current handle state to observe async hot-swap completions that // Reload the current handle state to observe async hot-swap completions that
// may happen between pipeline configuration and binding. // may happen between pipeline configuration and binding.
pipeline_cache_->GetPipelineAndLayoutByHandle(pipeline_handle, pipeline, pipeline_layout_provider, pipeline_cache_->GetPipelineAndLayoutByHandle(pipeline_handle, pipeline,
pipeline_layout_provider,
&pipeline_is_placeholder); &pipeline_is_placeholder);
if (REXCVAR_GET(async_shader_compilation) && pipeline_is_placeholder) { if (REXCVAR_GET(async_shader_compilation) && pipeline_is_placeholder) {
frame_used_async_placeholder_pipeline_ = true; frame_used_async_placeholder_pipeline_ = true;
@ -3896,6 +3927,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
if (pipeline == VK_NULL_HANDLE || pipeline_layout_provider == nullptr) { if (pipeline == VK_NULL_HANDLE || pipeline_layout_provider == nullptr) {
return draw_fail("pipeline_lookup"); return draw_fail("pipeline_lookup");
} }
}
if (current_guest_graphics_pipeline_ != pipeline) { if (current_guest_graphics_pipeline_ != pipeline) {
deferred_command_buffer_.CmdVkBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); deferred_command_buffer_.CmdVkBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
current_guest_graphics_pipeline_ = pipeline; current_guest_graphics_pipeline_ = pipeline;
@ -3937,6 +3969,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
// Get dynamic rasterizer state. // Get dynamic rasterizer state.
draw_util::ViewportInfo viewport_info; draw_util::ViewportInfo viewport_info;
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageFixedFunctionUs);
// Just handling maxViewportDimensions is enough - viewportBoundsRange[1] must // Just handling maxViewportDimensions is enough - viewportBoundsRange[1] must
// be at least 2 * max(maxViewportDimensions[0...1]) - 1, and // be at least 2 * max(maxViewportDimensions[0...1]) - 1, and
// maxViewportDimensions must be greater than or equal to the size of the // maxViewportDimensions must be greater than or equal to the size of the
@ -3955,13 +3989,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
// directly to pixel address and to things like ps_param_gen. // directly to pixel address and to things like ps_param_gen.
draw_util::GetHostViewportInfo( draw_util::GetHostViewportInfo(
regs, draw_resolution_scale_x, draw_resolution_scale_y, false, regs, draw_resolution_scale_x, draw_resolution_scale_y, false,
device_properties.maxViewportDimensions[0], device_properties.maxViewportDimensions[1], true, device_properties.maxViewportDimensions[0], device_properties.maxViewportDimensions[1],
normalized_depth_control, true, normalized_depth_control,
host_render_targets_used && render_target_cache_->depth_float24_convert_in_pixel_shader(), 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); host_render_targets_used, pixel_shader && pixel_shader->writes_depth(), viewport_info);
// Update dynamic graphics pipeline state. // Update dynamic graphics pipeline state.
UpdateDynamicState(viewport_info, primitive_polygonal, normalized_depth_control); UpdateDynamicState(viewport_info, primitive_polygonal, normalized_depth_control);
}
auto vgt_draw_initiator = regs.Get<reg::VGT_DRAW_INITIATOR>(); auto vgt_draw_initiator = regs.Get<reg::VGT_DRAW_INITIATOR>();
@ -4001,11 +4036,18 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
// Update uniform buffers and descriptor sets after binding the pipeline with // Update uniform buffers and descriptor sets after binding the pipeline with
// the new layout. // the new layout.
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageBindingsUs);
if (!UpdateBindings(vertex_shader, pixel_shader)) { if (!UpdateBindings(vertex_shader, pixel_shader)) {
return draw_fail("update_bindings"); return draw_fail("update_bindings");
} }
}
ScratchBufferAcquisition guest_dma_index_scratch_buffer;
// Ensure vertex buffers are resident. // 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(); 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) { 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 vfetch_bits_remaining = constant_map_vertex.vertex_fetch_bitmap[i];
@ -4080,7 +4122,6 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
} }
} }
ScratchBufferAcquisition guest_dma_index_scratch_buffer;
if (primitive_processing_result.index_buffer_type == if (primitive_processing_result.index_buffer_type ==
PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA && PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA &&
!shader_32bit_index_dma && memexport_writes_possible) { !shader_32bit_index_dma && memexport_writes_possible) {
@ -4135,13 +4176,17 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
} else { } else {
shared_memory_->Use(VulkanSharedMemory::Usage::kRead); shared_memory_->Use(VulkanSharedMemory::Usage::kRead);
} }
}
// After all commands that may dispatch, copy or insert barriers, submit // After all commands that may dispatch, copy or insert barriers, submit
// the barriers (may end the render pass), and (re)enter the render pass // the barriers (may end the render pass), and (re)enter the render pass
// before drawing. // before drawing.
{
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageBarriersUs);
SubmitBarriersAndEnterRenderTargetCacheRenderPass( SubmitBarriersAndEnterRenderTargetCacheRenderPass(
render_target_cache_->last_update_render_pass(), render_target_cache_->last_update_render_pass(),
render_target_cache_->last_update_framebuffer()); 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_vertex_count = primitive_processing_result.host_draw_vertex_count;
const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount( 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 ? rex::perf::DrawBucket::kDepthOnly
: (pixel_shader == nullptr ? rex::perf::DrawBucket::kNoPixelShader : (pixel_shader == nullptr ? rex::perf::DrawBucket::kNoPixelShader
: rex::perf::DrawBucket::kMainColorDepth)); : 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(); const bool collect_draw_fingerprints = rex::perf::ShouldCollectDrawFingerprints();
rex::perf::DrawFingerprint draw_fingerprint; rex::perf::DrawFingerprint draw_fingerprint;
if (collect_draw_fingerprints) { if (collect_draw_fingerprints) {
@ -4183,9 +4236,11 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
} }
// Draw. // Draw.
uint32_t gpu_timestamp_start = BeginGpuTimestampedDraw(draw_bucket);
if (primitive_processing_result.index_buffer_type == if (primitive_processing_result.index_buffer_type ==
PrimitiveProcessor::ProcessedIndexBufferType::kNone || PrimitiveProcessor::ProcessedIndexBufferType::kNone ||
shader_32bit_index_dma) { shader_32bit_index_dma) {
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSubmitUs);
PROFILE_DRAW_CALL(); PROFILE_DRAW_CALL();
PROFILE_VERTICES(host_draw_vertex_count); PROFILE_VERTICES(host_draw_vertex_count);
PROFILE_PRIMITIVES(host_draw_primitive_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); deferred_command_buffer_.CmdVkDraw(host_draw_vertex_count, 1, 0, 0);
} else { } else {
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSubmitUs);
std::pair<VkBuffer, VkDeviceSize> index_buffer; std::pair<VkBuffer, VkDeviceSize> index_buffer;
switch (primitive_processing_result.index_buffer_type) { switch (primitive_processing_result.index_buffer_type) {
case PrimitiveProcessor::ProcessedIndexBufferType::kGuestDMA: 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); 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. // Invalidate textures in memexported memory and watch for changes.
if (!memexport_ranges_.empty()) { if (!memexport_ranges_.empty()) {
@ -4661,6 +4718,9 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
texture_cache_->UseScaledResolveBufferForRead(); texture_cache_->UseScaledResolveBufferForRead();
SubmitBarriers(true); SubmitBarriers(true);
uint32_t resolve_downscale_timestamp_start =
BeginGpuTimestampedRegion(rex::perf::DrawBucket::kResolveDownscale);
VkBufferMemoryBarrier pre_barrier = {}; VkBufferMemoryBarrier pre_barrier = {};
pre_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; pre_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
pre_barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; 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; readback_region.size = written_length;
deferred_command_buffer_.CmdVkCopyBuffer(resolve_downscale_buffer_, deferred_command_buffer_.CmdVkCopyBuffer(resolve_downscale_buffer_,
readback.buffers[write_index], 1, &readback_region); readback.buffers[write_index], 1, &readback_region);
EndGpuTimestampedDraw(resolve_downscale_timestamp_start);
} else { } else {
shared_memory_->Use(VulkanSharedMemory::Usage::kRead); shared_memory_->Use(VulkanSharedMemory::Usage::kRead);
SubmitBarriers(true); 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<void**>(&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() { bool VulkanCommandProcessor::InitializeOcclusionQueryResources() {
active_occlusion_query_ = {}; active_occlusion_query_ = {};
occlusion_query_cursor_ = 0; occlusion_query_cursor_ = 0;
@ -5044,6 +5433,9 @@ void VulkanCommandProcessor::InitializeTrace() {
} }
void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_submission) { 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. // Only report once, no need to retry a wait that won't succeed anyway.
if (device_lost_) { if (device_lost_) {
return; return;
@ -5070,9 +5462,14 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su
// defined by vkQueueSubmit additionally include in the first // defined by vkQueueSubmit additionally include in the first
// synchronization scope all commands that occur earlier in submission // synchronization scope all commands that occur earlier in submission
// order." // order."
VkResult wait_result = VkResult wait_result;
{
rex::perf::ScopedCounterTimer fence_wait_timer(
rex::perf::CounterId::kCpuD3D12FenceWaitUs);
wait_result =
dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_), dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_),
submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX); submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX);
}
if (wait_result == VK_SUCCESS) { if (wait_result == VK_SUCCESS) {
fences_awaited += await_submission - submission_completed_; fences_awaited += await_submission - submission_completed_;
} else { } else {
@ -5106,6 +5503,10 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su
// Not updated - no need to reclaim or download things. // Not updated - no need to reclaim or download things.
return; return;
} }
rex::perf::ScopedCounterTimer fence_reclaim_timer(
rex::perf::CounterId::kCpuD3D12FenceReclaimUs);
// Reclaim fences. // Reclaim fences.
fences_free_.reserve(fences_free_.size() + fences_awaited); fences_free_.reserve(fences_free_.size() + fences_awaited);
auto submissions_in_flight_fences_awaited_end = submissions_in_flight_fences_.cbegin(); 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_); texture_cache_->CompletedSubmissionUpdated(submission_completed_);
ProcessGpuTimestampResults();
// Destroy objects scheduled for destruction. // Destroy objects scheduled for destruction.
while (!destroy_framebuffers_.empty()) { while (!destroy_framebuffers_.empty()) {
const auto& destroy_pair = destroy_framebuffers_.front(); 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 #if XE_GPU_FINE_GRAINED_DRAW_SCOPES
SCOPE_profile_cpu_f("gpu"); SCOPE_profile_cpu_f("gpu");
#endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES #endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES
rex::perf::ScopedCounterTimer begin_submission_timer(
rex::perf::CounterId::kCpuD3D12BeginSubmissionUs);
if (device_lost_) { if (device_lost_) {
return false; return false;
@ -5211,12 +5616,19 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
// is still available, and whether the await was successful. // is still available, and whether the await was successful.
uint64_t await_submission = uint64_t await_submission =
is_opening_frame ? closed_frame_submissions_[frame_current_ % kMaxFramesInFlight] : 0; is_opening_frame ? closed_frame_submissions_[frame_current_ % kMaxFramesInFlight] : 0;
{
rex::perf::ScopedCounterTimer begin_submission_fence_timer(
rex::perf::CounterId::kCpuD3D12BeginSubmissionFenceUs);
CheckSubmissionFenceAndDeviceLoss(await_submission); CheckSubmissionFenceAndDeviceLoss(await_submission);
}
if (device_lost_ || submission_completed_ < await_submission) { if (device_lost_ || submission_completed_ < await_submission) {
return false; return false;
} }
if (is_opening_frame) { 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 // Update the completed frame index, also obtaining the actual completed
// frame number (since the CPU may be actually less than 3 frames behind) // frame number (since the CPU may be actually less than 3 frames behind)
// before reclaiming resources tracked with the frame number. // before reclaiming resources tracked with the frame number.
@ -5230,6 +5642,9 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
} }
if (!submission_open_) { if (!submission_open_) {
rex::perf::ScopedCounterTimer begin_submission_open_timer(
rex::perf::CounterId::kCpuD3D12BeginSubmissionOpenUs);
submission_open_ = true; submission_open_ = true;
// Start a new deferred command buffer - will submit it to the real one in // 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(); primitive_processor_->BeginFrame();
texture_cache_->BeginFrame(); texture_cache_->BeginFrame();
BeginGpuTimestampFrame();
} }
return true; return true;
} }
bool VulkanCommandProcessor::EndSubmission(bool is_swap) { 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* const vulkan_device = GetVulkanDevice();
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
const VkDevice device = vulkan_device->device(); const VkDevice device = vulkan_device->device();
@ -5394,6 +5814,8 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
bool is_closing_frame = is_swap && frame_open_; bool is_closing_frame = is_swap && frame_open_;
if (is_closing_frame) { if (is_closing_frame) {
rex::perf::ScopedCounterTimer end_frame_timer(rex::perf::CounterId::kCpuD3D12EndFrameUs);
texture_cache_->EndFrame(); texture_cache_->EndFrame();
primitive_processor_->EndFrame(); primitive_processor_->EndFrame();
@ -5410,6 +5832,10 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
EndRenderPass(); EndRenderPass();
{
rex::perf::ScopedCounterTimer pipeline_end_timer(
rex::perf::CounterId::kCpuD3D12PipelineEndUs);
pipeline_cache_->EndSubmission(); pipeline_cache_->EndSubmission();
render_target_cache_->EndSubmission(); render_target_cache_->EndSubmission();
@ -5419,6 +5845,7 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
shared_memory_->EndSubmission(); shared_memory_->EndSubmission();
uniform_buffer_pool_->FlushWrites(); uniform_buffer_pool_->FlushWrites();
}
// Submit sparse binds earlier, before executing the deferred command // Submit sparse binds earlier, before executing the deferred command
// buffer, to reduce latency. // buffer, to reduce latency.
@ -5468,7 +5895,14 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
sparse_memory_binds_.clear(); sparse_memory_binds_.clear();
} }
{
rex::perf::ScopedCounterTimer submit_barriers_timer(
rex::perf::CounterId::kCpuD3D12SubmitBarriersUs);
SubmitBarriers(true); SubmitBarriers(true);
}
if (is_closing_frame) {
EndGpuTimestampFrame();
}
assert_false(command_buffers_writable_.empty()); assert_false(command_buffers_writable_.empty());
CommandBuffer command_buffer = command_buffers_writable_.back(); CommandBuffer command_buffer = command_buffers_writable_.back();
@ -5481,15 +5915,27 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
command_buffer_begin_info.pNext = nullptr; command_buffer_begin_info.pNext = nullptr;
command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; command_buffer_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
command_buffer_begin_info.pInheritanceInfo = nullptr; command_buffer_begin_info.pInheritanceInfo = nullptr;
if (dfn.vkBeginCommandBuffer(command_buffer.buffer, &command_buffer_begin_info) != VK_SUCCESS) { {
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"); REXGPU_ERROR("Failed to begin a Vulkan command buffer");
return false; return false;
} }
deferred_command_buffer_.Execute(command_buffer.buffer); deferred_command_buffer_.Execute(command_buffer.buffer);
if (is_closing_frame) {
ResolveGpuTimestampFrame(command_buffer.buffer);
}
}
{
rex::perf::ScopedCounterTimer command_list_close_timer(
rex::perf::CounterId::kCpuD3D12CommandListCloseUs);
if (dfn.vkEndCommandBuffer(command_buffer.buffer) != VK_SUCCESS) { if (dfn.vkEndCommandBuffer(command_buffer.buffer) != VK_SUCCESS) {
REXGPU_ERROR("Failed to end a Vulkan command buffer"); REXGPU_ERROR("Failed to end a Vulkan command buffer");
return false; return false;
} }
}
VkSubmitInfo submit_info; VkSubmitInfo submit_info;
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
@ -5515,6 +5961,8 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
} }
VkResult submit_result; VkResult submit_result;
{ {
rex::perf::ScopedCounterTimer execute_command_lists_timer(
rex::perf::CounterId::kCpuD3D12ExecuteCommandListsUs);
ui::vulkan::VulkanDevice::Queue::Acquisition queue_acquisition = ui::vulkan::VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device->AcquireQueue(vulkan_device->queue_family_graphics_compute(), 0); vulkan_device->AcquireQueue(vulkan_device->queue_family_graphics_compute(), 0);
submit_result = dfn.vkQueueSubmit(queue_acquisition.queue(), 1, &submit_info, fence); submit_result = dfn.vkQueueSubmit(queue_acquisition.queue(), 1, &submit_info, fence);
@ -5530,6 +5978,13 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
return false; return false;
} }
uint64_t submission_current = GetCurrentSubmission(); 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(); current_submission_wait_stage_masks_.clear();
for (VkSemaphore semaphore : current_submission_wait_semaphores_) { for (VkSemaphore semaphore : current_submission_wait_semaphores_) {
submissions_in_flight_semaphores_.emplace_back(submission_current, semaphore); submissions_in_flight_semaphores_.emplace_back(submission_current, semaphore);

View file

@ -314,6 +314,11 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) {
rex::align(sizeof(ArgsVkSetViewport), alignof(VkViewport)))); rex::align(sizeof(ArgsVkSetViewport), alignof(VkViewport))));
} break; } break;
case Command::kVkWriteTimestamp: {
auto& args = *reinterpret_cast<const ArgsVkWriteTimestamp*>(stream);
dfn.vkCmdWriteTimestamp(command_buffer, args.pipeline_stage, args.query_pool, args.query);
} break;
default: default:
assert_unhandled_case(header.command); assert_unhandled_case(header.command);
break; break;

View file

@ -29,6 +29,7 @@
#include <rex/filesystem.h> #include <rex/filesystem.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/platform.h>
#include <rex/thread.h> #include <rex/thread.h>
#include <rex/memory.h> #include <rex/memory.h>
#include <rex/graphics/util/draw.h> #include <rex/graphics/util/draw.h>
@ -3213,7 +3214,14 @@ bool VulkanPipelineCache::EnsurePipelineCreated(const PipelineCreationArguments&
assert_unhandled_case(description.primitive_topology); assert_unhandled_case(description.primitive_topology);
return false; 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; input_assembly_state.primitiveRestartEnable = description.primitive_restart ? VK_TRUE : VK_FALSE;
#endif
VkPipelineViewportStateCreateInfo viewport_state; VkPipelineViewportStateCreateInfo viewport_state;
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

View file

@ -35,6 +35,8 @@
#include <rex/graphics/xenos.h> #include <rex/graphics/xenos.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/perf/counter.h>
#include <rex/platform.h>
#include <rex/ui/vulkan/util.h> #include <rex/ui/vulkan/util.h>
REXCVAR_DEFINE_STRING(render_target_path_vulkan, "", "GPU/Vulkan", 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_attachments_supported_ = false;
msaa_2x_no_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 = bool integer_transfer_sample_1x_supported =
(device_properties.framebufferColorSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0 && (device_properties.framebufferColorSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0 &&
(device_properties.sampledImageIntegerSampleCounts & 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); UseEdramBuffer(EdramBufferUsage::kComputeRead);
command_processor_.BindExternalComputePipeline( command_processor_.BindExternalComputePipeline(
resolve_copy_pipelines_[size_t(copy_shader)]); 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] = {}; VkDescriptorSet descriptor_sets[kResolveCopyDescriptorSetCount] = {};
descriptor_sets[kResolveCopyDescriptorSetEdram] = edram_storage_buffer_descriptor_set_; descriptor_sets[kResolveCopyDescriptorSetEdram] = edram_storage_buffer_descriptor_set_;
descriptor_sets[kResolveCopyDescriptorSetDest] = descriptor_set_dest; descriptor_sets[kResolveCopyDescriptorSetDest] = descriptor_set_dest;
@ -1487,6 +1503,44 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory,
sizeof(copy_shader_constants), &copy_shader_constants); sizeof(copy_shader_constants), &copy_shader_constants);
} }
command_processor_.SubmitBarriers(true); 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); command_buffer.CmdVkDispatch(copy_group_count_x, copy_group_count_y, 1);
// Invalidate textures and mark the range as scaled if needed. // 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.pNext = nullptr;
input_assembly_state.flags = 0; input_assembly_state.flags = 0;
input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; 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; input_assembly_state.primitiveRestartEnable = VK_FALSE;
#endif
// Dynamic, to stay within maxViewportDimensions while preferring a // Dynamic, to stay within maxViewportDimensions while preferring a
// power-of-two factor for converting from pixel coordinates to NDC for exact // 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, bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dump_row_length_used,
uint32_t dump_rows, uint32_t dump_pitch) { uint32_t dump_rows, uint32_t dump_pitch) {
assert_true(GetPath() == Path::kHostRenderTargets); assert_true(GetPath() == Path::kHostRenderTargets);
PERF_counter_inc(kRtDumpCalls);
GetResolveCopyRectanglesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch, GetResolveCopyRectanglesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch,
dump_rectangles_); dump_rectangles_);
PERF_counter_add(kRtDumpRectangles, dump_rectangles_.size());
if (dump_rectangles_.empty()) { if (dump_rectangles_.empty()) {
return true; return true;
} }
@ -6270,6 +6330,7 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum
// Dump the render targets. // Dump the render targets.
DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer();
command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kCopyDump);
bool edram_buffer_bound = false; bool edram_buffer_bound = false;
VkDescriptorSet last_source_descriptor_set = VK_NULL_HANDLE; VkDescriptorSet last_source_descriptor_set = VK_NULL_HANDLE;
DumpPitches last_pitches; DumpPitches last_pitches;
@ -6340,16 +6401,19 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum
sizeof(last_offsets), &last_offsets); sizeof(last_offsets), &last_offsets);
} }
command_processor_.SubmitBarriers(true); command_processor_.SubmitBarriers(true);
command_buffer.CmdVkDispatch( const uint32_t group_count_x =
(draw_resolution_scale_x() * (draw_resolution_scale_x() *
(xenos::kEdramTileWidthSamples >> uint32_t(rt_key.Is64bpp())) * (xenos::kEdramTileWidthSamples >> uint32_t(rt_key.Is64bpp())) *
dispatch.width_tiles + dispatch.width_tiles +
(kDumpSamplesPerGroupX - 1)) / (kDumpSamplesPerGroupX - 1)) /
kDumpSamplesPerGroupX, kDumpSamplesPerGroupX;
const uint32_t group_count_y =
(draw_resolution_scale_y() * xenos::kEdramTileHeightSamples * dispatch.height_tiles + (draw_resolution_scale_y() * xenos::kEdramTileHeightSamples * dispatch.height_tiles +
(kDumpSamplesPerGroupY - 1)) / (kDumpSamplesPerGroupY - 1)) /
kDumpSamplesPerGroupY, kDumpSamplesPerGroupY;
1); 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(); MarkEdramBufferModified();
} }

View file

@ -29,11 +29,16 @@
#include <rex/graphics/vulkan/texture_cache.h> #include <rex/graphics/vulkan/texture_cache.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/perf/counter.h>
#include <rex/ui/vulkan/mem_alloc.h> #include <rex/ui/vulkan/mem_alloc.h>
#include <rex/ui/vulkan/ui_samplers.h> #include <rex/ui/vulkan/ui_samplers.h>
#include <rex/ui/vulkan/util.h> #include <rex/ui/vulkan/util.h>
REXCVAR_DEFINE_BOOL(non_seamless_cube_map, false, "GPU", "Use non-seamless cube map sampling"); 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 { namespace rex::graphics::vulkan {
@ -2293,6 +2298,14 @@ void VulkanTextureCache::UseScaledResolveBufferForWrite(uint64_t written_start_s
std::min(written_length_scaled, scaled_resolve_buffer_size_ - written_start_scaled); std::min(written_length_scaled, scaled_resolve_buffer_size_ - written_start_scaled);
if (scaled_resolve_last_usage_write_ && scaled_resolve_last_written_range_.second) { if (scaled_resolve_last_usage_write_ && scaled_resolve_last_written_range_.second) {
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; VkPipelineStageFlags stage_mask;
VkAccessFlags access_mask; VkAccessFlags access_mask;
GetScaledResolveUsageMasks(stage_mask, access_mask, true); GetScaledResolveUsageMasks(stage_mask, access_mask, true);
@ -2300,6 +2313,12 @@ void VulkanTextureCache::UseScaledResolveBufferForWrite(uint64_t written_start_s
scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first), scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first),
VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask, VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask,
access_mask, access_mask, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, false); 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_) { } else if (!scaled_resolve_last_usage_write_) {
VkPipelineStageFlags src_stage_mask, dst_stage_mask; VkPipelineStageFlags src_stage_mask, dst_stage_mask;
VkAccessFlags src_access_mask, dst_access_mask; VkAccessFlags src_access_mask, dst_access_mask;

View file

@ -23,6 +23,8 @@
#include <rex/input/xinput/xinput_input_driver.h> #include <rex/input/xinput/xinput_input_driver.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/platform.h> #include <rex/platform.h>
#include <rex/ui/ui_event.h>
#include <rex/ui/window_listener.h>
namespace { namespace {
@ -50,6 +52,15 @@ X_STATUS InputSystem::Setup() {
} }
void InputSystem::Shutdown() { void InputSystem::Shutdown() {
if (window_) {
rex::ui::UIEvent closing_event(window_);
for (auto& driver : drivers_) {
if (auto* window_listener = dynamic_cast<rex::ui::WindowListener*>(driver.get())) {
window_listener->OnClosing(closing_event);
}
}
window_ = nullptr;
}
drivers_.clear(); drivers_.clear();
} }

View file

@ -9,6 +9,7 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/ */
#include <algorithm>
#include <array> #include <array>
#include <filesystem> #include <filesystem>
@ -22,6 +23,8 @@
REXCVAR_DEFINE_STRING(hid_mappings_file, "gamecontrollerdb.txt", "Input", REXCVAR_DEFINE_STRING(hid_mappings_file, "gamecontrollerdb.txt", "Input",
"Path to SDL gamecontroller mappings file"); "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 { namespace rex::input::sdl {
@ -124,6 +127,7 @@ void SDLInputDriver::OnClosing(rex::ui::UIEvent&) {
} }
for (size_t i = 0; i < controllers_.size(); i++) { for (size_t i = 0; i < controllers_.size(); i++) {
if (controllers_.at(i).sdl) { if (controllers_.at(i).sdl) {
StopRumbleLocked(controllers_.at(i));
SDL_CloseGamepad(controllers_.at(i).sdl); SDL_CloseGamepad(controllers_.at(i).sdl);
controllers_.at(i) = {}; 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; return X_ERROR_DEVICE_NOT_CONNECTED;
} }
#if SDL_VERSION_ATLEAST(2, 0, 9) const bool has_rumble = vibration->left_motor_speed || vibration->right_motor_speed;
if (SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed, vibration->right_motor_speed, const uint32_t duration_ms =
0)) { 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; return X_ERROR_FUNCTION_FAILED;
} else {
return X_ERROR_SUCCESS;
} }
#else
return X_ERROR_SUCCESS; return X_ERROR_SUCCESS;
#endif
} }
X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags, 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) { void SDLInputDriver::OnControllerDeviceAddedLocked(const SDL_Event& event) {
// Open the controller. // Open the controller.
const auto controller = SDL_OpenGamepad(event.cdevice.which); 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. // Find the disconnected gamecontroller and close it.
auto idx = GetControllerIndexFromInstanceID(event.cdevice.which); auto idx = GetControllerIndexFromInstanceID(event.cdevice.which);
if (idx) { if (idx) {
StopRumbleLocked(controllers_.at(*idx));
SDL_CloseGamepad(controllers_.at(*idx).sdl); SDL_CloseGamepad(controllers_.at(*idx).sdl);
controllers_.at(*idx) = {}; controllers_.at(*idx) = {};
keystroke_states_.at(*idx) = {}; keystroke_states_.at(*idx) = {};

View file

@ -318,7 +318,8 @@ HeapDiagnostics ReXHeap::GetDiagnosticsLocked() const {
diagnostics.capacity += d.capacity; diagnostics.capacity += d.capacity;
diagnostics.allocated += d.allocated; diagnostics.allocated += d.allocated;
diagnostics.peak_allocated += d.peak_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<uint64_t>(d.peak_request_size));
diagnostics.oom_count += d.oom_count; diagnostics.oom_count += d.oom_count;
} }
return diagnostics; return diagnostics;

View file

@ -33,10 +33,11 @@
// NOTE: must be included last as it expects windows.h to already be included. // NOTE: must be included last as it expects windows.h to already be included.
#define _WINSOCK_DEPRECATED_NO_WARNINGS // inet_addr #define _WINSOCK_DEPRECATED_NO_WARNINGS // inet_addr
#include <winsock2.h> // NOLINT(build/include_order) #include <winsock2.h> // NOLINT(build/include_order)
#elif REX_PLATFORM_LINUX #elif REX_PLATFORM_LINUX || REX_PLATFORM_MAC
#include <arpa/inet.h> #include <arpa/inet.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <netinet/ip.h> #include <netinet/ip.h>
#include <sys/select.h>
#include <sys/socket.h> #include <sys/socket.h>
#endif #endif

View file

@ -17,6 +17,7 @@
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/platform.h>
#include <rex/ppc/function.h> #include <rex/ppc/function.h>
#include <rex/runtime.h> #include <rex/runtime.h>
#include <rex/stream.h> #include <rex/stream.h>
@ -841,6 +842,26 @@ void KernelState::TerminateTitle() {
REXSYS_DEBUG("KernelState::TerminateTitle"); REXSYS_DEBUG("KernelState::TerminateTitle");
auto global_lock = global_critical_region_.Acquire(); 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<object_ref<XThread>> 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. // Suspend all running guest threads so they stop touching shared state.
std::vector<XThread*> suspended_threads; std::vector<XThread*> suspended_threads;
for (auto it = threads_by_id_.begin(); it != threads_by_id_.end(); ++it) { for (auto it = threads_by_id_.begin(); it != threads_by_id_.end(); ++it) {
@ -857,6 +878,7 @@ void KernelState::TerminateTitle() {
thread->Terminate(0); thread->Terminate(0);
} }
global_lock.lock(); global_lock.lock();
#endif
// Remove all guest threads from the map. // Remove all guest threads from the map.
for (auto it = threads_by_id_.begin(); it != threads_by_id_.end();) { for (auto it = threads_by_id_.begin(); it != threads_by_id_.end();) {

View file

@ -16,6 +16,7 @@
#include <rex/filesystem/vfs.h> #include <rex/filesystem/vfs.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/perf/counter.h> #include <rex/perf/counter.h>
#include <rex/platform.h>
#include <rex/ppc/context.h> // PPCFuncMapping #include <rex/ppc/context.h> // PPCFuncMapping
#include <rex/platform/exceptions.h> // SEH exception support #include <rex/platform/exceptions.h> // SEH exception support
#include <rex/kernel/crt/heap.h> #include <rex/kernel/crt/heap.h>
@ -244,10 +245,22 @@ void Runtime::Shutdown() {
input_system_.reset(); input_system_.reset();
} }
kernel_state_.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(); function_dispatcher_.reset();
#endif
export_resolver_.reset(); export_resolver_.reset();
file_system_.reset(); file_system_.reset();
#if REX_PLATFORM_MAC
memory_.release();
#else
memory_.reset(); memory_.reset();
#endif
rex::perf::Profiler::Shutdown(); rex::perf::Profiler::Shutdown();
setup_complete_ = false; setup_complete_ = false;

View file

@ -29,6 +29,10 @@
// TODO(benvanik): move xbox.h out // TODO(benvanik): move xbox.h out
#include <rex/system/xtypes.h> #include <rex/system/xtypes.h>
#if REX_PLATFORM_MAC
#include <sys/mman.h>
#endif
REXCVAR_DEFINE_BOOL(protect_zero, false, "Memory", "Protect the zero page from reads and writes") REXCVAR_DEFINE_BOOL(protect_zero, false, "Memory", "Protect the zero page from reads and writes")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart); .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; 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() { void CrashDump() {
static std::atomic<int> in_crash_dump(0); static std::atomic<int> in_crash_dump(0);
if (in_crash_dump.fetch_add(1)) { if (in_crash_dump.fetch_add(1)) {
@ -135,7 +155,8 @@ bool Memory::Initialize() {
mapping_ = mapping_ =
rex::memory::CreateFileMappingHandle(file_name_, rex::memory::CreateFileMappingHandle(file_name_,
// entire 4gb space + 512mb physical: // entire 4gb space + 512mb physical:
0x11FFFFFFF, rex::memory::PageAccess::kReadWrite, false); kGuestBackingLength,
rex::memory::PageAccess::kReadWrite, false);
if (mapping_ == rex::memory::kFileMappingHandleInvalid) { if (mapping_ == rex::memory::kFileMappingHandleInvalid) {
REXSYS_ERROR("Unable to reserve the 4gb guest address space."); REXSYS_ERROR("Unable to reserve the 4gb guest address space.");
assert_always(); assert_always();
@ -148,10 +169,17 @@ bool Memory::Initialize() {
for (size_t n = 32; n < 64; n++) { for (size_t n = 32; n < 64; n++) {
auto mapping_base = reinterpret_cast<uint8_t*>(1ull << n); auto mapping_base = reinterpret_cast<uint8_t*>(1ull << n);
if (!MapViews(mapping_base)) { if (!MapViews(mapping_base)) {
mapping_base_ = mapping_base; mapping_base_ = views_.all_views[0];
break; break;
} }
} }
#if REX_PLATFORM_MAC
if (!mapping_base_) {
if (!MapViews(nullptr)) {
mapping_base_ = views_.all_views[0];
}
}
#endif
if (!mapping_base_) { if (!mapping_base_) {
REXSYS_ERROR("Unable to find a continuous block in the 64bit address space."); REXSYS_ERROR("Unable to find a continuous block in the 64bit address space.");
assert_always(); assert_always();
@ -209,6 +237,12 @@ bool Memory::Initialize() {
rex::memory::AllocFixed(heaps_.physical.TranslateRelative(0), heaps_.physical.heap_size(), rex::memory::AllocFixed(heaps_.physical.TranslateRelative(0), heaps_.physical.heap_size(),
rex::memory::AllocationType::kCommit, rex::memory::AllocationType::kCommit,
rex::memory::PageAccess::kReadWrite); 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 // Install MMIO handler for physical address translation and MMIO ranges
mmio_handler_ = runtime::MMIOHandler::Install( mmio_handler_ = runtime::MMIOHandler::Install(
@ -300,8 +334,34 @@ static const struct {
0x0000000100000000ull, 0x0000000100000000ull,
}, },
}; };
namespace {
constexpr size_t kGuestMappingLength = 0x120000000ull;
} // namespace
int Memory::MapViews(uint8_t* mapping_base) { int Memory::MapViews(uint8_t* mapping_base) {
assert_true(rex::countof(map_info) == rex::countof(views_.all_views)); 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<uint8_t*>(reservation);
#endif
// 0xE0000000 4 KB offset is emulated via host_address_offset and on the CPU // 0xE0000000 4 KB offset is emulated via host_address_offset and on the CPU
// side if system allocation granularity is bigger than 4 KB. // side if system allocation granularity is bigger than 4 KB.
uint64_t granularity_mask = ~uint64_t(system_allocation_granularity_ - 1); 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)); rex::memory::PageAccess::kReadWrite, map_info[n].target_address & granularity_mask));
if (!views_.all_views[n]) { if (!views_.all_views[n]) {
// Failed, so bail and try again. // 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(); UnmapViews();
#endif
return 1; return 1;
} }
} }
@ -1111,7 +1176,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, uint32_t alignme
// Allocate from host. // Allocate from host.
if (allocation_type == memory::kMemoryAllocationReserve) { if (allocation_type == memory::kMemoryAllocationReserve) {
// Reserve is not needed, as we are mapped already. // Reserve is not needed, as we are mapped already.
} else { } else if (!ShouldSkipHostCommit(*this)) {
auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit) auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit)
? rex::memory::AllocationType::kCommit ? rex::memory::AllocationType::kCommit
: rex::memory::AllocationType::kReserve; : rex::memory::AllocationType::kReserve;
@ -1265,7 +1330,7 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, uint32_t
// Allocate from host. // Allocate from host.
if (allocation_type == memory::kMemoryAllocationReserve) { if (allocation_type == memory::kMemoryAllocationReserve) {
// Reserve is not needed, as we are mapped already. // Reserve is not needed, as we are mapped already.
} else { } else if (!ShouldSkipHostCommit(*this)) {
auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit) auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit)
? rex::memory::AllocationType::kCommit ? rex::memory::AllocationType::kCommit
: rex::memory::AllocationType::kReserve; : rex::memory::AllocationType::kReserve;

View file

@ -29,6 +29,14 @@
#include <netinet/in.h> #include <netinet/in.h>
#include <netinet/ip.h> #include <netinet/ip.h>
#include <sys/socket.h> #include <sys/socket.h>
#if REX_PLATFORM_MAC
#ifdef IPPROTO_TCP
#undef IPPROTO_TCP
#endif
#ifdef IPPROTO_UDP
#undef IPPROTO_UDP
#endif
#endif
#endif #endif
namespace rex::system { namespace rex::system {

View file

@ -63,6 +63,12 @@ if(WIN32)
window_win.cpp window_win.cpp
windowed_app_context_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() else()
set(REXUI_PLATFORM_SOURCES set(REXUI_PLATFORM_SOURCES
surface_gnulinux.cpp surface_gnulinux.cpp
@ -138,6 +144,8 @@ if(WIN32)
dwmapi dwmapi
Shcore Shcore
) )
elseif(APPLE)
target_link_libraries(rexui PUBLIC SDL3::SDL3)
else() else()
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0) pkg_check_modules(GTK3 REQUIRED gtk+-3.0)

View file

@ -364,8 +364,15 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.08f, 0.07f, 1.00f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.38f, 0.08f, 0.07f, 1.00f));
if (ImGui::Button("Close Game", ImVec2(170.0f, 40.0f))) { if (ImGui::Button("Close Game", ImVec2(170.0f, 40.0f))) {
if (close_game_) { if (close_game_) {
Hide();
close_game_(); close_game_();
} }
ImGui::PopStyleColor(3);
ImGui::End();
ImGui::PopStyleColor(17);
ImGui::PopStyleVar(7);
ImGui::PopFont();
return;
} }
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
ImGui::Spacing(); ImGui::Spacing();

View file

@ -425,8 +425,10 @@ void Presenter::OnSurfaceResizeFromUIThread() {
bool request_repaint; bool request_repaint;
UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true); UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true);
// Request to repaint as soon as possible in the UI thread if needed. // Request to repaint as soon as possible in the UI thread if needed. Active
if (request_repaint) { // 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); RequestPaintOrConnectionRecoveryViaWindow(true);
} }
} }
@ -1453,13 +1455,13 @@ bool Presenter::RequestPaintOrConnectionRecoveryViaWindow(bool force_ui_thread_p
// theoretically. For safety, check whether the window exists unconditionally. // theoretically. For safety, check whether the window exists unconditionally.
assert_not_null(window_); assert_not_null(window_);
assert_not_null(surface_); assert_not_null(surface_);
if (force_ui_thread_paint_tick) {
ForceUIThreadPaintTick();
}
if (ui_thread_paint_requested_.exchange(true, std::memory_order_relaxed)) { if (ui_thread_paint_requested_.exchange(true, std::memory_order_relaxed)) {
// Invalidation pending already, no need to do it twice. // Invalidation pending already, no need to do it twice.
return false; return false;
} }
if (force_ui_thread_paint_tick) {
ForceUIThreadPaintTick();
}
window_->RequestPaint(); window_->RequestPaint();
return true; return true;
} }

View file

@ -48,6 +48,7 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <string_view> #include <string_view>
#include <utility>
namespace rex { namespace rex {
@ -98,9 +99,24 @@ bool ReXApp::OnInitialize() {
return true; 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)) if (!ConstructRuntime(*paths))
return false; return false;
LaunchModule(); LaunchModule();
#endif
return true; return true;
} }
@ -432,6 +448,9 @@ void ReXApp::LaunchModule() {
app_context().QuitFromUIThread(); app_context().QuitFromUIThread();
return; return;
} }
#if REX_PLATFORM_MAC
main_thread_ = main_thread;
#endif
auto* graphics_system = auto* graphics_system =
static_cast<rex::graphics::GraphicsSystem*>(runtime_->graphics_system()); static_cast<rex::graphics::GraphicsSystem*>(runtime_->graphics_system());
@ -478,6 +497,11 @@ void ReXApp::OnClosing(ui::UIEvent& e) {
(void)e; (void)e;
REXLOG_INFO("Window closing, shutting down..."); REXLOG_INFO("Window closing, shutting down...");
shutting_down_.store(true, std::memory_order_release); 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()) { if (runtime_ && runtime_->kernel_state()) {
runtime_->kernel_state()->TerminateTitle(); runtime_->kernel_state()->TerminateTitle();
} }
@ -488,6 +512,13 @@ void ReXApp::OnDestroy() {
// Notify subclass before cleanup // Notify subclass before cleanup
OnShutdown(); 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 // Unregister overlay keybinds before destroying dialogs
rex::ui::UnregisterBind("bind_debug_overlay"); rex::ui::UnregisterBind("bind_debug_overlay");
rex::ui::UnregisterBind("bind_console"); rex::ui::UnregisterBind("bind_console");
@ -516,6 +547,15 @@ void ReXApp::OnDestroy() {
if (module_thread_.joinable()) { if (module_thread_.joinable()) {
module_thread_.join(); 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<rex::input::InputSystem*>(runtime_->input_system())->Shutdown();
}
if (window_) { if (window_) {
window_->RemoveInputListener(this); window_->RemoveInputListener(this);
window_->RemoveListener(this); window_->RemoveListener(this);

21
src/ui/surface_sdl.cpp Normal file
View file

@ -0,0 +1,21 @@
#include <rex/ui/surface_sdl.h>
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

View file

@ -29,7 +29,13 @@ REXCVAR_DEFINE_BOOL(vulkan_require_vertex_pipeline_stores_and_atomics, true, "UI
"Deprecated and ignored for parity; vertexPipelineStoresAndAtomics is always " "Deprecated and ignored for parity; vertexPipelineStoresAndAtomics is always "
"required for Vulkan GPU emulation") "required for Vulkan GPU emulation")
.lifecycle(rex::cvar::Lifecycle::kInitOnly); .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 " "Require geometryShader support for Vulkan GPU emulation (disable to allow "
"fallback primitive emulation paths)") "fallback primitive emulation paths)")
.lifecycle(rex::cvar::Lifecycle::kInitOnly); .lifecycle(rex::cvar::Lifecycle::kInitOnly);
@ -621,6 +627,8 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
XE_UI_VULKAN_LIMIT(optimalBufferCopyOffsetAlignment) XE_UI_VULKAN_LIMIT(optimalBufferCopyOffsetAlignment)
XE_UI_VULKAN_LIMIT(optimalBufferCopyRowPitchAlignment) XE_UI_VULKAN_LIMIT(optimalBufferCopyRowPitchAlignment)
XE_UI_VULKAN_LIMIT(nonCoherentAtomSize) XE_UI_VULKAN_LIMIT(nonCoherentAtomSize)
XE_UI_VULKAN_LIMIT(timestampComputeAndGraphics)
XE_UI_VULKAN_LIMIT(timestampPeriod)
if (with_gpu_emulation) { if (with_gpu_emulation) {
XE_UI_VULKAN_FEATURE(robustBufferAccess) XE_UI_VULKAN_FEATURE(robustBufferAccess)

View file

@ -17,6 +17,7 @@
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/math.h> #include <rex/math.h>
#include <rex/platform.h>
#include <rex/ui/vulkan/immediate_drawer.h> #include <rex/ui/vulkan/immediate_drawer.h>
#include <rex/ui/vulkan/presenter.h> #include <rex/ui/vulkan/presenter.h>
#include <rex/ui/vulkan/util.h> #include <rex/ui/vulkan/util.h>
@ -603,7 +604,11 @@ bool VulkanImmediateDrawer::EnsurePipelinesCreatedForCurrentRenderPass() {
input_assembly_state.pNext = nullptr; input_assembly_state.pNext = nullptr;
input_assembly_state.flags = 0; input_assembly_state.flags = 0;
input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; 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; input_assembly_state.primitiveRestartEnable = VK_FALSE;
#endif
VkPipelineViewportStateCreateInfo viewport_state; VkPipelineViewportStateCreateInfo viewport_state;
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

View file

@ -9,6 +9,7 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/ */
#include <cstdlib>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@ -16,6 +17,7 @@
#include <vector> #include <vector>
#include <rex/cvar.h> #include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/platform.h> #include <rex/platform.h>
#include <rex/ui/vulkan/instance.h> #include <rex/ui/vulkan/instance.h>
@ -31,6 +33,21 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
const bool try_enable_validation) { const bool try_enable_validation) {
std::unique_ptr<VulkanInstance> vulkan_instance(new VulkanInstance()); std::unique_ptr<VulkanInstance> 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. // Load the RenderDoc API if connected.
vulkan_instance->renderdoc_api_ = RenderDocAPI::CreateIfConnected(); vulkan_instance->renderdoc_api_ = RenderDocAPI::CreateIfConnected();
@ -40,7 +57,12 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
Functions& ifn = vulkan_instance->functions_; Functions& ifn = vulkan_instance->functions_;
bool functions_loaded = true; 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); REXLOG_ERROR("Failed to load {}", platform::lib_names::kVulkanLoader);
return nullptr; return nullptr;
} }
@ -116,6 +138,11 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
// #10. // #10.
requested_extensions.emplace("VK_KHR_win32_surface", requested_extensions.emplace("VK_KHR_win32_surface",
&vulkan_instance->extensions_.ext_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 #endif
} }
@ -379,6 +406,11 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
if (vulkan_instance->extensions_.ext_KHR_win32_surface) { if (vulkan_instance->extensions_.ext_KHR_win32_surface) {
#include <rex/ui/vulkan/functions/instance_khr_win32_surface.inc> #include <rex/ui/vulkan/functions/instance_khr_win32_surface.inc>
} }
#endif
#ifdef VK_USE_PLATFORM_METAL_EXT
if (vulkan_instance->extensions_.ext_EXT_metal_surface) {
#include <rex/ui/vulkan/functions/instance_ext_metal_surface.inc>
}
#endif #endif
if (vulkan_instance->extensions_.ext_KHR_surface) { if (vulkan_instance->extensions_.ext_KHR_surface) {
#include <rex/ui/vulkan/functions/instance_khr_surface.inc> #include <rex/ui/vulkan/functions/instance_khr_surface.inc>

View file

@ -11,6 +11,7 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cmath> #include <cmath>
@ -39,6 +40,9 @@
#if REX_PLATFORM_GNU_LINUX #if REX_PLATFORM_GNU_LINUX
#include <rex/ui/surface_gnulinux.h> #include <rex/ui/surface_gnulinux.h>
#endif #endif
#if REX_PLATFORM_MAC
#include <rex/ui/surface_sdl.h>
#endif
#if REX_PLATFORM_WIN32 #if REX_PLATFORM_WIN32
#include <rex/ui/surface_win.h> #include <rex/ui/surface_win.h>
#endif #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", REXCVAR_DEFINE_BOOL(vulkan_allow_present_mode_fifo_relaxed, true, "UI/Vulkan",
"Allow FIFO relaxed present mode"); "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 rex {
namespace ui { namespace ui {
namespace vulkan { 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<std::chrono::microseconds>(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 #if defined(REX_HAS_FIDELITYFX_RUNTIME) && REX_HAS_FIDELITYFX_RUNTIME
namespace { namespace {
@ -430,6 +501,11 @@ Surface::TypeFlags VulkanPresenter::GetSurfaceTypesSupportedByInstance(
type_flags |= Surface::kTypeFlag_XcbWindow; type_flags |= Surface::kTypeFlag_XcbWindow;
} }
#endif #endif
#if REX_PLATFORM_MAC
if (instance_extensions.ext_EXT_metal_surface) {
type_flags |= Surface::kTypeFlag_SDLMetalView;
}
#endif
#if REX_PLATFORM_WIN32 #if REX_PLATFORM_WIN32
if (instance_extensions.ext_KHR_win32_surface) { if (instance_extensions.ext_KHR_win32_surface) {
type_flags |= Surface::kTypeFlag_Win32Hwnd; 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 // The retirement or destruction of the swapchain here will also cause
// awaiting completion of the usage of the swapchain and the surface on the // awaiting completion of the usage of the swapchain and the surface on the
// GPU. // 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) { if (paint_context_.vulkan_surface != VK_NULL_HANDLE) {
VkSwapchainKHR old_swapchain = paint_context_.PrepareForSwapchainRetirement(); VkSwapchainKHR old_swapchain = paint_context_.PrepareForSwapchainRetirement();
bool surface_unusable; bool surface_unusable;
@ -823,6 +907,18 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread(Surface& new_su
vulkan_surface_create_result = ifn.vkCreateWin32SurfaceKHR( vulkan_surface_create_result = ifn.vkCreateWin32SurfaceKHR(
instance, &surface_create_info, nullptr, &paint_context_.vulkan_surface); instance, &surface_create_info, nullptr, &paint_context_.vulkan_surface);
} break; } break;
#endif
#if REX_PLATFORM_MAC
case Surface::kTypeIndex_SDLMetalView: {
auto& sdl_metal_surface = static_cast<const SDLMetalViewSurface&>(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 #endif
default: default:
assert_unhandled_case(surface_type); assert_unhandled_case(surface_type);
@ -1347,7 +1443,8 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface(
// interfering with GPU command processing, and also to allow tearing so // interfering with GPU command processing, and also to allow tearing so
// variable refresh rate may be used where it's available. // variable refresh rate may be used where it's available.
// Note: If the priorities here are changes, update the cvar descriptions. // 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) != std::find(present_modes.cbegin(), present_modes.cend(), VK_PRESENT_MODE_IMMEDIATE_KHR) !=
present_modes.cend()) { present_modes.cend()) {
// Allowing tearing to reduce latency, and possibly variable refresh rate // 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) { 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 // Begin the submission in place of the one not currently potentially used on
// the GPU. // the GPU.
uint64_t current_paint_submission_index = 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(); VkSemaphore acquire_semaphore = paint_submission.acquire_semaphore();
uint32_t swapchain_image_index; uint32_t swapchain_image_index;
const auto timing_acquire_start = std::chrono::steady_clock::now();
VkResult acquire_result = VkResult acquire_result =
dfn.vkAcquireNextImageKHR(device, paint_context_.swapchain, UINT64_MAX, acquire_semaphore, dfn.vkAcquireNextImageKHR(device, paint_context_.swapchain, UINT64_MAX, acquire_semaphore,
VK_NULL_HANDLE, &swapchain_image_index); VK_NULL_HANDLE, &swapchain_image_index);
const auto timing_acquire_end = std::chrono::steady_clock::now();
switch (acquire_result) { switch (acquire_result) {
case VK_SUCCESS: case VK_SUCCESS:
case VK_SUBOPTIMAL_KHR: case VK_SUBOPTIMAL_KHR:
@ -1798,6 +1898,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
paint_context_.guest_output_image_paint_last_submission); paint_context_.guest_output_image_paint_last_submission);
util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device, util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device,
swapchain_effect_pipeline.swapchain_pipeline); swapchain_effect_pipeline.swapchain_pipeline);
swapchain_effect_pipeline.swapchain_format = VK_FORMAT_UNDEFINED;
} }
if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) { if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) {
assert_true(CanGuestOutputPaintEffectBeFinal(swapchain_effect)); assert_true(CanGuestOutputPaintEffectBeFinal(swapchain_effect));
@ -1806,6 +1907,9 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
swapchain_effect, paint_context_.swapchain_render_pass); swapchain_effect, paint_context_.swapchain_render_pass);
if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) { if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) {
guest_output_flow.effect_count = 0; 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); dfn.vkCmdEndRenderPass(draw_command_buffer);
const auto timing_end_command_buffer_start = std::chrono::steady_clock::now();
dfn.vkEndCommandBuffer(draw_command_buffer); 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; VkPipelineStageFlags acquire_semaphore_wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkCommandBuffer command_buffers[2]; VkCommandBuffer command_buffers[2];
@ -2151,6 +2257,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
submit_info.pCommandBuffers = command_buffers; submit_info.pCommandBuffers = command_buffers;
submit_info.signalSemaphoreCount = 1; submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &present_semaphore; submit_info.pSignalSemaphores = &present_semaphore;
const auto timing_submit_start = std::chrono::steady_clock::now();
{ {
VulkanSubmissionTracker::FenceAcquisition fence_acqusition( VulkanSubmissionTracker::FenceAcquisition fence_acqusition(
paint_context_.submission_tracker.AcquireFenceToAdvanceSubmission()); paint_context_.submission_tracker.AcquireFenceToAdvanceSubmission());
@ -2192,6 +2299,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
return PaintResult::kNotPresentedConnectionOutdated; return PaintResult::kNotPresentedConnectionOutdated;
} }
} }
const auto timing_submit_end = std::chrono::steady_clock::now();
VkPresentInfoKHR present_info; VkPresentInfoKHR present_info;
present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; 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.pImageIndices = &swapchain_image_index;
present_info.pResults = nullptr; present_info.pResults = nullptr;
VkResult present_result; VkResult present_result;
const auto timing_present_start = std::chrono::steady_clock::now();
{ {
const VulkanDevice::Queue::Acquisition queue_acquisition = const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(paint_context_.present_queue_family, 0); vulkan_device_->AcquireQueue(paint_context_.present_queue_family, 0);
present_result = dfn.vkQueuePresentKHR(queue_acquisition.queue(), &present_info); 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) { switch (present_result) {
case VK_SUCCESS: case VK_SUCCESS:
return PaintResult::kPresented; return PaintResult::kPresented;
@ -2555,6 +2672,9 @@ VkPipeline VulkanPresenter::CreateGuestOutputPaintPipeline(GuestOutputPaintEffec
VkPipelineInputAssemblyStateCreateInfo input_assembly_state = {}; VkPipelineInputAssemblyStateCreateInfo input_assembly_state = {};
input_assembly_state.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; input_assembly_state.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
#if REX_PLATFORM_MAC
input_assembly_state.primitiveRestartEnable = VK_TRUE;
#endif
VkPipelineViewportStateCreateInfo viewport_state = {}; VkPipelineViewportStateCreateInfo viewport_state = {};
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

View file

@ -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 // 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 // an inner one is done, a "greater than" comparison will cause the inner
// loop to effectively terminate all outer ones). // loop to effectively terminate all outer ones).
break;
} }
auto it_new = std::prev(std::make_reverse_iterator(input_listeners_.emplace(z_order, listener))); 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 // If adding to layers in between the currently being processed ones (from

743
src/ui/window_sdl.cpp Normal file
View file

@ -0,0 +1,743 @@
#include <rex/ui/window_sdl.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <rex/assert.h>
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/graphics/video_mode_util.h>
#include <rex/logging.h>
#include <rex/platform.h>
#include <rex/ui/surface_sdl.h>
#include <rex/ui/virtual_key.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_scancode.h>
#include <SDL3/SDL_timer.h>
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<SDL_WindowID, rex::ui::SDLWindow*>& WindowMap() {
static std::unordered_map<SDL_WindowID, rex::ui::SDLWindow*> 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<void*>(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> 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<SDLWindow>(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<uintptr_t>(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<Surface> SDLWindow::CreateSurfaceImpl(Surface::TypeFlags allowed_types) {
if (!(allowed_types & Surface::kTypeFlag_SDLMetalView)) {
return nullptr;
}
return std::make_unique<SDLMetalViewSurface>(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<uintptr_t>(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<void*>(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<ui::MenuItem> MenuItem::Create(Type type, const std::string& text,
const std::string& hotkey,
std::function<void()> callback) {
return std::make_unique<SDLMenuItem>(type, text, hotkey, std::move(callback));
}
} // namespace ui
} // namespace rex

View file

@ -0,0 +1,70 @@
#include <rex/ui/windowed_app_context_sdl.h>
#include <rex/ui/window_sdl.h>
#include <rex/logging.h>
#include <cstdlib>
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

View file

@ -0,0 +1,61 @@
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/platform.h>
#include <rex/ui/windowed_app.h>
#include <rex/ui/windowed_app_context_sdl.h>
#include <SDL3/SDL.h>
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<rex::ui::WindowedApp> app = rex::ui::GetWindowedAppCreator()(app_context);
const auto& option_names = app->GetPositionalOptions();
std::map<std::string, std::string> 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;
}

View file

@ -5,10 +5,22 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
#============================================================================= #=============================================================================
# Architecture detection # 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_ARM64 FALSE)
set(IS_X86_64 TRUE) 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_ARM64 TRUE)
set(IS_X86_64 FALSE) set(IS_X86_64 FALSE)
else() else()
@ -197,6 +209,9 @@ if(REXGLUE_USE_VULKAN)
add_subdirectory(vulkan-memory-allocator) add_subdirectory(vulkan-memory-allocator)
# volk # 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_INSTALL ON CACHE BOOL "" FORCE)
set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE) set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE)
add_subdirectory(volk) add_subdirectory(volk)
@ -237,7 +252,11 @@ set(SDL_TEST OFF CACHE BOOL "" FORCE)
set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "" FORCE) set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "" FORCE)
# Disable unused subsystems # 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_RENDER OFF CACHE BOOL "" FORCE)
set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
set(SDL_SENSOR 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_OPENGL OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)
set(SDL_VULKAN 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 # Disable dummy drivers
set(SDL_DISKAUDIO OFF CACHE BOOL "" FORCE) set(SDL_DISKAUDIO OFF CACHE BOOL "" FORCE)

View file

@ -9,8 +9,18 @@
// big endian architectures need #define __BYTE_ORDER __BIG_ENDIAN // big endian architectures need #define __BYTE_ORDER __BIG_ENDIAN
#ifndef _MSC_VER #ifndef _MSC_VER
#ifdef __APPLE__
#include <machine/endian.h>
#ifndef __BYTE_ORDER
#define __BYTE_ORDER BYTE_ORDER
#endif
#ifndef __BIG_ENDIAN
#define __BIG_ENDIAN BIG_ENDIAN
#endif
#else
#include <endian.h> #include <endian.h>
#endif #endif
#endif
namespace sha256 { namespace sha256 {

View file

@ -30,7 +30,7 @@
#if defined(WIN32) || defined(_WIN32) #if defined(WIN32) || defined(_WIN32)
#define RENDERDOC_CC __cdecl #define RENDERDOC_CC __cdecl
#elif defined(__linux__) #elif defined(__linux__) || defined(__APPLE__)
#define RENDERDOC_CC #define RENDERDOC_CC
#else #else
#error "Unknown platform" #error "Unknown platform"