git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitebdd7618e3069ee872a996aa367e2b0363720767
parent33837eeae1
authorIsaac Freund <[email protected]>
date2026-01-06 21:00
Server: recover from GPU resets

 river/Server.zig | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/river/Server.zig b/river/Server.zig
index 613691c..2a1b915 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -45,6 +45,7 @@ session: ?*wlr.Session,
 
 renderer: *wlr.Renderer,
 allocator: *wlr.Allocator,
+gpu_reset_recover: ?*wl.EventSource = null,
 
 security_context_manager: *wlr.SecurityContextManagerV1,
 
@@ -87,6 +88,7 @@ xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_optio
 new_xsurface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void =
     if (build_options.xwayland) .init(handleNewXwaylandSurface),
 
+renderer_lost: wl.Listener(void) = .init(handleRendererLost),
 new_xdg_toplevel: wl.Listener(*wlr.XdgToplevel) = .init(handleNewXdgToplevel),
 new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) = .init(handleNewToplevelDecoration),
 request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) = .init(handleRequestActivate),
@@ -178,6 +180,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
     try server.idle_inhibit_manager.init();
     try server.lock_manager.init();
 
+    server.renderer.events.lost.add(&server.renderer_lost);
     server.xdg_shell.events.new_toplevel.add(&server.new_xdg_toplevel);
     server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration);
     server.xdg_activation.events.request_activate.add(&server.request_activate);
@@ -191,6 +194,7 @@ pub fn deinit(server: *Server) void {
     server.sigint_source.remove();
     server.sigterm_source.remove();
 
+    server.renderer_lost.link.remove();
     server.new_xdg_toplevel.link.remove();
     server.new_toplevel_decoration.link.remove();
     server.request_activate.link.remove();
@@ -341,6 +345,72 @@ fn terminate(_: c_int, wl_server: *wl.Server) c_int {
     return 0;
 }
 
+fn handleRendererLost(listener: *wl.Listener(void)) void {
+    const server: *Server = @fieldParentPtr("renderer_lost", listener);
+    if (server.gpu_reset_recover != null) {
+        log.info("ignoring GPU reset event, recovery already scheduled", .{});
+        return;
+    }
+    log.info("received GPU reset event, scheduling recovery", .{});
+    // There's a design wart in this wlroots API: calling wlr_renderer_destroy()
+    // from inside this listener for the renderer lost event causes the assertion
+    // that all listener lists are empty in wlr_renderer_destroy() to fail. This
+    // happens even if river has already called server.renderer_lost.link.remove()
+    // since wlroots uses wl_signal_emit_mutable(), which is implemented by adding
+    // temporary links to the list during iteration.
+    // Using an idle callback is the most straightforward way to work around this
+    // design wart.
+    const event_loop = server.wl_server.getEventLoop();
+    server.gpu_reset_recover = event_loop.addIdle(*Server, gpuResetRecoverIdle, server) catch |err| switch (err) {
+        error.OutOfMemory => {
+            log.err("out of memory", .{});
+            return;
+        },
+    };
+}
+
+fn gpuResetRecoverIdle(server: *Server) void {
+    server.gpu_reset_recover = null;
+    // There's not much that can be done if creating a new renderer or allocator fails.
+    // With luck there might be another GPU reset after which we try again and succeed.
+    server.gpuResetRecover() catch |err| switch (err) {
+        error.RendererCreateFailed => log.err("failed to create new renderer after GPU reset", .{}),
+        error.AllocatorCreateFailed => log.err("failed to create new allocator after GPU reset", .{}),
+    };
+}
+
+fn gpuResetRecover(server: *Server) !void {
+    log.info("recovering from GPU reset", .{});
+    const new_renderer = try wlr.Renderer.autocreate(server.backend);
+    errdefer new_renderer.destroy();
+
+    const new_allocator = try wlr.Allocator.autocreate(server.backend, new_renderer);
+    errdefer comptime unreachable; // no failure allowed after this point
+
+    server.renderer_lost.link.remove();
+    new_renderer.events.lost.add(&server.renderer_lost);
+
+    server.compositor.setRenderer(new_renderer);
+
+    {
+        var it = server.om.outputs.iterator(.forward);
+        while (it.next()) |output| {
+            if (output.wlr_output) |wlr_output| {
+                // This should never fail here as failure with this combination of
+                // renderer, allocator, and backend should have prevented creating
+                // the output in the first place.
+                _ = wlr_output.initRender(new_allocator, new_renderer);
+            }
+        }
+    }
+
+    server.renderer.destroy();
+    server.renderer = new_renderer;
+
+    server.allocator.destroy();
+    server.allocator = new_allocator;
+}
+
 fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void {
     XdgToplevel.create(xdg_toplevel) catch {
         log.err("out of memory", .{});