crt: rebind the modeline ops when the display server is rebuilt under a live engine

The CRT consumer captured the display server instance pointer at
engine init. A content load or close goes through the main deinit,
which clears the runloop inited flag, so the next video init destroys
that instance and builds a new one; the win32 modeline_close puts the
desktop timing back on the wire and the consumer's ops table points
at freed memory. Every later set read ml->opened from the fresh
zeroed struct, returned false before logging, and the screen stayed
on the desktop mode (480i on CRT Emudriver) while the log showed the
modeline calculated and "Error switching" with no CDS call.

video_display_server_destroy now tells the consumer the instance is
going away: the ops table is dropped, current is cleared and the
change detection is reset. On the next switch the engine rebinds to
the server that is up and applies the mode again through a real set.

Fixes #19528
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-10 23:50:26 +02:00
1 parent 75d79f9009
commit 94d2365126
3 files changed
+70 -13

No files matched your search

+57 -13
View File
@@ -250,6 +250,53 @@ static bool crt_load_config_ini(videocrt_switch_t *p_switch)
return true;
}
/* Point the ops table at the display server instance that is up
* right now and open its modeline path. The instance is torn down
* and rebuilt under a full video init (content load and close go
* through the main deinit), so this runs at engine init and again
* after crt_switch_display_server_lost() dropped the old table. */
static void crt_bind_display_server(videocrt_switch_t *p_switch)
{
video_modeline_gen_t *gen = p_switch->gen;
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
p_switch->ops_valid = false;
p_switch->ops_lost = false;
if (!p_switch->khr_ctx && video_display_server_get_modeline_ops(&p_switch->ops))
{
if (p_switch->ops.open && !p_switch->ops.open(p_switch->ops.data, &gen->disp))
{
RARCH_ERR("[CRT] Display server could not open the modeline path, generating only.\n");
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
}
else
p_switch->ops_valid = true;
}
p_switch->ops.name = p_switch->ops_valid ? video_display_server_get_ident() : "dummy";
}
void crt_switch_display_server_lost(videocrt_switch_t *p_switch, void *data)
{
if (!p_switch->gen || !p_switch->ops_valid || p_switch->ops.data != data)
return;
/* The server closes its modeline path as it goes down, which
* puts the desktop timing back on the wire, and the ops table
* points into memory that is about to be freed. Drop the table,
* forget what was current, and make the next frame's request
* look new so the mode is applied again through the rebound
* server. */
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
p_switch->ops.name = "dummy";
p_switch->ops_valid = false;
p_switch->ops_lost = true;
p_switch->gen->current = NULL;
p_switch->ra_tmp_height = 0;
p_switch->ra_tmp_width = 0;
p_switch->ra_tmp_core_hz = 0.0f;
RARCH_LOG("[CRT] Display server going down, rebinding on the next switch.\n");
}
static bool crt_engine_init(videocrt_switch_t *p_switch,
int monitor_index, unsigned int crt_mode, unsigned int super_width)
{
@@ -299,19 +346,7 @@ static bool crt_engine_init(videocrt_switch_t *p_switch,
modeline_ini_load(gen, "display0.ini");
modeline_parse_options(gen);
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
p_switch->ops_valid = false;
if (!p_switch->khr_ctx && video_display_server_get_modeline_ops(&p_switch->ops))
{
if (p_switch->ops.open && !p_switch->ops.open(p_switch->ops.data, &gen->disp))
{
RARCH_ERR("[CRT] Display server could not open the modeline path, generating only.\n");
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
}
else
p_switch->ops_valid = true;
}
p_switch->ops.name = p_switch->ops_valid ? video_display_server_get_ident() : "dummy";
crt_bind_display_server(p_switch);
p_switch->rtn = modeline_list_init(gen, &p_switch->ops) ? 0 : -1;
RARCH_LOG("[CRT] Engine rtn %d.\n", p_switch->rtn);
@@ -326,6 +361,14 @@ static bool crt_engine_init(videocrt_switch_t *p_switch,
}
}
else if (p_switch->ops_lost && p_switch->rtn >= 0)
{
/* Engine alive, display server rebuilt underneath it */
crt_bind_display_server(p_switch);
if (p_switch->ops_valid)
RARCH_LOG("[CRT] Rebound to display server \"%s\".\n", p_switch->ops.name);
}
if (p_switch->rtn >= 0)
{
p_switch->active = true;
@@ -547,6 +590,7 @@ void crt_destroy_modes(videocrt_switch_t *p_switch)
}
memset(&p_switch->ops, 0, sizeof(p_switch->ops));
p_switch->ops_valid = false;
p_switch->ops_lost = false;
}
void crt_switch_res_core(
+7
View File
@@ -72,6 +72,7 @@ typedef struct videocrt_switch
uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan;
bool active;
bool ops_valid;
bool ops_lost;
bool menu_active;
bool hh_core;
@@ -100,6 +101,12 @@ void crt_switch_res_core(
void crt_destroy_modes(videocrt_switch_t *p_switch);
/* The display server instance whose data is @data is about to be
* destroyed: drop the ops table bound to it so nothing dereferences
* freed memory, and arrange for the next switch to rebind and apply
* the mode again. No-op when the consumer is not bound to it. */
void crt_switch_display_server_lost(videocrt_switch_t *p_switch, void *data);
/* Write an EDID block for the configured CRT preset (menu mode, or
* the ini set for mode 4) to <config>/edid/<preset>.bin; s receives
* the path. Generation only: installing the block on a connector is
+6
View File
@@ -1614,7 +1614,13 @@ void video_display_server_destroy(void)
if (current_display_server && (current_display_server != &dispserv_null))
if (video_st->current_display_server_data)
{
#if defined(HAVE_MODELINE)
crt_switch_display_server_lost(&video_st->crt_switch_st,
video_st->current_display_server_data);
#endif
current_display_server->destroy(video_st->current_display_server_data);
}
#if defined(HAVE_SDL2) || defined(HAVE_SDL3)
if (sdl_display_server_data)
{