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(CMakePackageConfigHelpers)
if(UNIX AND NOT APPLE)
if(APPLE)
# Keep app bundles and command-line launches able to find dylibs staged
# beside the executable.
list(APPEND CMAKE_INSTALL_RPATH
"@loader_path"
"@loader_path/../${CMAKE_INSTALL_LIBDIR}"
)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH ON)
elseif(UNIX AND NOT APPLE)
# Keep installed binaries self-contained by resolving shared libs relative
# to the install prefix (for example, bin -> ../lib).
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
@ -86,15 +94,26 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_DEBUG_POSTFIX d)
set(CMAKE_RELWITHDEBINFO_POSTFIX rd)
if(NOT WIN32)
set(REX_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES REX_OSX_ARCH_COUNT)
if(REX_OSX_ARCH_COUNT EQUAL 1)
set(REX_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}")
endif()
endif()
if(UNIX AND NOT APPLE)
# Use appropriate code model based on architecture
if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64")
if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64")
# Large code model on x86_64 for linking with very large recompiled executables (35MB+)
add_compile_options(-mcmodel=large)
else()
# Small code model for other architectures (ARM64, etc.)
add_compile_options(-mcmodel=small)
endif()
endif()
if(NOT WIN32)
# Position-independent code for all targets
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
@ -112,25 +131,32 @@ add_compile_options(
# Platform detection (supports AMD64 and ARM64)
if(WIN32)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64")
if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64")
set(REX_PLATFORM "win-amd64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64")
set(REX_PLATFORM "win-arm64")
else()
message(FATAL_ERROR "Unsupported Windows architecture: ${CMAKE_SYSTEM_PROCESSOR}")
message(FATAL_ERROR "Unsupported Windows architecture: ${REX_TARGET_PROCESSOR}")
endif()
add_compile_definitions(REX_PLATFORM_WINDOWS=1)
elseif(APPLE)
if(REX_TARGET_PROCESSOR MATCHES "aarch64|arm64|ARM64")
set(REX_PLATFORM "macos-arm64")
else()
message(FATAL_ERROR "Unsupported macOS architecture: ${REX_TARGET_PROCESSOR}. macOS builds are ARM64-only.")
endif()
add_compile_definitions(REX_PLATFORM_MAC=1)
elseif(UNIX AND NOT APPLE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64")
if(REX_TARGET_PROCESSOR MATCHES "AMD64|x86_64")
set(REX_PLATFORM "linux-amd64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
elseif(REX_TARGET_PROCESSOR MATCHES "aarch64|ARM64")
set(REX_PLATFORM "linux-arm64")
else()
set(REX_PLATFORM "linux-${CMAKE_SYSTEM_PROCESSOR}")
set(REX_PLATFORM "linux-${REX_TARGET_PROCESSOR}")
endif()
add_compile_definitions(REX_PLATFORM_LINUX=1)
else()
message(FATAL_ERROR "Unsupported platform. ReXGlue supports Windows and Linux only.")
message(FATAL_ERROR "Unsupported platform. ReXGlue supports Windows, Linux and macOS only.")
endif()
# Build config as compile definition (multi-config generators don't know config at configure time)

View file

@ -14,22 +14,30 @@
# runtime DLL staging is the host's job (see rexglue_configure_target).
#==========================================================
function(rexglue_apply_target_settings target_name)
set(_rexglue_target_processor "${CMAKE_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _rexglue_osx_arch_count)
if(_rexglue_osx_arch_count EQUAL 1)
set(_rexglue_target_processor "${CMAKE_OSX_ARCHITECTURES}")
endif()
endif()
if(UNIX AND NOT APPLE)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
target_include_directories(${target_name} PRIVATE ${GTK3_INCLUDE_DIRS})
target_link_libraries(${target_name} PRIVATE ${GTK3_LIBRARIES})
# Large executable support
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(_rexglue_target_processor MATCHES "x86_64|AMD64")
target_link_options(${target_name} PRIVATE -Wl,--no-relax)
target_compile_options(${target_name} PRIVATE -mcmodel=large)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
elseif(_rexglue_target_processor MATCHES "aarch64|ARM64")
target_compile_options(${target_name} PRIVATE -march=armv8-a)
endif()
endif()
if(NOT MSVC)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(NOT MSVC AND NOT APPLE)
if(_rexglue_target_processor MATCHES "x86_64|AMD64")
target_compile_options(${target_name} PRIVATE -msse4.1)
endif()
endif()
@ -51,6 +59,9 @@ function(rexglue_configure_target target_name)
if(WIN32)
target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_win.cpp)
elseif(APPLE)
target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_sdl.cpp)
else()
target_sources(${target_name} PRIVATE
${REXGLUE_SHARE_DIR}/windowed_app_main_posix.cpp)
@ -68,7 +79,12 @@ function(rexglue_configure_target target_name)
target_include_directories(${target_name} PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/rexglue-sdk/include)
if(UNIX AND NOT APPLE)
if(APPLE)
set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "@loader_path"
BUILD_WITH_INSTALL_RPATH ON
)
elseif(UNIX AND NOT APPLE)
set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH ON
@ -97,6 +113,56 @@ function(rexglue_configure_target target_name)
)
endif()
endforeach()
elseif(APPLE)
set(_rexglue_macos_vulkan_lib_dirs)
set(_rexglue_macos_vulkan_roots)
if(DEFINED ENV{VULKAN_SDK})
list(APPEND _rexglue_macos_vulkan_lib_dirs "$ENV{VULKAN_SDK}/lib")
list(APPEND _rexglue_macos_vulkan_roots "$ENV{VULKAN_SDK}")
endif()
list(APPEND _rexglue_macos_vulkan_lib_dirs /opt/homebrew/lib /usr/local/lib)
foreach(_vk_lib libvulkan.1.dylib libvulkan.dylib libMoltenVK.dylib)
foreach(_vk_lib_dir ${_rexglue_macos_vulkan_lib_dirs})
if(EXISTS "${_vk_lib_dir}/${_vk_lib}")
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_vk_lib_dir}/${_vk_lib}"
$<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()
endfunction()
@ -131,7 +197,12 @@ function(rexglue_configure_module_target target_name)
)
endif()
if(UNIX AND NOT APPLE)
if(APPLE)
set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "@loader_path"
BUILD_WITH_INSTALL_RPATH ON
)
elseif(UNIX AND NOT APPLE)
set_target_properties(${target_name} PROPERTIES
INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH ON

View file

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

View file

@ -17,6 +17,20 @@
#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 {
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_data_ = 0;
std::atomic<bool> shutting_down_{false};
std::atomic<bool> vsync_worker_running_;
system::object_ref<system::XHostThread> vsync_worker_thread_;

View file

@ -991,8 +991,11 @@ class SpirvShaderTranslator : public ShaderTranslator {
spv::Id main_memexport_allowed_;
// VS rectangle-list fallback only - uint.
spv::Id var_main_rectangle_guest_vertex_index_;
// VS rectangle-list fallback only - gl_PerVertex[3].
spv::Id var_main_rectangle_per_vertex_;
// VS rectangle-list fallback only - float4[3].
spv::Id var_main_rectangle_position_;
// VS rectangle-list fallback only - float[3 * distance_count], when needed.
spv::Id var_main_rectangle_clip_distances_;
spv::Id var_main_rectangle_cull_distances_;
// VS rectangle-list fallback only - float4[3] for each used interpolator.
std::array<spv::Id, xenos::kMaxInterpolators> var_main_rectangle_interpolators_;
// VS only - float3 (special exports).

View file

@ -37,6 +37,7 @@
#include <rex/graphics/vulkan/texture_cache.h>
#include <rex/graphics/xenos.h>
#include <rex/hash.h>
#include <rex/perf/counter.h>
#include <rex/system/kernel_state.h>
#include <rex/ui/vulkan/linked_type_descriptor_set_allocator.h>
#include <rex/ui/vulkan/presenter.h>
@ -242,6 +243,9 @@ class VulkanCommandProcessor : public CommandProcessor {
bool keep_dynamic_blend_constants = false,
bool keep_dynamic_stencil_mask_ref = false);
void BindExternalComputePipeline(VkPipeline pipeline);
uint32_t BeginGpuTimestampedRegion(rex::perf::DrawBucket bucket) {
return BeginGpuTimestampedDraw(bucket);
}
void SetViewport(const VkViewport& viewport);
void SetScissor(const VkRect2D& scissor);
@ -446,6 +450,14 @@ class VulkanCommandProcessor : public CommandProcessor {
void DisableHostOcclusionQueries();
uint64_t NormalizeOcclusionSamples(uint64_t samples) const;
void WriteGuestOcclusionResult(uint32_t sample_count_address, uint64_t samples);
bool InitializeGpuTimestampResources();
void ShutdownGpuTimestampResources();
void BeginGpuTimestampFrame();
void EndGpuTimestampFrame();
void ResolveGpuTimestampFrame(VkCommandBuffer command_buffer);
void ProcessGpuTimestampResults();
uint32_t BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket);
void EndGpuTimestampedDraw(uint32_t start_query);
void InvalidateAllVertexBufferResidency();
void InvalidateVertexBufferResidency(uint32_t vfetch_index);
void InvalidateVertexBufferResidencyRange(uint32_t first_vfetch, uint32_t last_vfetch);
@ -747,6 +759,32 @@ class VulkanCommandProcessor : public CommandProcessor {
uint32_t host_index = UINT32_MAX;
bool valid = false;
} active_occlusion_query_;
// MoltenVK maps timestamp queries to Metal counter sample buffers with a
// small per-pool limit, so keep the per-frame query budget modest.
static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 1024;
VkQueryPool gpu_timestamp_query_pool_ = VK_NULL_HANDLE;
VkBuffer gpu_timestamp_readback_buffer_ = VK_NULL_HANDLE;
VkDeviceMemory gpu_timestamp_readback_memory_ = VK_NULL_HANDLE;
uint32_t gpu_timestamp_readback_memory_type_ = UINT32_MAX;
VkDeviceSize gpu_timestamp_readback_memory_size_ = 0;
uint64_t* gpu_timestamp_readback_mapping_ = nullptr;
bool gpu_timestamp_resources_available_ = false;
struct GpuTimestampFrame {
uint64_t frame = 0;
uint64_t submission = 0;
uint32_t query_base = 0;
uint32_t query_count = 0;
uint32_t frame_start_query = UINT32_MAX;
uint32_t frame_end_query = UINT32_MAX;
bool pending = false;
bool active_bucket_valid = false;
rex::perf::DrawBucket active_bucket = rex::perf::DrawBucket::kMainColorDepth;
uint32_t active_bucket_index = UINT32_MAX;
std::vector<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 {
uint32_t address = 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);
}
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:
enum class Command {
kVkBeginRenderPass,
@ -381,6 +390,7 @@ class DeferredCommandBuffer {
kVkSetStencilReference,
kVkSetStencilWriteMask,
kVkSetViewport,
kVkWriteTimestamp,
};
struct CommandHeader {
@ -526,6 +536,12 @@ class DeferredCommandBuffer {
uint32_t query_count;
};
struct ArgsVkWriteTimestamp {
VkPipelineStageFlagBits pipeline_stage;
VkQueryPool query_pool;
uint32_t query;
};
struct ArgsVkPipelineBarrier {
VkPipelineStageFlags src_stage_mask;
VkPipelineStageFlags dst_stage_mask;

View file

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

View file

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

View file

@ -29,6 +29,11 @@ enum class DrawBucket : uint8_t {
kCopyResolve,
kMemexport,
kNoPixelShader,
kCopyDump,
kCopyResolveShader,
kCopyResolveFast32,
kCopyResolveFull32,
kResolveDownscale,
};
struct DrawFingerprint {
@ -91,6 +96,11 @@ enum class CounterId : uint16_t {
kGpuCopyUs,
kGpuMemexportUs,
kGpuNoPixelShaderUs,
kGpuCopyDumpUs,
kGpuCopyResolveUs,
kGpuCopyResolveFast32Us,
kGpuCopyResolveFull32Us,
kGpuResolveDownscaleUs,
kGpuTimestampedDraws,
kGpuCommandProcessorFrameUs,
kGpuTimestampedFrames,
@ -178,6 +188,37 @@ enum class CounterId : uint16_t {
kRtResolveTransferClearUs,
kRtResolveReadbackCalls,
kRtResolveReadbackUs,
kRtDumpCalls,
kRtDumpRectangles,
kRtDumpDispatches,
kRtDumpGroups,
kRtResolveCopyDispatches,
kRtResolveCopyGroups,
kRtResolveScaledBytes,
kRtScaledResolveWriteBarriers,
kRtScaledResolveWriteBarrierOverlaps,
kRtScaledResolveWriteBarrierSkipped,
kRtScaledResolveWriteBarrierBytes,
kRtResolveCopyFast32bpp1x2xMsaaDispatches,
kRtResolveCopyFast32bpp1x2xMsaaGroups,
kRtResolveCopyFast32bpp4xMsaaDispatches,
kRtResolveCopyFast32bpp4xMsaaGroups,
kRtResolveCopyFast64bpp1x2xMsaaDispatches,
kRtResolveCopyFast64bpp1x2xMsaaGroups,
kRtResolveCopyFast64bpp4xMsaaDispatches,
kRtResolveCopyFast64bpp4xMsaaGroups,
kRtResolveCopyFull8bppDispatches,
kRtResolveCopyFull8bppGroups,
kRtResolveCopyFull16bppDispatches,
kRtResolveCopyFull16bppGroups,
kRtResolveCopyFull32bppDispatches,
kRtResolveCopyFull32bppGroups,
kRtResolveCopyFull64bppDispatches,
kRtResolveCopyFull64bppGroups,
kRtResolveCopyFull128bppDispatches,
kRtResolveCopyFull128bppGroups,
kDrawNoPixelDepthTestCalls,
kDrawNoPixelDepthWriteCalls,
// Audio
kXmaFramesDecoded,

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* kSpirvToolsSdkPath = "bin/libSPIRV-Tools-shared.so";
#elif REX_PLATFORM_MAC
inline constexpr const char* kVulkanLoader = "libvulkan.1.dylib";
inline constexpr const char* kVulkanLoaderFallback = "libvulkan.dylib";
inline constexpr const char* kMoltenVK = "libMoltenVK.dylib";
inline constexpr const char* kRenderDoc = "librenderdoc.dylib";
inline constexpr const char* kSpirvToolsSdkPath = "bin/libSPIRV-Tools-shared.dylib";
#else
#error No library names provided for the target platform.
#endif

View file

@ -75,7 +75,10 @@ struct FPSCRPlatform {
return csr;
}
static inline void setcsr(u32 csr) noexcept { __asm__ __volatile__("msr fpcr, %0" : : "r"(csr)); }
static inline void setcsr(u32 csr) noexcept {
u64 fpcr = csr;
__asm__ __volatile__("msr fpcr, %0" : : "r"(fpcr));
}
static inline void InitHostExceptions(u32& csr) noexcept {
csr &= ~ExceptionMask; // Clear enable bits to disable exceptions

View file

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

View file

@ -22,7 +22,11 @@
#include <thread>
#include <rex/image_info.h>
#include <rex/platform.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_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::unique_ptr<Runtime> runtime_;
std::unique_ptr<ui::Window> window_;
#if REX_PLATFORM_MAC
system::object_ref<system::XThread> main_thread_;
#endif
std::thread module_thread_;
std::atomic<bool> shutting_down_{false};
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/logging.h>
#include <rex/std_compat.h>
#include <rex/system/thread_state.h>
#include <rex/thread/fiber.h>
#include <rex/system/util/native_list.h>
@ -278,7 +279,6 @@ class KernelState {
void OnThreadExecute(XThread* thread);
void OnThreadExit(XThread* thread);
object_ref<XThread> GetThreadByID(uint32_t thread_id);
FiberInfo* LookupFiber(uint32_t guest_addr);
void RegisterFiber(uint32_t guest_addr, const FiberInfo& info);
void UnregisterFiber(uint32_t guest_addr);

View file

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

View file

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

View file

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

View file

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

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
#if REX_PLATFORM_MAC
#ifndef VK_USE_PLATFORM_METAL_EXT
#define VK_USE_PLATFORM_METAL_EXT
#endif
#endif
#if REX_PLATFORM_WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN

View file

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

View file

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

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)
#ifdef VK_USE_PLATFORM_WIN32_KHR
#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
// 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>
@ -91,6 +95,9 @@ class VulkanInstance {
#endif
#ifdef VK_USE_PLATFORM_WIN32_KHR
bool ext_KHR_win32_surface = false; // #10
#endif
#ifdef VK_USE_PLATFORM_METAL_EXT
bool ext_EXT_metal_surface = false; // #218
#endif
bool ext_1_1_KHR_get_physical_device_properties2 = false; // #60
bool ext_EXT_debug_utils = false; // #129

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(__imp__##name)
#if defined(__APPLE__)
#define DEFINE_REX_FUNC(name) \
REX_EXTERN(__imp__##name); \
REX_WEAK_FUNC(name) { \
__imp__##name(ctx, base); \
} \
REX_EXTERN(__imp__##name)
#else
#define DEFINE_REX_FUNC(name) \
__attribute__((alias("__imp__" #name))) REX_WEAK_FUNC(name); \
REX_EXTERN(__imp__##name)
#endif
//=============================================================================
// Function Prologue

View file

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

View file

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

View file

@ -76,7 +76,10 @@ target_include_directories(rexcore PRIVATE
)
if(UNIX)
target_link_libraries(rexcore PRIVATE pthread rt dl)
target_link_libraries(rexcore PRIVATE pthread dl)
if(NOT APPLE)
target_link_libraries(rexcore PRIVATE rt)
endif()
elseif(WIN32)
target_link_libraries(rexcore PUBLIC ws2_32)
endif()

View file

@ -16,19 +16,27 @@ static_assert(REX_PLATFORM_LINUX || REX_PLATFORM_MAC, "This file is POSIX-only")
namespace rex::chrono {
#if REX_PLATFORM_MAC
constexpr clockid_t kHostClock = CLOCK_MONOTONIC;
#else
constexpr clockid_t kHostClock = CLOCK_MONOTONIC_RAW;
#endif
uint64_t Clock::host_tick_frequency_platform() {
timespec res;
int error = clock_getres(CLOCK_MONOTONIC_RAW, &res);
int error = clock_getres(kHostClock, &res);
assert_zero(error);
assert_zero(res.tv_sec); // Sub second resolution is required.
assert_true(res.tv_nsec > 0);
// Convert nano seconds to hertz. Resolution is 1ns on most systems.
return 1000000000ull / res.tv_nsec;
// host_tick_count_platform returns elapsed nanoseconds, not ticks in units of
// clock_getres. Report the frequency for those returned units.
return 1000000000ull;
}
uint64_t Clock::host_tick_count_platform() {
timespec tp;
int error = clock_gettime(CLOCK_MONOTONIC_RAW, &tp);
int error = clock_gettime(kHostClock, &tp);
assert_zero(error);
return tp.tv_nsec + tp.tv_sec * 1000000000ull;

View file

@ -49,6 +49,17 @@ std::unordered_map<std::string, size_t>& GetRegistryIndex() {
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
std::string FlagNameToEnvVar(std::string_view name) {
std::string result = "REX_";
@ -538,11 +549,19 @@ std::vector<std::string> Init(int argc, char** argv) {
if (entry.type == FlagType::Boolean) {
app.add_flag_function(
"--" + entry.name + ",!--no-" + entry.name,
[&entry](int64_t count) { entry.setter(count > 0 ? "true" : "false"); },
[&entry](int64_t count) {
std::string value = count > 0 ? "true" : "false";
GetCommandLineOverrides().emplace_back(entry.name, value);
entry.setter(value);
},
entry.description);
} else {
app.add_option_function<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);
}
}
@ -567,6 +586,8 @@ void LoadConfig(const std::filesystem::path& config_path) {
try {
auto config = toml::parse_file(config_path.string());
ApplyTomlTable(config, "");
ApplyCommandLineOverrides();
ApplyEnvironment();
REXLOG_INFO("Loaded config from {}", config_path.string());
} catch (const toml::parse_error& err) {
REXLOG_ERROR("Failed to parse config {}: {}", config_path.string(), err.what());

View file

@ -9,12 +9,17 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#if defined(__APPLE__) && !defined(_XOPEN_SOURCE)
#define _XOPEN_SOURCE 700
#endif
#include <rex/exception_handler.h>
#if REX_PLATFORM_LINUX || REX_PLATFORM_MAC
#include <signal.h>
#include <cstdlib>
#include <cstdint>
#include <cstring>
@ -30,6 +35,7 @@ namespace rex::arch {
bool signal_handlers_installed_ = false;
struct sigaction original_sigill_handler_;
struct sigaction original_sigsegv_handler_;
struct sigaction original_sigbus_handler_;
// This can be as large as needed, but isn't often needed.
// As we will be sometimes firing many exceptions we want to avoid having to
@ -40,9 +46,45 @@ constexpr size_t kMaxHandlerCount = 8;
// Executed in order.
std::pair<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,
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;
#endif
HostThreadContext thread_context;
@ -70,6 +112,19 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
std::memcpy(thread_context.xmm_registers, mcontext.fpregs->_xmm,
sizeof(thread_context.xmm_registers));
#elif REX_ARCH_ARM64
#if REX_PLATFORM_MAC
auto& thread_state = mcontext->__ss;
auto& neon_state = mcontext->__ns;
std::memcpy(thread_context.x, thread_state.__x, sizeof(thread_state.__x));
thread_context.x[29] = thread_state.__fp;
thread_context.x[30] = thread_state.__lr;
thread_context.sp = thread_state.__sp;
thread_context.pc = thread_state.__pc;
thread_context.pstate = thread_state.__cpsr;
thread_context.fpsr = neon_state.__fpsr;
thread_context.fpcr = neon_state.__fpcr;
std::memcpy(thread_context.v, neon_state.__v, sizeof(thread_context.v));
#else
std::memcpy(thread_context.x, mcontext.regs, sizeof(thread_context.x));
thread_context.sp = mcontext.sp;
thread_context.pc = mcontext.pc;
@ -98,6 +153,7 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
thread_context.fpcr = mcontext_fpsimd->fpcr;
std::memcpy(thread_context.v, mcontext_fpsimd->vregs, sizeof(thread_context.v));
}
#endif // REX_PLATFORM_MAC
#endif // REX_ARCH
Exception ex;
@ -105,7 +161,8 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
case SIGILL:
ex.InitializeIllegalInstruction(&thread_context);
break;
case SIGSEGV: {
case SIGSEGV:
case SIGBUS: {
Exception::AccessViolationOperation access_violation_operation;
#if REX_ARCH_AMD64
// x86_pf_error_code::X86_PF_WRITE
@ -114,6 +171,24 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
? Exception::AccessViolationOperation::kWrite
: Exception::AccessViolationOperation::kRead;
#elif REX_ARCH_ARM64
#if REX_PLATFORM_MAC
const uint32_t esr = mcontext->__es.__esr;
if (((esr >> 26) & 0b111110) == 0b100100) {
access_violation_operation = (esr & (UINT64_C(1) << 6))
? Exception::AccessViolationOperation::kWrite
: Exception::AccessViolationOperation::kRead;
} else {
bool instruction_is_store;
if (IsArm64LoadPrefetchStore(*reinterpret_cast<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
// Exception Level, 0b100101 without a change in the Exception Level),
// bit 6 is 0 for reading from a memory location, 1 for writing to a
@ -145,6 +220,7 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
access_violation_operation = Exception::AccessViolationOperation::kUnknown;
}
}
#endif // REX_PLATFORM_MAC
#else
access_violation_operation = Exception::AccessViolationOperation::kUnknown;
#endif // REX_ARCH
@ -181,6 +257,34 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
&thread_context.xmm_registers[modified_register_index], sizeof(vec128_t));
}
#elif REX_ARCH_ARM64
#if REX_PLATFORM_MAC
auto& thread_state = mcontext->__ss;
auto& neon_state = mcontext->__ns;
uint32_t modified_register_index;
uint32_t modified_x_registers_remaining = ex.modified_x_registers();
while (rex::bit_scan_forward(modified_x_registers_remaining, &modified_register_index)) {
modified_x_registers_remaining &= ~(UINT32_C(1) << modified_register_index);
if (modified_register_index < 29) {
thread_state.__x[modified_register_index] =
thread_context.x[modified_register_index];
} else if (modified_register_index == 29) {
thread_state.__fp = thread_context.x[29];
} else if (modified_register_index == 30) {
thread_state.__lr = thread_context.x[30];
}
}
thread_state.__sp = thread_context.sp;
thread_state.__pc = thread_context.pc;
thread_state.__cpsr = uint32_t(thread_context.pstate);
neon_state.__fpsr = thread_context.fpsr;
neon_state.__fpcr = thread_context.fpcr;
uint32_t modified_v_registers_remaining = ex.modified_v_registers();
while (rex::bit_scan_forward(modified_v_registers_remaining, &modified_register_index)) {
modified_v_registers_remaining &= ~(UINT32_C(1) << modified_register_index);
std::memcpy(&neon_state.__v[modified_register_index],
&thread_context.v[modified_register_index], sizeof(vec128_t));
}
#else
uint32_t modified_register_index;
uint32_t modified_x_registers_remaining = ex.modified_x_registers();
while (rex::bit_scan_forward(modified_x_registers_remaining, &modified_register_index)) {
@ -201,10 +305,13 @@ static void ExceptionHandlerCallback(int signal_number, siginfo_t* signal_info,
mcontext.regs[modified_register_index] = thread_context.x[modified_register_index];
}
}
#endif // REX_PLATFORM_MAC
#endif // REX_ARCH
return;
}
}
DispatchUnhandledSignal(signal_number, signal_info, signal_context);
}
void ExceptionHandler::Install(Handler fn, void* data) {
@ -221,6 +328,9 @@ void ExceptionHandler::Install(Handler fn, void* data) {
if (sigaction(SIGSEGV, &signal_handler, &original_sigsegv_handler_) != 0) {
assert_always("Failed to install new SIGSEGV handler");
}
if (sigaction(SIGBUS, &signal_handler, &original_sigbus_handler_) != 0) {
assert_always("Failed to install new SIGBUS handler");
}
signal_handlers_installed_ = true;
}
@ -261,6 +371,9 @@ void ExceptionHandler::Uninstall(Handler fn, void* data) {
if (sigaction(SIGSEGV, &original_sigsegv_handler_, NULL) != 0) {
assert_always("Failed to restore original SIGSEGV handler");
}
if (sigaction(SIGBUS, &original_sigbus_handler_, NULL) != 0) {
assert_always("Failed to restore original SIGBUS handler");
}
signal_handlers_installed_ = false;
}
}

View file

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

View file

@ -10,10 +10,13 @@
*/
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <vector>
#include <fcntl.h>
#include <sys/stat.h>
@ -23,6 +26,7 @@
#include <rex/assert.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <rex/platform.h>
#include <rex/string.h>
#include <dirent.h>
@ -30,6 +34,15 @@
#include <libgen.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 {
std::string path_to_utf8(const std::filesystem::path& path) {
@ -51,10 +64,31 @@ std::filesystem::path to_path(const std::u16string_view source) {
namespace filesystem {
std::filesystem::path GetExecutablePath() {
#if REX_PLATFORM_MAC
uint32_t size = PATH_MAX;
std::vector<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] = "";
readlink("/proc/self/exe", buff, FILENAME_MAX);
ssize_t length = readlink("/proc/self/exe", buff, FILENAME_MAX - 1);
if (length < 0) {
return {};
}
buff[length] = '\0';
std::string s(buff);
return s;
#endif
}
std::filesystem::path GetExecutableFolder() {

View file

@ -20,6 +20,13 @@
#include <rex/memory/mapped_memory.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 {
class PosixMappedMemory : public MappedMemory {

View file

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

View file

@ -112,6 +112,12 @@ bool IsDelayedGpuTimingCounter(size_t counter_index) {
return id == CounterId::kGpuMainUs || id == CounterId::kGpuDepthUs ||
id == CounterId::kGpuCopyUs || id == CounterId::kGpuMemexportUs ||
id == CounterId::kGpuNoPixelShaderUs ||
id == CounterId::kGpuCopyDumpUs ||
id == CounterId::kGpuCopyResolveUs ||
id == CounterId::kGpuCopyResolveFast32Us ||
id == CounterId::kGpuCopyResolveFull32Us ||
id == CounterId::kGpuResolveDownscaleUs ||
id == CounterId::kGpuTimestampedDraws ||
id == CounterId::kGpuCommandProcessorFrameUs ||
id == CounterId::kGpuTimestampedFrames ||
id == CounterId::kGpuBarrierUs || id == CounterId::kGpuClearUs ||
@ -158,6 +164,11 @@ constexpr const char* kCounterNames[] = {
"gpu_copy_us",
"gpu_memexport_us",
"gpu_no_pixel_shader_us",
"gpu_copy_dump_us",
"gpu_copy_resolve_us",
"gpu_copy_resolve_fast32_us",
"gpu_copy_resolve_full32_us",
"gpu_resolve_downscale_us",
"gpu_timestamped_draws",
"gpu_command_processor_frame_us",
"gpu_timestamped_frames",
@ -245,6 +256,37 @@ constexpr const char* kCounterNames[] = {
"rt_resolve_transfer_clear_us",
"rt_resolve_readback_calls",
"rt_resolve_readback_us",
"rt_dump_calls",
"rt_dump_rectangles",
"rt_dump_dispatches",
"rt_dump_groups",
"rt_resolve_copy_dispatches",
"rt_resolve_copy_groups",
"rt_resolve_scaled_bytes",
"rt_scaled_resolve_write_barriers",
"rt_scaled_resolve_write_barrier_overlaps",
"rt_scaled_resolve_write_barrier_skipped",
"rt_scaled_resolve_write_barrier_bytes",
"rt_resolve_copy_fast32_1x2x_dispatches",
"rt_resolve_copy_fast32_1x2x_groups",
"rt_resolve_copy_fast32_4x_dispatches",
"rt_resolve_copy_fast32_4x_groups",
"rt_resolve_copy_fast64_1x2x_dispatches",
"rt_resolve_copy_fast64_1x2x_groups",
"rt_resolve_copy_fast64_4x_dispatches",
"rt_resolve_copy_fast64_4x_groups",
"rt_resolve_copy_full8_dispatches",
"rt_resolve_copy_full8_groups",
"rt_resolve_copy_full16_dispatches",
"rt_resolve_copy_full16_groups",
"rt_resolve_copy_full32_dispatches",
"rt_resolve_copy_full32_groups",
"rt_resolve_copy_full64_dispatches",
"rt_resolve_copy_full64_groups",
"rt_resolve_copy_full128_dispatches",
"rt_resolve_copy_full128_groups",
"draw_no_pixel_depth_test_calls",
"draw_no_pixel_depth_write_calls",
"xma_frames_decoded",
"audio_frame_latency_us",
"buffer_queue_depth",
@ -300,6 +342,11 @@ constexpr bool kIsGauge[] = {
false, // kGpuCopyUs
false, // kGpuMemexportUs
false, // kGpuNoPixelShaderUs
false, // kGpuCopyDumpUs
false, // kGpuCopyResolveUs
false, // kGpuCopyResolveFast32Us
false, // kGpuCopyResolveFull32Us
false, // kGpuResolveDownscaleUs
false, // kGpuTimestampedDraws
false, // kGpuCommandProcessorFrameUs
false, // kGpuTimestampedFrames
@ -387,6 +434,37 @@ constexpr bool kIsGauge[] = {
false, // kRtResolveTransferClearUs
false, // kRtResolveReadbackCalls
false, // kRtResolveReadbackUs
false, // kRtDumpCalls
false, // kRtDumpRectangles
false, // kRtDumpDispatches
false, // kRtDumpGroups
false, // kRtResolveCopyDispatches
false, // kRtResolveCopyGroups
false, // kRtResolveScaledBytes
false, // kRtScaledResolveWriteBarriers
false, // kRtScaledResolveWriteBarrierOverlaps
false, // kRtScaledResolveWriteBarrierSkipped
false, // kRtScaledResolveWriteBarrierBytes
false, // kRtResolveCopyFast32bpp1x2xMsaaDispatches
false, // kRtResolveCopyFast32bpp1x2xMsaaGroups
false, // kRtResolveCopyFast32bpp4xMsaaDispatches
false, // kRtResolveCopyFast32bpp4xMsaaGroups
false, // kRtResolveCopyFast64bpp1x2xMsaaDispatches
false, // kRtResolveCopyFast64bpp1x2xMsaaGroups
false, // kRtResolveCopyFast64bpp4xMsaaDispatches
false, // kRtResolveCopyFast64bpp4xMsaaGroups
false, // kRtResolveCopyFull8bppDispatches
false, // kRtResolveCopyFull8bppGroups
false, // kRtResolveCopyFull16bppDispatches
false, // kRtResolveCopyFull16bppGroups
false, // kRtResolveCopyFull32bppDispatches
false, // kRtResolveCopyFull32bppGroups
false, // kRtResolveCopyFull64bppDispatches
false, // kRtResolveCopyFull64bppGroups
false, // kRtResolveCopyFull128bppDispatches
false, // kRtResolveCopyFull128bppGroups
false, // kDrawNoPixelDepthTestCalls
false, // kDrawNoPixelDepthWriteCalls
false, // kXmaFramesDecoded
false, // kAudioFrameLatencyUs
false, // kBufferQueueDepth (set each frame)
@ -421,6 +499,16 @@ const char* DrawBucketName(DrawBucket bucket) {
return "MemExp";
case DrawBucket::kNoPixelShader:
return "NoPS";
case DrawBucket::kCopyDump:
return "CopyDump";
case DrawBucket::kCopyResolveShader:
return "CopyResolve";
case DrawBucket::kCopyResolveFast32:
return "CopyResolveFast32";
case DrawBucket::kCopyResolveFull32:
return "CopyResolveFull32";
case DrawBucket::kResolveDownscale:
return "ResolveDownscale";
default:
return "Unknown";
}

View file

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

View file

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

View file

@ -145,7 +145,9 @@ void SpirvShaderTranslator::Reset() {
var_main_memexport_data_written_ = spv::NoResult;
main_memexport_allowed_ = spv::NoResult;
var_main_rectangle_guest_vertex_index_ = spv::NoResult;
var_main_rectangle_per_vertex_ = spv::NoResult;
var_main_rectangle_position_ = spv::NoResult;
var_main_rectangle_clip_distances_ = spv::NoResult;
var_main_rectangle_cull_distances_ = spv::NoResult;
std::fill(var_main_rectangle_interpolators_.begin(), var_main_rectangle_interpolators_.end(),
spv::NoResult);
var_main_point_size_edge_flag_kill_vertex_ = spv::NoResult;
@ -514,11 +516,34 @@ void SpirvShaderTranslator::StartTranslation() {
var_main_rectangle_guest_vertex_index_ =
builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction, type_uint_,
"xe_var_rectangle_guest_vertex_index", const_uint_0_);
spv::Id type_rectangle_per_vertex_array =
builder_->makeArrayType(type_output_per_vertex_, builder_->makeUintConstant(3), 0);
var_main_rectangle_per_vertex_ =
builder_->createVariable(spv::NoPrecision, spv::StorageClassFunction,
type_rectangle_per_vertex_array, "xe_var_rectangle_per_vertex");
spv::Id type_rectangle_position_array =
builder_->makeArrayType(type_float4_, builder_->makeUintConstant(3), sizeof(float) * 4);
builder_->addDecoration(type_rectangle_position_array, spv::DecorationArrayStride,
sizeof(float) * 4);
var_main_rectangle_position_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_rectangle_position_array,
"xe_var_rectangle_position");
Modification shader_modification = GetSpirvShaderModification();
uint32_t clip_distance_count = shader_modification.GetVertexClipDistanceCount();
if (clip_distance_count) {
spv::Id type_rectangle_clip_distance_array = builder_->makeArrayType(
type_float_, builder_->makeUintConstant(3 * clip_distance_count), sizeof(float));
builder_->addDecoration(type_rectangle_clip_distance_array, spv::DecorationArrayStride,
sizeof(float));
var_main_rectangle_clip_distances_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_rectangle_clip_distance_array,
"xe_var_rectangle_clip_distances");
}
uint32_t cull_distance_count = shader_modification.GetVertexCullDistanceCount();
if (cull_distance_count) {
spv::Id type_rectangle_cull_distance_array = builder_->makeArrayType(
type_float_, builder_->makeUintConstant(3 * cull_distance_count), sizeof(float));
builder_->addDecoration(type_rectangle_cull_distance_array, spv::DecorationArrayStride,
sizeof(float));
var_main_rectangle_cull_distances_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_rectangle_cull_distance_array,
"xe_var_rectangle_cull_distances");
}
uint32_t interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index;
while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) {
@ -693,13 +718,51 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
spv::Id rectangle_guest_vertex_index_int =
builder_->createUnaryOp(spv::OpBitcast, type_int_, rectangle_guest_vertex_index);
// Store current iteration outputs.
// Store current iteration outputs. Keep system values in plain function
// arrays rather than copying the gl_PerVertex block because SPIRV-Cross
// emits invalid MSL for gl_PerVertex used as a local function type.
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition));
spv::Id rectangle_output_position = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassOutput, output_per_vertex_, id_vector_temp_),
spv::NoPrecision);
id_vector_temp_.clear();
id_vector_temp_.push_back(rectangle_guest_vertex_index_int);
builder_->createStore(
builder_->createLoad(output_per_vertex_, spv::NoPrecision),
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_,
id_vector_temp_));
builder_->createStore(rectangle_output_position,
builder_->createAccessChain(spv::StorageClassFunction,
var_main_rectangle_position_,
id_vector_temp_));
auto store_rectangle_distance_member =
[this, rectangle_guest_vertex_index_int](uint32_t output_member_index,
uint32_t component_count, spv::Id scratch_var) {
for (uint32_t i = 0; i < component_count; ++i) {
id_vector_temp_.clear();
id_vector_temp_.push_back(builder_->makeIntConstant(int(output_member_index)));
id_vector_temp_.push_back(builder_->makeIntConstant(int(i)));
spv::Id output_value = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassOutput, output_per_vertex_,
id_vector_temp_),
spv::NoPrecision);
spv::Id scratch_index = builder_->createBinOp(
spv::OpIAdd, type_int_,
builder_->createBinOp(spv::OpIMul, type_int_, rectangle_guest_vertex_index_int,
builder_->makeIntConstant(int(component_count))),
builder_->makeIntConstant(int(i)));
id_vector_temp_.clear();
id_vector_temp_.push_back(scratch_index);
builder_->createStore(output_value,
builder_->createAccessChain(spv::StorageClassFunction,
scratch_var, id_vector_temp_));
}
};
if (clip_distance_count && var_main_rectangle_clip_distances_ != spv::NoResult) {
store_rectangle_distance_member(output_per_vertex_member_clip_distance_,
clip_distance_count, var_main_rectangle_clip_distances_);
}
if (cull_distance_count && var_main_rectangle_cull_distances_ != spv::NoResult) {
store_rectangle_distance_member(output_per_vertex_member_cull_distance_,
cull_distance_count, var_main_rectangle_cull_distances_);
}
uint32_t interpolators_remaining = GetModificationInterpolatorMask();
uint32_t interpolator_index;
while (rex::bit_scan_forward(interpolators_remaining, &interpolator_index)) {
@ -884,7 +947,7 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
}
builder_->createBranch(main_loop_continue_);
}
if_not_last_guest_vertex.makeBeginElse();
if_not_last_guest_vertex.makeBeginElse(false);
{
spv::Id rectangle_host_vertex_in_primitive = builder_->createBinOp(
spv::OpBitwiseAnd, type_uint_,
@ -903,10 +966,9 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
auto load_rectangle_position_xy = [this](spv::Id vertex_index, uint32_t component) {
id_vector_temp_.clear();
id_vector_temp_.push_back(vertex_index);
id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition));
id_vector_temp_.push_back(builder_->makeIntConstant(int(component)));
return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_per_vertex_,
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_,
id_vector_temp_),
spv::NoPrecision);
};
@ -1002,17 +1064,27 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
input_output_interpolators_[interpolator_index]);
}
auto load_rectangle_per_vertex_component =
[this](spv::Id vertex_index, uint32_t member_index, spv::Id component_index) {
auto load_rectangle_position = [this](spv::Id vertex_index) {
id_vector_temp_.clear();
id_vector_temp_.push_back(vertex_index);
return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction, var_main_rectangle_position_,
id_vector_temp_),
spv::NoPrecision);
};
auto load_rectangle_distance_component =
[this](spv::Id vertex_index, uint32_t component_count, spv::Id component_index,
spv::Id scratch_var) {
spv::Id scratch_index = builder_->createBinOp(
spv::OpIAdd, type_int_,
builder_->createBinOp(spv::OpIMul, type_int_, vertex_index,
builder_->makeIntConstant(int(component_count))),
component_index);
id_vector_temp_.clear();
id_vector_temp_.push_back(vertex_index);
id_vector_temp_.push_back(builder_->makeIntConstant(int(member_index)));
if (component_index != spv::NoResult) {
id_vector_temp_.push_back(component_index);
}
id_vector_temp_.push_back(scratch_index);
return builder_->createLoad(
builder_->createAccessChain(spv::StorageClassFunction,
var_main_rectangle_per_vertex_, id_vector_temp_),
builder_->createAccessChain(spv::StorageClassFunction, scratch_var,
id_vector_temp_),
spv::NoPrecision);
};
@ -1020,12 +1092,9 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
id_vector_temp_.push_back(builder_->makeIntConstant(kOutputPerVertexMemberPosition));
spv::Id rectangle_position_ptr = builder_->createAccessChain(
spv::StorageClassOutput, output_per_vertex_, id_vector_temp_);
spv::Id position_0 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[0], kOutputPerVertexMemberPosition, spv::NoResult);
spv::Id position_1 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[1], kOutputPerVertexMemberPosition, spv::NoResult);
spv::Id position_2 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[2], kOutputPerVertexMemberPosition, spv::NoResult);
spv::Id position_0 = load_rectangle_position(rectangle_vertex_indices[0]);
spv::Id position_1 = load_rectangle_position(rectangle_vertex_indices[1]);
spv::Id position_2 = load_rectangle_position(rectangle_vertex_indices[2]);
spv::Id position_original =
select_rectangle_vertex_component(position_0, position_1, position_2, type_float4_);
spv::Id position_synthetic = builder_->createNoContractionBinOp(
@ -1039,16 +1108,17 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
auto write_rectangle_distance_member =
[this, &select_rectangle_vertex_component, rectangle_host_vertex_is_synthetic,
&load_rectangle_per_vertex_component,
rectangle_vertex_indices](uint32_t member_index, uint32_t component_count) {
&load_rectangle_distance_component,
rectangle_vertex_indices](uint32_t member_index, uint32_t component_count,
spv::Id scratch_var) {
for (uint32_t i = 0; i < component_count; ++i) {
spv::Id component_index = builder_->makeIntConstant(int(i));
spv::Id value_0 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[0], member_index, component_index);
spv::Id value_1 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[1], member_index, component_index);
spv::Id value_2 = load_rectangle_per_vertex_component(
rectangle_vertex_indices[2], member_index, component_index);
spv::Id value_0 = load_rectangle_distance_component(
rectangle_vertex_indices[0], component_count, component_index, scratch_var);
spv::Id value_1 = load_rectangle_distance_component(
rectangle_vertex_indices[1], component_count, component_index, scratch_var);
spv::Id value_2 = load_rectangle_distance_component(
rectangle_vertex_indices[2], component_count, component_index, scratch_var);
spv::Id value_original =
select_rectangle_vertex_component(value_0, value_1, value_2, type_float_);
spv::Id value_synthetic = builder_->createNoContractionBinOp(
@ -1069,11 +1139,11 @@ std::vector<uint8_t> SpirvShaderTranslator::CompleteTranslation() {
if (clip_distance_count && output_per_vertex_member_clip_distance_ != UINT32_MAX) {
write_rectangle_distance_member(output_per_vertex_member_clip_distance_,
clip_distance_count);
clip_distance_count, var_main_rectangle_clip_distances_);
}
if (cull_distance_count && output_per_vertex_member_cull_distance_ != UINT32_MAX) {
write_rectangle_distance_member(output_per_vertex_member_cull_distance_,
cull_distance_count);
cull_distance_count, var_main_rectangle_cull_distances_);
}
}
if_not_last_guest_vertex.makeEndIf();

File diff suppressed because it is too large Load diff

View file

@ -314,6 +314,11 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) {
rex::align(sizeof(ArgsVkSetViewport), alignof(VkViewport))));
} 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:
assert_unhandled_case(header.command);
break;

View file

@ -29,6 +29,7 @@
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/platform.h>
#include <rex/thread.h>
#include <rex/memory.h>
#include <rex/graphics/util/draw.h>
@ -3213,7 +3214,14 @@ bool VulkanPipelineCache::EnsurePipelineCreated(const PipelineCreationArguments&
assert_unhandled_case(description.primitive_topology);
return false;
}
#if REX_PLATFORM_MAC
// Metal always has primitive restart enabled in the paths MoltenVK exposes.
// Requesting it disabled produces VK_ERROR_FEATURE_NOT_PRESENT pipeline
// creation warnings and repeated failed/retried pipeline setup on macOS.
input_assembly_state.primitiveRestartEnable = VK_TRUE;
#else
input_assembly_state.primitiveRestartEnable = description.primitive_restart ? VK_TRUE : VK_FALSE;
#endif
VkPipelineViewportStateCreateInfo viewport_state;
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

View file

@ -35,6 +35,8 @@
#include <rex/graphics/xenos.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/perf/counter.h>
#include <rex/platform.h>
#include <rex/ui/vulkan/util.h>
REXCVAR_DEFINE_STRING(render_target_path_vulkan, "", "GPU/Vulkan",
@ -387,6 +389,12 @@ bool VulkanRenderTargetCache::Initialize(uint32_t shared_memory_binding_count) {
msaa_2x_attachments_supported_ = false;
msaa_2x_no_attachments_supported_ = false;
}
REXGPU_INFO(
"VulkanRenderTargetCache: Using {} render target path, native 2x MSAA attachments {}, "
"native 2x MSAA no-attachments {}",
path_ == Path::kHostRenderTargets ? "host/FBO" : "fragment shader interlock/FSI",
msaa_2x_attachments_supported_ ? "available" : "unavailable",
msaa_2x_no_attachments_supported_ ? "available" : "unavailable");
bool integer_transfer_sample_1x_supported =
(device_properties.framebufferColorSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0 &&
(device_properties.sampledImageIntegerSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0;
@ -1466,6 +1474,14 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory,
UseEdramBuffer(EdramBufferUsage::kComputeRead);
command_processor_.BindExternalComputePipeline(
resolve_copy_pipelines_[size_t(copy_shader)]);
rex::perf::DrawBucket resolve_timing_bucket =
rex::perf::DrawBucket::kCopyResolveShader;
if (copy_shader == draw_util::ResolveCopyShaderIndex::kFast32bpp1x2xMSAA) {
resolve_timing_bucket = rex::perf::DrawBucket::kCopyResolveFast32;
} else if (copy_shader == draw_util::ResolveCopyShaderIndex::kFull32bpp) {
resolve_timing_bucket = rex::perf::DrawBucket::kCopyResolveFull32;
}
command_processor_.BeginGpuTimestampedRegion(resolve_timing_bucket);
VkDescriptorSet descriptor_sets[kResolveCopyDescriptorSetCount] = {};
descriptor_sets[kResolveCopyDescriptorSetEdram] = edram_storage_buffer_descriptor_set_;
descriptor_sets[kResolveCopyDescriptorSetDest] = descriptor_set_dest;
@ -1487,6 +1503,44 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory,
sizeof(copy_shader_constants), &copy_shader_constants);
}
command_processor_.SubmitBarriers(true);
PERF_counter_inc(kRtResolveCopyDispatches);
PERF_counter_add(kRtResolveCopyGroups,
uint64_t(copy_group_count_x) * copy_group_count_y);
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
static constexpr rex::perf::CounterId kResolveCopyDispatchCounters[] = {
rex::perf::CounterId::kRtResolveCopyFast32bpp1x2xMsaaDispatches,
rex::perf::CounterId::kRtResolveCopyFast32bpp4xMsaaDispatches,
rex::perf::CounterId::kRtResolveCopyFast64bpp1x2xMsaaDispatches,
rex::perf::CounterId::kRtResolveCopyFast64bpp4xMsaaDispatches,
rex::perf::CounterId::kRtResolveCopyFull8bppDispatches,
rex::perf::CounterId::kRtResolveCopyFull16bppDispatches,
rex::perf::CounterId::kRtResolveCopyFull32bppDispatches,
rex::perf::CounterId::kRtResolveCopyFull64bppDispatches,
rex::perf::CounterId::kRtResolveCopyFull128bppDispatches,
};
static constexpr rex::perf::CounterId kResolveCopyGroupCounters[] = {
rex::perf::CounterId::kRtResolveCopyFast32bpp1x2xMsaaGroups,
rex::perf::CounterId::kRtResolveCopyFast32bpp4xMsaaGroups,
rex::perf::CounterId::kRtResolveCopyFast64bpp1x2xMsaaGroups,
rex::perf::CounterId::kRtResolveCopyFast64bpp4xMsaaGroups,
rex::perf::CounterId::kRtResolveCopyFull8bppGroups,
rex::perf::CounterId::kRtResolveCopyFull16bppGroups,
rex::perf::CounterId::kRtResolveCopyFull32bppGroups,
rex::perf::CounterId::kRtResolveCopyFull64bppGroups,
rex::perf::CounterId::kRtResolveCopyFull128bppGroups,
};
static_assert(std::size(kResolveCopyDispatchCounters) ==
size_t(draw_util::ResolveCopyShaderIndex::kCount));
static_assert(std::size(kResolveCopyGroupCounters) ==
size_t(draw_util::ResolveCopyShaderIndex::kCount));
size_t copy_shader_index = size_t(copy_shader);
rex::perf::IncrementCounter(kResolveCopyDispatchCounters[copy_shader_index]);
rex::perf::IncrementCounter(kResolveCopyGroupCounters[copy_shader_index],
uint64_t(copy_group_count_x) * copy_group_count_y);
#endif
if (draw_resolution_scaled) {
PERF_counter_add(kRtResolveScaledBytes, copy_dest_use_length);
}
command_buffer.CmdVkDispatch(copy_group_count_x, copy_group_count_y, 1);
// Invalidate textures and mark the range as scaled if needed.
@ -4558,7 +4612,11 @@ VkPipeline const* VulkanRenderTargetCache::GetTransferPipelines(TransferPipeline
input_assembly_state.pNext = nullptr;
input_assembly_state.flags = 0;
input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
#if REX_PLATFORM_MAC
input_assembly_state.primitiveRestartEnable = VK_TRUE;
#else
input_assembly_state.primitiveRestartEnable = VK_FALSE;
#endif
// Dynamic, to stay within maxViewportDimensions while preferring a
// power-of-two factor for converting from pixel coordinates to NDC for exact
@ -6226,9 +6284,11 @@ bool VulkanRenderTargetCache::TryResolveCopyDirectly(const draw_util::ResolveInf
bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dump_row_length_used,
uint32_t dump_rows, uint32_t dump_pitch) {
assert_true(GetPath() == Path::kHostRenderTargets);
PERF_counter_inc(kRtDumpCalls);
GetResolveCopyRectanglesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch,
dump_rectangles_);
PERF_counter_add(kRtDumpRectangles, dump_rectangles_.size());
if (dump_rectangles_.empty()) {
return true;
}
@ -6270,6 +6330,7 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum
// Dump the render targets.
DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer();
command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kCopyDump);
bool edram_buffer_bound = false;
VkDescriptorSet last_source_descriptor_set = VK_NULL_HANDLE;
DumpPitches last_pitches;
@ -6340,16 +6401,19 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum
sizeof(last_offsets), &last_offsets);
}
command_processor_.SubmitBarriers(true);
command_buffer.CmdVkDispatch(
const uint32_t group_count_x =
(draw_resolution_scale_x() *
(xenos::kEdramTileWidthSamples >> uint32_t(rt_key.Is64bpp())) *
dispatch.width_tiles +
(kDumpSamplesPerGroupX - 1)) /
kDumpSamplesPerGroupX,
kDumpSamplesPerGroupX;
const uint32_t group_count_y =
(draw_resolution_scale_y() * xenos::kEdramTileHeightSamples * dispatch.height_tiles +
(kDumpSamplesPerGroupY - 1)) /
kDumpSamplesPerGroupY,
1);
kDumpSamplesPerGroupY;
PERF_counter_inc(kRtDumpDispatches);
PERF_counter_add(kRtDumpGroups, uint64_t(group_count_x) * group_count_y);
command_buffer.CmdVkDispatch(group_count_x, group_count_y, 1);
}
MarkEdramBufferModified();
}

View file

@ -29,11 +29,16 @@
#include <rex/graphics/vulkan/texture_cache.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/perf/counter.h>
#include <rex/ui/vulkan/mem_alloc.h>
#include <rex/ui/vulkan/ui_samplers.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(vulkan_scaled_resolve_write_barrier_overlap_only, false, "GPU/Vulkan",
"Skip consecutive scaled resolve write barriers when destination ranges do "
"not overlap")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
namespace rex::graphics::vulkan {
@ -2293,13 +2298,27 @@ void VulkanTextureCache::UseScaledResolveBufferForWrite(uint64_t written_start_s
std::min(written_length_scaled, scaled_resolve_buffer_size_ - written_start_scaled);
if (scaled_resolve_last_usage_write_ && scaled_resolve_last_written_range_.second) {
VkPipelineStageFlags stage_mask;
VkAccessFlags access_mask;
GetScaledResolveUsageMasks(stage_mask, access_mask, true);
command_processor_.PushBufferMemoryBarrier(
scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first),
VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask,
access_mask, access_mask, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, false);
uint64_t previous_start = scaled_resolve_last_written_range_.first;
uint64_t previous_end = previous_start + scaled_resolve_last_written_range_.second;
uint64_t written_end = written_start_scaled + written_length_scaled;
bool ranges_overlap = previous_start < written_end && written_start_scaled < previous_end;
if (ranges_overlap) {
PERF_counter_inc(kRtScaledResolveWriteBarrierOverlaps);
}
if (ranges_overlap || !REXCVAR_GET(vulkan_scaled_resolve_write_barrier_overlap_only)) {
VkPipelineStageFlags stage_mask;
VkAccessFlags access_mask;
GetScaledResolveUsageMasks(stage_mask, access_mask, true);
command_processor_.PushBufferMemoryBarrier(
scaled_resolve_buffer_, VkDeviceSize(scaled_resolve_last_written_range_.first),
VkDeviceSize(scaled_resolve_last_written_range_.second), stage_mask, stage_mask,
access_mask, access_mask, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, false);
PERF_counter_inc(kRtScaledResolveWriteBarriers);
PERF_counter_add(kRtScaledResolveWriteBarrierBytes,
scaled_resolve_last_written_range_.second);
} else {
PERF_counter_inc(kRtScaledResolveWriteBarrierSkipped);
}
} else if (!scaled_resolve_last_usage_write_) {
VkPipelineStageFlags src_stage_mask, dst_stage_mask;
VkAccessFlags src_access_mask, dst_access_mask;

View file

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

View file

@ -9,6 +9,7 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <array>
#include <filesystem>
@ -22,6 +23,8 @@
REXCVAR_DEFINE_STRING(hid_mappings_file, "gamecontrollerdb.txt", "Input",
"Path to SDL gamecontroller mappings file");
REXCVAR_DEFINE_UINT32(hid_sdl_rumble_duration_ms, 100, "Input",
"SDL rumble pulse duration for nonzero XInput vibration");
namespace rex::input::sdl {
@ -124,6 +127,7 @@ void SDLInputDriver::OnClosing(rex::ui::UIEvent&) {
}
for (size_t i = 0; i < controllers_.size(); i++) {
if (controllers_.at(i).sdl) {
StopRumbleLocked(controllers_.at(i));
SDL_CloseGamepad(controllers_.at(i).sdl);
controllers_.at(i) = {};
}
@ -220,16 +224,16 @@ X_RESULT SDLInputDriver::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibrat
return X_ERROR_DEVICE_NOT_CONNECTED;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed, vibration->right_motor_speed,
0)) {
const bool has_rumble = vibration->left_motor_speed || vibration->right_motor_speed;
const uint32_t duration_ms =
has_rumble ? std::clamp(REXCVAR_GET(hid_sdl_rumble_duration_ms), uint32_t(1),
uint32_t(1000))
: 0;
if (!SDL_RumbleGamepad(controller->sdl, vibration->left_motor_speed,
vibration->right_motor_speed, duration_ms)) {
return X_ERROR_FUNCTION_FAILED;
} else {
return X_ERROR_SUCCESS;
}
#else
return X_ERROR_SUCCESS;
#endif
}
X_RESULT SDLInputDriver::GetKeystroke(uint32_t users, uint32_t flags,
@ -437,6 +441,12 @@ void SDLInputDriver::ProcessEventLocked(const SDL_Event& event) {
}
}
void SDLInputDriver::StopRumbleLocked(ControllerState& state) {
if (state.sdl) {
SDL_RumbleGamepad(state.sdl, 0, 0, 0);
}
}
void SDLInputDriver::OnControllerDeviceAddedLocked(const SDL_Event& event) {
// Open the controller.
const auto controller = SDL_OpenGamepad(event.cdevice.which);
@ -493,6 +503,7 @@ void SDLInputDriver::OnControllerDeviceRemovedLocked(const SDL_Event& event) {
// Find the disconnected gamecontroller and close it.
auto idx = GetControllerIndexFromInstanceID(event.cdevice.which);
if (idx) {
StopRumbleLocked(controllers_.at(*idx));
SDL_CloseGamepad(controllers_.at(*idx).sdl);
controllers_.at(*idx) = {};
keystroke_states_.at(*idx) = {};

View file

@ -318,7 +318,8 @@ HeapDiagnostics ReXHeap::GetDiagnosticsLocked() const {
diagnostics.capacity += d.capacity;
diagnostics.allocated += d.allocated;
diagnostics.peak_allocated += d.peak_allocated;
diagnostics.peak_request_size = std::max(diagnostics.peak_request_size, d.peak_request_size);
diagnostics.peak_request_size =
std::max(diagnostics.peak_request_size, static_cast<uint64_t>(d.peak_request_size));
diagnostics.oom_count += d.oom_count;
}
return diagnostics;

View file

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

View file

@ -17,6 +17,7 @@
#include <rex/assert.h>
#include <rex/logging.h>
#include <rex/math.h>
#include <rex/platform.h>
#include <rex/ppc/function.h>
#include <rex/runtime.h>
#include <rex/stream.h>
@ -841,6 +842,26 @@ void KernelState::TerminateTitle() {
REXSYS_DEBUG("KernelState::TerminateTitle");
auto global_lock = global_critical_region_.Acquire();
#if REX_PLATFORM_MAC
// pthread_cancel is asynchronous on macOS in the host thread wrapper, but
// suspending a guest thread first can strand it in the suspend wait during
// title teardown. Terminate directly on macOS and let the thread wrapper
// signal waiters before requesting cancellation.
std::vector<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.
std::vector<XThread*> suspended_threads;
for (auto it = threads_by_id_.begin(); it != threads_by_id_.end(); ++it) {
@ -857,6 +878,7 @@ void KernelState::TerminateTitle() {
thread->Terminate(0);
}
global_lock.lock();
#endif
// Remove all guest threads from the map.
for (auto it = threads_by_id_.begin(); it != threads_by_id_.end();) {

View file

@ -16,6 +16,7 @@
#include <rex/filesystem/vfs.h>
#include <rex/logging.h>
#include <rex/perf/counter.h>
#include <rex/platform.h>
#include <rex/ppc/context.h> // PPCFuncMapping
#include <rex/platform/exceptions.h> // SEH exception support
#include <rex/kernel/crt/heap.h>
@ -244,10 +245,22 @@ void Runtime::Shutdown() {
input_system_.reset();
}
kernel_state_.reset();
#if REX_PLATFORM_MAC
// macOS tears down guest threads asynchronously. During process exit, a late
// guest callback can still hold references into the dispatcher or mapped
// guest memory after the owning systems have stopped. Keep those mappings
// alive until the process exits; the OS reclaims them immediately afterward.
function_dispatcher_.release();
#else
function_dispatcher_.reset();
#endif
export_resolver_.reset();
file_system_.reset();
#if REX_PLATFORM_MAC
memory_.release();
#else
memory_.reset();
#endif
rex::perf::Profiler::Shutdown();
setup_complete_ = false;

View file

@ -29,6 +29,10 @@
// TODO(benvanik): move xbox.h out
#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")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
@ -77,6 +81,22 @@ uint32_t get_page_count(uint32_t value, uint32_t page_size, uint32_t page_size_s
static memory::Memory* active_memory_ = nullptr;
namespace {
constexpr size_t kGuestBackingLength = 0x120001000ull;
bool ShouldSkipHostCommit(const BaseHeap& heap) {
#if REX_PLATFORM_MAC
return heap.heap_type() == HeapType::kGuestPhysical && heap.heap_base() == 0x00000000 &&
rex::memory::page_size() > 0x1000;
#else
(void)heap;
return false;
#endif
}
} // namespace
void CrashDump() {
static std::atomic<int> in_crash_dump(0);
if (in_crash_dump.fetch_add(1)) {
@ -135,7 +155,8 @@ bool Memory::Initialize() {
mapping_ =
rex::memory::CreateFileMappingHandle(file_name_,
// entire 4gb space + 512mb physical:
0x11FFFFFFF, rex::memory::PageAccess::kReadWrite, false);
kGuestBackingLength,
rex::memory::PageAccess::kReadWrite, false);
if (mapping_ == rex::memory::kFileMappingHandleInvalid) {
REXSYS_ERROR("Unable to reserve the 4gb guest address space.");
assert_always();
@ -148,10 +169,17 @@ bool Memory::Initialize() {
for (size_t n = 32; n < 64; n++) {
auto mapping_base = reinterpret_cast<uint8_t*>(1ull << n);
if (!MapViews(mapping_base)) {
mapping_base_ = mapping_base;
mapping_base_ = views_.all_views[0];
break;
}
}
#if REX_PLATFORM_MAC
if (!mapping_base_) {
if (!MapViews(nullptr)) {
mapping_base_ = views_.all_views[0];
}
}
#endif
if (!mapping_base_) {
REXSYS_ERROR("Unable to find a continuous block in the 64bit address space.");
assert_always();
@ -209,6 +237,12 @@ bool Memory::Initialize() {
rex::memory::AllocFixed(heaps_.physical.TranslateRelative(0), heaps_.physical.heap_size(),
rex::memory::AllocationType::kCommit,
rex::memory::PageAccess::kReadWrite);
#if REX_PLATFORM_MAC
// The 0x7F000000-0x7FFFFFFF view aliases the start of physical memory. Touch
// it explicitly on macOS so raw generated loads through this alias don't SIGBUS
// before any guest write has materialized the shared-memory pages.
std::memset(virtual_membase_ + 0x7F000000, 0, 0x01000000);
#endif
// Install MMIO handler for physical address translation and MMIO ranges
mmio_handler_ = runtime::MMIOHandler::Install(
@ -300,8 +334,34 @@ static const struct {
0x0000000100000000ull,
},
};
namespace {
constexpr size_t kGuestMappingLength = 0x120000000ull;
} // namespace
int Memory::MapViews(uint8_t* mapping_base) {
assert_true(rex::countof(map_info) == rex::countof(views_.all_views));
#if REX_PLATFORM_MAC
// macOS doesn't provide MAP_FIXED_NOREPLACE. Direct MAP_FIXED probing can
// overwrite unrelated host mappings, so first reserve the entire range as a
// hint and only proceed with a fixed base we own.
void* reservation =
mmap(mapping_base, kGuestMappingLength, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0);
if (reservation == MAP_FAILED) {
return 1;
}
if (mapping_base && reservation != mapping_base) {
if (reservation != MAP_FAILED) {
munmap(reservation, kGuestMappingLength);
}
return 1;
}
mapping_base = reinterpret_cast<uint8_t*>(reservation);
#endif
// 0xE0000000 4 KB offset is emulated via host_address_offset and on the CPU
// side if system allocation granularity is bigger than 4 KB.
uint64_t granularity_mask = ~uint64_t(system_allocation_granularity_ - 1);
@ -312,7 +372,12 @@ int Memory::MapViews(uint8_t* mapping_base) {
rex::memory::PageAccess::kReadWrite, map_info[n].target_address & granularity_mask));
if (!views_.all_views[n]) {
// Failed, so bail and try again.
#if REX_PLATFORM_MAC
munmap(mapping_base, kGuestMappingLength);
std::fill(std::begin(views_.all_views), std::end(views_.all_views), nullptr);
#else
UnmapViews();
#endif
return 1;
}
}
@ -1111,7 +1176,7 @@ bool BaseHeap::AllocFixed(uint32_t base_address, uint32_t size, uint32_t alignme
// Allocate from host.
if (allocation_type == memory::kMemoryAllocationReserve) {
// Reserve is not needed, as we are mapped already.
} else {
} else if (!ShouldSkipHostCommit(*this)) {
auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit)
? rex::memory::AllocationType::kCommit
: rex::memory::AllocationType::kReserve;
@ -1265,7 +1330,7 @@ bool BaseHeap::AllocRange(uint32_t low_address, uint32_t high_address, uint32_t
// Allocate from host.
if (allocation_type == memory::kMemoryAllocationReserve) {
// Reserve is not needed, as we are mapped already.
} else {
} else if (!ShouldSkipHostCommit(*this)) {
auto alloc_type = (allocation_type & memory::kMemoryAllocationCommit)
? rex::memory::AllocationType::kCommit
: rex::memory::AllocationType::kReserve;

View file

@ -29,6 +29,14 @@
#include <netinet/in.h>
#include <netinet/ip.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
namespace rex::system {
@ -247,4 +255,4 @@ bool XSocket::QueuePacket(uint32_t src_ip, uint16_t src_port, const uint8_t* buf
return true;
}
} // namespace rex::system
} // namespace rex::system

View file

@ -63,6 +63,12 @@ if(WIN32)
window_win.cpp
windowed_app_context_win.cpp
)
elseif(APPLE)
set(REXUI_PLATFORM_SOURCES
surface_sdl.cpp
window_sdl.cpp
windowed_app_context_sdl.cpp
)
else()
set(REXUI_PLATFORM_SOURCES
surface_gnulinux.cpp
@ -138,6 +144,8 @@ if(WIN32)
dwmapi
Shcore
)
elseif(APPLE)
target_link_libraries(rexui PUBLIC SDL3::SDL3)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)

View file

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

View file

@ -425,8 +425,10 @@ void Presenter::OnSurfaceResizeFromUIThread() {
bool request_repaint;
UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true);
// Request to repaint as soon as possible in the UI thread if needed.
if (request_repaint) {
// Request to repaint as soon as possible in the UI thread if needed. Active
// UI drawers need a repaint after every resize even if the connection reports
// unchanged, because UI-only presentation has no guest frame to kick it.
if (request_repaint || !ui_drawers_.empty()) {
RequestPaintOrConnectionRecoveryViaWindow(true);
}
}
@ -1453,13 +1455,13 @@ bool Presenter::RequestPaintOrConnectionRecoveryViaWindow(bool force_ui_thread_p
// theoretically. For safety, check whether the window exists unconditionally.
assert_not_null(window_);
assert_not_null(surface_);
if (force_ui_thread_paint_tick) {
ForceUIThreadPaintTick();
}
if (ui_thread_paint_requested_.exchange(true, std::memory_order_relaxed)) {
// Invalidation pending already, no need to do it twice.
return false;
}
if (force_ui_thread_paint_tick) {
ForceUIThreadPaintTick();
}
window_->RequestPaint();
return true;
}

View file

@ -48,6 +48,7 @@
#include <filesystem>
#include <optional>
#include <string_view>
#include <utility>
namespace rex {
@ -98,9 +99,24 @@ bool ReXApp::OnInitialize() {
return true;
}
#if REX_PLATFORM_MAC
// Let the platform event loop run once before runtime setup. SDL creates the
// Cocoa window synchronously, but the app may not visibly activate if guest
// initialization starts before the first event-loop pass.
app_context().CallInUIThreadDeferred([this, paths = std::move(*paths)]() mutable {
if (shutting_down_.load(std::memory_order_acquire))
return;
if (!ConstructRuntime(paths)) {
app_context().QuitFromUIThread();
return;
}
LaunchModule();
});
#else
if (!ConstructRuntime(*paths))
return false;
LaunchModule();
#endif
return true;
}
@ -432,6 +448,9 @@ void ReXApp::LaunchModule() {
app_context().QuitFromUIThread();
return;
}
#if REX_PLATFORM_MAC
main_thread_ = main_thread;
#endif
auto* graphics_system =
static_cast<rex::graphics::GraphicsSystem*>(runtime_->graphics_system());
@ -478,6 +497,11 @@ void ReXApp::OnClosing(ui::UIEvent& e) {
(void)e;
REXLOG_INFO("Window closing, shutting down...");
shutting_down_.store(true, std::memory_order_release);
#if REX_PLATFORM_MAC
if (main_thread_ && main_thread_->is_running()) {
main_thread_->Terminate(0);
}
#endif
if (runtime_ && runtime_->kernel_state()) {
runtime_->kernel_state()->TerminateTitle();
}
@ -488,6 +512,13 @@ void ReXApp::OnDestroy() {
// Notify subclass before cleanup
OnShutdown();
#if REX_PLATFORM_MAC
shutting_down_.store(true, std::memory_order_release);
if (main_thread_ && main_thread_->is_running()) {
main_thread_->Terminate(0);
}
#endif
// Unregister overlay keybinds before destroying dialogs
rex::ui::UnregisterBind("bind_debug_overlay");
rex::ui::UnregisterBind("bind_console");
@ -516,6 +547,15 @@ void ReXApp::OnDestroy() {
if (module_thread_.joinable()) {
module_thread_.join();
}
#if REX_PLATFORM_MAC
main_thread_ = nullptr;
#endif
// Input drivers may still be registered as window listeners. Detach them
// before destroying the SDL window so their backend state is released in
// listener order.
if (runtime_ && runtime_->input_system()) {
static_cast<rex::input::InputSystem*>(runtime_->input_system())->Shutdown();
}
if (window_) {
window_->RemoveInputListener(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 "
"required for Vulkan GPU emulation")
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
REXCVAR_DEFINE_BOOL(vulkan_require_geometry_shader, true, "UI/Vulkan",
REXCVAR_DEFINE_BOOL(vulkan_require_geometry_shader,
#if REX_PLATFORM_MAC
false,
#else
true,
#endif
"UI/Vulkan",
"Require geometryShader support for Vulkan GPU emulation (disable to allow "
"fallback primitive emulation paths)")
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
@ -621,6 +627,8 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
XE_UI_VULKAN_LIMIT(optimalBufferCopyOffsetAlignment)
XE_UI_VULKAN_LIMIT(optimalBufferCopyRowPitchAlignment)
XE_UI_VULKAN_LIMIT(nonCoherentAtomSize)
XE_UI_VULKAN_LIMIT(timestampComputeAndGraphics)
XE_UI_VULKAN_LIMIT(timestampPeriod)
if (with_gpu_emulation) {
XE_UI_VULKAN_FEATURE(robustBufferAccess)

View file

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

View file

@ -9,6 +9,7 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <cstdlib>
#include <sstream>
#include <string>
#include <unordered_map>
@ -16,6 +17,7 @@
#include <vector>
#include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <rex/platform.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) {
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.
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_;
bool functions_loaded = true;
if (!vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoader)) {
if (!vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoader)
#if REX_PLATFORM_MAC
&& !vulkan_instance->loader_.Load(platform::lib_names::kVulkanLoaderFallback)
&& !vulkan_instance->loader_.Load(platform::lib_names::kMoltenVK)
#endif
) {
REXLOG_ERROR("Failed to load {}", platform::lib_names::kVulkanLoader);
return nullptr;
}
@ -116,6 +138,11 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
// #10.
requested_extensions.emplace("VK_KHR_win32_surface",
&vulkan_instance->extensions_.ext_KHR_win32_surface);
#endif
#ifdef VK_USE_PLATFORM_METAL_EXT
// #218.
requested_extensions.emplace("VK_EXT_metal_surface",
&vulkan_instance->extensions_.ext_EXT_metal_surface);
#endif
}
@ -379,6 +406,11 @@ std::unique_ptr<VulkanInstance> VulkanInstance::Create(const bool with_surface,
if (vulkan_instance->extensions_.ext_KHR_win32_surface) {
#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
if (vulkan_instance->extensions_.ext_KHR_surface) {
#include <rex/ui/vulkan/functions/instance_khr_surface.inc>

View file

@ -11,6 +11,7 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cmath>
@ -39,6 +40,9 @@
#if REX_PLATFORM_GNU_LINUX
#include <rex/ui/surface_gnulinux.h>
#endif
#if REX_PLATFORM_MAC
#include <rex/ui/surface_sdl.h>
#endif
#if REX_PLATFORM_WIN32
#include <rex/ui/surface_win.h>
#endif
@ -55,10 +59,77 @@ REXCVAR_DEFINE_BOOL(vulkan_allow_present_mode_mailbox, true, "UI/Vulkan",
REXCVAR_DEFINE_BOOL(vulkan_allow_present_mode_fifo_relaxed, true, "UI/Vulkan",
"Allow FIFO relaxed present mode");
REXCVAR_DEFINE_BOOL(vulkan_present_timing_log, false, "UI/Vulkan",
"Log averaged Vulkan host presentation timing")
.lifecycle(rex::cvar::Lifecycle::kHotReload)
.debug_only();
REXCVAR_DEFINE_UINT32(vulkan_present_timing_interval, 120, "UI/Vulkan",
"Number of presented frames per Vulkan timing log entry")
.range(1, 10000)
.lifecycle(rex::cvar::Lifecycle::kHotReload)
.debug_only();
namespace rex {
namespace ui {
namespace vulkan {
namespace {
struct VulkanPresentTimingStats {
uint64_t frames = 0;
uint64_t ui_thread_frames = 0;
uint64_t acquire_us = 0;
uint64_t consume_and_record_us = 0;
uint64_t end_command_buffer_us = 0;
uint64_t submit_us = 0;
uint64_t present_us = 0;
uint64_t total_us = 0;
};
uint64_t ElapsedUs(std::chrono::steady_clock::time_point start,
std::chrono::steady_clock::time_point end) {
return uint64_t(std::chrono::duration_cast<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
namespace {
@ -430,6 +501,11 @@ Surface::TypeFlags VulkanPresenter::GetSurfaceTypesSupportedByInstance(
type_flags |= Surface::kTypeFlag_XcbWindow;
}
#endif
#if REX_PLATFORM_MAC
if (instance_extensions.ext_EXT_metal_surface) {
type_flags |= Surface::kTypeFlag_SDLMetalView;
}
#endif
#if REX_PLATFORM_WIN32
if (instance_extensions.ext_KHR_win32_surface) {
type_flags |= Surface::kTypeFlag_Win32Hwnd;
@ -752,6 +828,14 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread(Surface& new_su
// The retirement or destruction of the swapchain here will also cause
// awaiting completion of the usage of the swapchain and the surface on the
// GPU.
#if REX_PLATFORM_MAC
// Native macOS fullscreen transitions can reconfigure the CAMetalLayer behind
// MoltenVK without making the existing VkSurface visibly recoverable. Rebind
// from the layer instead of preserving the old surface through the transition.
if (paint_context_.vulkan_surface != VK_NULL_HANDLE) {
paint_context_.DestroySwapchainAndVulkanSurface();
}
#endif
if (paint_context_.vulkan_surface != VK_NULL_HANDLE) {
VkSwapchainKHR old_swapchain = paint_context_.PrepareForSwapchainRetirement();
bool surface_unusable;
@ -823,6 +907,18 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread(Surface& new_su
vulkan_surface_create_result = ifn.vkCreateWin32SurfaceKHR(
instance, &surface_create_info, nullptr, &paint_context_.vulkan_surface);
} break;
#endif
#if REX_PLATFORM_MAC
case Surface::kTypeIndex_SDLMetalView: {
auto& sdl_metal_surface = static_cast<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
default:
assert_unhandled_case(surface_type);
@ -1347,7 +1443,8 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface(
// interfering with GPU command processing, and also to allow tearing so
// variable refresh rate may be used where it's available.
// Note: If the priorities here are changes, update the cvar descriptions.
if (REXCVAR_GET(vulkan_allow_present_mode_immediate) &&
constexpr bool allow_immediate_present_mode = true;
if (allow_immediate_present_mode && REXCVAR_GET(vulkan_allow_present_mode_immediate) &&
std::find(present_modes.cbegin(), present_modes.cend(), VK_PRESENT_MODE_IMMEDIATE_KHR) !=
present_modes.cend()) {
// Allowing tearing to reduce latency, and possibly variable refresh rate
@ -1495,6 +1592,7 @@ bool VulkanPresenter::GuestOutputImage::Initialize() {
}
Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_drawers) {
const auto timing_start = std::chrono::steady_clock::now();
// Begin the submission in place of the one not currently potentially used on
// the GPU.
uint64_t current_paint_submission_index =
@ -1535,9 +1633,11 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
VkSemaphore acquire_semaphore = paint_submission.acquire_semaphore();
uint32_t swapchain_image_index;
const auto timing_acquire_start = std::chrono::steady_clock::now();
VkResult acquire_result =
dfn.vkAcquireNextImageKHR(device, paint_context_.swapchain, UINT64_MAX, acquire_semaphore,
VK_NULL_HANDLE, &swapchain_image_index);
const auto timing_acquire_end = std::chrono::steady_clock::now();
switch (acquire_result) {
case VK_SUCCESS:
case VK_SUBOPTIMAL_KHR:
@ -1798,6 +1898,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
paint_context_.guest_output_image_paint_last_submission);
util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device,
swapchain_effect_pipeline.swapchain_pipeline);
swapchain_effect_pipeline.swapchain_format = VK_FORMAT_UNDEFINED;
}
if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) {
assert_true(CanGuestOutputPaintEffectBeFinal(swapchain_effect));
@ -1806,6 +1907,9 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
swapchain_effect, paint_context_.swapchain_render_pass);
if (swapchain_effect_pipeline.swapchain_pipeline == VK_NULL_HANDLE) {
guest_output_flow.effect_count = 0;
} else {
swapchain_effect_pipeline.swapchain_format =
paint_context_.swapchain_render_pass_format;
}
}
}
@ -2119,7 +2223,9 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
dfn.vkCmdEndRenderPass(draw_command_buffer);
const auto timing_end_command_buffer_start = std::chrono::steady_clock::now();
dfn.vkEndCommandBuffer(draw_command_buffer);
const auto timing_end_command_buffer_end = std::chrono::steady_clock::now();
VkPipelineStageFlags acquire_semaphore_wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkCommandBuffer command_buffers[2];
@ -2151,6 +2257,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
submit_info.pCommandBuffers = command_buffers;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &present_semaphore;
const auto timing_submit_start = std::chrono::steady_clock::now();
{
VulkanSubmissionTracker::FenceAcquisition fence_acqusition(
paint_context_.submission_tracker.AcquireFenceToAdvanceSubmission());
@ -2192,6 +2299,7 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
return PaintResult::kNotPresentedConnectionOutdated;
}
}
const auto timing_submit_end = std::chrono::steady_clock::now();
VkPresentInfoKHR present_info;
present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
@ -2203,11 +2311,20 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl(bool execute_ui_draw
present_info.pImageIndices = &swapchain_image_index;
present_info.pResults = nullptr;
VkResult present_result;
const auto timing_present_start = std::chrono::steady_clock::now();
{
const VulkanDevice::Queue::Acquisition queue_acquisition =
vulkan_device_->AcquireQueue(paint_context_.present_queue_family, 0);
present_result = dfn.vkQueuePresentKHR(queue_acquisition.queue(), &present_info);
}
const auto timing_present_end = std::chrono::steady_clock::now();
AccumulateAndMaybeLogPresentTiming(
execute_ui_drawers, ElapsedUs(timing_acquire_start, timing_acquire_end),
ElapsedUs(timing_acquire_end, timing_submit_start),
ElapsedUs(timing_end_command_buffer_start, timing_end_command_buffer_end),
ElapsedUs(timing_submit_start, timing_submit_end),
ElapsedUs(timing_present_start, timing_present_end),
ElapsedUs(timing_start, timing_present_end));
switch (present_result) {
case VK_SUCCESS:
return PaintResult::kPresented;
@ -2555,6 +2672,9 @@ VkPipeline VulkanPresenter::CreateGuestOutputPaintPipeline(GuestOutputPaintEffec
VkPipelineInputAssemblyStateCreateInfo input_assembly_state = {};
input_assembly_state.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
#if REX_PLATFORM_MAC
input_assembly_state.primitiveRestartEnable = VK_TRUE;
#endif
VkPipelineViewportStateCreateInfo viewport_state = {};
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;

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

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
#=============================================================================
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")
set(REX_THIRDPARTY_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES REX_THIRDPARTY_OSX_ARCH_COUNT)
if(REX_THIRDPARTY_OSX_ARCH_COUNT EQUAL 1)
set(REX_THIRDPARTY_TARGET_PROCESSOR "${CMAKE_OSX_ARCHITECTURES}")
endif()
endif()
if(APPLE AND NOT REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
message(FATAL_ERROR "Unsupported macOS architecture: ${REX_THIRDPARTY_TARGET_PROCESSOR}. macOS builds are ARM64-only.")
endif()
if(REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(x86_64|AMD64)$")
set(IS_ARM64 FALSE)
set(IS_X86_64 TRUE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|ARM64)$")
elseif(REX_THIRDPARTY_TARGET_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(IS_ARM64 TRUE)
set(IS_X86_64 FALSE)
else()
@ -197,6 +209,9 @@ if(REXGLUE_USE_VULKAN)
add_subdirectory(vulkan-memory-allocator)
# volk
if(APPLE)
set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_METAL_EXT CACHE STRING "" FORCE)
endif()
set(VOLK_INSTALL ON CACHE BOOL "" FORCE)
set(VOLK_PULL_IN_VULKAN OFF CACHE BOOL "" FORCE)
add_subdirectory(volk)
@ -237,7 +252,11 @@ set(SDL_TEST OFF CACHE BOOL "" FORCE)
set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "" FORCE)
# Disable unused subsystems
set(SDL_VIDEO OFF CACHE BOOL "" FORCE)
if(APPLE)
set(SDL_VIDEO ON CACHE BOOL "" FORCE)
else()
set(SDL_VIDEO OFF CACHE BOOL "" FORCE)
endif()
set(SDL_RENDER OFF CACHE BOOL "" FORCE)
set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
set(SDL_SENSOR OFF CACHE BOOL "" FORCE)
@ -254,7 +273,11 @@ set(SDL_CAMERA OFF CACHE BOOL "" FORCE)
set(SDL_OPENGL OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)
set(SDL_VULKAN OFF CACHE BOOL "" FORCE)
set(SDL_METAL OFF CACHE BOOL "" FORCE)
if(APPLE)
set(SDL_METAL ON CACHE BOOL "" FORCE)
else()
set(SDL_METAL OFF CACHE BOOL "" FORCE)
endif()
# Disable dummy drivers
set(SDL_DISKAUDIO OFF CACHE BOOL "" FORCE)

View file

@ -9,8 +9,18 @@
// big endian architectures need #define __BYTE_ORDER __BIG_ENDIAN
#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>
#endif
#endif
namespace sha256 {
@ -573,4 +583,4 @@ std::string SHA256::operator()(const std::string& text) {
return getHash();
}
}
}

View file

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