aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/duckstation
diff options
context:
space:
mode:
authorRSDuck <RSDuck@users.noreply.github.com>2022-10-17 22:55:11 +0200
committerGitHub <noreply@github.com>2022-10-17 22:55:11 +0200
commitac3118cbc54715da55856958b42817fbd1eed666 (patch)
tree717e66ec7df79171821f46e02b47d1357256ace2 /src/frontend/duckstation
parent31ba585d392c9745917a141c72b3349d029586f0 (diff)
No more context mess (#1531)
* WIP: use Duckstation's context code to directly render into QT Widget from separate thread without two OpenGL contexts currently only works on Windows * reenable gay OSD * add back vsync * make it atleast a little more thread safe * linux support * don't segfault on closing * reorganise and cleanup build system it's still not good, but better than before * macos? * try to get it working on Ubuntu CI also update instructions * let's try this * ok how about this * try creating an OGL 4.3 context first (https://i.kym-cdn.com/photos/images/original/001/264/842/220.png) * fix Ubuntu * hm * try again for Windows * let's try this * make the OpenGL renderer work again that was stupid * do OGL surface resizing from the mainthread * Fix small mistake in GL context creation on macOS causing version 3.2 to be considered invalid * C stupidness * cleanup * don't let the emuthread deinit OGL if there's no OGL * reset lastScreenWidth/Height when deiniting OpenGL * disable stencil test while drawing framebuffers * macOS: Link Cocoa framework explicitly when not building with Qt6 Seems to be needed for the classes used by DuckStation's GL context code. * Set ScreenPanelGL's minimum size immediately Fixes GL context creation for OpenGL display on macOS using the wrong size as the underlying window was not resized to the correct size by Qt yet. * don't emit window updates when OGL display is used * stuff Arisotura said Co-authored-by: Nadia Holmquist Pedersen <nadia@nhp.sh>
Diffstat (limited to 'src/frontend/duckstation')
-rw-r--r--src/frontend/duckstation/duckstation_compat.h17
-rw-r--r--src/frontend/duckstation/gl/context.cpp175
-rw-r--r--src/frontend/duckstation/gl/context.h76
-rw-r--r--src/frontend/duckstation/gl/context_agl.h49
-rw-r--r--src/frontend/duckstation/gl/context_agl.mm214
-rw-r--r--src/frontend/duckstation/gl/context_egl.cpp432
-rw-r--r--src/frontend/duckstation/gl/context_egl.h48
-rw-r--r--src/frontend/duckstation/gl/context_egl_wayland.cpp86
-rw-r--r--src/frontend/duckstation/gl/context_egl_wayland.h33
-rw-r--r--src/frontend/duckstation/gl/context_egl_x11.cpp69
-rw-r--r--src/frontend/duckstation/gl/context_egl_x11.h28
-rw-r--r--src/frontend/duckstation/gl/context_glx.cpp328
-rw-r--r--src/frontend/duckstation/gl/context_glx.h44
-rw-r--r--src/frontend/duckstation/gl/context_wgl.cpp452
-rw-r--r--src/frontend/duckstation/gl/context_wgl.h53
-rw-r--r--src/frontend/duckstation/gl/loader.h8
-rw-r--r--src/frontend/duckstation/gl/x11_window.cpp101
-rw-r--r--src/frontend/duckstation/gl/x11_window.h48
-rw-r--r--src/frontend/duckstation/log.h46
-rw-r--r--src/frontend/duckstation/scoped_guard.h34
-rw-r--r--src/frontend/duckstation/window_info.cpp191
-rw-r--r--src/frontend/duckstation/window_info.h43
-rw-r--r--src/frontend/duckstation/windows_headers.h26
23 files changed, 2601 insertions, 0 deletions
diff --git a/src/frontend/duckstation/duckstation_compat.h b/src/frontend/duckstation/duckstation_compat.h
new file mode 100644
index 0000000..a661e92
--- /dev/null
+++ b/src/frontend/duckstation/duckstation_compat.h
@@ -0,0 +1,17 @@
+#ifndef DUCKSTATION_COMPAT_H
+#define DUCKSTATION_COMPAT_H
+
+#include "../types.h"
+
+#include <assert.h>
+
+#define ALWAYS_INLINE __attribute__((always_inline)) inline
+
+#define AssertMsg(cond, msg) assert(cond && msg)
+#define Assert(cond) assert(cond)
+
+#define Panic(msg) assert(false && msg)
+
+#define UnreachableCode() __builtin_unreachable
+
+#endif \ No newline at end of file
diff --git a/src/frontend/duckstation/gl/context.cpp b/src/frontend/duckstation/gl/context.cpp
new file mode 100644
index 0000000..98e6bd1
--- /dev/null
+++ b/src/frontend/duckstation/gl/context.cpp
@@ -0,0 +1,175 @@
+#include "context.h"
+#include "../log.h"
+#include "loader.h"
+#include <cstdlib>
+#include <cstring>
+#ifdef __APPLE__
+#include <stdlib.h>
+#else
+#include <malloc.h>
+#endif
+Log_SetChannel(GL::Context);
+
+#if defined(_WIN32) && !defined(_M_ARM64)
+#include "context_wgl.h"
+#elif defined(__APPLE__)
+#include "context_agl.h"
+#else
+#include "context_egl_wayland.h"
+#include "context_egl_x11.h"
+#include "context_glx.h"
+#endif
+
+namespace GL {
+
+static bool ShouldPreferESContext()
+{
+#ifndef _MSC_VER
+ const char* value = std::getenv("PREFER_GLES_CONTEXT");
+ return (value && strcmp(value, "1") == 0);
+#else
+ char buffer[2] = {};
+ size_t buffer_size = sizeof(buffer);
+ getenv_s(&buffer_size, buffer, "PREFER_GLES_CONTEXT");
+ return (std::strcmp(buffer, "1") == 0);
+#endif
+}
+
+Context::Context(const WindowInfo& wi) : m_wi(wi) {}
+
+Context::~Context() = default;
+
+std::vector<Context::FullscreenModeInfo> Context::EnumerateFullscreenModes()
+{
+ return {};
+}
+
+std::unique_ptr<GL::Context> Context::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ if (ShouldPreferESContext())
+ {
+ // move ES versions to the front
+ Version* new_versions_to_try = static_cast<Version*>(alloca(sizeof(Version) * num_versions_to_try));
+ size_t count = 0;
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ if (versions_to_try[i].profile == Profile::ES)
+ new_versions_to_try[count++] = versions_to_try[i];
+ }
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ if (versions_to_try[i].profile != Profile::ES)
+ new_versions_to_try[count++] = versions_to_try[i];
+ }
+ versions_to_try = new_versions_to_try;
+ }
+
+ std::unique_ptr<Context> context;
+#if defined(_WIN32) && !defined(_M_ARM64)
+ context = ContextWGL::Create(wi, versions_to_try, num_versions_to_try);
+#elif defined(__APPLE__)
+ context = ContextAGL::Create(wi, versions_to_try, num_versions_to_try);
+#else
+ if (wi.type == WindowInfo::Type::X11)
+ {
+ const char* use_egl_x11 = std::getenv("USE_EGL_X11");
+ if (use_egl_x11 && std::strcmp(use_egl_x11, "1") == 0)
+ context = ContextEGLX11::Create(wi, versions_to_try, num_versions_to_try);
+ else
+ context = ContextGLX::Create(wi, versions_to_try, num_versions_to_try);
+ }
+
+ if (wi.type == WindowInfo::Type::Wayland)
+ context = ContextEGLWayland::Create(wi, versions_to_try, num_versions_to_try);
+#endif
+
+ if (!context)
+ return nullptr;
+
+ Log_InfoPrintf("Created a %s context", context->IsGLES() ? "OpenGL ES" : "OpenGL");
+
+ // TODO: Not thread-safe.
+ static Context* context_being_created;
+ context_being_created = context.get();
+
+ if (!gladLoadGLLoader([](const char* name) { return context_being_created->GetProcAddress(name); }))
+ {
+ Log_ErrorPrintf("Failed to load GL functions for GLAD");
+ return nullptr;
+ }
+
+ const char* gl_vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
+ const char* gl_renderer = reinterpret_cast<const char*>(glGetString(GL_RENDERER));
+ const char* gl_version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+ const char* gl_shading_language_version = reinterpret_cast<const char*>(glGetString(GL_SHADING_LANGUAGE_VERSION));
+ Log_InfoPrintf("GL_VENDOR: %s", gl_vendor);
+ Log_InfoPrintf("GL_RENDERER: %s", gl_renderer);
+ Log_InfoPrintf("GL_VERSION: %s", gl_version);
+ Log_InfoPrintf("GL_SHADING_LANGUAGE_VERSION: %s", gl_shading_language_version);
+
+ return context;
+}
+
+const std::array<Context::Version, 11>& Context::GetAllDesktopVersionsList()
+{
+ static constexpr std::array<Version, 11> vlist = {{{Profile::Core, 4, 6},
+ {Profile::Core, 4, 5},
+ {Profile::Core, 4, 4},
+ {Profile::Core, 4, 3},
+ {Profile::Core, 4, 2},
+ {Profile::Core, 4, 1},
+ {Profile::Core, 4, 0},
+ {Profile::Core, 3, 3},
+ {Profile::Core, 3, 2},
+ {Profile::Core, 3, 1},
+ {Profile::Core, 3, 0}}};
+ return vlist;
+}
+
+const std::array<Context::Version, 12>& Context::GetAllDesktopVersionsListWithFallback()
+{
+ static constexpr std::array<Version, 12> vlist = {{{Profile::Core, 4, 6},
+ {Profile::Core, 4, 5},
+ {Profile::Core, 4, 4},
+ {Profile::Core, 4, 3},
+ {Profile::Core, 4, 2},
+ {Profile::Core, 4, 1},
+ {Profile::Core, 4, 0},
+ {Profile::Core, 3, 3},
+ {Profile::Core, 3, 2},
+ {Profile::Core, 3, 1},
+ {Profile::Core, 3, 0},
+ {Profile::NoProfile, 0, 0}}};
+ return vlist;
+}
+
+const std::array<Context::Version, 4>& Context::GetAllESVersionsList()
+{
+ static constexpr std::array<Version, 4> vlist = {
+ {{Profile::ES, 3, 2}, {Profile::ES, 3, 1}, {Profile::ES, 3, 0}, {Profile::ES, 2, 0}}};
+ return vlist;
+}
+
+const std::array<Context::Version, 16>& Context::GetAllVersionsList()
+{
+ static constexpr std::array<Version, 16> vlist = {{{Profile::Core, 4, 6},
+ {Profile::Core, 4, 5},
+ {Profile::Core, 4, 4},
+ {Profile::Core, 4, 3},
+ {Profile::Core, 4, 2},
+ {Profile::Core, 4, 1},
+ {Profile::Core, 4, 0},
+ {Profile::Core, 3, 3},
+ {Profile::Core, 3, 2},
+ {Profile::Core, 3, 1},
+ {Profile::Core, 3, 0},
+ {Profile::ES, 3, 2},
+ {Profile::ES, 3, 1},
+ {Profile::ES, 3, 0},
+ {Profile::ES, 2, 0},
+ {Profile::NoProfile, 0, 0}}};
+ return vlist;
+}
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context.h b/src/frontend/duckstation/gl/context.h
new file mode 100644
index 0000000..41d8a2c
--- /dev/null
+++ b/src/frontend/duckstation/gl/context.h
@@ -0,0 +1,76 @@
+#pragma once
+#include "../duckstation_compat.h"
+#include "../window_info.h"
+#include <array>
+#include <memory>
+#include <vector>
+
+namespace GL {
+class Context
+{
+public:
+ Context(const WindowInfo& wi);
+ virtual ~Context();
+
+ enum class Profile
+ {
+ NoProfile,
+ Core,
+ ES
+ };
+
+ struct Version
+ {
+ Profile profile;
+ int major_version;
+ int minor_version;
+ };
+
+ struct FullscreenModeInfo
+ {
+ u32 width;
+ u32 height;
+ float refresh_rate;
+ };
+
+ ALWAYS_INLINE const WindowInfo& GetWindowInfo() const { return m_wi; }
+ ALWAYS_INLINE bool IsGLES() const { return (m_version.profile == Profile::ES); }
+ ALWAYS_INLINE u32 GetSurfaceWidth() const { return m_wi.surface_width; }
+ ALWAYS_INLINE u32 GetSurfaceHeight() const { return m_wi.surface_height; }
+ ALWAYS_INLINE WindowInfo::SurfaceFormat GetSurfaceFormat() const { return m_wi.surface_format; }
+
+ virtual void* GetProcAddress(const char* name) = 0;
+ virtual bool ChangeSurface(const WindowInfo& new_wi) = 0;
+ virtual void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) = 0;
+ virtual bool SwapBuffers() = 0;
+ virtual bool MakeCurrent() = 0;
+ virtual bool DoneCurrent() = 0;
+ virtual bool SetSwapInterval(s32 interval) = 0;
+ virtual std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) = 0;
+
+ virtual std::vector<FullscreenModeInfo> EnumerateFullscreenModes();
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ template<size_t N>
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const std::array<Version, N>& versions_to_try)
+ {
+ return Create(wi, versions_to_try.data(), versions_to_try.size());
+ }
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi) { return Create(wi, GetAllVersionsList()); }
+
+ static const std::array<Version, 11>& GetAllDesktopVersionsList();
+ static const std::array<Version, 12>& GetAllDesktopVersionsListWithFallback();
+ static const std::array<Version, 4>& GetAllESVersionsList();
+ static const std::array<Version, 16>& GetAllVersionsList();
+
+protected:
+#ifdef _WIN32
+#endif
+
+ WindowInfo m_wi;
+ Version m_version = {};
+};
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_agl.h b/src/frontend/duckstation/gl/context_agl.h
new file mode 100644
index 0000000..459bf2f
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_agl.h
@@ -0,0 +1,49 @@
+#pragma once
+#include "context.h"
+#include "loader.h"
+
+#if defined(__APPLE__) && defined(__OBJC__)
+#import <AppKit/AppKit.h>
+#else
+struct NSOpenGLContext;
+struct NSOpenGLPixelFormat;
+struct NSView;
+#define __bridge
+#endif
+
+namespace GL {
+
+class ContextAGL final : public Context
+{
+public:
+ ContextAGL(const WindowInfo& wi);
+ ~ContextAGL() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ void* GetProcAddress(const char* name) override;
+ bool ChangeSurface(const WindowInfo& new_wi) override;
+ void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+ bool SwapBuffers() override;
+ bool MakeCurrent() override;
+ bool DoneCurrent() override;
+ bool SetSwapInterval(s32 interval) override;
+ std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+
+private:
+ ALWAYS_INLINE NSView* GetView() const { return static_cast<NSView*>((__bridge NSView*)m_wi.window_handle); }
+
+ bool Initialize(const Version* versions_to_try, size_t num_versions_to_try);
+ bool CreateContext(NSOpenGLContext* share_context, int profile, bool make_current);
+ void BindContextToView();
+
+ // returns true if dimensions have changed
+ bool UpdateDimensions();
+
+ NSOpenGLContext* m_context = nullptr;
+ NSOpenGLPixelFormat* m_pixel_format = nullptr;
+ void* m_opengl_module_handle = nullptr;
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_agl.mm b/src/frontend/duckstation/gl/context_agl.mm
new file mode 100644
index 0000000..f833518
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_agl.mm
@@ -0,0 +1,214 @@
+#include "context_agl.h"
+#include "../duckstation_compat.h"
+#include "../log.h"
+#include "loader.h"
+#include <dlfcn.h>
+Log_SetChannel(GL::ContextAGL);
+
+namespace GL {
+ContextAGL::ContextAGL(const WindowInfo& wi) : Context(wi)
+{
+ m_opengl_module_handle = dlopen("/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL", RTLD_NOW);
+ if (!m_opengl_module_handle)
+ Log_ErrorPrint("Could not open OpenGL.framework, function lookups will probably fail");
+}
+
+ContextAGL::~ContextAGL()
+{
+ if ([NSOpenGLContext currentContext] == m_context)
+ [NSOpenGLContext clearCurrentContext];
+
+ if (m_context)
+ [m_context release];
+
+ if (m_pixel_format)
+ [m_pixel_format release];
+
+ if (m_opengl_module_handle)
+ dlclose(m_opengl_module_handle);
+}
+
+std::unique_ptr<Context> ContextAGL::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextAGL> context = std::make_unique<ContextAGL>(wi);
+ if (!context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+bool ContextAGL::Initialize(const Version* versions_to_try, size_t num_versions_to_try)
+{
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ const Version& cv = versions_to_try[i];
+ if (cv.profile == Profile::NoProfile && CreateContext(nullptr, NSOpenGLProfileVersionLegacy, true))
+ {
+ // we already have the dummy context, so just use that
+ m_version = cv;
+ return true;
+ }
+ else if (cv.profile == Profile::Core)
+ {
+ if (cv.major_version > 4 && cv.minor_version > 1)
+ continue;
+
+ const NSOpenGLPixelFormatAttribute profile = (cv.major_version > 3 || cv.minor_version > 2) ? NSOpenGLProfileVersion4_1Core : NSOpenGLProfileVersion3_2Core;
+ if (CreateContext(nullptr, static_cast<int>(profile), true))
+ {
+ m_version = cv;
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void* ContextAGL::GetProcAddress(const char* name)
+{
+ void* addr = m_opengl_module_handle ? dlsym(m_opengl_module_handle, name) : nullptr;
+ if (addr)
+ return addr;
+
+ return dlsym(RTLD_NEXT, name);
+}
+
+bool ContextAGL::ChangeSurface(const WindowInfo& new_wi)
+{
+ m_wi = new_wi;
+ BindContextToView();
+ return true;
+}
+
+void ContextAGL::ResizeSurface(u32 new_surface_width /*= 0*/, u32 new_surface_height /*= 0*/)
+{
+ UpdateDimensions();
+}
+
+bool ContextAGL::UpdateDimensions()
+{
+ const NSSize window_size = [GetView() frame].size;
+ const CGFloat window_scale = [[GetView() window] backingScaleFactor];
+ const u32 new_width = static_cast<u32>(static_cast<CGFloat>(window_size.width) * window_scale);
+ const u32 new_height = static_cast<u32>(static_cast<CGFloat>(window_size.height) * window_scale);
+
+ if (m_wi.surface_width == new_width && m_wi.surface_height == new_height)
+ return false;
+
+ m_wi.surface_width = new_width;
+ m_wi.surface_height = new_height;
+
+ dispatch_block_t block = ^{
+ [m_context update];
+ };
+
+ if ([NSThread isMainThread])
+ block();
+ else
+ dispatch_sync(dispatch_get_main_queue(), block);
+
+ return true;
+}
+
+bool ContextAGL::SwapBuffers()
+{
+ [m_context flushBuffer];
+ return true;
+}
+
+bool ContextAGL::MakeCurrent()
+{
+ [m_context makeCurrentContext];
+ return true;
+}
+
+bool ContextAGL::DoneCurrent()
+{
+ [NSOpenGLContext clearCurrentContext];
+ return true;
+}
+
+bool ContextAGL::SetSwapInterval(s32 interval)
+{
+ GLint gl_interval = static_cast<GLint>(interval);
+ [m_context setValues:&gl_interval forParameter:NSOpenGLCPSwapInterval];
+ return true;
+}
+
+std::unique_ptr<Context> ContextAGL::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextAGL> context = std::make_unique<ContextAGL>(wi);
+
+ context->m_context = [[NSOpenGLContext alloc] initWithFormat:m_pixel_format shareContext:m_context];
+ if (context->m_context == nil)
+ return nullptr;
+
+ context->m_version = m_version;
+ context->m_pixel_format = m_pixel_format;
+ [context->m_pixel_format retain];
+
+ if (wi.type == WindowInfo::Type::MacOS)
+ context->BindContextToView();
+
+ return context;
+}
+
+bool ContextAGL::CreateContext(NSOpenGLContext* share_context, int profile, bool make_current)
+{
+ if (m_context)
+ {
+ [m_context release];
+ m_context = nullptr;
+ }
+
+ if (m_pixel_format)
+ [m_pixel_format release];
+
+ const std::array<NSOpenGLPixelFormatAttribute, 5> attribs = {{
+ NSOpenGLPFADoubleBuffer,
+ NSOpenGLPFAOpenGLProfile,
+ static_cast<NSOpenGLPixelFormatAttribute>(profile),
+ NSOpenGLPFAAccelerated,
+ 0}};
+ m_pixel_format = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs.data()];
+ if (m_pixel_format == nil)
+ {
+ Log_ErrorPrintf("Failed to initialize pixel format");
+ return false;
+ }
+
+ m_context = [[NSOpenGLContext alloc] initWithFormat:m_pixel_format shareContext:nil];
+ if (m_context == nil)
+ return false;
+
+ if (m_wi.type == WindowInfo::Type::MacOS)
+ BindContextToView();
+
+ if (make_current)
+ [m_context makeCurrentContext];
+
+ return true;
+}
+
+void ContextAGL::BindContextToView()
+{
+ NSView* const view = GetView();
+ NSWindow* const window = [view window];
+ [view setWantsBestResolutionOpenGLSurface:YES];
+
+ UpdateDimensions();
+
+ dispatch_block_t block = ^{
+ [window makeFirstResponder:view];
+ [m_context setView:view];
+ [window makeKeyAndOrderFront:nil];
+ };
+
+ if ([NSThread isMainThread])
+ block();
+ else
+ dispatch_sync(dispatch_get_main_queue(), block);
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl.cpp b/src/frontend/duckstation/gl/context_egl.cpp
new file mode 100644
index 0000000..d251a39
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl.cpp
@@ -0,0 +1,432 @@
+#include "context_egl.h"
+#include "../log.h"
+#include "../duckstation_compat.h"
+#include <optional>
+#include <vector>
+#include <cstring>
+Log_SetChannel(GL::ContextEGL);
+
+namespace GL {
+ContextEGL::ContextEGL(const WindowInfo& wi) : Context(wi) {}
+
+ContextEGL::~ContextEGL()
+{
+ DestroySurface();
+ DestroyContext();
+}
+
+std::unique_ptr<Context> ContextEGL::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextEGL> context = std::make_unique<ContextEGL>(wi);
+ if (!context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+bool ContextEGL::Initialize(const Version* versions_to_try, size_t num_versions_to_try)
+{
+ if (!gladLoadEGL())
+ {
+ Log_ErrorPrintf("Loading GLAD EGL functions failed");
+ return false;
+ }
+
+ if (!SetDisplay())
+ return false;
+
+ int egl_major, egl_minor;
+ if (!eglInitialize(m_display, &egl_major, &egl_minor))
+ {
+ Log_ErrorPrintf("eglInitialize() failed: %d", eglGetError());
+ return false;
+ }
+ Log_InfoPrintf("EGL Version: %d.%d", egl_major, egl_minor);
+
+ const char* extensions = eglQueryString(m_display, EGL_EXTENSIONS);
+ if (extensions)
+ {
+ Log_InfoPrintf("EGL Extensions: %s", extensions);
+ m_supports_surfaceless = std::strstr(extensions, "EGL_KHR_surfaceless_context") != nullptr;
+ }
+ if (!m_supports_surfaceless)
+ Log_WarningPrint("EGL implementation does not support surfaceless contexts, emulating with pbuffers");
+
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ if (CreateContextAndSurface(versions_to_try[i], nullptr, true))
+ return true;
+ }
+
+ return false;
+}
+
+bool ContextEGL::SetDisplay()
+{
+ m_display = eglGetDisplay(static_cast<EGLNativeDisplayType>(m_wi.display_connection));
+ if (!m_display)
+ {
+ Log_ErrorPrintf("eglGetDisplay() failed: %d", eglGetError());
+ return false;
+ }
+
+ return true;
+}
+
+void* ContextEGL::GetProcAddress(const char* name)
+{
+ return reinterpret_cast<void*>(eglGetProcAddress(name));
+}
+
+bool ContextEGL::ChangeSurface(const WindowInfo& new_wi)
+{
+ const bool was_current = (eglGetCurrentContext() == m_context);
+ if (was_current)
+ eglMakeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+
+ if (m_surface != EGL_NO_SURFACE)
+ {
+ eglDestroySurface(m_display, m_surface);
+ m_surface = EGL_NO_SURFACE;
+ }
+
+ m_wi = new_wi;
+ if (!CreateSurface())
+ return false;
+
+ if (was_current && !eglMakeCurrent(m_display, m_surface, m_surface, m_context))
+ {
+ Log_ErrorPrintf("Failed to make context current again after surface change");
+ return false;
+ }
+
+ return true;
+}
+
+void ContextEGL::ResizeSurface(u32 new_surface_width /*= 0*/, u32 new_surface_height /*= 0*/)
+{
+ if (new_surface_width == 0 && new_surface_height == 0)
+ {
+ EGLint surface_width, surface_height;
+ if (eglQuerySurface(m_display, m_surface, EGL_WIDTH, &surface_width) &&
+ eglQuerySurface(m_display, m_surface, EGL_HEIGHT, &surface_height))
+ {
+ m_wi.surface_width = static_cast<u32>(surface_width);
+ m_wi.surface_height = static_cast<u32>(surface_height);
+ return;
+ }
+ else
+ {
+ Log_ErrorPrintf("eglQuerySurface() failed: %d", eglGetError());
+ }
+ }
+
+ m_wi.surface_width = new_surface_width;
+ m_wi.surface_height = new_surface_height;
+}
+
+bool ContextEGL::SwapBuffers()
+{
+ return eglSwapBuffers(m_display, m_surface);
+}
+
+bool ContextEGL::MakeCurrent()
+{
+ if (!eglMakeCurrent(m_display, m_surface, m_surface, m_context))
+ {
+ Log_ErrorPrintf("eglMakeCurrent() failed: %d", eglGetError());
+ return false;
+ }
+
+ return true;
+}
+
+bool ContextEGL::DoneCurrent()
+{
+ return eglMakeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+}
+
+bool ContextEGL::SetSwapInterval(s32 interval)
+{
+ return eglSwapInterval(m_display, interval);
+}
+
+std::unique_ptr<Context> ContextEGL::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextEGL> context = std::make_unique<ContextEGL>(wi);
+ context->m_display = m_display;
+ context->m_supports_surfaceless = m_supports_surfaceless;
+
+ if (!context->CreateContextAndSurface(m_version, m_context, false))
+ return nullptr;
+
+ return context;
+}
+
+EGLNativeWindowType ContextEGL::GetNativeWindow(EGLConfig config)
+{
+ return {};
+}
+
+bool ContextEGL::CreateSurface()
+{
+ if (m_wi.type == WindowInfo::Type::Surfaceless)
+ {
+ if (m_supports_surfaceless)
+ return true;
+ else
+ return CreatePBufferSurface();
+ }
+
+ EGLNativeWindowType native_window = GetNativeWindow(m_config);
+ m_surface = eglCreateWindowSurface(m_display, m_config, native_window, nullptr);
+ if (!m_surface)
+ {
+ Log_ErrorPrintf("eglCreateWindowSurface() failed: %d", eglGetError());
+ return false;
+ }
+
+ // Some implementations may require the size to be queried at runtime.
+ EGLint surface_width, surface_height;
+ if (eglQuerySurface(m_display, m_surface, EGL_WIDTH, &surface_width) &&
+ eglQuerySurface(m_display, m_surface, EGL_HEIGHT, &surface_height))
+ {
+ m_wi.surface_width = static_cast<u32>(surface_width);
+ m_wi.surface_height = static_cast<u32>(surface_height);
+ }
+ else
+ {
+ Log_ErrorPrintf("eglQuerySurface() failed: %d", eglGetError());
+ }
+
+ return true;
+}
+
+bool ContextEGL::CreatePBufferSurface()
+{
+ const u32 width = std::max<u32>(m_wi.surface_width, 1);
+ const u32 height = std::max<u32>(m_wi.surface_height, 1);
+
+ // TODO: Format
+ EGLint attrib_list[] = {
+ EGL_WIDTH, static_cast<EGLint>(width), EGL_HEIGHT, static_cast<EGLint>(height), EGL_NONE,
+ };
+
+ m_surface = eglCreatePbufferSurface(m_display, m_config, attrib_list);
+ if (!m_surface)
+ {
+ Log_ErrorPrintf("eglCreatePbufferSurface() failed: %d", eglGetError());
+ return false;
+ }
+
+ Log_DevPrintf("Created %ux%u pbuffer surface", width, height);
+ return true;
+}
+
+bool ContextEGL::CheckConfigSurfaceFormat(EGLConfig config, WindowInfo::SurfaceFormat format) const
+{
+ int red_size, green_size, blue_size, alpha_size;
+ if (!eglGetConfigAttrib(m_display, config, EGL_RED_SIZE, &red_size) ||
+ !eglGetConfigAttrib(m_display, config, EGL_GREEN_SIZE, &green_size) ||
+ !eglGetConfigAttrib(m_display, config, EGL_BLUE_SIZE, &blue_size) ||
+ !eglGetConfigAttrib(m_display, config, EGL_ALPHA_SIZE, &alpha_size))
+ {
+ return false;
+ }
+
+ switch (format)
+ {
+ case WindowInfo::SurfaceFormat::Auto:
+ return true;
+
+ case WindowInfo::SurfaceFormat::RGB8:
+ return (red_size == 8 && green_size == 8 && blue_size == 8);
+
+ case WindowInfo::SurfaceFormat::RGBA8:
+ return (red_size == 8 && green_size == 8 && blue_size == 8 && alpha_size == 8);
+
+ case WindowInfo::SurfaceFormat::RGB565:
+ return (red_size == 5 && green_size == 6 && blue_size == 5);
+
+ default:
+ return false;
+ }
+}
+
+void ContextEGL::DestroyContext()
+{
+ if (eglGetCurrentContext() == m_context)
+ eglMakeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+
+ if (m_context != EGL_NO_CONTEXT)
+ {
+ eglDestroyContext(m_display, m_context);
+ m_context = EGL_NO_CONTEXT;
+ }
+}
+
+void ContextEGL::DestroySurface()
+{
+ if (eglGetCurrentSurface(EGL_DRAW) == m_surface)
+ eglMakeCurrent(m_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+
+ if (m_surface != EGL_NO_SURFACE)
+ {
+ eglDestroySurface(m_display, m_surface);
+ m_surface = EGL_NO_SURFACE;
+ }
+}
+
+bool ContextEGL::CreateContext(const Version& version, EGLContext share_context)
+{
+ Log_DevPrintf(
+ "Trying version %u.%u (%s)", version.major_version, version.minor_version,
+ version.profile == Context::Profile::ES ? "ES" : (version.profile == Context::Profile::Core ? "Core" : "None"));
+ int surface_attribs[16] = {
+ EGL_RENDERABLE_TYPE,
+ (version.profile == Profile::ES) ?
+ ((version.major_version >= 3) ? EGL_OPENGL_ES3_BIT :
+ ((version.major_version == 2) ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_ES_BIT)) :
+ EGL_OPENGL_BIT,
+ EGL_SURFACE_TYPE,
+ (m_wi.type != WindowInfo::Type::Surfaceless) ? EGL_WINDOW_BIT : 0,
+ };
+ int nsurface_attribs = 4;
+
+ switch (m_wi.surface_format)
+ {
+ case WindowInfo::SurfaceFormat::RGB8:
+ surface_attribs[nsurface_attribs++] = EGL_RED_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ surface_attribs[nsurface_attribs++] = EGL_GREEN_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ surface_attribs[nsurface_attribs++] = EGL_BLUE_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ break;
+
+ case WindowInfo::SurfaceFormat::RGBA8:
+ surface_attribs[nsurface_attribs++] = EGL_RED_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ surface_attribs[nsurface_attribs++] = EGL_GREEN_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ surface_attribs[nsurface_attribs++] = EGL_BLUE_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ surface_attribs[nsurface_attribs++] = EGL_ALPHA_SIZE;
+ surface_attribs[nsurface_attribs++] = 8;
+ break;
+
+ case WindowInfo::SurfaceFormat::RGB565:
+ surface_attribs[nsurface_attribs++] = EGL_RED_SIZE;
+ surface_attribs[nsurface_attribs++] = 5;
+ surface_attribs[nsurface_attribs++] = EGL_GREEN_SIZE;
+ surface_attribs[nsurface_attribs++] = 6;
+ surface_attribs[nsurface_attribs++] = EGL_BLUE_SIZE;
+ surface_attribs[nsurface_attribs++] = 5;
+ break;
+
+ case WindowInfo::SurfaceFormat::Auto:
+ break;
+
+ default:
+ UnreachableCode();
+ break;
+ }
+
+ surface_attribs[nsurface_attribs++] = EGL_NONE;
+ surface_attribs[nsurface_attribs++] = 0;
+
+ EGLint num_configs;
+ if (!eglChooseConfig(m_display, surface_attribs, nullptr, 0, &num_configs) || num_configs == 0)
+ {
+ Log_ErrorPrintf("eglChooseConfig() failed: %d", eglGetError());
+ return false;
+ }
+
+ std::vector<EGLConfig> configs(static_cast<u32>(num_configs));
+ if (!eglChooseConfig(m_display, surface_attribs, configs.data(), num_configs, &num_configs))
+ {
+ Log_ErrorPrintf("eglChooseConfig() failed: %d", eglGetError());
+ return false;
+ }
+ configs.resize(static_cast<u32>(num_configs));
+
+ std::optional<EGLConfig> config;
+ for (EGLConfig check_config : configs)
+ {
+ if (CheckConfigSurfaceFormat(check_config, m_wi.surface_format))
+ {
+ config = check_config;
+ break;
+ }
+ }
+
+ if (!config.has_value())
+ {
+ Log_WarningPrintf("No EGL configs matched exactly, using first.");
+ config = configs.front();
+ }
+
+ int attribs[8];
+ int nattribs = 0;
+ if (version.profile != Profile::NoProfile)
+ {
+ attribs[nattribs++] = EGL_CONTEXT_MAJOR_VERSION;
+ attribs[nattribs++] = version.major_version;
+ attribs[nattribs++] = EGL_CONTEXT_MINOR_VERSION;
+ attribs[nattribs++] = version.minor_version;
+ }
+ attribs[nattribs++] = EGL_NONE;
+ attribs[nattribs++] = 0;
+
+ if (!eglBindAPI((version.profile == Profile::ES) ? EGL_OPENGL_ES_API : EGL_OPENGL_API))
+ {
+ Log_ErrorPrintf("eglBindAPI(%s) failed", (version.profile == Profile::ES) ? "EGL_OPENGL_ES_API" : "EGL_OPENGL_API");
+ return false;
+ }
+
+ m_context = eglCreateContext(m_display, config.value(), share_context, attribs);
+ if (!m_context)
+ {
+ Log_ErrorPrintf("eglCreateContext() failed: %d", eglGetError());
+ return false;
+ }
+
+ Log_InfoPrintf(
+ "Got version %u.%u (%s)", version.major_version, version.minor_version,
+ version.profile == Context::Profile::ES ? "ES" : (version.profile == Context::Profile::Core ? "Core" : "None"));
+
+ m_config = config.value();
+ m_version = version;
+ return true;
+}
+
+bool ContextEGL::CreateContextAndSurface(const Version& version, EGLContext share_context, bool make_current)
+{
+ if (!CreateContext(version, share_context))
+ return false;
+
+ if (!CreateSurface())
+ {
+ Log_ErrorPrintf("Failed to create surface for context");
+ eglDestroyContext(m_display, m_context);
+ m_context = EGL_NO_CONTEXT;
+ return false;
+ }
+
+ if (make_current && !eglMakeCurrent(m_display, m_surface, m_surface, m_context))
+ {
+ Log_ErrorPrintf("eglMakeCurrent() failed: %d", eglGetError());
+ if (m_surface != EGL_NO_SURFACE)
+ {
+ eglDestroySurface(m_display, m_surface);
+ m_surface = EGL_NO_SURFACE;
+ }
+ eglDestroyContext(m_display, m_context);
+ m_context = EGL_NO_CONTEXT;
+ return false;
+ }
+
+ return true;
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl.h b/src/frontend/duckstation/gl/context_egl.h
new file mode 100644
index 0000000..c5eff20
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl.h
@@ -0,0 +1,48 @@
+#pragma once
+#include "context.h"
+#include "../../glad/glad_egl.h"
+
+namespace GL {
+
+class ContextEGL : public Context
+{
+public:
+ ContextEGL(const WindowInfo& wi);
+ ~ContextEGL() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ void* GetProcAddress(const char* name) override;
+ virtual bool ChangeSurface(const WindowInfo& new_wi) override;
+ virtual void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+ bool SwapBuffers() override;
+ bool MakeCurrent() override;
+ bool DoneCurrent() override;
+ bool SetSwapInterval(s32 interval) override;
+ virtual std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+
+protected:
+ virtual bool SetDisplay();
+ virtual EGLNativeWindowType GetNativeWindow(EGLConfig config);
+
+ bool Initialize(const Version* versions_to_try, size_t num_versions_to_try);
+ bool CreateDisplay();
+ bool CreateContext(const Version& version, EGLContext share_context);
+ bool CreateContextAndSurface(const Version& version, EGLContext share_context, bool make_current);
+ bool CreateSurface();
+ bool CreatePBufferSurface();
+ bool CheckConfigSurfaceFormat(EGLConfig config, WindowInfo::SurfaceFormat format) const;
+ void DestroyContext();
+ void DestroySurface();
+
+ EGLDisplay m_display = EGL_NO_DISPLAY;
+ EGLSurface m_surface = EGL_NO_SURFACE;
+ EGLContext m_context = EGL_NO_CONTEXT;
+
+ EGLConfig m_config = {};
+
+ bool m_supports_surfaceless = false;
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl_wayland.cpp b/src/frontend/duckstation/gl/context_egl_wayland.cpp
new file mode 100644
index 0000000..16532e8
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl_wayland.cpp
@@ -0,0 +1,86 @@
+#include "context_egl_wayland.h"
+#include "../log.h"
+#include <dlfcn.h>
+Log_SetChannel(ContextEGLWayland);
+
+namespace GL {
+static const char* WAYLAND_EGL_MODNAME = "libwayland-egl.so.1";
+
+ContextEGLWayland::ContextEGLWayland(const WindowInfo& wi) : ContextEGL(wi) {}
+ContextEGLWayland::~ContextEGLWayland()
+{
+ if (m_wl_window)
+ m_wl_egl_window_destroy(m_wl_window);
+ if (m_wl_module)
+ dlclose(m_wl_module);
+}
+
+std::unique_ptr<Context> ContextEGLWayland::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextEGLWayland> context = std::make_unique<ContextEGLWayland>(wi);
+ if (!context->LoadModule() || !context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+std::unique_ptr<Context> ContextEGLWayland::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextEGLWayland> context = std::make_unique<ContextEGLWayland>(wi);
+ context->m_display = m_display;
+
+ if (!context->LoadModule() || !context->CreateContextAndSurface(m_version, m_context, false))
+ return nullptr;
+
+ return context;
+}
+
+void ContextEGLWayland::ResizeSurface(u32 new_surface_width, u32 new_surface_height)
+{
+ if (m_wl_window)
+ m_wl_egl_window_resize(m_wl_window, new_surface_width, new_surface_height, 0, 0);
+
+ ContextEGL::ResizeSurface(new_surface_width, new_surface_height);
+}
+
+EGLNativeWindowType ContextEGLWayland::GetNativeWindow(EGLConfig config)
+{
+ if (m_wl_window)
+ {
+ m_wl_egl_window_destroy(m_wl_window);
+ m_wl_window = nullptr;
+ }
+
+ m_wl_window =
+ m_wl_egl_window_create(static_cast<wl_surface*>(m_wi.window_handle), m_wi.surface_width, m_wi.surface_height);
+ if (!m_wl_window)
+ return {};
+
+ return reinterpret_cast<EGLNativeWindowType>(m_wl_window);
+}
+
+bool ContextEGLWayland::LoadModule()
+{
+ m_wl_module = dlopen(WAYLAND_EGL_MODNAME, RTLD_NOW | RTLD_GLOBAL);
+ if (!m_wl_module)
+ {
+ Log_ErrorPrintf("Failed to load %s.", WAYLAND_EGL_MODNAME);
+ return false;
+ }
+
+ m_wl_egl_window_create =
+ reinterpret_cast<decltype(m_wl_egl_window_create)>(dlsym(m_wl_module, "wl_egl_window_create"));
+ m_wl_egl_window_destroy =
+ reinterpret_cast<decltype(m_wl_egl_window_destroy)>(dlsym(m_wl_module, "wl_egl_window_destroy"));
+ m_wl_egl_window_resize =
+ reinterpret_cast<decltype(m_wl_egl_window_resize)>(dlsym(m_wl_module, "wl_egl_window_resize"));
+ if (!m_wl_egl_window_create || !m_wl_egl_window_destroy || !m_wl_egl_window_resize)
+ {
+ Log_ErrorPrintf("Failed to load one or more functions from %s.", WAYLAND_EGL_MODNAME);
+ return false;
+ }
+
+ return true;
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl_wayland.h b/src/frontend/duckstation/gl/context_egl_wayland.h
new file mode 100644
index 0000000..4682f42
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl_wayland.h
@@ -0,0 +1,33 @@
+#pragma once
+#include "context_egl.h"
+#include <wayland-egl.h>
+
+namespace GL {
+
+class ContextEGLWayland final : public ContextEGL
+{
+public:
+ ContextEGLWayland(const WindowInfo& wi);
+ ~ContextEGLWayland() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+ void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+
+protected:
+ EGLNativeWindowType GetNativeWindow(EGLConfig config) override;
+
+private:
+ bool LoadModule();
+
+ wl_egl_window* m_wl_window = nullptr;
+
+ void* m_wl_module = nullptr;
+ wl_egl_window* (*m_wl_egl_window_create)(struct wl_surface* surface, int width, int height);
+ void (*m_wl_egl_window_destroy)(struct wl_egl_window* egl_window);
+ void (*m_wl_egl_window_resize)(struct wl_egl_window* egl_window, int width, int height, int dx, int dy);
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl_x11.cpp b/src/frontend/duckstation/gl/context_egl_x11.cpp
new file mode 100644
index 0000000..c7310e3
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl_x11.cpp
@@ -0,0 +1,69 @@
+#include "context_egl_x11.h"
+#include "../log.h"
+Log_SetChannel(GL::ContextEGLX11);
+
+namespace GL {
+ContextEGLX11::ContextEGLX11(const WindowInfo& wi) : ContextEGL(wi) {}
+ContextEGLX11::~ContextEGLX11() = default;
+
+std::unique_ptr<Context> ContextEGLX11::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextEGLX11> context = std::make_unique<ContextEGLX11>(wi);
+ if (!context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+std::unique_ptr<Context> ContextEGLX11::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextEGLX11> context = std::make_unique<ContextEGLX11>(wi);
+ context->m_display = m_display;
+
+ if (!context->CreateContextAndSurface(m_version, m_context, false))
+ return nullptr;
+
+ return context;
+}
+
+void ContextEGLX11::ResizeSurface(u32 new_surface_width, u32 new_surface_height)
+{
+ m_window.Resize();
+ ContextEGL::ResizeSurface(new_surface_width, new_surface_height);
+}
+
+EGLNativeWindowType ContextEGLX11::GetNativeWindow(EGLConfig config)
+{
+ X11InhibitErrors ei;
+
+ EGLint native_visual_id = 0;
+ if (!eglGetConfigAttrib(m_display, m_config, EGL_NATIVE_VISUAL_ID, &native_visual_id))
+ {
+ Log_ErrorPrintf("Failed to get X11 visual ID");
+ return false;
+ }
+
+ XVisualInfo vi_query = {};
+ vi_query.visualid = native_visual_id;
+
+ int num_vis;
+ XVisualInfo* vi = XGetVisualInfo(static_cast<Display*>(m_wi.display_connection), VisualIDMask, &vi_query, &num_vis);
+ if (num_vis <= 0 || !vi)
+ {
+ Log_ErrorPrintf("Failed to query visual from X11");
+ return false;
+ }
+
+ m_window.Destroy();
+ if (!m_window.Create(GetDisplay(), static_cast<Window>(reinterpret_cast<uintptr_t>(m_wi.window_handle)), vi))
+ {
+ Log_ErrorPrintf("Faild to create X11 child window");
+ XFree(vi);
+ return false;
+ }
+
+ XFree(vi);
+ return static_cast<EGLNativeWindowType>(m_window.GetWindow());
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_egl_x11.h b/src/frontend/duckstation/gl/context_egl_x11.h
new file mode 100644
index 0000000..7def8bf
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_egl_x11.h
@@ -0,0 +1,28 @@
+#pragma once
+#include "context_egl.h"
+#include "x11_window.h"
+
+namespace GL {
+
+class ContextEGLX11 final : public ContextEGL
+{
+public:
+ ContextEGLX11(const WindowInfo& wi);
+ ~ContextEGLX11() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+ void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+
+protected:
+ EGLNativeWindowType GetNativeWindow(EGLConfig config) override;
+
+private:
+ ALWAYS_INLINE Display* GetDisplay() const { return static_cast<Display*>(m_wi.display_connection); }
+
+ X11Window m_window;
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_glx.cpp b/src/frontend/duckstation/gl/context_glx.cpp
new file mode 100644
index 0000000..bd32039
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_glx.cpp
@@ -0,0 +1,328 @@
+#include "context_glx.h"
+#include "../duckstation_compat.h"
+#include "../log.h"
+#include <dlfcn.h>
+Log_SetChannel(GL::ContextGLX);
+
+namespace GL {
+ContextGLX::ContextGLX(const WindowInfo& wi) : Context(wi) {}
+
+ContextGLX::~ContextGLX()
+{
+ if (glXGetCurrentContext() == m_context)
+ glXMakeCurrent(GetDisplay(), None, nullptr);
+
+ if (m_context)
+ glXDestroyContext(GetDisplay(), m_context);
+
+ if (m_vi)
+ XFree(m_vi);
+
+ if (m_libGL_handle)
+ dlclose(m_libGL_handle);
+}
+
+std::unique_ptr<Context> ContextGLX::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextGLX> context = std::make_unique<ContextGLX>(wi);
+ if (!context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+bool ContextGLX::Initialize(const Version* versions_to_try, size_t num_versions_to_try)
+{
+ // We need libGL loaded, because GLAD loads its own, then releases it.
+ m_libGL_handle = dlopen("libGL.so.1", RTLD_NOW | RTLD_GLOBAL);
+ if (!m_libGL_handle)
+ {
+ m_libGL_handle = dlopen("libGL.so", RTLD_NOW | RTLD_GLOBAL);
+ if (!m_libGL_handle)
+ {
+ Log_ErrorPrintf("Failed to load libGL.so: %s", dlerror());
+ return false;
+ }
+ }
+
+ const int screen = DefaultScreen(GetDisplay());
+ if (!gladLoadGLX(GetDisplay(), screen))
+ {
+ Log_ErrorPrintf("Loading GLAD GLX functions failed");
+ return false;
+ }
+
+ if (m_wi.type == WindowInfo::Type::X11)
+ {
+ if (!CreateWindow(screen))
+ return false;
+ }
+ else
+ {
+ Panic("Create pbuffer");
+ }
+
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ const Version& cv = versions_to_try[i];
+ if (cv.profile == Profile::NoProfile && CreateAnyContext(nullptr, true))
+ {
+ m_version = cv;
+ return true;
+ }
+ else if (cv.profile != Profile::NoProfile && CreateVersionContext(cv, nullptr, true))
+ {
+ m_version = cv;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void* ContextGLX::GetProcAddress(const char* name)
+{
+ return reinterpret_cast<void*>(glXGetProcAddress(reinterpret_cast<const GLubyte*>(name)));
+}
+
+bool ContextGLX::ChangeSurface(const WindowInfo& new_wi)
+{
+ const bool was_current = (glXGetCurrentContext() == m_context);
+ if (was_current)
+ glXMakeCurrent(GetDisplay(), None, nullptr);
+
+ m_window.Destroy();
+ m_wi = new_wi;
+
+ if (new_wi.type == WindowInfo::Type::X11)
+ {
+ const int screen = DefaultScreen(GetDisplay());
+ if (!CreateWindow(screen))
+ return false;
+ }
+
+ if (was_current && !glXMakeCurrent(GetDisplay(), GetDrawable(), m_context))
+ {
+ Log_ErrorPrintf("Failed to make context current again after surface change");
+ return false;
+ }
+
+ return true;
+}
+
+void ContextGLX::ResizeSurface(u32 new_surface_width /*= 0*/, u32 new_surface_height /*= 0*/)
+{
+ m_window.Resize(new_surface_width, new_surface_height);
+ m_wi.surface_width = m_window.GetWidth();
+ m_wi.surface_height = m_window.GetHeight();
+}
+
+bool ContextGLX::SwapBuffers()
+{
+ glXSwapBuffers(GetDisplay(), GetDrawable());
+ return true;
+}
+
+bool ContextGLX::MakeCurrent()
+{
+ return (glXMakeCurrent(GetDisplay(), GetDrawable(), m_context) == True);
+}
+
+bool ContextGLX::DoneCurrent()
+{
+ return (glXMakeCurrent(GetDisplay(), None, nullptr) == True);
+}
+
+bool ContextGLX::SetSwapInterval(s32 interval)
+{
+ if (GLAD_GLX_EXT_swap_control)
+ {
+ glXSwapIntervalEXT(GetDisplay(), GetDrawable(), interval);
+ return true;
+ }
+ else if (GLAD_GLX_MESA_swap_control)
+ {
+ return (glXSwapIntervalMESA(static_cast<u32>(std::max(interval, 0))) != 0);
+ }
+ else if (GLAD_GLX_SGI_swap_control)
+ {
+ return (glXSwapIntervalSGI(interval) != 0);
+ }
+ else
+ {
+ return false;
+ }
+}
+
+std::unique_ptr<Context> ContextGLX::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextGLX> context = std::make_unique<ContextGLX>(wi);
+ if (wi.type == WindowInfo::Type::X11)
+ {
+ const int screen = DefaultScreen(context->GetDisplay());
+ if (!context->CreateWindow(screen))
+ return nullptr;
+ }
+ else
+ {
+ Panic("Create pbuffer");
+ }
+
+ if (m_version.profile == Profile::NoProfile)
+ {
+ if (!context->CreateAnyContext(m_context, false))
+ return nullptr;
+ }
+ else
+ {
+ if (!context->CreateVersionContext(m_version, m_context, false))
+ return nullptr;
+ }
+
+ context->m_version = m_version;
+ return context;
+}
+
+bool ContextGLX::CreateWindow(int screen)
+{
+ int attribs[32] = {GLX_X_RENDERABLE, True, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
+ GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, GLX_DOUBLEBUFFER, True};
+ int nattribs = 8;
+
+ switch (m_wi.surface_format)
+ {
+ case WindowInfo::SurfaceFormat::RGB8:
+ attribs[nattribs++] = GLX_RED_SIZE;
+ attribs[nattribs++] = 8;
+ attribs[nattribs++] = GLX_GREEN_SIZE;
+ attribs[nattribs++] = 8;
+ attribs[nattribs++] = GLX_BLUE_SIZE;
+ attribs[nattribs++] = 8;
+ break;
+
+ case WindowInfo::SurfaceFormat::RGBA8:
+ attribs[nattribs++] = GLX_RED_SIZE;
+ attribs[nattribs++] = 8;
+ attribs[nattribs++] = GLX_GREEN_SIZE;
+ attribs[nattribs++] = 8;
+ attribs[nattribs++] = GLX_BLUE_SIZE;
+ attribs[nattribs++] = 8;
+ attribs[nattribs++] = GLX_ALPHA_SIZE;
+ attribs[nattribs++] = 8;
+ break;
+
+ case WindowInfo::SurfaceFormat::RGB565:
+ attribs[nattribs++] = GLX_RED_SIZE;
+ attribs[nattribs++] = 5;
+ attribs[nattribs++] = GLX_GREEN_SIZE;
+ attribs[nattribs++] = 6;
+ attribs[nattribs++] = GLX_BLUE_SIZE;
+ attribs[nattribs++] = 5;
+ break;
+
+ case WindowInfo::SurfaceFormat::Auto:
+ break;
+
+ default:
+ UnreachableCode();
+ break;
+ }
+
+ attribs[nattribs++] = None;
+ attribs[nattribs++] = 0;
+
+ int fbcount = 0;
+ GLXFBConfig* fbc = glXChooseFBConfig(GetDisplay(), screen, attribs, &fbcount);
+ if (!fbc || !fbcount)
+ {
+ Log_ErrorPrintf("glXChooseFBConfig() failed");
+ return false;
+ }
+ m_fb_config = *fbc;
+ XFree(fbc);
+
+ if (!GLAD_GLX_VERSION_1_3)
+ {
+ Log_ErrorPrintf("GLX Version 1.3 is required");
+ return false;
+ }
+
+ m_vi = glXGetVisualFromFBConfig(GetDisplay(), m_fb_config);
+ if (!m_vi)
+ {
+ Log_ErrorPrintf("glXGetVisualFromFBConfig() failed");
+ return false;
+ }
+
+ return m_window.Create(GetDisplay(), static_cast<Window>(reinterpret_cast<uintptr_t>(m_wi.window_handle)), m_vi);
+}
+
+bool ContextGLX::CreateAnyContext(GLXContext share_context, bool make_current)
+{
+ X11InhibitErrors ie;
+
+ m_context = glXCreateContext(GetDisplay(), m_vi, share_context, True);
+ if (!m_context || ie.HadError())
+ {
+ Log_ErrorPrintf("glxCreateContext() failed");
+ return false;
+ }
+
+ if (make_current)
+ {
+ if (!glXMakeCurrent(GetDisplay(), GetDrawable(), m_context))
+ {
+ Log_ErrorPrintf("glXMakeCurrent() failed");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ContextGLX::CreateVersionContext(const Version& version, GLXContext share_context, bool make_current)
+{
+ // we need create context attribs
+ if (!GLAD_GLX_VERSION_1_3)
+ {
+ Log_ErrorPrint("Missing GLX version 1.3.");
+ return false;
+ }
+
+ int attribs[32];
+ int nattribs = 0;
+ attribs[nattribs++] = GLX_CONTEXT_PROFILE_MASK_ARB;
+ attribs[nattribs++] =
+ ((version.profile == Profile::ES) ?
+ ((version.major_version >= 2) ? GLX_CONTEXT_ES2_PROFILE_BIT_EXT : GLX_CONTEXT_ES_PROFILE_BIT_EXT) :
+ GLX_CONTEXT_CORE_PROFILE_BIT_ARB);
+ attribs[nattribs++] = GLX_CONTEXT_MAJOR_VERSION_ARB;
+ attribs[nattribs++] = version.major_version;
+ attribs[nattribs++] = GLX_CONTEXT_MINOR_VERSION_ARB;
+ attribs[nattribs++] = version.minor_version;
+ attribs[nattribs++] = None;
+ attribs[nattribs++] = 0;
+
+ X11InhibitErrors ie;
+ m_context = glXCreateContextAttribsARB(GetDisplay(), m_fb_config, share_context, True, attribs);
+ XSync(GetDisplay(), False);
+ if (ie.HadError())
+ m_context = nullptr;
+ if (!m_context)
+ return false;
+
+ if (make_current)
+ {
+ if (!glXMakeCurrent(GetDisplay(), GetDrawable(), m_context))
+ {
+ Log_ErrorPrint("glXMakeCurrent() failed");
+ glXDestroyContext(GetDisplay(), m_context);
+ m_context = nullptr;
+ return false;
+ }
+ }
+
+ return true;
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_glx.h b/src/frontend/duckstation/gl/context_glx.h
new file mode 100644
index 0000000..c8416e5
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_glx.h
@@ -0,0 +1,44 @@
+#pragma once
+#include "context.h"
+#include "../../glad/glad_glx.h"
+#include "x11_window.h"
+
+namespace GL {
+
+class ContextGLX final : public Context
+{
+public:
+ ContextGLX(const WindowInfo& wi);
+ ~ContextGLX() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ void* GetProcAddress(const char* name) override;
+ bool ChangeSurface(const WindowInfo& new_wi) override;
+ void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+ bool SwapBuffers() override;
+ bool MakeCurrent() override;
+ bool DoneCurrent() override;
+ bool SetSwapInterval(s32 interval) override;
+ std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+
+private:
+ ALWAYS_INLINE Display* GetDisplay() const { return static_cast<Display*>(m_wi.display_connection); }
+ ALWAYS_INLINE GLXDrawable GetDrawable() const { return static_cast<GLXDrawable>(m_window.GetWindow()); }
+
+ bool Initialize(const Version* versions_to_try, size_t num_versions_to_try);
+ bool CreateWindow(int screen);
+ bool CreateAnyContext(GLXContext share_context, bool make_current);
+ bool CreateVersionContext(const Version& version, GLXContext share_context, bool make_current);
+
+ GLXContext m_context = nullptr;
+ GLXFBConfig m_fb_config = {};
+ XVisualInfo* m_vi = nullptr;
+ X11Window m_window;
+
+ // GLAD releases its reference to libGL.so, so we need to maintain our own.
+ void* m_libGL_handle = nullptr;
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_wgl.cpp b/src/frontend/duckstation/gl/context_wgl.cpp
new file mode 100644
index 0000000..03c18e8
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_wgl.cpp
@@ -0,0 +1,452 @@
+#include "context_wgl.h"
+#include "../duckstation_compat.h"
+#include "../log.h"
+#include "../scoped_guard.h"
+#include "loader.h"
+Log_SetChannel(GL::ContextWGL);
+
+// TODO: get rid of this
+#pragma comment(lib, "opengl32.lib")
+
+static void* GetProcAddressCallback(const char* name)
+{
+ void* addr = reinterpret_cast<void*>(wglGetProcAddress(name));
+ if (addr)
+ return addr;
+
+ // try opengl32.dll
+ return reinterpret_cast<void*>(::GetProcAddress(GetModuleHandleA("opengl32.dll"), name));
+}
+
+namespace GL {
+ContextWGL::ContextWGL(const WindowInfo& wi) : Context(wi) {}
+
+ContextWGL::~ContextWGL()
+{
+ if (wglGetCurrentContext() == m_rc)
+ wglMakeCurrent(m_dc, nullptr);
+
+ if (m_rc)
+ wglDeleteContext(m_rc);
+
+ ReleaseDC();
+}
+
+std::unique_ptr<Context> ContextWGL::Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try)
+{
+ std::unique_ptr<ContextWGL> context = std::make_unique<ContextWGL>(wi);
+ if (!context->Initialize(versions_to_try, num_versions_to_try))
+ return nullptr;
+
+ return context;
+}
+
+bool ContextWGL::Initialize(const Version* versions_to_try, size_t num_versions_to_try)
+{
+ if (m_wi.type == WindowInfo::Type::Win32)
+ {
+ if (!InitializeDC())
+ return false;
+ }
+ else
+ {
+ Log_ErrorPrint("ContextWGL must always start with a valid surface.");
+ return false;
+ }
+
+ // Everything including core/ES requires a dummy profile to load the WGL extensions.
+ if (!CreateAnyContext(nullptr, true))
+ return false;
+
+ for (size_t i = 0; i < num_versions_to_try; i++)
+ {
+ const Version& cv = versions_to_try[i];
+ if (cv.profile == Profile::NoProfile)
+ {
+ // we already have the dummy context, so just use that
+ m_version = cv;
+ return true;
+ }
+ else if (CreateVersionContext(cv, nullptr, true))
+ {
+ m_version = cv;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void* ContextWGL::GetProcAddress(const char* name)
+{
+ return GetProcAddressCallback(name);
+}
+
+bool ContextWGL::ChangeSurface(const WindowInfo& new_wi)
+{
+ const bool was_current = (wglGetCurrentContext() == m_rc);
+
+ ReleaseDC();
+
+ m_wi = new_wi;
+ if (!InitializeDC())
+ return false;
+
+ if (was_current && !wglMakeCurrent(m_dc, m_rc))
+ {
+ Log_ErrorPrintf("Failed to make context current again after surface change: 0x%08X", GetLastError());
+ return false;
+ }
+
+ return true;
+}
+
+void ContextWGL::ResizeSurface(u32 new_surface_width /*= 0*/, u32 new_surface_height /*= 0*/)
+{
+ RECT client_rc = {};
+ GetClientRect(GetHWND(), &client_rc);
+ m_wi.surface_width = static_cast<u32>(client_rc.right - client_rc.left);
+ m_wi.surface_height = static_cast<u32>(client_rc.bottom - client_rc.top);
+}
+
+bool ContextWGL::SwapBuffers()
+{
+ return ::SwapBuffers(m_dc);
+}
+
+bool ContextWGL::MakeCurrent()
+{
+ if (!wglMakeCurrent(m_dc, m_rc))
+ {
+ Log_ErrorPrintf("wglMakeCurrent() failed: 0x%08X", GetLastError());
+ return false;
+ }
+
+ return true;
+}
+
+bool ContextWGL::DoneCurrent()
+{
+ return wglMakeCurrent(m_dc, nullptr);
+}
+
+bool ContextWGL::SetSwapInterval(s32 interval)
+{
+ if (!GLAD_WGL_EXT_swap_control)
+ return false;
+
+ return wglSwapIntervalEXT(interval);
+}
+
+std::unique_ptr<Context> ContextWGL::CreateSharedContext(const WindowInfo& wi)
+{
+ std::unique_ptr<ContextWGL> context = std::make_unique<ContextWGL>(wi);
+ if (wi.type == WindowInfo::Type::Win32)
+ {
+ if (!context->InitializeDC())
+ return nullptr;
+ }
+ else
+ {
+ Log_ErrorPrint("PBuffer not implemented");
+ return nullptr;
+ }
+
+ if (m_version.profile == Profile::NoProfile)
+ {
+ if (!context->CreateAnyContext(m_rc, false))
+ return nullptr;
+ }
+ else
+ {
+ if (!context->CreateVersionContext(m_version, m_rc, false))
+ return nullptr;
+ }
+
+ context->m_version = m_version;
+ return context;
+}
+
+HDC ContextWGL::GetDCAndSetPixelFormat(HWND hwnd)
+{
+ PIXELFORMATDESCRIPTOR pfd = {};
+ pfd.nSize = sizeof(pfd);
+ pfd.nVersion = 1;
+ pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
+ pfd.iPixelType = PFD_TYPE_RGBA;
+ pfd.dwLayerMask = PFD_MAIN_PLANE;
+ pfd.cRedBits = 8;
+ pfd.cGreenBits = 8;
+ pfd.cBlueBits = 8;
+ pfd.cColorBits = 24;
+
+ HDC hDC = ::GetDC(hwnd);
+ if (!hDC)
+ {
+ Log_ErrorPrintf("GetDC() failed: 0x%08X", GetLastError());
+ return {};
+ }
+
+ if (!m_pixel_format.has_value())
+ {
+ const int pf = ChoosePixelFormat(hDC, &pfd);
+ if (pf == 0)
+ {
+ Log_ErrorPrintf("ChoosePixelFormat() failed: 0x%08X", GetLastError());
+ ::ReleaseDC(hwnd, hDC);
+ return {};
+ }
+
+ m_pixel_format = pf;
+ }
+
+ if (!SetPixelFormat(hDC, m_pixel_format.value(), &pfd))
+ {
+ Log_ErrorPrintf("SetPixelFormat() failed: 0x%08X", GetLastError());
+ ::ReleaseDC(hwnd, hDC);
+ return {};
+ }
+
+ return hDC;
+}
+
+bool ContextWGL::InitializeDC()
+{
+ if (m_wi.type == WindowInfo::Type::Win32)
+ {
+ m_dc = GetDCAndSetPixelFormat(GetHWND());
+ if (!m_dc)
+ {
+ Log_ErrorPrint("Failed to get DC for window");
+ return false;
+ }
+
+ return true;
+ }
+ else if (m_wi.type == WindowInfo::Type::Surfaceless)
+ {
+ return CreatePBuffer();
+ }
+ else
+ {
+ Log_ErrorPrintf("Unknown window info type %u", static_cast<unsigned>(m_wi.type));
+ return false;
+ }
+}
+
+void ContextWGL::ReleaseDC()
+{
+ if (m_pbuffer)
+ {
+ wglReleasePbufferDCARB(m_pbuffer, m_dc);
+ m_dc = {};
+
+ wglDestroyPbufferARB(m_pbuffer);
+ m_pbuffer = {};
+
+ ::ReleaseDC(m_dummy_window, m_dummy_dc);
+ m_dummy_dc = {};
+
+ DestroyWindow(m_dummy_window);
+ m_dummy_window = {};
+ }
+ else if (m_dc)
+ {
+ ::ReleaseDC(GetHWND(), m_dc);
+ m_dc = {};
+ }
+}
+
+bool ContextWGL::CreatePBuffer()
+{
+ static bool window_class_registered = false;
+ static const wchar_t* window_class_name = L"ContextWGLPBuffer";
+
+ if (!window_class_registered)
+ {
+ WNDCLASSEXW wc = {};
+ wc.cbSize = sizeof(WNDCLASSEXW);
+ wc.style = 0;
+ wc.lpfnWndProc = DefWindowProcW;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.hIcon = NULL;
+ wc.hCursor = LoadCursor(NULL, IDC_ARROW);
+ wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
+ wc.lpszMenuName = NULL;
+ wc.lpszClassName = window_class_name;
+ wc.hIconSm = NULL;
+
+ if (!RegisterClassExW(&wc))
+ {
+ Log_ErrorPrint("(ContextWGL::CreatePBuffer) RegisterClassExW() failed");
+ return false;
+ }
+
+ window_class_registered = true;
+ }
+
+ HWND hwnd = CreateWindowExW(0, window_class_name, window_class_name, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL);
+ if (!hwnd)
+ {
+ Log_ErrorPrint("(ContextWGL::CreatePBuffer) CreateWindowEx() failed");
+ return false;
+ }
+
+ ScopedGuard hwnd_guard([hwnd]() { DestroyWindow(hwnd); });
+
+ HDC hdc = GetDCAndSetPixelFormat(hwnd);
+ if (!hdc)
+ return false;
+
+ ScopedGuard hdc_guard([hdc, hwnd]() { ::ReleaseDC(hwnd, hdc); });
+
+ static constexpr const int pb_attribs[] = {0, 0};
+
+ AssertMsg(m_pixel_format.has_value(), "Has pixel format for pbuffer");
+ HPBUFFERARB pbuffer = wglCreatePbufferARB(hdc, m_pixel_format.value(), 1, 1, pb_attribs);
+ if (!pbuffer)
+ {
+ Log_ErrorPrint("(ContextWGL::CreatePBuffer) wglCreatePbufferARB() failed");
+ return false;
+ }
+
+ ScopedGuard pbuffer_guard([pbuffer]() { wglDestroyPbufferARB(pbuffer); });
+
+ m_dc = wglGetPbufferDCARB(pbuffer);
+ if (!m_dc)
+ {
+ Log_ErrorPrint("(ContextWGL::CreatePbuffer) wglGetPbufferDCARB() failed");
+ return false;
+ }
+
+ m_dummy_window = hwnd;
+ m_dummy_dc = hdc;
+ m_pbuffer = pbuffer;
+
+ pbuffer_guard.Cancel();
+ hdc_guard.Cancel();
+ hwnd_guard.Cancel();
+ return true;
+}
+
+bool ContextWGL::CreateAnyContext(HGLRC share_context, bool make_current)
+{
+ m_rc = wglCreateContext(m_dc);
+ if (!m_rc)
+ {
+ Log_ErrorPrintf("wglCreateContext() failed: 0x%08X", GetLastError());
+ return false;
+ }
+
+ if (make_current)
+ {
+ if (!wglMakeCurrent(m_dc, m_rc))
+ {
+ Log_ErrorPrintf("wglMakeCurrent() failed: 0x%08X", GetLastError());
+ return false;
+ }
+
+ // re-init glad-wgl
+ if (!gladLoadWGLLoader([](const char* name) -> void* { return reinterpret_cast<void*>(wglGetProcAddress(name)); }, m_dc))
+ {
+ Log_ErrorPrint("Loading GLAD WGL functions failed");
+ return false;
+ }
+ }
+
+ if (share_context && !wglShareLists(share_context, m_rc))
+ {
+ Log_ErrorPrintf("wglShareLists() failed: 0x%08X", GetLastError());
+ return false;
+ }
+
+ return true;
+}
+
+bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_context, bool make_current)
+{
+ // we need create context attribs
+ if (!GLAD_WGL_ARB_create_context)
+ {
+ Log_ErrorPrint("Missing GLAD_WGL_ARB_create_context.");
+ return false;
+ }
+
+ HGLRC new_rc;
+ if (version.profile == Profile::Core)
+ {
+ const int attribs[] = {WGL_CONTEXT_PROFILE_MASK_ARB,
+ WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+ WGL_CONTEXT_MAJOR_VERSION_ARB,
+ version.major_version,
+ WGL_CONTEXT_MINOR_VERSION_ARB,
+ version.minor_version,
+#ifdef _DEBUG
+ WGL_CONTEXT_FLAGS_ARB,
+ WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB | WGL_CONTEXT_DEBUG_BIT_ARB,
+#else
+ WGL_CONTEXT_FLAGS_ARB,
+ WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
+#endif
+ 0,
+ 0};
+
+ new_rc = wglCreateContextAttribsARB(m_dc, share_context, attribs);
+ }
+ else if (version.profile == Profile::ES)
+ {
+ if ((version.major_version >= 2 && !GLAD_WGL_EXT_create_context_es2_profile) ||
+ (version.major_version < 2 && !GLAD_WGL_EXT_create_context_es_profile))
+ {
+ Log_ErrorPrint("WGL_EXT_create_context_es_profile not supported");
+ return false;
+ }
+
+ const int attribs[] = {
+ WGL_CONTEXT_PROFILE_MASK_ARB,
+ ((version.major_version >= 2) ? WGL_CONTEXT_ES2_PROFILE_BIT_EXT : WGL_CONTEXT_ES_PROFILE_BIT_EXT),
+ WGL_CONTEXT_MAJOR_VERSION_ARB,
+ version.major_version,
+ WGL_CONTEXT_MINOR_VERSION_ARB,
+ version.minor_version,
+ 0,
+ 0};
+
+ new_rc = wglCreateContextAttribsARB(m_dc, share_context, attribs);
+ }
+ else
+ {
+ Log_ErrorPrint("Unknown profile");
+ return false;
+ }
+
+ if (!new_rc)
+ return false;
+
+ // destroy and swap contexts
+ if (m_rc)
+ {
+ if (!wglMakeCurrent(m_dc, make_current ? new_rc : nullptr))
+ {
+ Log_ErrorPrintf("wglMakeCurrent() failed: 0x%08X", GetLastError());
+ wglDeleteContext(new_rc);
+ return false;
+ }
+
+ // re-init glad-wgl
+ if (make_current && !gladLoadWGLLoader([](const char* name) -> void* { return reinterpret_cast<void*>(wglGetProcAddress(name)); }, m_dc))
+ {
+ Log_ErrorPrint("Loading GLAD WGL functions failed");
+ return false;
+ }
+
+ wglDeleteContext(m_rc);
+ }
+
+ m_rc = new_rc;
+ return true;
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/context_wgl.h b/src/frontend/duckstation/gl/context_wgl.h
new file mode 100644
index 0000000..d742333
--- /dev/null
+++ b/src/frontend/duckstation/gl/context_wgl.h
@@ -0,0 +1,53 @@
+#pragma once
+#include "../windows_headers.h"
+
+#include "context.h"
+#include "../../glad/glad_wgl.h"
+#include "loader.h"
+#include <optional>
+
+namespace GL {
+
+class ContextWGL final : public Context
+{
+public:
+ ContextWGL(const WindowInfo& wi);
+ ~ContextWGL() override;
+
+ static std::unique_ptr<Context> Create(const WindowInfo& wi, const Version* versions_to_try,
+ size_t num_versions_to_try);
+
+ void* GetProcAddress(const char* name) override;
+ bool ChangeSurface(const WindowInfo& new_wi) override;
+ void ResizeSurface(u32 new_surface_width = 0, u32 new_surface_height = 0) override;
+ bool SwapBuffers() override;
+ bool MakeCurrent() override;
+ bool DoneCurrent() override;
+ bool SetSwapInterval(s32 interval) override;
+ std::unique_ptr<Context> CreateSharedContext(const WindowInfo& wi) override;
+
+private:
+ ALWAYS_INLINE HWND GetHWND() const { return static_cast<HWND>(m_wi.window_handle); }
+
+ HDC GetDCAndSetPixelFormat(HWND hwnd);
+
+ bool Initialize(const Version* versions_to_try, size_t num_versions_to_try);
+ bool InitializeDC();
+ void ReleaseDC();
+ bool CreatePBuffer();
+ bool CreateAnyContext(HGLRC share_context, bool make_current);
+ bool CreateVersionContext(const Version& version, HGLRC share_context, bool make_current);
+
+ HDC m_dc = {};
+ HGLRC m_rc = {};
+
+ // Can't change pixel format once it's set for a RC.
+ std::optional<int> m_pixel_format;
+
+ // Dummy window for creating a PBuffer off when we're surfaceless.
+ HWND m_dummy_window = {};
+ HDC m_dummy_dc = {};
+ HPBUFFERARB m_pbuffer = {};
+};
+
+} // namespace GL \ No newline at end of file
diff --git a/src/frontend/duckstation/gl/loader.h b/src/frontend/duckstation/gl/loader.h
new file mode 100644
index 0000000..0c2ba12
--- /dev/null
+++ b/src/frontend/duckstation/gl/loader.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// Fix glad.h including windows.h
+#ifdef _WIN32
+#include "../windows_headers.h"
+#endif
+
+#include "../../glad/glad.h"
diff --git a/src/frontend/duckstation/gl/x11_window.cpp b/src/frontend/duckstation/gl/x11_window.cpp
new file mode 100644
index 0000000..bf09fcc
--- /dev/null
+++ b/src/frontend/duckstation/gl/x11_window.cpp
@@ -0,0 +1,101 @@
+#include "x11_window.h"
+#include "../log.h"
+#include "../duckstation_compat.h"
+#include <cstdio>
+Log_SetChannel(X11Window);
+
+namespace GL {
+X11Window::X11Window() = default;
+
+X11Window::~X11Window()
+{
+ Destroy();
+}
+
+bool X11Window::Create(Display* display, Window parent_window, const XVisualInfo* vi)
+{
+ m_display = display;
+ m_parent_window = parent_window;
+ XSync(m_display, True);
+
+ XWindowAttributes parent_wa = {};
+ XGetWindowAttributes(m_display, m_parent_window, &parent_wa);
+ m_width = static_cast<u32>(parent_wa.width);
+ m_height = static_cast<u32>(parent_wa.height);
+
+ // Failed X calls terminate the process so no need to check for errors.
+ // We could swap the error handler out here as well.
+ m_colormap = XCreateColormap(m_display, m_parent_window, vi->visual, AllocNone);
+
+ XSetWindowAttributes wa = {};
+ wa.colormap = m_colormap;
+
+ m_window = XCreateWindow(m_display, m_parent_window, 0, 0, m_width, m_height, 0, vi->depth, InputOutput, vi->visual,
+ CWColormap, &wa);
+ XMapWindow(m_display, m_window);
+ XSync(m_display, True);
+
+ return true;
+}
+
+void X11Window::Destroy()
+{
+ if (m_window)
+ {
+ XUnmapWindow(m_display, m_window);
+ XDestroyWindow(m_display, m_window);
+ m_window = {};
+ }
+
+ if (m_colormap)
+ {
+ XFreeColormap(m_display, m_colormap);
+ m_colormap = {};
+ }
+}
+
+void X11Window::Resize(u32 width, u32 height)
+{
+ if (width != 0 && height != 0)
+ {
+ m_width = width;
+ m_height = height;
+ }
+ else
+ {
+ XWindowAttributes parent_wa = {};
+ XGetWindowAttributes(m_display, m_parent_window, &parent_wa);
+ m_width = static_cast<u32>(parent_wa.width);
+ m_height = static_cast<u32>(parent_wa.height);
+ }
+
+ XResizeWindow(m_display, m_window, m_width, m_height);
+}
+
+static X11InhibitErrors* s_current_error_inhibiter;
+
+X11InhibitErrors::X11InhibitErrors()
+{
+ Assert(!s_current_error_inhibiter);
+ m_old_handler = XSetErrorHandler(ErrorHandler);
+ s_current_error_inhibiter = this;
+}
+
+X11InhibitErrors::~X11InhibitErrors()
+{
+ Assert(s_current_error_inhibiter == this);
+ s_current_error_inhibiter = nullptr;
+ XSetErrorHandler(m_old_handler);
+}
+
+int X11InhibitErrors::ErrorHandler(Display* display, XErrorEvent* ee)
+{
+ char error_string[256] = {};
+ XGetErrorText(display, ee->error_code, error_string, sizeof(error_string));
+ Log_WarningPrintf("X11 Error: %s (Error %u Minor %u Request %u)", error_string, ee->error_code, ee->minor_code,
+ ee->request_code);
+
+ s_current_error_inhibiter->m_had_error = true;
+ return 0;
+}
+} // namespace GL
diff --git a/src/frontend/duckstation/gl/x11_window.h b/src/frontend/duckstation/gl/x11_window.h
new file mode 100644
index 0000000..aff38b7
--- /dev/null
+++ b/src/frontend/duckstation/gl/x11_window.h
@@ -0,0 +1,48 @@
+#pragma once
+#include "../duckstation_compat.h"
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+namespace GL {
+class X11Window
+{
+public:
+ X11Window();
+ ~X11Window();
+
+ ALWAYS_INLINE Window GetWindow() const { return m_window; }
+ ALWAYS_INLINE u32 GetWidth() const { return m_width; }
+ ALWAYS_INLINE u32 GetHeight() const { return m_height; }
+
+ bool Create(Display* display, Window parent_window, const XVisualInfo* vi);
+ void Destroy();
+
+ // Setting a width/height of 0 will use parent dimensions.
+ void Resize(u32 width = 0, u32 height = 0);
+
+private:
+ Display* m_display = nullptr;
+ Window m_parent_window = {};
+ Window m_window = {};
+ Colormap m_colormap = {};
+ u32 m_width = 0;
+ u32 m_height = 0;
+};
+
+// Helper class for managing X errors
+class X11InhibitErrors
+{
+public:
+ X11InhibitErrors();
+ ~X11InhibitErrors();
+
+ ALWAYS_INLINE bool HadError() const { return m_had_error; }
+
+private:
+ static int ErrorHandler(Display* display, XErrorEvent* ee);
+
+ XErrorHandler m_old_handler = {};
+ bool m_had_error = false;
+};
+
+} // namespace GL
diff --git a/src/frontend/duckstation/log.h b/src/frontend/duckstation/log.h
new file mode 100644
index 0000000..a945bb3
--- /dev/null
+++ b/src/frontend/duckstation/log.h
@@ -0,0 +1,46 @@
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdio.h>
+
+#define Log_SetChannel(ChannelName)
+#define Log_ErrorPrint(msg) puts(msg "\n");
+#define Log_ErrorPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_WarningPrint(msg) puts(msg)
+#define Log_WarningPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_PerfPrint(msg) puts(msg)
+#define Log_PerfPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_InfoPrint(msg) puts(msg)
+#define Log_InfoPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_VerbosePrint(msg) puts(msg)
+#define Log_VerbosePrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_DevPrint(msg) puts(msg)
+#define Log_DevPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_ProfilePrint(msg) puts(msg)
+#define Log_ProfilePrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+
+#ifdef _DEBUG
+#define Log_DebugPrint(msg) puts(msg)
+#define Log_DebugPrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#define Log_TracePrint(msg) puts(msg)
+#define Log_TracePrintf(...) do { printf(__VA_ARGS__); putchar('\n'); } while (0)
+#else
+#define Log_DebugPrint(msg) \
+ do \
+ { \
+ } while (0)
+#define Log_DebugPrintf(...) \
+ do \
+ { \
+ } while (0)
+#define Log_TracePrint(msg) \
+ do \
+ { \
+ } while (0)
+#define Log_TracePrintf(...) \
+ do \
+ { \
+ } while (0)
+#endif
+
+#endif \ No newline at end of file
diff --git a/src/frontend/duckstation/scoped_guard.h b/src/frontend/duckstation/scoped_guard.h
new file mode 100644
index 0000000..89f35d9
--- /dev/null
+++ b/src/frontend/duckstation/scoped_guard.h
@@ -0,0 +1,34 @@
+#pragma once
+#include <optional>
+#include <utility>
+
+/// ScopedGuard provides an object which runs a function (usually a lambda) when
+/// it goes out of scope. This can be useful for releasing resources or handles
+/// which do not normally have C++ types to automatically release.
+template<typename T>
+class ScopedGuard final
+{
+public:
+ ALWAYS_INLINE ScopedGuard(T&& func) : m_func(std::forward<T>(func)) {}
+ ALWAYS_INLINE ScopedGuard(ScopedGuard&& other) : m_func(std::move(other.m_func)) { other.m_func = nullptr; }
+ ALWAYS_INLINE ~ScopedGuard() { Invoke(); }
+
+ ScopedGuard(const ScopedGuard&) = delete;
+ void operator=(const ScopedGuard&) = delete;
+
+ /// Prevents the function from being invoked when we go out of scope.
+ ALWAYS_INLINE void Cancel() { m_func.reset(); }
+
+ /// Explicitly fires the function.
+ ALWAYS_INLINE void Invoke()
+ {
+ if (!m_func.has_value())
+ return;
+
+ m_func.value()();
+ m_func.reset();
+ }
+
+private:
+ std::optional<T> m_func;
+};
diff --git a/src/frontend/duckstation/window_info.cpp b/src/frontend/duckstation/window_info.cpp
new file mode 100644
index 0000000..5176dad
--- /dev/null
+++ b/src/frontend/duckstation/window_info.cpp
@@ -0,0 +1,191 @@
+#include "window_info.h"
+#include "common/log.h"
+Log_SetChannel(WindowInfo);
+
+#if defined(_WIN32)
+
+#include "common/windows_headers.h"
+#include <dwmapi.h>
+
+static bool GetRefreshRateFromDWM(HWND hwnd, float* refresh_rate)
+{
+ static HMODULE dwm_module = nullptr;
+ static HRESULT(STDAPICALLTYPE * is_composition_enabled)(BOOL * pfEnabled) = nullptr;
+ static HRESULT(STDAPICALLTYPE * get_timing_info)(HWND hwnd, DWM_TIMING_INFO * pTimingInfo) = nullptr;
+ static bool load_tried = false;
+ if (!load_tried)
+ {
+ load_tried = true;
+ dwm_module = LoadLibrary("dwmapi.dll");
+ if (dwm_module)
+ {
+ std::atexit([]() {
+ FreeLibrary(dwm_module);
+ dwm_module = nullptr;
+ });
+ is_composition_enabled =
+ reinterpret_cast<decltype(is_composition_enabled)>(GetProcAddress(dwm_module, "DwmIsCompositionEnabled"));
+ get_timing_info =
+ reinterpret_cast<decltype(get_timing_info)>(GetProcAddress(dwm_module, "DwmGetCompositionTimingInfo"));
+ }
+ }
+
+ BOOL composition_enabled;
+ if (!is_composition_enabled || FAILED(is_composition_enabled(&composition_enabled) || !get_timing_info))
+ return false;
+
+ DWM_TIMING_INFO ti = {};
+ ti.cbSize = sizeof(ti);
+ HRESULT hr = get_timing_info(nullptr, &ti);
+ if (SUCCEEDED(hr))
+ {
+ if (ti.rateRefresh.uiNumerator == 0 || ti.rateRefresh.uiDenominator == 0)
+ return false;
+
+ *refresh_rate = static_cast<float>(ti.rateRefresh.uiNumerator) / static_cast<float>(ti.rateRefresh.uiDenominator);
+ return true;
+ }
+
+ return false;
+}
+
+static bool GetRefreshRateFromMonitor(HWND hwnd, float* refresh_rate)
+{
+ HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
+ if (!mon)
+ return false;
+
+ MONITORINFOEXW mi = {};
+ mi.cbSize = sizeof(mi);
+ if (GetMonitorInfoW(mon, &mi))
+ {
+ DEVMODEW dm = {};
+ dm.dmSize = sizeof(dm);
+
+ // 0/1 are reserved for "defaults".
+ if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1)
+ {
+ *refresh_rate = static_cast<float>(dm.dmDisplayFrequency);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
+{
+ if (wi.type != Type::Win32 || !wi.window_handle)
+ return false;
+
+ // Try DWM first, then fall back to integer values.
+ const HWND hwnd = static_cast<HWND>(wi.window_handle);
+ return GetRefreshRateFromDWM(hwnd, refresh_rate) || GetRefreshRateFromMonitor(hwnd, refresh_rate);
+}
+
+#else
+
+#ifdef USE_X11
+
+#include "common/scoped_guard.h"
+#include "gl/x11_window.h"
+#include <X11/extensions/Xrandr.h>
+
+static bool GetRefreshRateFromXRandR(const WindowInfo& wi, float* refresh_rate)
+{
+ Display* display = static_cast<Display*>(wi.display_connection);
+ Window window = static_cast<Window>(reinterpret_cast<uintptr_t>(wi.window_handle));
+ if (!display || !window)
+ return false;
+
+ GL::X11InhibitErrors inhibiter;
+
+ XRRScreenResources* res = XRRGetScreenResources(display, window);
+ if (!res)
+ {
+ Log_ErrorPrint("XRRGetScreenResources() failed");
+ return false;
+ }
+
+ ScopedGuard res_guard([res]() { XRRFreeScreenResources(res); });
+
+ int num_monitors;
+ XRRMonitorInfo* mi = XRRGetMonitors(display, window, True, &num_monitors);
+ if (num_monitors < 0)
+ {
+ Log_ErrorPrint("XRRGetMonitors() failed");
+ return false;
+ }
+ else if (num_monitors > 1)
+ {
+ Log_WarningPrintf("XRRGetMonitors() returned %d monitors, using first", num_monitors);
+ }
+
+ ScopedGuard mi_guard([mi]() { XRRFreeMonitors(mi); });
+ if (mi->noutput <= 0)
+ {
+ Log_ErrorPrint("Monitor has no outputs");
+ return false;
+ }
+ else if (mi->noutput > 1)
+ {
+ Log_WarningPrintf("Monitor has %d outputs, using first", mi->noutput);
+ }
+
+ XRROutputInfo* oi = XRRGetOutputInfo(display, res, mi->outputs[0]);
+ if (!oi)
+ {
+ Log_ErrorPrint("XRRGetOutputInfo() failed");
+ return false;
+ }
+
+ ScopedGuard oi_guard([oi]() { XRRFreeOutputInfo(oi); });
+
+ XRRCrtcInfo* ci = XRRGetCrtcInfo(display, res, oi->crtc);
+ if (!ci)
+ {
+ Log_ErrorPrint("XRRGetCrtcInfo() failed");
+ return false;
+ }
+
+ ScopedGuard ci_guard([ci]() { XRRFreeCrtcInfo(ci); });
+
+ XRRModeInfo* mode = nullptr;
+ for (int i = 0; i < res->nmode; i++)
+ {
+ if (res->modes[i].id == ci->mode)
+ {
+ mode = &res->modes[i];
+ break;
+ }
+ }
+ if (!mode)
+ {
+ Log_ErrorPrintf("Failed to look up mode %d (of %d)", static_cast<int>(ci->mode), res->nmode);
+ return false;
+ }
+
+ if (mode->dotClock == 0 || mode->hTotal == 0 || mode->vTotal == 0)
+ {
+ Log_ErrorPrintf("Modeline is invalid: %ld/%d/%d", mode->dotClock, mode->hTotal, mode->vTotal);
+ return false;
+ }
+
+ *refresh_rate =
+ static_cast<double>(mode->dotClock) / (static_cast<double>(mode->hTotal) * static_cast<double>(mode->vTotal));
+ return true;
+}
+
+#endif // USE_X11
+
+bool WindowInfo::QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate)
+{
+#if defined(USE_X11)
+ if (wi.type == WindowInfo::Type::X11)
+ return GetRefreshRateFromXRandR(wi, refresh_rate);
+#endif
+
+ return false;
+}
+
+#endif \ No newline at end of file
diff --git a/src/frontend/duckstation/window_info.h b/src/frontend/duckstation/window_info.h
new file mode 100644
index 0000000..44912ca
--- /dev/null
+++ b/src/frontend/duckstation/window_info.h
@@ -0,0 +1,43 @@
+#pragma once
+#include "../types.h"
+
+// Contains the information required to create a graphics context in a window.
+struct WindowInfo
+{
+ enum class Type
+ {
+ Surfaceless,
+ Win32,
+ X11,
+ Wayland,
+ MacOS,
+ Android,
+ Display,
+ };
+
+ enum class SurfaceFormat
+ {
+ None,
+ Auto,
+ RGB8,
+ RGBA8,
+ RGB565,
+ Count
+ };
+
+ Type type = Type::Surfaceless;
+ void* display_connection = nullptr;
+ void* window_handle = nullptr;
+ u32 surface_width = 0;
+ u32 surface_height = 0;
+ float surface_refresh_rate = 0.0f;
+ float surface_scale = 1.0f;
+ SurfaceFormat surface_format = SurfaceFormat::RGB8;
+
+ // Needed for macOS.
+#ifdef __APPLE__
+ void* surface_handle = nullptr;
+#endif
+
+ static bool QueryRefreshRateForWindow(const WindowInfo& wi, float* refresh_rate);
+};
diff --git a/src/frontend/duckstation/windows_headers.h b/src/frontend/duckstation/windows_headers.h
new file mode 100644
index 0000000..6ff6ed3
--- /dev/null
+++ b/src/frontend/duckstation/windows_headers.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#ifndef NOMINMAX
+#define NOMINMAX 1
+#endif
+
+// require vista+
+#ifdef _WIN32_WINNT
+#undef _WIN32_WINNT
+#endif
+#define _WIN32_WINNT _WIN32_WINNT_VISTA
+
+#include <windows.h>
+
+#if defined(CreateDirectory)
+#undef CreateDirectory
+#endif
+#if defined(CopyFile)
+#undef CopyFile
+#endif
+#if defined(DeleteFile)
+#undef DeleteFile
+#endif