glsm: survive a lost GL context in every GL plugin

A frontend can hand the core a new GL context at any time: RetroArch
does so when threaded video is toggled, when the video driver is
reinitialised, or when the shared core context changes.  The core then
gets context_destroy (sometimes) and context_reset.

Two things went wrong on the second context_reset:

- glsm called GLideN64's retroChangeWindow() from its RESET handler for
  whichever plugin was active.  With Glide64, Rice or glN64 this ran
  GLideN64 code on state it never set up and crashed.  The GLideN64
  reinit already happens in context_reset() in libretro.c, so the call
  in glsm is dropped and glsm now only rebinds its own state for the
  new context.

- GLideN64's teardown deleted its textures, buffers, framebuffers,
  VAOs, syncs and queries by name in the new context.  Those names
  belonged to the lost context; in the new one they may already name
  the frontend's own objects.  glsm_abandon_objects() makes glsm's
  delete calls no-ops while the old objects are forgotten, and
  context_reset() uses it around gliden64DestroyGfxContext().

lrhost_gl gains LRHOST_GL_SWITCH_EVERY (and LRHOST_GL_SWITCH_DESTROY)
to replace the context every N frames, and the GL renderer lane runs
every plugin through a context switch with and without a destroy.
Each must finish all frames; GLideN64 must render the same frames as
without the switch.  On the old code Glide64, Rice and glN64 segfault.
This commit is contained in:
libretroadmin committed 2026-11-03 14:06:17 +01:00
1 parent 3654ab56e1
commit e8cbdbbfa6
6 files changed
+102 -10

No files matched your search

+28 -9
View File
@@ -295,8 +295,14 @@ static GLuint frontend_framebuffer;
static GLenum active_texture;
static int window_first = 0;
/* the objects named are the lost context's: deleting them would delete
* whatever the new context has under those names */
static int resetting_context = 0;
extern void retroChangeWindow();
void glsm_abandon_objects(bool abandon)
{
resetting_context = abandon ? 1 : 0;
}
static void bindFBO(GLenum target)
{
@@ -1003,6 +1009,8 @@ void rglCompressedTexImage2D(GLenum target, GLint level,
void rglDeleteFramebuffers(GLsizei n, const GLuint *_framebuffers)
{
int i, p;
if (resetting_context)
return;
for (i = 0; i < n; ++i)
{
if (_framebuffers[i] == 0)
@@ -1026,6 +1034,8 @@ void rglDeleteTextures(GLsizei n, const GLuint *textures)
int i;
size_t f;
unsigned u;
if (resetting_context)
return;
for (i = 0; i < n; ++i)
{
if (textures[i] == 0)
@@ -1077,6 +1087,8 @@ void rglBindRenderbuffer(GLenum target, GLuint renderbuffer)
*/
void rglDeleteRenderbuffers(GLsizei n, GLuint *renderbuffers)
{
if (resetting_context)
return;
glDeleteRenderbuffers(n, renderbuffers);
}
@@ -1214,6 +1226,8 @@ void rglGetQueryObjectuiv( GLuint id,
void rglDeleteQueries( GLsizei n,
const GLuint * ids)
{
if (resetting_context)
return;
#if defined(HAVE_OPENGL) || defined(HAVE_OPENGLES) && defined(HAVE_OPENGLES3)
glDeleteQueries(n, ids);
#endif
@@ -1710,6 +1724,8 @@ void rglDeleteBuffers(GLsizei n, const GLuint *buffers)
{
int i;
unsigned a;
if (resetting_context)
return;
for (i = 0; i < n; ++i)
{
if (buffers[i] == 0)
@@ -2280,6 +2296,8 @@ void * rglMapBuffer( GLenum target, GLenum access)
*/
GLboolean rglUnmapBuffer( GLenum target)
{
if (resetting_context)
return GL_TRUE;
#if defined(HAVE_OPENGLES)
return glUnmapBufferOES(target);
#else
@@ -2446,6 +2464,8 @@ void rglDeleteVertexArrays(GLsizei n, const GLuint *arrays)
{
#if defined(HAVE_OPENGL) || defined(HAVE_OPENGLES) && defined(HAVE_OPENGLES3)
int i;
if (resetting_context)
return;
for (i = 0; i < n; ++i)
{
if (arrays[i] != 0 && arrays[i] == gl_state.bindvertex.array)
@@ -2487,6 +2507,8 @@ void *rglFenceSync(GLenum condition, GLbitfield flags)
*/
void rglDeleteSync(GLsync sync)
{
if (resetting_context)
return;
#if defined(HAVE_OPENGL) || defined(HAVE_OPENGLES) && defined(HAVE_OPENGLES3)
glDeleteSync((GLsync)sync);
#endif
@@ -3411,17 +3433,14 @@ bool glsm_ctl(enum glsm_state_ctl state, void *data)
initGLFunctions();
#endif
if (window_first > 0) {
resetting_context = 1;
/* a new context: what glsm knew of the old one is dropped */
if (window_first > 0)
{
glsm_state_setup();
#ifdef HAVE_GLIDEN64
retroChangeWindow();
#endif
glsm_state_unbind();
resetting_context = 0;
}
}
else
window_first = 1;
window_first = 1;
break;
case GLSM_CTL_STATE_CONTEXT_DESTROY:
glsm_state_ctx_destroy(data);
+5
View File
@@ -141,6 +141,11 @@ void glsm_draw_target(unsigned width, unsigned height);
void glsm_draw_target_present(void);
bool glsm_draw_target_show(void);
/* While set, deleting GL objects does nothing: for tearing down a
* renderer whose context was lost, after GLSM_CTL_STATE_CONTEXT_RESET,
* where the names may be the new context's. */
void glsm_abandon_objects(bool abandon);
RETRO_END_DECLS
#endif
+3
View File
@@ -1520,7 +1520,10 @@ static void context_reset(void)
* before a context exists. */
if (gfx_plugin == GFX_GLIDEN64 && !first_context_reset)
{
/* its objects were the lost context's: forgotten, not deleted */
glsm_abandon_objects(true);
gliden64DestroyGfxContext();
glsm_abandon_objects(false);
gliden64ReinitGfxContext();
}
#endif
+25
View File
@@ -14,6 +14,15 @@ across drivers, so the frames are not held to stored hashes; the lane holds them
GLideN64's under -fcommon, crashed at exit, and Rice crashed in the
driver putting a frame the CPU left in RDRAM on screen with no program
bound (shade32, tlut16 and zbuf16 draw once, so Rice shows RDRAM after);
* every renderer running on when the frontend moves it to a new
context every 10 runs and calls context_reset (lrhost_gl's
LRHOST_GL_SWITCH_EVERY), with and without context_destroy first, as
RetroArch does when its video driver restarts - threaded video
toggled moves a hardware core between two contexts - and GLideN64
drawing the same frames as without: Glide64, Rice and glN64 crashed
in the second context_reset, which drove GLideN64's window change
whichever renderer was in use, and GLideN64 deleted the lost
context's objects by name in the new one;
* GLideN64 drawing the same frames whatever its colour buffer copy to
RDRAM is set to (Off, Sync, Async, TripleBuffer): the copy writes RDRAM
and must not disturb what is drawn, its pixel buffers or the vertex
@@ -94,6 +103,22 @@ def main():
if not ok:
failures += 1
sys.stdout.write(out[-2000:])
for plugin in PLUGINS:
rc, base, out = run(a.host, a.core, path, plugin, [], tmp)
for name, env_extra in (("switch", {"LRHOST_GL_SWITCH_EVERY": "10"}),
("destroy+switch", {"LRHOST_GL_SWITCH_EVERY": "10",
"LRHOST_GL_SWITCH_DESTROY": "1"})):
rc, frames, out = run(a.host, a.core, path, plugin, [], tmp, env_extra)
ok = rc == 0 and len(frames) == len(base)
if ok and plugin == "gliden64" and frames != base:
ok = False
print("%-10s %-9s context %-14s %s" % (rom, plugin, name,
"ok" if ok else "FAILED (exit %d, %d of %d frames%s)" % (
rc, len(frames), len(base),
", other frames" if rc == 0 and len(frames) == len(base) else "")))
if not ok:
failures += 1
sys.stdout.write(out[-2000:])
reference = None
for mode in COPY_MODES:
rc, frames, out = run(a.host, a.core, path, "gliden64",
+5
View File
@@ -523,6 +523,11 @@ int main(int argc, char **argv)
for (k = 0; k < len; k++)
ram[off + k] = (unsigned char)((k * 7u + (unsigned)i * 13u) ^ (k >> 3));
}
#ifdef LRHOST_GL
if (getenv("LRHOST_GL_SWITCH_EVERY") && i > 0 && gl_egl_ctx != EGL_NO_CONTEXT
&& i % atoi(getenv("LRHOST_GL_SWITCH_EVERY")) == 0 && !gl_switch_context())
return 1;
#endif
run_frame = i; t0 = now_ms();
if (getenv("LRHOST_RUNAHEAD") && i > 0)
{
+36 -1
View File
@@ -29,6 +29,8 @@ typedef int GLHsizei;
static struct retro_hw_render_callback gl_hw;
static EGLDisplay gl_dpy = EGL_NO_DISPLAY;
static EGLContext gl_egl_ctx = EGL_NO_CONTEXT;
static EGLConfig gl_cfg;
static EGLint gl_ctx_attr[16];
static GLHuint gl_fbo, gl_rb_color, gl_rb_depth;
static unsigned gl_fb_w, gl_fb_h;
static uint32_t *gl_frame; static size_t gl_frame_cap;
@@ -51,12 +53,14 @@ static retro_proc_address_t gl_get_proc_address(const char *sym)
return (retro_proc_address_t)eglGetProcAddress(sym);
}
static int gl_make_framebuffer(unsigned max_w, unsigned max_h);
static int gl_init(unsigned max_w, unsigned max_h)
{
EGLint major, minor, n;
EGLConfig cfg;
EGLint *ctx_attr = gl_ctx_attr;
const EGLint cfg_attr[] = { EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE };
EGLint ctx_attr[16];
int i = 0;
PFNEGLGETPLATFORMDISPLAYEXTPROC get_platform_display =
(PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT");
@@ -83,6 +87,7 @@ static int gl_init(unsigned max_w, unsigned max_h)
ctx_attr[i++] = EGL_CONTEXT_OPENGL_PROFILE_MASK; ctx_attr[i++] = EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT;
}
ctx_attr[i++] = EGL_NONE;
gl_cfg = cfg;
gl_egl_ctx = eglCreateContext(gl_dpy, cfg, EGL_NO_CONTEXT, ctx_attr);
if (gl_egl_ctx == EGL_NO_CONTEXT || !eglMakeCurrent(gl_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, gl_egl_ctx))
{ fprintf(stderr, "lrhost_gl: cannot make a %s %u.%u context current\n",
@@ -96,6 +101,12 @@ static int gl_init(unsigned max_w, unsigned max_h)
#undef GLP
gl_fb_w = max_w; gl_fb_h = max_h;
return gl_make_framebuffer(max_w, max_h);
}
/* the framebuffer the core draws into, in the context current */
static int gl_make_framebuffer(unsigned max_w, unsigned max_h)
{
p_glGenFramebuffers(1, &gl_fbo);
p_glBindFramebuffer(GLH_FRAMEBUFFER, gl_fbo);
p_glGenRenderbuffers(1, &gl_rb_color);
@@ -115,6 +126,30 @@ static int gl_init(unsigned max_w, unsigned max_h)
return 1;
}
/* LRHOST_GL_SWITCH_EVERY=n: every n runs the core is moved to a new
* context sharing objects with the old one, with a new framebuffer, as
* a frontend restarting its video driver moves it (RetroArch toggling
* threaded video moves a hardware core between its own context and a
* shared one). The new context is made current and context_reset
* called; LRHOST_GL_SWITCH_DESTROY=1 calls context_destroy in the old
* one first, as a frontend that tears the core down before does. */
static int gl_switch_context(void)
{
EGLContext old = gl_egl_ctx;
if (getenv("LRHOST_GL_SWITCH_DESTROY") && gl_hw.context_destroy)
gl_hw.context_destroy();
gl_egl_ctx = eglCreateContext(gl_dpy, gl_cfg, old, gl_ctx_attr);
if (gl_egl_ctx == EGL_NO_CONTEXT
|| !eglMakeCurrent(gl_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, gl_egl_ctx))
{ fprintf(stderr, "lrhost_gl: no second context\n"); return 0; }
eglDestroyContext(gl_dpy, old);
if (!gl_make_framebuffer(gl_fb_w, gl_fb_h))
return 0;
if (gl_hw.context_reset)
gl_hw.context_reset();
return 1;
}
/* the frame the core drew, top-down XRGB8888 */
static const uint32_t *gl_read_frame(unsigned w, unsigned h)
{