libretro: OpenGL contexts for a core's own threads

RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE (104, experimental)
gives an OpenGL or OpenGL ES hardware core contexts in its own
context's share group, for threads of its own: create and destroy on
the core's thread, make_current on the thread that uses one. A core
can then render or compile shaders off its main thread with nothing of
the window system in the core. A frontend that cannot make such
contexts answers false, and the core keeps its GL work on the thread
that calls retro_run.

RetroArch answers it with the context drivers' shared contexts, which
GLX, Wayland and X11 EGL already had for OpenXR; WGL, KMS/DRM and
Android get them here. The WGL one shares with the context current on
the calling thread (the core's), made with the window context's
attributes, and binds on the window's device context.

samples/gfx/gl_shared_context is a core whose second thread clears a
texture through such a context that retro_run then shows; the EGL
hardware core workflow runs it on GLX and EGL, threaded and not, with
a compatibility and a 3.3 core context.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-12 04:32:16 +01:00
1 parent fd7e99a94c
commit ae907bff65
11 files changed
+789 -8

No files matched your search

+19
View File
@@ -7,6 +7,10 @@ name: CI Linux EGL hardware core
# bound to the window surface the video thread holds, and with it off.
# Mesa takes the surfaceless path by itself; a preloaded shim hides
# parts of its EGL so the pbuffer paths run too.
#
# Then a core that draws on a thread of its own through
# RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE, on GLX and on EGL,
# threaded and not: the colour on screen must be that thread's drawing.
on:
push:
@@ -21,6 +25,10 @@ on:
- 'gfx/video_thread_hw.c'
- 'gfx/video_thread_wrapper.c'
- 'samples/gfx/egl_hw_core/**'
- 'samples/gfx/gl_shared_context/**'
- 'gfx/drivers_context/x_ctx.c'
- 'runloop.c'
- 'libretro-common/include/libretro.h'
- '.github/workflows/Linux-egl-hw-core.yml'
pull_request:
paths:
@@ -32,6 +40,10 @@ on:
- 'gfx/video_thread_hw.c'
- 'gfx/video_thread_wrapper.c'
- 'samples/gfx/egl_hw_core/**'
- 'samples/gfx/gl_shared_context/**'
- 'gfx/drivers_context/x_ctx.c'
- 'runloop.c'
- 'libretro-common/include/libretro.h'
- '.github/workflows/Linux-egl-hw-core.yml'
workflow_dispatch:
@@ -125,6 +137,13 @@ jobs:
make -C samples/gfx/egl_hw_core
timeout 600 python3 samples/gfx/egl_hw_core/egl_hw_core_test.py ./retroarch
- name: Run gl_shared_context_test
shell: bash
run: |
set -eu
make -C samples/gfx/gl_shared_context
timeout 300 python3 samples/gfx/gl_shared_context/gl_shared_context_test.py ./retroarch
- name: Save ccache
# Only master's pushes keep a cache: pull requests restore
# master's, so one push does not evict the next one's.
+19
View File
@@ -432,6 +432,19 @@ static bool android_gfx_ctx_destroy_surface(void *data)
#endif
}
#ifdef HAVE_EGL
/* A context in the share group, of the window's client API: OpenGL ES */
static bool android_gfx_ctx_shared_context_new(void *data,
gfx_ctx_shared_context_t *out)
{
android_ctx_data_t *and = (android_ctx_data_t*)data;
memset(out, 0, sizeof(*out));
if (!and || android_api != GFX_CTX_OPENGL_ES_API)
return false;
return egl_shared_context_new(&and->egl, EGL_OPENGL_ES_API, out);
}
#endif
const gfx_ctx_driver_t gfx_ctx_android = {
android_gfx_ctx_init,
android_gfx_ctx_destroy,
@@ -473,9 +486,15 @@ const gfx_ctx_driver_t gfx_ctx_android = {
NULL, /* presentable */
NULL, /* last_present_time */
android_gfx_ctx_release_current,
#ifdef HAVE_EGL
android_gfx_ctx_shared_context_new,
egl_shared_context_bind,
egl_shared_context_free,
#else
NULL, /* shared_context_new */
NULL, /* shared_context_bind */
NULL, /* shared_context_free */
#endif
#ifdef HAVE_EGL
&android_gfx_ctx_windows
#else
+35 -1
View File
@@ -1447,6 +1447,28 @@ static bool gfx_ctx_drm_destroy_surface(void *data)
#endif
}
#ifdef HAVE_EGL
/* A context in the share group, of the window's client API */
static bool gfx_ctx_drm_shared_context_new(void *data,
gfx_ctx_shared_context_t *out)
{
gfx_ctx_drm_data_t *drm = (gfx_ctx_drm_data_t*)data;
memset(out, 0, sizeof(*out));
if (!drm)
return false;
switch (drm_api)
{
case GFX_CTX_OPENGL_API:
return egl_shared_context_new(&drm->egl, EGL_OPENGL_API, out);
case GFX_CTX_OPENGL_ES_API:
return egl_shared_context_new(&drm->egl, EGL_OPENGL_ES_API, out);
default:
break;
}
return false;
}
#endif
const gfx_ctx_driver_t gfx_ctx_drm = {
gfx_ctx_drm_init,
gfx_ctx_drm_destroy,
@@ -1484,5 +1506,17 @@ const gfx_ctx_driver_t gfx_ctx_drm = {
NULL,
NULL,
gfx_ctx_drm_create_surface,
gfx_ctx_drm_destroy_surface
gfx_ctx_drm_destroy_surface,
NULL, /* presentable */
NULL, /* last_present_time */
NULL, /* release_current */
#ifdef HAVE_EGL
gfx_ctx_drm_shared_context_new,
egl_shared_context_bind,
egl_shared_context_free
#else
NULL, /* shared_context_new */
NULL, /* shared_context_bind */
NULL /* shared_context_free */
#endif
};
+83 -1
View File
@@ -152,6 +152,10 @@ static uint8_t wgl_flags;
#ifdef HAVE_EGL
static egl_ctx_data_t win32_egl;
#endif
/* The attributes the window's context was made with, for contexts in its
* share group (gfx_ctx_wgl_shared_context_new); none for a legacy one */
static int wgl_ctx_attribs[16];
static bool wgl_ctx_attribs_used = false;
static unsigned win32_major = 0;
static unsigned win32_minor = 0;
static int win32_interval = 0;
@@ -368,6 +372,10 @@ void create_gl_context(HWND hwnd, bool *quit)
}
}
for (i = 0; i < 16; i++)
wgl_ctx_attribs[i] = attribs[i];
wgl_ctx_attribs_used = true;
/* Found a suitable version that is high enough, we can stop now */
break;
}
@@ -819,6 +827,71 @@ static void gfx_ctx_wgl_release_current(void *data)
}
}
#if (defined(HAVE_OPENGL) || defined(HAVE_OPENGL1) || defined(HAVE_OPENGL_CORE)) && !defined(HAVE_OPENGLES)
/* A context in the share group of the one current on this thread - the
* core's, or the window's - with the window's attributes, for a thread
* of its own. It is made current on the window's device context: it
* draws into framebuffers of its own, never into the window. */
static bool gfx_ctx_wgl_shared_context_new(void *data,
gfx_ctx_shared_context_t *out)
{
typedef HGLRC (APIENTRY *wglCreateContextAttribsProc)
(HDC, HGLRC, const int*);
HGLRC share = wglGetCurrentContext();
HGLRC ctx = NULL;
memset(out, 0, sizeof(*out));
if (win32_api != GFX_CTX_OPENGL_API || !win32_hdc || !win32_hrc)
return false;
if (!share)
share = win32_hw_hrc ? win32_hw_hrc : win32_hrc;
if (wgl_ctx_attribs_used)
{
wglCreateContextAttribsProc create = (wglCreateContextAttribsProc)
gfx_ctx_wgl_get_proc_address("wglCreateContextAttribsARB");
if (create)
ctx = create(win32_hdc, share, wgl_ctx_attribs);
}
else if ((ctx = wglCreateContext(win32_hdc)) && !wglShareLists(share, ctx))
{
wglDeleteContext(ctx);
ctx = NULL;
}
if (!ctx)
{
RARCH_ERR("[WGL] A shared context could not be made: 0x%08lx.\n",
(unsigned long)GetLastError());
return false;
}
out->kind = GFX_CTX_SHARED_WGL;
out->context = ctx;
out->get_proc_address = gfx_ctx_wgl_get_proc_address;
return true;
}
static bool gfx_ctx_wgl_shared_context_bind(gfx_ctx_shared_context_t *ctx,
bool bind)
{
if (ctx->kind != GFX_CTX_SHARED_WGL)
return false;
if (!bind)
return wglMakeCurrent(NULL, NULL) ? true : false;
/* The window's, read now: a window made again has another one, of the
* same pixel format */
return wglMakeCurrent(win32_hdc, (HGLRC)ctx->context) ? true : false;
}
static void gfx_ctx_wgl_shared_context_free(gfx_ctx_shared_context_t *ctx)
{
if (ctx->kind != GFX_CTX_SHARED_WGL)
return;
wglDeleteContext((HGLRC)ctx->context);
memset(ctx, 0, sizeof(*ctx));
}
#endif
static void gfx_ctx_wgl_bind_hw_render(void *data, bool enable)
{
switch (win32_api)
@@ -1058,5 +1131,14 @@ const gfx_ctx_driver_t gfx_ctx_wgl = {
gfx_ctx_wgl_destroy_surface,
gfx_ctx_wgl_presentable,
gfx_ctx_wgl_last_present_time,
gfx_ctx_wgl_release_current
gfx_ctx_wgl_release_current,
#if (defined(HAVE_OPENGL) || defined(HAVE_OPENGL1) || defined(HAVE_OPENGL_CORE)) && !defined(HAVE_OPENGLES)
gfx_ctx_wgl_shared_context_new,
gfx_ctx_wgl_shared_context_bind,
gfx_ctx_wgl_shared_context_free
#else
NULL, /* shared_context_new */
NULL, /* shared_context_bind */
NULL /* shared_context_free */
#endif
};
+81
View File
@@ -5102,6 +5102,87 @@ bool video_driver_hw_render_interface_kept(void)
return iface == video_hw_render_iface_held;
}
/* RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE: the context driver's
* contexts in the window context's share group, which the core's context
* is in too, handed to the core for threads of its own. Made at most a few
* times a session (one per thread of the core's), so each its own block. */
struct retro_hw_shared_context
{
gfx_ctx_shared_context_t shared;
};
static struct retro_hw_shared_context *RETRO_CALLCONV
video_hw_shared_context_create(void)
{
video_driver_state_t *video_st = &video_driver_st;
const gfx_ctx_driver_t *ctx = &video_st->current_video_context;
struct retro_hw_shared_context *out;
if ( !ctx->shared_context_new
|| !video_st->context_data
|| !(out = (struct retro_hw_shared_context*)calloc(1, sizeof(*out))))
return NULL;
if (!ctx->shared_context_new((void*)video_st->context_data, &out->shared))
{
free(out);
return NULL;
}
return out;
}
static bool RETRO_CALLCONV video_hw_shared_context_make_current(
struct retro_hw_shared_context *ctx, bool current)
{
const gfx_ctx_driver_t *drv = &video_driver_st.current_video_context;
if (!ctx || !drv->shared_context_bind)
return false;
return drv->shared_context_bind(&ctx->shared, current);
}
static void RETRO_CALLCONV video_hw_shared_context_destroy(
struct retro_hw_shared_context *ctx)
{
const gfx_ctx_driver_t *drv = &video_driver_st.current_video_context;
if (!ctx)
return;
if (drv->shared_context_free)
drv->shared_context_free(&ctx->shared);
free(ctx);
}
static const struct retro_hw_shared_context_interface
video_hw_shared_context_iface = {
RETRO_HW_SHARED_CONTEXT_INTERFACE_VERSION,
video_hw_shared_context_create,
video_hw_shared_context_make_current,
video_hw_shared_context_destroy
};
const struct retro_hw_shared_context_interface
*video_driver_hw_shared_context_interface(void)
{
video_driver_state_t *video_st = &video_driver_st;
const gfx_ctx_driver_t *ctx = &video_st->current_video_context;
switch (video_st->hw_render.context_type)
{
case RETRO_HW_CONTEXT_OPENGL:
case RETRO_HW_CONTEXT_OPENGL_CORE:
case RETRO_HW_CONTEXT_OPENGLES2:
case RETRO_HW_CONTEXT_OPENGLES3:
case RETRO_HW_CONTEXT_OPENGLES_VERSION:
break;
default:
return NULL;
}
if ( !video_st->context_data
|| !ctx->shared_context_new
|| !ctx->shared_context_bind
|| !ctx->shared_context_free)
return NULL;
return &video_hw_shared_context_iface;
}
bool video_driver_hw_context_moved(int before, bool shared)
{
return before != -1 && before != (shared ? 1 : 0);
+15 -6
View File
@@ -829,7 +829,8 @@ enum gfx_ctx_shared_kind
{
GFX_CTX_SHARED_NONE = 0,
GFX_CTX_SHARED_EGL,
GFX_CTX_SHARED_GLX
GFX_CTX_SHARED_GLX,
GFX_CTX_SHARED_WGL
};
/* A second context in the window context's share group, for a thread
@@ -841,7 +842,7 @@ typedef struct gfx_ctx_shared_context
enum gfx_ctx_shared_kind kind;
void *display; /* EGLDisplay, or the X Display */
void *config; /* EGLConfig, or GLXFBConfig */
void *context; /* EGLContext, or GLXContext */
void *context; /* EGLContext, GLXContext or HGLRC */
void *surface; /* EGL: its own pbuffer, NULL surfaceless */
unsigned long drawable; /* GLX: its own pbuffer */
unsigned visual_id; /* GLX: the config's visual */
@@ -1011,10 +1012,12 @@ typedef struct gfx_ctx_driver
void (*release_current)(void *data);
/* Optional, desktop GL: a context in the window context's share
* group (gfx_ctx_shared_context_t). New and free run on the video
* thread with the window's context current, and leave it so; bind
* and unbind run on the thread that uses the context, which never
* gets the window's surface. NULL where the driver has none. Placed
* group (gfx_ctx_shared_context_t). New and free run on a thread
* with a context of the group current - the video thread with the
* window's, or a hardware core's thread with the core's
* (RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE) - and leave it
* so; bind and unbind run on the thread that uses the context, which
* never gets the window's surface. NULL where the driver has none. Placed
* last, as above. */
bool (*shared_context_new)(void *data, gfx_ctx_shared_context_t *out);
bool (*shared_context_bind)(gfx_ctx_shared_context_t *ctx, bool bind);
@@ -2827,6 +2830,12 @@ void video_driver_cache_context_ack_set(void);
* restart - and the core has to be reset after all. True for a core
* that holds none. */
void video_driver_hw_render_interface_note(const struct retro_hw_render_interface *iface);
/* RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE's answer: NULL when the
* core's context is not OpenGL or the context driver cannot make contexts
* in its share group. */
const struct retro_hw_shared_context_interface
*video_driver_hw_shared_context_interface(void);
bool video_driver_hw_render_interface_kept(void);
/* Which of a GL driver's contexts a hardware core renders in: its own,
* shared with the driver's (@shared), or the driver's. Called by the
+59
View File
@@ -3618,6 +3618,65 @@ struct retro_hw_render_upscaler_interface
void (*set_inputs)(void *handle, const struct retro_hw_upscaler_inputs *inputs);
};
/**
* Gets an interface with which an OpenGL or OpenGL ES hardware core makes
* contexts for threads of its own: each is in the share group of the
* core's context (RETRO_ENVIRONMENT_SET_HW_RENDER), of the same API,
* version and profile, so textures, buffers, shaders, programs and syncs
* made on one are seen on the others; framebuffers and vertex arrays are
* not shared, as in OpenGL. A core renders on another thread this way -
* into a texture of its own, with a fence the core's context waits on
* before using it in retro_run - and compiles shaders on threads of its
* own, with nothing of the window system in the core.
*
* Call it from context_reset or later; the interface is valid until
* context_destroy, before which the core destroys every context it
* made. Returns false when the frontend cannot make such contexts for
* the video driver it runs; a core then keeps its OpenGL work on the
* thread that calls retro_run, as it always could.
*
* @param[out] data <tt>const struct retro_hw_shared_context_interface **</tt>.
* @see retro_hw_shared_context_interface
*/
#define RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE (104 | RETRO_ENVIRONMENT_EXPERIMENTAL)
#define RETRO_HW_SHARED_CONTEXT_INTERFACE_VERSION 1
/** A context made with retro_hw_shared_context_interface; opaque. */
struct retro_hw_shared_context;
struct retro_hw_shared_context_interface
{
/** RETRO_HW_SHARED_CONTEXT_INTERFACE_VERSION. */
unsigned interface_version;
/**
* A new context, current on no thread; NULL when one cannot be made.
* Call it on the thread that calls retro_run, in context_reset or
* retro_run. Each costs the driver a context: make them up front,
* one for each thread, not one per piece of work.
*/
struct retro_hw_shared_context *(RETRO_CALLCONV *create)(void);
/**
* Makes @ctx current on the calling thread (@current true), or no
* context of these at all (false), and returns whether it could. A
* context is current on one thread at a time. It has no framebuffer
* of the window's: framebuffer 0 is not to be drawn to or read, and
* it draws into framebuffers of its own. Functions are looked up with
* the hardware render callback's get_proc_address, with this context
* current or the core's.
*/
bool (RETRO_CALLCONV *make_current)(struct retro_hw_shared_context *ctx,
bool current);
/**
* Destroys @ctx, which is current on no thread. Call it on the thread
* that calls retro_run, in retro_run or context_destroy.
*/
void (RETRO_CALLCONV *destroy)(struct retro_hw_shared_context *ctx);
};
/* Speaker positions, as bits of a layout mask; a frame's channels are
* interleaved in ascending bit order. The bits are those of the
* WAVEFORMATEXTENSIBLE channel mask. */
+13
View File
@@ -3849,6 +3849,19 @@ bool runloop_environment_cb(unsigned cmd, void *data)
return false;
}
case RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE:
{
const struct retro_hw_shared_context_interface **iface =
(const struct retro_hw_shared_context_interface **)data;
const struct retro_hw_shared_context_interface *got =
video_driver_hw_shared_context_interface();
if (!got)
return false;
if (iface)
*iface = got;
return true;
}
case RETRO_ENVIRONMENT_GET_HW_RENDER_UPSCALER_INTERFACE:
{
#ifdef HAVE_TEMPORAL_UPSCALER
+19
View File
@@ -0,0 +1,19 @@
CORE := gl_shared_context_libretro.so
CFLAGS += -Wall -O2 -fPIC -I../../../libretro-common/include
all: $(CORE)
# C99 for unsigned long long, which glWaitSync's timeout is
$(CORE): gl_shared_context.c
$(CC) -std=c99 -pedantic $(CFLAGS) -shared -o $@ gl_shared_context.c $(LDFLAGS) -lpthread
# Needs a built ./retroarch with the gl driver on X11 (GLX and EGL), Xvfb
# and Mesa.
check: all
python3 gl_shared_context_test.py ../../../retroarch
clean:
rm -f $(CORE)
.PHONY: all check clean
@@ -0,0 +1,325 @@
/* An OpenGL hardware core for gl_shared_context_test.py that draws on a
* thread of its own (RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE).
*
* In context_reset it makes a context in its own context's share group
* and starts a thread on it, which makes a texture, clears it to one
* colour through a framebuffer of its own, fences that and hands the
* texture and the fence over. retro_run, on the core's context, waits on
* the fence on the GPU and blits the texture into the frontend's
* framebuffer - so the colour on screen is the other thread's drawing,
* seen through the share group. Until then it clears to black.
*
* Every entry point comes from the frontend's get_proc_address; it links
* no GL library. What happened goes to stderr for the test to read. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <libretro.h>
#include <retro_atomic.h>
#define W 64
#define H 48
#define CORE_GL_VERSION 0x1F02
#define CORE_GL_TEXTURE_2D 0x0DE1
#define CORE_GL_RGBA 0x1908
#define CORE_GL_RGBA8 0x8058
#define CORE_GL_UNSIGNED_BYTE 0x1401
#define CORE_GL_TEXTURE_MIN_FILTER 0x2801
#define CORE_GL_NEAREST 0x2600
#define CORE_GL_FRAMEBUFFER 0x8D40
#define CORE_GL_READ_FRAMEBUFFER 0x8CA8
#define CORE_GL_DRAW_FRAMEBUFFER 0x8CA9
#define CORE_GL_COLOR_ATTACHMENT0 0x8CE0
#define CORE_GL_FRAMEBUFFER_COMPLETE 0x8CD5
#define CORE_GL_COLOR_BUFFER_BIT 0x00004000
#define CORE_GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
typedef struct core_sync *core_sync_t;
static const unsigned char *(*p_GetString)(unsigned);
static void (*p_GenTextures)(int, unsigned*);
static void (*p_DeleteTextures)(int, const unsigned*);
static void (*p_BindTexture)(unsigned, unsigned);
static void (*p_TexImage2D)(unsigned, int, int, int, int, int, unsigned, unsigned, const void*);
static void (*p_TexParameteri)(unsigned, unsigned, int);
static void (*p_GenFramebuffers)(int, unsigned*);
static void (*p_DeleteFramebuffers)(int, const unsigned*);
static void (*p_BindFramebuffer)(unsigned, unsigned);
static void (*p_FramebufferTexture2D)(unsigned, unsigned, unsigned, unsigned, int);
static unsigned (*p_CheckFramebufferStatus)(unsigned);
static void (*p_Viewport)(int, int, int, int);
static void (*p_ClearColor)(float, float, float, float);
static void (*p_Clear)(unsigned);
static core_sync_t (*p_FenceSync)(unsigned, unsigned);
static void (*p_WaitSync)(core_sync_t, unsigned, unsigned long long);
static void (*p_DeleteSync)(core_sync_t);
static void (*p_Flush)(void);
static void (*p_BlitFramebuffer)(int, int, int, int, int, int, int, int, unsigned, unsigned);
static retro_video_refresh_t video_cb;
static retro_environment_t environ_cb;
static struct retro_hw_render_callback hw;
static const struct retro_hw_shared_context_interface *shared_iface;
static struct retro_hw_shared_context *worker_ctx;
static pthread_t worker;
static int worker_started;
static int have_context;
/* The worker's texture and fence, published with the flag after them */
static unsigned worker_texture;
static core_sync_t worker_fence;
static retro_atomic_int_t worker_done; /* 1 drawn, -1 failed */
static unsigned read_fbo;
static int shown;
static int load_proc(void *dst, const char *name)
{
retro_proc_address_t f = hw.get_proc_address(name);
memcpy(dst, &f, sizeof(f));
return f != NULL;
}
#define PROC(name) load_proc(&p_##name, "gl" #name)
static void *worker_main(void *arg)
{
unsigned fbo = 0;
int ok;
(void)arg;
if (!shared_iface->make_current(worker_ctx, true))
{
fprintf(stderr, "[gl_shared_context] worker: make_current failed\n");
retro_atomic_store_release_int(&worker_done, -1);
return NULL;
}
fprintf(stderr, "[gl_shared_context] worker: GL_VERSION \"%s\"\n",
(const char*)p_GetString(CORE_GL_VERSION));
p_GenTextures(1, &worker_texture);
p_BindTexture(CORE_GL_TEXTURE_2D, worker_texture);
p_TexParameteri(CORE_GL_TEXTURE_2D, CORE_GL_TEXTURE_MIN_FILTER, CORE_GL_NEAREST);
p_TexImage2D(CORE_GL_TEXTURE_2D, 0, CORE_GL_RGBA8, W, H, 0,
CORE_GL_RGBA, CORE_GL_UNSIGNED_BYTE, NULL);
p_BindTexture(CORE_GL_TEXTURE_2D, 0);
/* Framebuffers are not shared: this one is the worker's */
p_GenFramebuffers(1, &fbo);
p_BindFramebuffer(CORE_GL_FRAMEBUFFER, fbo);
p_FramebufferTexture2D(CORE_GL_FRAMEBUFFER, CORE_GL_COLOR_ATTACHMENT0,
CORE_GL_TEXTURE_2D, worker_texture, 0);
ok = p_CheckFramebufferStatus(CORE_GL_FRAMEBUFFER) == CORE_GL_FRAMEBUFFER_COMPLETE;
if (ok)
{
p_Viewport(0, 0, W, H);
/* 204, 102, 51 in a screenshot */
p_ClearColor(0.8f, 0.4f, 0.2f, 1.0f);
p_Clear(CORE_GL_COLOR_BUFFER_BIT);
}
p_BindFramebuffer(CORE_GL_FRAMEBUFFER, 0);
p_DeleteFramebuffers(1, &fbo);
worker_fence = p_FenceSync(CORE_GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
p_Flush();
shared_iface->make_current(worker_ctx, false);
fprintf(stderr, "[gl_shared_context] worker: %s\n",
ok ? "drew the texture" : "framebuffer incomplete");
fflush(stderr);
retro_atomic_store_release_int(&worker_done, ok ? 1 : -1);
return NULL;
}
static void context_reset(void)
{
const unsigned char *version;
have_context = PROC(GetString) && PROC(GenTextures) && PROC(DeleteTextures)
&& PROC(BindTexture) && PROC(TexImage2D) && PROC(TexParameteri)
&& PROC(GenFramebuffers) && PROC(DeleteFramebuffers)
&& PROC(BindFramebuffer) && PROC(FramebufferTexture2D)
&& PROC(CheckFramebufferStatus) && PROC(Viewport)
&& PROC(ClearColor) && PROC(Clear) && PROC(FenceSync)
&& PROC(WaitSync) && PROC(DeleteSync) && PROC(Flush)
&& PROC(BlitFramebuffer);
version = have_context ? p_GetString(CORE_GL_VERSION) : NULL;
if (!version)
{
have_context = 0;
fprintf(stderr, "[gl_shared_context] context_reset: NO CONTEXT\n");
fflush(stderr);
return;
}
fprintf(stderr, "[gl_shared_context] context_reset: GL_VERSION \"%s\"\n",
(const char*)version);
shared_iface = NULL;
if ( !environ_cb(RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE, &shared_iface)
|| !shared_iface
|| shared_iface->interface_version < RETRO_HW_SHARED_CONTEXT_INTERFACE_VERSION)
{
fprintf(stderr, "[gl_shared_context] context_reset: NO INTERFACE\n");
fflush(stderr);
return;
}
if (!(worker_ctx = shared_iface->create()))
{
fprintf(stderr, "[gl_shared_context] context_reset: create failed\n");
fflush(stderr);
return;
}
fprintf(stderr, "[gl_shared_context] context_reset: shared context made\n");
fflush(stderr);
retro_atomic_store_release_int(&worker_done, 0);
worker_started = pthread_create(&worker, NULL, worker_main, NULL) == 0;
}
static void context_destroy(void)
{
if (worker_started)
pthread_join(worker, NULL);
worker_started = 0;
if (have_context)
{
if (read_fbo)
p_DeleteFramebuffers(1, &read_fbo);
if (worker_texture)
p_DeleteTextures(1, &worker_texture);
if (worker_fence)
p_DeleteSync(worker_fence);
}
read_fbo = worker_texture = 0;
worker_fence = NULL;
if (worker_ctx)
shared_iface->destroy(worker_ctx);
worker_ctx = NULL;
have_context = 0;
}
void retro_set_environment(retro_environment_t cb)
{
bool no_content = true;
environ_cb = cb;
cb(RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME, &no_content);
}
void retro_set_video_refresh(retro_video_refresh_t cb) { video_cb = cb; }
void retro_set_audio_sample(retro_audio_sample_t cb) { (void)cb; }
void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { (void)cb; }
void retro_set_input_poll(retro_input_poll_t cb) { (void)cb; }
void retro_set_input_state(retro_input_state_t cb) { (void)cb; }
void retro_init(void) { }
void retro_deinit(void) { }
unsigned retro_api_version(void) { return RETRO_API_VERSION; }
void retro_get_system_info(struct retro_system_info *info)
{
memset(info, 0, sizeof(*info));
info->library_name = "gl_shared_context";
info->library_version = "1";
info->valid_extensions = "";
}
void retro_get_system_av_info(struct retro_system_av_info *info)
{
memset(info, 0, sizeof(*info));
info->timing.fps = 60.0;
info->timing.sample_rate = 48000.0;
info->geometry.base_width = W;
info->geometry.base_height = H;
info->geometry.max_width = W;
info->geometry.max_height = H;
info->geometry.aspect_ratio = (float)W / (float)H;
}
void retro_set_controller_port_device(unsigned port, unsigned device)
{
(void)port;
(void)device;
}
void retro_reset(void) { }
void retro_run(void)
{
unsigned fb;
if (!have_context)
{
video_cb(NULL, W, H, 0);
return;
}
fb = (unsigned)hw.get_current_framebuffer();
if (retro_atomic_load_acquire_int(&worker_done) == 1)
{
if (!read_fbo)
{
/* The GPU, not this thread, waits for the worker's drawing */
p_WaitSync(worker_fence, 0, ~0ULL);
p_GenFramebuffers(1, &read_fbo);
p_BindFramebuffer(CORE_GL_READ_FRAMEBUFFER, read_fbo);
p_FramebufferTexture2D(CORE_GL_READ_FRAMEBUFFER, CORE_GL_COLOR_ATTACHMENT0,
CORE_GL_TEXTURE_2D, worker_texture, 0);
}
p_BindFramebuffer(CORE_GL_READ_FRAMEBUFFER, read_fbo);
p_BindFramebuffer(CORE_GL_DRAW_FRAMEBUFFER, fb);
p_BlitFramebuffer(0, 0, W, H, 0, 0, W, H, CORE_GL_COLOR_BUFFER_BIT, CORE_GL_NEAREST);
p_BindFramebuffer(CORE_GL_FRAMEBUFFER, fb);
if (!shown++)
{
fprintf(stderr, "[gl_shared_context] retro_run: showing the worker's texture\n");
fflush(stderr);
}
}
else
{
p_BindFramebuffer(CORE_GL_FRAMEBUFFER, fb);
p_Viewport(0, 0, W, H);
p_ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
p_Clear(CORE_GL_COLOR_BUFFER_BIT);
}
video_cb(RETRO_HW_FRAME_BUFFER_VALID, W, H, 0);
}
bool retro_load_game(const struct retro_game_info *game)
{
enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888;
(void)game;
environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt);
memset(&hw, 0, sizeof(hw));
if (getenv("GL_SHARED_CONTEXT_CORE"))
{
hw.context_type = RETRO_HW_CONTEXT_OPENGL_CORE;
hw.version_major = 3;
hw.version_minor = 3;
}
else
hw.context_type = RETRO_HW_CONTEXT_OPENGL;
hw.context_reset = context_reset;
hw.context_destroy = context_destroy;
if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw))
{
fprintf(stderr, "[gl_shared_context] SET_HW_RENDER refused\n");
return false;
}
return true;
}
bool retro_load_game_special(unsigned type,
const struct retro_game_info *info, size_t num)
{
(void)type;
(void)info;
(void)num;
return false;
}
void retro_unload_game(void) { }
unsigned retro_get_region(void) { return RETRO_REGION_NTSC; }
size_t retro_serialize_size(void) { return 0; }
bool retro_serialize(void *data, size_t size) { (void)data; (void)size; return false; }
bool retro_unserialize(const void *data, size_t size) { (void)data; (void)size; return false; }
void retro_cheat_reset(void) { }
void retro_cheat_set(unsigned index, bool enabled, const char *code) { (void)index; (void)enabled; (void)code; }
void *retro_get_memory_data(unsigned id) { (void)id; return NULL; }
size_t retro_get_memory_size(unsigned id) { (void)id; return 0; }
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Check that an OpenGL hardware core can draw on a thread of its own.
Runs retroarch on the gl driver under Xvfb with gl_shared_context, a
core that takes RETRO_ENVIRONMENT_GET_HW_SHARED_CONTEXT_INTERFACE in
context_reset, makes a context in its own context's share group, and
on a thread of its own clears a texture to one colour through that
context. retro_run blits the texture to the frontend's framebuffer
after a GPU wait on the thread's fence, so the colour on screen is the
other thread's drawing, seen through the share group.
On GLX (x) and on EGL (egl_x), with threaded video on and off, and with
a compatibility and a 3.3 core profile context - the second made with
the window's context attributes.
Needs Xvfb (xvfb-run) and a software GL (Mesa llvmpipe).
Usage: gl_shared_context_test.py path/to/retroarch
"""
import os
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
CORE = os.path.join(HERE, "gl_shared_context_libretro.so")
COLOR = (204, 102, 51)
sys.path.insert(0, os.path.join(HERE, "..", "egl_hw_core"))
from egl_hw_core_test import read_png # noqa: E402
def run(retroarch, name, ctx, threaded, core_profile):
d = tempfile.mkdtemp(prefix="gl_shared_context_")
try:
shot = os.path.join(d, "shot.png")
cfg = os.path.join(d, "retroarch.cfg")
with open(cfg, "w") as f:
f.write("\n".join([
'video_driver = "gl"',
'video_context_driver = "%s"' % ctx,
'video_threaded = "%s"' % ("true" if threaded else "false"),
'video_smooth = "false"',
'video_scale = "1.000000"',
'video_scale_integer = "true"',
'video_fullscreen = "false"',
'video_windowed_fullscreen = "false"',
'video_gpu_screenshot = "true"',
'video_font_enable = "false"',
'audio_driver = "null"',
'input_driver = "null"',
'menu_driver = "rgui"',
'config_save_on_exit = "false"',
'screenshot_directory = "%s"' % d,
""]))
env = dict(os.environ)
env["LIBGL_ALWAYS_SOFTWARE"] = "1"
if core_profile:
env["GL_SHARED_CONTEXT_CORE"] = "1"
cmd = ["xvfb-run", "-a", "-s", "-screen 0 1024x768x24", retroarch,
"--config=" + cfg, "-L", CORE, "--max-frames=30",
"--max-frames-ss", "--max-frames-ss-path=" + shot, "-v"]
try:
proc = subprocess.run(cmd, env=env, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=120)
except subprocess.TimeoutExpired as e:
sys.stdout.write((e.output or b"").decode("utf-8", "replace")[-4000:])
print("FAIL %s: retroarch did not finish (hung)" % name)
return False
log = proc.stdout.decode("utf-8", "replace")
def fail(why):
sys.stdout.write(log[-4000:])
print("FAIL %s: %s" % (name, why))
return False
if 'Found GL context: "%s"' % ctx not in log:
return fail("the gl driver did not come up on %s" % ctx)
if threaded != ("Starting threaded video driver" in log):
return fail("threaded video is not %s" % ("on" if threaded else "off"))
for line in ("context_reset: GL_VERSION",
"context_reset: shared context made",
"worker: GL_VERSION",
"worker: drew the texture",
"retro_run: showing the worker's texture"):
if "[gl_shared_context] " + line not in log:
return fail("missing \"%s\"" % line)
if proc.returncode != 0:
return fail("retroarch exited with %d" % proc.returncode)
if not os.path.exists(shot):
return fail("no screenshot")
w, h, rows = read_png(shot)
bad = sum(1 for row in rows for px in row
if any(abs(px[i] - COLOR[i]) > 1 for i in range(3)))
if bad:
return fail("%d of %d pixels are not the worker's colour (centre %s)"
% (bad, w * h, rows[h // 2][w // 2]))
print("ok %s" % name)
return True
finally:
shutil.rmtree(d, ignore_errors=True)
def main():
if len(sys.argv) != 2:
raise SystemExit(__doc__)
retroarch = os.path.abspath(sys.argv[1])
ok = True
for ctx, label in (("x", "GLX"), ("egl_x", "EGL")):
for threaded in (False, True):
for core_profile in (False, True):
name = "%s, %s, %s" % (label,
"threaded" if threaded else "unthreaded",
"3.3 core" if core_profile else "compatibility")
ok = run(retroarch, name, ctx, threaded, core_profile) and ok
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()