aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/duckstation
diff options
context:
space:
mode:
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