From 687da98fe114e19c78401e888964baedbb837f82 Mon Sep 17 00:00:00 2001 From: sse2 Date: Thu, 5 Mar 2026 20:53:01 +0200 Subject: [PATCH 1/2] feat(wsi): add sdl2 wsi backend --- README.md | 1 + meson.build | 10 ++ meson.options | 2 + src/d3d11/d3d11_swapchain.cpp | 8 ++ src/util/meson.build | 35 ++++++- src/util/wsi_monitor_headless.cpp | 6 +- src/util/wsi_monitor_sdl2.cpp | 120 ++++++++++++++++++++++ src/util/wsi_platform_sdl2.cpp | 47 +++++++++ src/util/wsi_platform_sdl2.hpp | 69 +++++++++++++ src/util/wsi_platform_sdl2_funcs.hpp | 18 ++++ src/util/wsi_window.hpp | 10 ++ src/util/wsi_window_headless.cpp | 8 ++ src/util/wsi_window_sdl2.cpp | 148 +++++++++++++++++++++++++++ src/util/wsi_window_sdl2.hpp | 0 14 files changed, 476 insertions(+), 6 deletions(-) create mode 100644 src/util/wsi_monitor_sdl2.cpp create mode 100644 src/util/wsi_platform_sdl2.cpp create mode 100644 src/util/wsi_platform_sdl2.hpp create mode 100644 src/util/wsi_platform_sdl2_funcs.hpp create mode 100644 src/util/wsi_window_sdl2.cpp create mode 100644 src/util/wsi_window_sdl2.hpp diff --git a/README.md b/README.md index f3f014dda..f2f47d140 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,4 @@ As an alternative for the default wine based build DXMT can be built natively on meson setup --native-file build-osx.txt -Ddxmt_native=true -Dnative_llvm_path=toolchains/llvm-darwin build meson compile -C build ``` +The following WSI backends are present: headless, SDL2 and SDL3. You can hint which one to use at compile-time via the `-Ddxmt_wsi_sdl2=true` or `-Ddxmt_wsi_sdl3=true` flags. (headless is selected by default). \ No newline at end of file diff --git a/meson.build b/meson.build index 3588c2d57..9e68c438f 100644 --- a/meson.build +++ b/meson.build @@ -142,6 +142,16 @@ add_project_arguments('-DDXMT_NATIVE=1', language: 'cpp') add_project_arguments('-DDXMT_NATIVE=1', language: 'c') endif +if get_option('dxmt_wsi_sdl2') == true + add_project_arguments('-DDXMT_WSI_SDL2=1', language: 'cpp') + add_project_arguments('-DDXMT_WSI_SDL2=1', language: 'c') +endif + +if get_option('dxmt_wsi_sdl3') == true + add_project_arguments('-DDXMT_WSI_SDL3=1', language: 'cpp') + add_project_arguments('-DDXMT_WSI_SDL3=1', language: 'c') +endif + add_project_arguments('-DDXMT_PAGE_SIZE=4096', language: 'cpp') dxmt_version = vcs_tag( diff --git a/meson.options b/meson.options index 644c404bb..069fac80f 100644 --- a/meson.options +++ b/meson.options @@ -2,6 +2,8 @@ option('native_llvm_path', type : 'string', value: '/usr/local/opt/llvm@15') option('build_airconv_for_windows', type : 'boolean', value : false) option('dxmt_debug', type : 'boolean', value : false) option('dxmt_native', type : 'boolean', value : false) +option('dxmt_wsi_sdl2', type : 'boolean', value : false) +option('dxmt_wsi_sdl3', type : 'boolean', value : false) option('wine_build_path', type : 'string') option('wine_install_path', type : 'string') option('wine_builtin_dll', type : 'boolean', value : false) diff --git a/src/d3d11/d3d11_swapchain.cpp b/src/d3d11/d3d11_swapchain.cpp index 8e277f322..9449deab1 100644 --- a/src/d3d11/d3d11_swapchain.cpp +++ b/src/d3d11/d3d11_swapchain.cpp @@ -82,13 +82,21 @@ class MTLD3D11SwapChain final : public MTLDXGISubObjectGetMTLDevice(), layer_weak_); + if (!native_view_) { + ERR("Failed to create metal view."); + abort(); + } +#else native_view_ = WMT::CreateMetalViewFromHWND((intptr_t)hWnd, pDevice->GetMTLDevice(), layer_weak_); if (!native_view_) { ERR("Failed to create metal view, it seems like your Wine has no exported symbols needed by DXMT."); abort(); } +#endif if constexpr (EnableMetalFX) { scale_factor = std::max(Config::getInstance().getOption("d3d11.metalSpatialUpscaleFactor", 2), 1.0f); diff --git a/src/util/meson.build b/src/util/meson.build index 995de13ca..74a9adbab 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -23,12 +23,36 @@ util_src = files([ 'sha1/sha1_util.cpp', ]) +wsi_deps = [] + if dxmt_native - util_src += files([ - 'wsi_monitor_headless.cpp', - 'wsi_window_headless.cpp', - 'wsi_platform_darwin.cpp', - ]) + if get_option('dxmt_wsi_sdl2') == true + lib_sdl2 = dependency('SDL2', required: true) + + util_src += files([ + 'wsi_monitor_sdl2.cpp', + 'wsi_window_sdl2.cpp', + 'wsi_platform_sdl2.cpp', + ]) + + wsi_deps += [ lib_sdl2.partial_dependency(compile_args: true, includes: true)] + elif get_option('dxmt_wsi_sdl3') == true + lib_sdl3 = dependency('SDL3', required: true) + + util_src += files([ + 'wsi_monitor_sdl3.cpp', + 'wsi_window_sdl3.cpp', + 'wsi_platform_sdl3.cpp', + ]) + + wsi_deps += [ lib_sdl3.partial_dependency(compile_args: true, includes: true)] + else + util_src += files([ + 'wsi_monitor_headless.cpp', + 'wsi_window_headless.cpp', + ]) + endif + util_src += files(['wsi_platform_darwin.cpp']) else util_src += files([ 'wsi_monitor_win32.cpp', @@ -38,6 +62,7 @@ else endif util_lib = static_library('util', util_src, + dependencies: wsi_deps, include_directories : [ dxmt_include_path ], ) diff --git a/src/util/wsi_monitor_headless.cpp b/src/util/wsi_monitor_headless.cpp index 2aaa61c18..fe987a4e0 100644 --- a/src/util/wsi_monitor_headless.cpp +++ b/src/util/wsi_monitor_headless.cpp @@ -8,6 +8,8 @@ * See */ +#if !defined(DXMT_WSI_SDL2) && !defined(DXMT_WSI_SDL3) + #include "wsi_monitor.hpp" namespace dxmt::wsi { @@ -45,4 +47,6 @@ bool getDesktopDisplayMode(HMONITOR hMonitor, WsiMode *pMode) { return retrieveDisplayMode(hMonitor, ENUM_REGISTRY_SETTINGS, pMode); } -} // namespace dxmt::wsi \ No newline at end of file +} // namespace dxmt::wsi + +#endif // !defined(DXMT_WSI_SDL2) && !defined(DXMT_WSI_SDL3) \ No newline at end of file diff --git a/src/util/wsi_monitor_sdl2.cpp b/src/util/wsi_monitor_sdl2.cpp new file mode 100644 index 000000000..a55bb31d1 --- /dev/null +++ b/src/util/wsi_monitor_sdl2.cpp @@ -0,0 +1,120 @@ +#if defined(DXMT_WSI_SDL2) + +#include "wsi_monitor.hpp" +#include "wsi_platform_sdl2.hpp" + +#include "log/log.hpp" +#include "util_string.hpp" + +#include + +#include + +namespace dxmt::wsi { + +HMONITOR enumMonitors(uint32_t index) { + if (!isValidDisplay(index)) { + Logger::err(str::format("SDL2 WSI: Failed to get display for index ", index)); + return nullptr; + } + return toHmonitor((int32_t)index); +} + +HMONITOR getDefaultMonitor() { + return enumMonitors(0); +} + +bool getDisplayName(HMONITOR hMonitor, WCHAR (&Name)[32]) { + int32_t display_id = fromHmonitor(hMonitor); + if (!isValidDisplay(display_id)) { + Logger::err("SDL2 WSI: Invalid display ID for monitor"); + return false; + } + + std::wstringstream display_name; + display_name << L"\\\\.\\DISPLAY" << display_id; + std::wstring display_name_str = display_name.str(); + + std::memset(Name, 0, sizeof(WCHAR) * 32); + if (display_name_str.length() >= 32) { + Logger::err("SDL2 WSI: Display name exceeds maximum length of 32 characters"); + return false; + } + display_name_str.copy(Name, display_name_str.length(), 0); + + return true; +} + +bool getDesktopCoordinates(HMONITOR hMonitor, RECT *pRect) { + const int32_t displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + SDL_Rect rect = { }; + SDL2Initializer::get().SDL_GetDisplayBounds(displayId, &rect); + + pRect->left = rect.x; + pRect->top = rect.y; + pRect->right = rect.x + rect.w; + pRect->bottom = rect.y + rect.h; + + return true; +} + +bool getDisplayMode(HMONITOR hMonitor, uint32_t modeNumber, WsiMode *pMode) { + const int32_t displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + SDL_DisplayMode mode = { }; + if (SDL2Initializer::get().SDL_GetDisplayMode(displayId, modeNumber, &mode) != 0) + return false; + + pMode->width = uint32_t(mode.w); + pMode->height = uint32_t(mode.h); + pMode->refreshRate = WsiRational{ uint32_t(mode.refresh_rate) * 1000, 1000 }; + pMode->bitsPerPixel = roundToNextPow2(SDL_BITSPERPIXEL(mode.format)); + pMode->interlaced = false; + + return true; +} + +bool getCurrentDisplayMode(HMONITOR hMonitor, WsiMode *pMode) { + const int32_t displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + SDL_DisplayMode mode = { }; + if (SDL2Initializer::get().SDL_GetCurrentDisplayMode(displayId, &mode) != 0) { + Logger::err(str::format("SDL_GetCurrentDisplayMode: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + convertMode(mode, pMode); + + return true; +} + +bool getDesktopDisplayMode(HMONITOR hMonitor, WsiMode *pMode) { + const int32_t displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + SDL_DisplayMode mode = { }; + if (SDL2Initializer::get().SDL_GetDesktopDisplayMode(displayId, &mode) != 0) { + Logger::err(str::format("SDL_GetCurrentDisplayMode: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + convertMode(mode, pMode); + + return true; +} + +} // namespace dxmt::wsi + +#endif // defined(DXMT_WSI_SDL2) \ No newline at end of file diff --git a/src/util/wsi_platform_sdl2.cpp b/src/util/wsi_platform_sdl2.cpp new file mode 100644 index 000000000..d5e38dc1f --- /dev/null +++ b/src/util/wsi_platform_sdl2.cpp @@ -0,0 +1,47 @@ +#if defined(DXMT_WSI_SDL2) + +#include "wsi_platform_sdl2.hpp" + +#include "log/log.hpp" +#include "util_string.hpp" + +#include +#include + +#include + +namespace dxmt::wsi { + +SDL2Initializer::SDL2Initializer() { + + // first, try to see if we already loaded the dylib in the process... + uint32_t image_count = _dyld_image_count(); + for (uint32_t i = 0; i < image_count; i++) { + const char* image_name = _dyld_get_image_name(i); + if(strstr(image_name, "SDL2") != nullptr) { + libsdl = dlopen(image_name, RTLD_NOW | RTLD_LOCAL); + if (libsdl) { + Logger::trace(str::format("SDL2 WSI: Found SDL2 in loaded images: ", image_name)); + break; + } + } + } + + // if there's no sdl, the app might've statically linked against sdl, so we can try dlsym with RTLD_DEFAULT + #define SDL_PROC(ret, name, params) \ + name = reinterpret_cast(dlsym((libsdl == nullptr ? RTLD_DEFAULT : libsdl), #name)); \ + if (!name) { \ + Logger::err(str::format("SDL2 WSI: Failed to get function named ", #name)); \ + } + #include "wsi_platform_sdl2_funcs.hpp" + + if (!SDL_GetClosestDisplayMode) { + ERR("SDL2 WSI: failed to get SDL function pointers."); + abort(); + } + +} + +} + +#endif // defined(DXMT_WSI_SDL2) \ No newline at end of file diff --git a/src/util/wsi_platform_sdl2.hpp b/src/util/wsi_platform_sdl2.hpp new file mode 100644 index 000000000..24913f759 --- /dev/null +++ b/src/util/wsi_platform_sdl2.hpp @@ -0,0 +1,69 @@ +#pragma once + +#if defined(DXMT_WSI_SDL2) + +#include "wsi_monitor.hpp" + +#include + +namespace dxmt::wsi { + +class SDL2Initializer { // using this to force loadlibrary sdl2 + public: +void* libsdl; +#define SDL_PROC(ret, name, params) \ + typedef ret (SDLCALL *pfn_##name) params; \ + pfn_##name name; +#include "wsi_platform_sdl2_funcs.hpp" + + SDL2Initializer(); + + static SDL2Initializer get() { + static SDL2Initializer instance; + return instance; + } +}; + +static inline int32_t fromHmonitor(HMONITOR hMonitor) { + return static_cast(reinterpret_cast(hMonitor)) - 1; +} + +static inline HMONITOR toHmonitor(int32_t displayId) { + return reinterpret_cast(static_cast(displayId + 1)); +} + +static inline bool isValidDisplay(int32_t display_id) { + const int32_t display_cnt = SDL2Initializer::get().SDL_GetNumVideoDisplays(); + return display_id >= 0 && display_id < display_cnt; +} + +static inline SDL_Window* fromHwnd(HWND hWindow) { + return reinterpret_cast(hWindow); +} + +static inline HWND toHwnd(SDL_Window* pWindow) { + return reinterpret_cast(pWindow); +} + +static inline uint32_t roundToNextPow2(uint32_t num) { + if (num-- == 0) + return 0; + num |= num >> 1; num |= num >> 2; + num |= num >> 4; num |= num >> 8; + num |= num >> 16; + return ++num; +} + +static inline void convertMode(const SDL_DisplayMode& mode, WsiMode* pMode) { + pMode->width = uint32_t(mode.w); + pMode->height = uint32_t(mode.h); + pMode->refreshRate = WsiRational{ uint32_t(mode.refresh_rate) * 1000, 1000 }; + // BPP should always be a power of two + // to match Windows behaviour of including padding. + pMode->bitsPerPixel = roundToNextPow2(SDL_BITSPERPIXEL(mode.format)); + pMode->interlaced = false; +} + +} + +#endif // defined(DXMT_WSI_SDL2) \ No newline at end of file diff --git a/src/util/wsi_platform_sdl2_funcs.hpp b/src/util/wsi_platform_sdl2_funcs.hpp new file mode 100644 index 000000000..b08321c7e --- /dev/null +++ b/src/util/wsi_platform_sdl2_funcs.hpp @@ -0,0 +1,18 @@ +#if defined(DXMT_WSI_SDL2) +SDL_PROC(SDL_DisplayMode*, SDL_GetClosestDisplayMode, (int, const SDL_DisplayMode*, SDL_DisplayMode*)) +SDL_PROC(int, SDL_GetCurrentDisplayMode, (int, SDL_DisplayMode*)) +SDL_PROC(int, SDL_GetDesktopDisplayMode, (int, SDL_DisplayMode*)) +SDL_PROC(int, SDL_GetDisplayBounds, (int, SDL_Rect*)) +SDL_PROC(int, SDL_GetDisplayMode, (int, int, SDL_DisplayMode*)) +SDL_PROC(const char*, SDL_GetError, (void)) +SDL_PROC(int, SDL_GetNumVideoDisplays, (void)) +SDL_PROC(int, SDL_GetWindowDisplayIndex, (SDL_Window*)) +SDL_PROC(int, SDL_SetWindowDisplayMode, (SDL_Window*, const SDL_DisplayMode*)) +SDL_PROC(int, SDL_SetWindowFullscreen, (SDL_Window*, Uint32)) +SDL_PROC(SDL_WindowFlags, SDL_GetWindowFlags, (SDL_Window *)) +SDL_PROC(void, SDL_GetWindowSize, (SDL_Window*, int*, int*)) +SDL_PROC(void, SDL_SetWindowSize, (SDL_Window*, int, int)) +SDL_PROC(SDL_MetalView, SDL_Metal_CreateView, (SDL_Window*)) +SDL_PROC(void*, SDL_Metal_GetLayer, (SDL_MetalView)) +#undef SDL_PROC +#endif // defined(DXMT_WSI_SDL2) \ No newline at end of file diff --git a/src/util/wsi_window.hpp b/src/util/wsi_window.hpp index 6f51f77cd..e612eed44 100644 --- a/src/util/wsi_window.hpp +++ b/src/util/wsi_window.hpp @@ -14,6 +14,7 @@ #include "wsi_monitor.hpp" #include "wsi_platform_win32.hpp" +#include "../winemetal/Metal.hpp" namespace dxmt::wsi { @@ -103,4 +104,13 @@ bool isWindow(HWND hWindow); */ void updateFullscreenWindow(HMONITOR hMonitor, HWND hWindow, bool forceTopmost); +/** + * \brief Create a Metal view from a window handle + * + * \param [in] hwnd The HWND to create the view from + * \param [in] device The MTLDevice to use + * \param [out] layer The MetalLayer to fill in + * \returns A WMT::Object representing the created view + */ +WMT::Object createMetalViewFromHWND(intptr_t hwnd, WMT::Device device, WMT::MetalLayer &layer); } // namespace dxmt::wsi diff --git a/src/util/wsi_window_headless.cpp b/src/util/wsi_window_headless.cpp index 7196d7fd4..cf9de44a1 100644 --- a/src/util/wsi_window_headless.cpp +++ b/src/util/wsi_window_headless.cpp @@ -8,6 +8,8 @@ * See */ +#if !defined(DXMT_WSI_SDL2) && !defined(DXMT_WSI_SDL3) + #include "wsi_window.hpp" #include "wsi_monitor.hpp" @@ -57,4 +59,10 @@ void updateFullscreenWindow(HMONITOR hMonitor, HWND hWindow, bool forceTopmost) { } +WMT::Object createMetalViewFromHWND(intptr_t hwnd, WMT::Device device, WMT::MetalLayer &layer) { + return WMT::Object(); +} + } // namespace dxmt::wsi + +#endif // !defined(DXMT_WSI_SDL2) && !defined(DXMT_WSI_SDL3) \ No newline at end of file diff --git a/src/util/wsi_window_sdl2.cpp b/src/util/wsi_window_sdl2.cpp new file mode 100644 index 000000000..89830fa54 --- /dev/null +++ b/src/util/wsi_window_sdl2.cpp @@ -0,0 +1,148 @@ +#if defined(DXMT_WSI_SDL2) + +#include "wsi_window.hpp" +#include "wsi_monitor.hpp" + +#include "wsi_platform_sdl2.hpp" + +#include "util_string.hpp" +#include "log/log.hpp" + +#include +#include + +namespace dxmt::wsi { + +void getWindowSize(HWND hWindow, uint32_t *pWidth, uint32_t *pHeight) { + SDL_Window* window = fromHwnd(hWindow); + + int32_t w, h; + SDL2Initializer::get().SDL_GetWindowSize(window, &w, &h); + + if (pWidth) + *pWidth = uint32_t(w); + + if (pHeight) + *pHeight = uint32_t(h); +} + +void resizeWindow(HWND hWindow, DXMTWindowState *pState, uint32_t width, + uint32_t height) { + SDL_Window* window = fromHwnd(hWindow); + + SDL2Initializer::get().SDL_SetWindowSize(window, int32_t(width), int32_t(height)); +} + +bool setWindowMode(HMONITOR hMonitor, HWND hWindow, const WsiMode &mode) { + const int32_t displayId = fromHmonitor(hMonitor); + SDL_Window* window = fromHwnd(hWindow); + + if (!isValidDisplay(displayId)) + return false; + + SDL_DisplayMode wantedMode = { }; + wantedMode.w = mode.width; + wantedMode.h = mode.height; + wantedMode.refresh_rate = mode.refreshRate.numerator != 0 + ? mode.refreshRate.numerator / mode.refreshRate.denominator + : 0; + // TODO: Implement lookup format for bitsPerPixel here. + + SDL_DisplayMode sdlmode = { }; + if (SDL2Initializer::get().SDL_GetClosestDisplayMode(displayId, &wantedMode, &sdlmode) == nullptr) { + Logger::err(str::format("SDL2 WSI: setWindowMode: SDL_GetClosestDisplayMode: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + if (SDL2Initializer::get().SDL_SetWindowDisplayMode(window, &sdlmode) != 0) { + Logger::err(str::format("SDL2 WSI: setWindowMode: SDL_SetWindowDisplayMode: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool enterFullscreenMode(HMONITOR hMonitor, HWND hWindow, + DXMTWindowState *pState, + [[maybe_unused]] bool modeSwitch) { + + const int32_t displayId = fromHmonitor(hMonitor); + SDL_Window* window = fromHwnd(hWindow); + + if (!isValidDisplay(displayId)) + return false; + + uint32_t flags = modeSwitch + ? SDL_WINDOW_FULLSCREEN + : SDL_WINDOW_FULLSCREEN_DESKTOP; + + // TODO: Set this on the correct monitor. + // Docs aren't clear on this... + if (SDL2Initializer::get().SDL_SetWindowFullscreen(window, flags) != 0) { + Logger::err(str::format("SDL2 WSI: enterFullscreenMode: SDL_SetWindowFullscreen: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool leaveFullscreenMode(HWND hWindow, DXMTWindowState *pState, + bool restoreCoordinates) { + SDL_Window* window = fromHwnd(hWindow); + + // @todo; pState handling + (void)pState; + + if (SDL2Initializer::get().SDL_SetWindowFullscreen(window, 0) != 0) { + Logger::err(str::format("SDL2 WSI: leaveFullscreenMode: SDL_SetWindowFullscreen: ", SDL2Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool restoreDisplayMode() { + return true; +} + +HMONITOR getWindowMonitor(HWND hWindow) { + SDL_Window* window = fromHwnd(hWindow); + const int32_t displayId = SDL2Initializer::get().SDL_GetWindowDisplayIndex(window); + + return toHmonitor(displayId); +} + +bool isWindow(HWND hWindow) { return true; } + +void updateFullscreenWindow(HMONITOR hMonitor, HWND hWindow, + bool forceTopmost) { + (void)hMonitor; + (void)forceTopmost; + (void)hWindow; +} + +WMT::Object createMetalViewFromHWND(intptr_t hwnd, WMT::Device device, WMT::MetalLayer &layer) { + SDL_MetalView sdlView = SDL2Initializer::get().SDL_Metal_CreateView((SDL_Window*)hwnd); + + if (!sdlView) { + Logger::err(str::format("SDL2 WSI: createMetalViewFromHWND: SDL_Metal_CreateView failed with error: ", SDL2Initializer::get().SDL_GetError())); + return WMT::Object(); + } + + void* metalLayer = SDL2Initializer::get().SDL_Metal_GetLayer(sdlView); + if (!metalLayer) { + Logger::err(str::format("SDL2 WSI: createMetalViewFromHWND: SDL_Metal_GetLayer failed with error: ", SDL2Initializer::get().SDL_GetError())); + return WMT::Object(); + } + + layer.handle = (obj_handle_t)((void*)metalLayer); + WMTLayerProps props = {}; + layer.getProps(props); + props.device = device.handle; + layer.setProps(props); + return WMT::Object((obj_handle_t)((void*)sdlView)); +} + +} // namespace dxmt::wsi + +#endif // defined(DXMT_WSI_SDL2) \ No newline at end of file diff --git a/src/util/wsi_window_sdl2.hpp b/src/util/wsi_window_sdl2.hpp new file mode 100644 index 000000000..e69de29bb From eb567a3c515a16f9b09626f3b51e0ecfcf1836ec Mon Sep 17 00:00:00 2001 From: sse2 Date: Thu, 5 Mar 2026 21:30:01 +0200 Subject: [PATCH 2/2] feat(wsi): add sdl3 wsi backend --- src/util/wsi_monitor_sdl3.cpp | 133 ++++++++++++++++++++++++ src/util/wsi_platform_sdl3.cpp | 47 +++++++++ src/util/wsi_platform_sdl3.hpp | 81 +++++++++++++++ src/util/wsi_platform_sdl3_funcs.hpp | 19 ++++ src/util/wsi_window_sdl3.cpp | 146 +++++++++++++++++++++++++++ src/util/wsi_window_sdl3.hpp | 14 +++ 6 files changed, 440 insertions(+) create mode 100644 src/util/wsi_monitor_sdl3.cpp create mode 100644 src/util/wsi_platform_sdl3.cpp create mode 100644 src/util/wsi_platform_sdl3.hpp create mode 100644 src/util/wsi_platform_sdl3_funcs.hpp create mode 100644 src/util/wsi_window_sdl3.cpp create mode 100644 src/util/wsi_window_sdl3.hpp diff --git a/src/util/wsi_monitor_sdl3.cpp b/src/util/wsi_monitor_sdl3.cpp new file mode 100644 index 000000000..61d144bc2 --- /dev/null +++ b/src/util/wsi_monitor_sdl3.cpp @@ -0,0 +1,133 @@ +#if defined(DXMT_WSI_SDL3) + +#include "wsi_monitor.hpp" +#include "wsi_platform_sdl3.hpp" + +#include "log/log.hpp" +#include "util_string.hpp" + +#include + +#include + +namespace dxmt::wsi { + +HMONITOR enumMonitors(uint32_t index) { + int count = 0; + SDL_DisplayID *displays = SDL3Initializer::get().SDL_GetDisplays(&count); + if (!displays || int(index) >= count) { + Logger::err(str::format("SDL3 WSI: Failed to get display for index ", index)); + if (displays) + SDL3Initializer::get().SDL_free(displays); + return nullptr; + } + SDL_DisplayID displayId = displays[index]; + SDL3Initializer::get().SDL_free(displays); + return toHmonitor(displayId); +} + +HMONITOR getDefaultMonitor() { + return enumMonitors(0); +} + +bool getDisplayName(HMONITOR hMonitor, WCHAR (&Name)[32]) { + SDL_DisplayID displayId = fromHmonitor(hMonitor); + if (!isValidDisplay(displayId)) { + Logger::err("SDL3 WSI: Invalid display ID for monitor"); + return false; + } + + std::wstringstream display_name; + display_name << L"\\\\.\\DISPLAY" << displayId; + std::wstring display_name_str = display_name.str(); + + std::memset(Name, 0, sizeof(WCHAR) * 32); + if (display_name_str.length() >= 32) { + Logger::err("SDL3 WSI: Display name exceeds maximum length of 32 characters"); + return false; + } + display_name_str.copy(Name, display_name_str.length(), 0); + + return true; +} + +bool getDesktopCoordinates(HMONITOR hMonitor, RECT *pRect) { + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + SDL_Rect rect = { }; + SDL3Initializer::get().SDL_GetDisplayBounds(displayId, &rect); + + pRect->left = rect.x; + pRect->top = rect.y; + pRect->right = rect.x + rect.w; + pRect->bottom = rect.y + rect.h; + + return true; +} + +bool getDisplayMode(HMONITOR hMonitor, uint32_t modeNumber, WsiMode *pMode) { + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + int count = 0; + SDL_DisplayMode **modes = SDL3Initializer::get().SDL_GetFullscreenDisplayModes(displayId, &count); + if (!modes || int(modeNumber) >= count) { + if (modes) + SDL3Initializer::get().SDL_free(modes); + return false; + } + + const SDL_DisplayMode *mode = modes[modeNumber]; + pMode->width = uint32_t(mode->w); + pMode->height = uint32_t(mode->h); + pMode->refreshRate = WsiRational{ uint32_t(mode->refresh_rate) * 1000, 1000 }; + pMode->bitsPerPixel = roundToNextPow2(SDL_BITSPERPIXEL(mode->format)); + pMode->interlaced = false; + + SDL3Initializer::get().SDL_free(modes); + + return true; +} + +bool getCurrentDisplayMode(HMONITOR hMonitor, WsiMode *pMode) { + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + const SDL_DisplayMode *mode = SDL3Initializer::get().SDL_GetCurrentDisplayMode(displayId); + if (!mode) { + Logger::err(str::format("SDL_GetCurrentDisplayMode: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + convertMode(*mode, pMode); + + return true; +} + +bool getDesktopDisplayMode(HMONITOR hMonitor, WsiMode *pMode) { + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + + if (!isValidDisplay(displayId)) + return false; + + const SDL_DisplayMode *mode = SDL3Initializer::get().SDL_GetDesktopDisplayMode(displayId); + if (!mode) { + Logger::err(str::format("SDL_GetDesktopDisplayMode: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + convertMode(*mode, pMode); + + return true; +} + +} // namespace dxmt::wsi + +#endif // defined(DXMT_WSI_SDL3) diff --git a/src/util/wsi_platform_sdl3.cpp b/src/util/wsi_platform_sdl3.cpp new file mode 100644 index 000000000..8d6d9602a --- /dev/null +++ b/src/util/wsi_platform_sdl3.cpp @@ -0,0 +1,47 @@ +#if defined(DXMT_WSI_SDL3) + +#include "wsi_platform_sdl3.hpp" + +#include "log/log.hpp" +#include "util_string.hpp" + +#include +#include + +#include + +namespace dxmt::wsi { + +SDL3Initializer::SDL3Initializer() { + + // first, try to see if we already loaded the dylib in the process... + uint32_t image_count = _dyld_image_count(); + for (uint32_t i = 0; i < image_count; i++) { + const char* image_name = _dyld_get_image_name(i); + if(strstr(image_name, "SDL3") != nullptr) { + libsdl = dlopen(image_name, RTLD_NOW | RTLD_LOCAL); + if (libsdl) { + Logger::trace(str::format("SDL3 WSI: Found SDL3 in loaded images: ", image_name)); + break; + } + } + } + + // if there's no sdl, the app might've statically linked against sdl, so we can try dlsym with RTLD_DEFAULT + #define SDL_PROC(ret, name, params) \ + name = reinterpret_cast(dlsym((libsdl == nullptr ? RTLD_DEFAULT : libsdl), #name)); \ + if (!name) { \ + Logger::err(str::format("SDL3 WSI: Failed to get function named ", #name)); \ + } + #include "wsi_platform_sdl3_funcs.hpp" + + if (!SDL_GetClosestFullscreenDisplayMode) { + ERR("SDL3 WSI: failed to get SDL function pointers."); + abort(); + } + +} + +} + +#endif // defined(DXMT_WSI_SDL3) diff --git a/src/util/wsi_platform_sdl3.hpp b/src/util/wsi_platform_sdl3.hpp new file mode 100644 index 000000000..11725056d --- /dev/null +++ b/src/util/wsi_platform_sdl3.hpp @@ -0,0 +1,81 @@ +#pragma once + +#if defined(DXMT_WSI_SDL3) + +#include "wsi_monitor.hpp" + +#include + +namespace dxmt::wsi { + +class SDL3Initializer { + public: +void* libsdl; +#define SDL_PROC(ret, name, params) \ + typedef ret (SDLCALL *pfn_##name) params; \ + pfn_##name name; +#include "wsi_platform_sdl3_funcs.hpp" + + SDL3Initializer(); + + static SDL3Initializer get() { + static SDL3Initializer instance; + return instance; + } +}; + +static inline SDL_DisplayID fromHmonitor(HMONITOR hMonitor) { + return static_cast(reinterpret_cast(hMonitor)); +} + +static inline HMONITOR toHmonitor(SDL_DisplayID displayId) { + return reinterpret_cast(static_cast(displayId)); +} + +static inline bool isValidDisplay(SDL_DisplayID displayId) { + int count = 0; + SDL_DisplayID *displays = SDL3Initializer::get().SDL_GetDisplays(&count); + if (!displays) + return false; + + bool found = false; + for (int i = 0; i < count; i++) { + if (displays[i] == displayId) { + found = true; + break; + } + } + SDL3Initializer::get().SDL_free(displays); + return found; +} + +static inline SDL_Window* fromHwnd(HWND hWindow) { + return reinterpret_cast(hWindow); +} + +static inline HWND toHwnd(SDL_Window* pWindow) { + return reinterpret_cast(pWindow); +} + +static inline uint32_t roundToNextPow2(uint32_t num) { + if (num-- == 0) + return 0; + num |= num >> 1; num |= num >> 2; + num |= num >> 4; num |= num >> 8; + num |= num >> 16; + return ++num; +} + +static inline void convertMode(const SDL_DisplayMode& mode, WsiMode* pMode) { + pMode->width = uint32_t(mode.w); + pMode->height = uint32_t(mode.h); + pMode->refreshRate = WsiRational{ uint32_t(mode.refresh_rate) * 1000, 1000 }; + // BPP should always be a power of two + // to match Windows behaviour of including padding. + pMode->bitsPerPixel = roundToNextPow2(SDL_BITSPERPIXEL(mode.format)); + pMode->interlaced = false; +} + +} + +#endif // defined(DXMT_WSI_SDL3) diff --git a/src/util/wsi_platform_sdl3_funcs.hpp b/src/util/wsi_platform_sdl3_funcs.hpp new file mode 100644 index 000000000..764c18d4f --- /dev/null +++ b/src/util/wsi_platform_sdl3_funcs.hpp @@ -0,0 +1,19 @@ +#if defined(DXMT_WSI_SDL3) +SDL_PROC(const SDL_DisplayMode*, SDL_GetClosestFullscreenDisplayMode, (SDL_DisplayID, int, int, float, bool)) +SDL_PROC(const SDL_DisplayMode*, SDL_GetCurrentDisplayMode, (SDL_DisplayID)) +SDL_PROC(const SDL_DisplayMode*, SDL_GetDesktopDisplayMode, (SDL_DisplayID)) +SDL_PROC(bool, SDL_GetDisplayBounds, (SDL_DisplayID, SDL_Rect*)) +SDL_PROC(SDL_DisplayMode**, SDL_GetFullscreenDisplayModes, (SDL_DisplayID, int*)) +SDL_PROC(const char*, SDL_GetError, (void)) +SDL_PROC(SDL_DisplayID*, SDL_GetDisplays, (int*)) +SDL_PROC(SDL_DisplayID, SDL_GetDisplayForWindow, (SDL_Window*)) +SDL_PROC(bool, SDL_SetWindowFullscreenMode, (SDL_Window*, const SDL_DisplayMode*)) +SDL_PROC(bool, SDL_SetWindowFullscreen, (SDL_Window*, bool)) +SDL_PROC(SDL_WindowFlags, SDL_GetWindowFlags, (SDL_Window*)) +SDL_PROC(bool, SDL_GetWindowSize, (SDL_Window*, int*, int*)) +SDL_PROC(bool, SDL_SetWindowSize, (SDL_Window*, int, int)) +SDL_PROC(SDL_MetalView, SDL_Metal_CreateView, (SDL_Window*)) +SDL_PROC(void*, SDL_Metal_GetLayer, (SDL_MetalView)) +SDL_PROC(void, SDL_free, (void*)) +#undef SDL_PROC +#endif // defined(DXMT_WSI_SDL3) diff --git a/src/util/wsi_window_sdl3.cpp b/src/util/wsi_window_sdl3.cpp new file mode 100644 index 000000000..b62b4bbf3 --- /dev/null +++ b/src/util/wsi_window_sdl3.cpp @@ -0,0 +1,146 @@ +#if defined(DXMT_WSI_SDL3) + +#include "wsi_window.hpp" +#include "wsi_monitor.hpp" + +#include "wsi_platform_sdl3.hpp" + +#include "util_string.hpp" +#include "log/log.hpp" + +#include +#include + +namespace dxmt::wsi { + +void getWindowSize(HWND hWindow, uint32_t *pWidth, uint32_t *pHeight) { + SDL_Window* window = fromHwnd(hWindow); + + int32_t w, h; + SDL3Initializer::get().SDL_GetWindowSize(window, &w, &h); + + if (pWidth) + *pWidth = uint32_t(w); + + if (pHeight) + *pHeight = uint32_t(h); +} + +void resizeWindow(HWND hWindow, DXMTWindowState *pState, uint32_t width, + uint32_t height) { + SDL_Window* window = fromHwnd(hWindow); + + SDL3Initializer::get().SDL_SetWindowSize(window, int32_t(width), int32_t(height)); +} + +bool setWindowMode(HMONITOR hMonitor, HWND hWindow, const WsiMode &mode) { + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + SDL_Window* window = fromHwnd(hWindow); + + if (!isValidDisplay(displayId)) + return false; + + float refreshRate = mode.refreshRate.numerator != 0 + ? float(mode.refreshRate.numerator) / float(mode.refreshRate.denominator) + : 0.0f; + // TODO: Implement lookup format for bitsPerPixel here. + + const SDL_DisplayMode *sdlmode = SDL3Initializer::get().SDL_GetClosestFullscreenDisplayMode(displayId, mode.width, mode.height, refreshRate, true); + if (!sdlmode) { + Logger::err(str::format("SDL3 WSI: setWindowMode: SDL_GetClosestFullscreenDisplayMode: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + if (!SDL3Initializer::get().SDL_SetWindowFullscreenMode(window, sdlmode)) { + Logger::err(str::format("SDL3 WSI: setWindowMode: SDL_SetWindowFullscreenMode: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool enterFullscreenMode(HMONITOR hMonitor, HWND hWindow, + DXMTWindowState *pState, + [[maybe_unused]] bool modeSwitch) { + + const SDL_DisplayID displayId = fromHmonitor(hMonitor); + SDL_Window* window = fromHwnd(hWindow); + + if (!isValidDisplay(displayId)) + return false; + + if (!modeSwitch) { + // borderless fullscreen: set no fullscreen mode (NULL = borderless) + SDL3Initializer::get().SDL_SetWindowFullscreenMode(window, nullptr); + } + + // TODO: Set this on the correct monitor. + // Docs aren't clear on this... + if (!SDL3Initializer::get().SDL_SetWindowFullscreen(window, true)) { + Logger::err(str::format("SDL3 WSI: enterFullscreenMode: SDL_SetWindowFullscreen: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool leaveFullscreenMode(HWND hWindow, DXMTWindowState *pState, + bool restoreCoordinates) { + SDL_Window* window = fromHwnd(hWindow); + + // @todo; pState handling + (void)pState; + + if (!SDL3Initializer::get().SDL_SetWindowFullscreen(window, false)) { + Logger::err(str::format("SDL3 WSI: leaveFullscreenMode: SDL_SetWindowFullscreen: ", SDL3Initializer::get().SDL_GetError())); + return false; + } + + return true; +} + +bool restoreDisplayMode() { + return true; +} + +HMONITOR getWindowMonitor(HWND hWindow) { + SDL_Window* window = fromHwnd(hWindow); + const SDL_DisplayID displayId = SDL3Initializer::get().SDL_GetDisplayForWindow(window); + + return toHmonitor(displayId); +} + +bool isWindow(HWND hWindow) { return true; } + +void updateFullscreenWindow(HMONITOR hMonitor, HWND hWindow, + bool forceTopmost) { + (void)hMonitor; + (void)forceTopmost; + (void)hWindow; +} + +WMT::Object createMetalViewFromHWND(intptr_t hwnd, WMT::Device device, WMT::MetalLayer &layer) { + SDL_MetalView sdlView = SDL3Initializer::get().SDL_Metal_CreateView((SDL_Window*)hwnd); + + if (!sdlView) { + Logger::err(str::format("SDL3 WSI: createMetalViewFromHWND: SDL_Metal_CreateView failed with error: ", SDL3Initializer::get().SDL_GetError())); + return WMT::Object(); + } + + void* metalLayer = SDL3Initializer::get().SDL_Metal_GetLayer(sdlView); + if (!metalLayer) { + Logger::err(str::format("SDL3 WSI: createMetalViewFromHWND: SDL_Metal_GetLayer failed with error: ", SDL3Initializer::get().SDL_GetError())); + return WMT::Object(); + } + + layer.handle = (obj_handle_t)((void*)metalLayer); + WMTLayerProps props = {}; + layer.getProps(props); + props.device = device.handle; + layer.setProps(props); + return WMT::Object((obj_handle_t)((void*)sdlView)); +} + +} // namespace dxmt::wsi + +#endif // defined(DXMT_WSI_SDL3) diff --git a/src/util/wsi_window_sdl3.hpp b/src/util/wsi_window_sdl3.hpp new file mode 100644 index 000000000..abe5b0154 --- /dev/null +++ b/src/util/wsi_window_sdl3.hpp @@ -0,0 +1,14 @@ +#pragma once + +#if defined(DXMT_WSI_SDL3) + +#include + +namespace dxmt::wsi { + +struct DXMTWindowState { +}; + +} + +#endif // defined(DXMT_WSI_SDL3)