mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-12 16:19:29 -10:00
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:
1 parent
fd7e99a94c
commit
ae907bff65
11 files changed
+789
-8
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user