From a3d9a3f933fae0bf2874c3ce658f8e0c5e2e82c4 Mon Sep 17 00:00:00 2001 From: Dzulfikar Ats Tsauri Date: Sun, 31 May 2026 09:31:23 +0700 Subject: [PATCH] feat: add surface attach/detach C API --- include/ghostty.h | 34 +++++++ src/Surface.zig | 172 ++++++++++++++++++++++++++++++++-- src/apprt/embedded.zig | 156 ++++++++++++++++++++++++++++++ src/font/SharedGridSet.zig | 11 +++ src/termio/Termio.zig | 51 ++++++++-- src/termio/Thread.zig | 2 +- src/termio/stream_handler.zig | 32 ++++--- 7 files changed, 427 insertions(+), 31 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index fbfe3ee2c..8f83d0733 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -414,6 +414,31 @@ typedef struct { uintptr_t text_len; } ghostty_text_s; +typedef struct { + uint32_t codepoint; + uint32_t fg_rgb; + uint32_t bg_rgb; + uint32_t flags; +} ghostty_cell_s; + +typedef struct { + uint32_t cols; + uint32_t rows; + ghostty_cell_s* cells; + uintptr_t cells_len; + uint32_t cursor_x; + uint32_t cursor_y; + bool cursor_visible; + uint32_t default_fg; + uint32_t default_bg; + bool alt_screen; + bool cursor_keys; + bool bracketed_paste; + bool focus_event; + uint32_t mouse_event; + uint32_t mouse_format; +} ghostty_cells_s; + typedef enum { GHOSTTY_POINT_ACTIVE, GHOSTTY_POINT_VIEWPORT, @@ -1113,6 +1138,8 @@ GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t); GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool); GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool); +GHOSTTY_API bool ghostty_surface_detach(ghostty_surface_t); +GHOSTTY_API bool ghostty_surface_attach(ghostty_surface_t); GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t); GHOSTTY_API uint64_t ghostty_surface_foreground_pid(ghostty_surface_t); @@ -1162,6 +1189,13 @@ GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t, ghostty_text_s*); GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); +GHOSTTY_API void ghostty_surface_set_data_callback(ghostty_surface_t, + void (*)(void*, const uint8_t*, uintptr_t), + void*); +GHOSTTY_API void ghostty_surface_send_input_raw(ghostty_surface_t, const uint8_t*, uintptr_t); +GHOSTTY_API bool ghostty_surface_read_cells(ghostty_surface_t, ghostty_cells_s*); +GHOSTTY_API void ghostty_surface_free_cells(ghostty_surface_t, ghostty_cells_s*); + #ifdef __APPLE__ GHOSTTY_API void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); GHOSTTY_API void* ghostty_surface_quicklook_font(ghostty_surface_t); diff --git a/src/Surface.zig b/src/Surface.zig index 410f717b0..cff8cde6d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -94,6 +94,10 @@ renderer_thread: rendererpkg.Thread, /// The actual thread renderer_thr: std.Thread, +/// True when the renderer is detached. The renderer thread is stopped, +/// GPU resources are freed, but the PTY/terminal remain alive. +renderer_detached: bool = false, + /// Mouse state. mouse: Mouse, @@ -791,8 +795,8 @@ pub fn deinit(self: *Surface) void { // Stop search thread if (self.search) |*s| s.deinit(); - // Stop rendering thread - { + // Stop rendering thread (skip if already detached) + if (!self.renderer_detached) { self.renderer_thread.stop.notify() catch |err| log.err("error notifying renderer thread to stop, may stall err={}", .{err}); self.renderer_thr.join(); @@ -810,8 +814,10 @@ pub fn deinit(self: *Surface) void { // We need to deinit AFTER everything is stopped, since there are // shared values between the two threads. - self.renderer_thread.deinit(); - self.renderer.deinit(); + if (!self.renderer_detached) { + self.renderer_thread.deinit(); + self.renderer.deinit(); + } self.io_thread.deinit(); self.mouse.selection_gesture.deinit(&self.io.terminal); self.io.deinit(); @@ -843,6 +849,150 @@ pub fn close(self: *Surface) void { self.rt_surface.close(self.needsConfirmQuit()); } +/// Detach the renderer from this surface. This frees all GPU resources +/// and stops the renderer thread. The PTY and terminal state remain alive. +/// Must be called from the main thread. Returns false if already detached. +pub fn detachRenderer(self: *Surface) bool { + if (self.renderer_detached) return false; + + log.info("detaching renderer from surface addr={x}", .{@intFromPtr(self)}); + + // The IO thread stays alive while detached. Disconnect its renderer + // handles before destroying renderer-thread resources so PTY output does + // not notify freed async handles or push to a freed mailbox. + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.io.setRenderer(null, null); + } + + // Stop rendering thread + { + self.renderer_thread.stop.notify() catch |err| + log.err("error notifying renderer thread to stop, may stall err={}", .{err}); + self.renderer_thr.join(); + + // We need to become the active rendering thread again for cleanup + self.renderer.threadEnter(self.rt_surface) catch unreachable; + } + + // Clear the display callback on the Metal layer so CoreAnimation + // stops calling into the renderer. Without this, CA's display + // callback fires after deinit and accesses freed Metal resources. + self.renderer.api.layer.setDisplayCallback(null, null); + + // Deinit renderer thread resources (event loop, timers, mailbox) + self.renderer_thread.deinit(); + + // Deinit the renderer (frees all GPU resources) + self.renderer.deinit(); + + self.renderer_detached = true; + return true; +} + +/// Attach (or reattach) the renderer to this surface. This rebuilds the +/// GPU pipeline, font atlas, and restarts the renderer thread. The terminal +/// grid is redrawn on the first frame. +/// Must be called from the main thread. Returns false if already attached. +/// The caller must provide the current full configuration. +pub fn attachRenderer(self: *Surface, config: *const configpkg.Config) !bool { + if (!self.renderer_detached) return false; + + log.info("attaching renderer to surface addr={x}", .{@intFromPtr(self)}); + + // Initialize our renderer with the current surface. + try Renderer.surfaceInit(self.rt_surface); + + // Determine our DPI configurations. + const content_scale = try self.rt_surface.getContentScale(); + const x_dpi = content_scale.x * font.face.default_dpi; + const y_dpi = content_scale.y * font.face.default_dpi; + + // Get our current font grid (we kept the reference alive during detach). + const font_grid = self.app.font_grid_set.get(self.font_grid_key) orelse + return error.FontGridNotFound; + + // Rebuild our size struct from current state. + const size: rendererpkg.Size = size: { + var size: rendererpkg.Size = .{ + .screen = screen: { + const surface_size = try self.rt_surface.getSize(); + break :screen .{ + .width = surface_size.width, + .height = surface_size.height, + }; + }, + .cell = font_grid.cellSize(), + .padding = .{}, + }; + + const explicit: rendererpkg.Padding = self.config.scaledPadding( + x_dpi, + y_dpi, + ); + if (self.config.window_padding_balance != .false) { + size.balancePadding(explicit, self.config.window_padding_balance); + } else { + size.padding = explicit; + } + + break :size size; + }; + self.size = size; + + const app_mailbox: App.Mailbox = .{ .rt_app = self.rt_app, .mailbox = &self.app.mailbox }; + + // Rebuild the renderer + const renderer_impl = try Renderer.init(self.alloc, .{ + .config = try Renderer.DerivedConfig.init(self.alloc, config), + .font_grid = font_grid, + .size = size, + .surface_mailbox = .{ .surface = self, .app = app_mailbox }, + .rt_surface = self.rt_surface, + .thread = &self.renderer_thread, + }); + + // Create the renderer thread + const render_thread = try rendererpkg.Thread.init( + self.alloc, + config, + self.rt_surface, + &self.renderer, + &self.renderer_state, + app_mailbox, + ); + + // Update our fields + self.renderer = renderer_impl; + self.renderer_thread = render_thread; + self.renderer_state.terminal = &self.io.terminal; + + // Finalize surface setup on the main thread + try self.renderer.finalizeSurfaceInit(self.rt_surface); + + // Start our renderer thread + self.renderer_thr = try std.Thread.spawn( + .{}, + rendererpkg.Thread.threadMain, + .{&self.renderer_thread}, + ); + self.renderer_thr.setName("renderer") catch {}; + + // Reconnect the live IO thread to the newly-created renderer thread. + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.io.setRenderer(self.renderer_thread.wakeup, self.renderer_thread.mailbox); + } + + // Trigger a full redraw + try self.resize(self.size.screen); + + self.renderer_detached = false; + return true; +} + /// Returns a mailbox that can be used to send messages to this surface. inline fn surfaceMailbox(self: *Surface) Mailbox { return .{ @@ -879,6 +1029,8 @@ fn queueIo( /// is in the middle of animation (such as a resize, etc.) or when /// the render timer is managed manually by the apprt. pub fn draw(self: *Surface) !void { + if (self.renderer_detached) return; + // Renderers are required to support `drawFrame` being called from // the main thread, so that they can update contents during resize. try self.renderer.drawFrame(true); @@ -2438,6 +2590,8 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void { /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. fn queueRender(self: *Surface) !void { + if (self.renderer_detached) return; + try self.renderer_thread.wakeup.notify(); } @@ -3268,6 +3422,8 @@ pub fn textCallback(self: *Surface, text: []const u8) !void { /// of focus state. This is used to pause rendering when the surface /// is not visible, and also re-render when it becomes visible again. pub fn occlusionCallback(self: *Surface, visible: bool) !void { + if (self.renderer_detached) return; + // Crash metadata in case we crash in here crash.sentry.thread_state = self.crashThreadState(); defer crash.sentry.thread_state = null; @@ -3292,9 +3448,11 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { self.focused = focused; // Notify our render thread of the new state - _ = self.renderer_thread.mailbox.push(.{ - .focus = focused, - }, .{ .forever = {} }); + if (!self.renderer_detached) { + _ = self.renderer_thread.mailbox.push(.{ + .focus = focused, + }, .{ .forever = {} }); + } if (!focused) unfocused: { // If we lost focus and we have a keypress, then we want to send a key diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7310159cc..4a21eb8a5 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -19,6 +19,7 @@ const CoreApp = @import("../App.zig"); const CoreInspector = @import("../inspector/main.zig").Inspector; const CoreSurface = @import("../Surface.zig"); const configpkg = @import("../config.zig"); +const termio = @import("../termio.zig"); const Config = configpkg.Config; const String = @import("../main_c.zig").String; @@ -421,6 +422,11 @@ pub const Surface = struct { /// that getTitle works without the implementer needing to save it. title: ?[:0]const u8 = null, + /// Callback for receiving PTY output data. Called from the IO thread + /// with raw bytes read from the PTY. + data_callback: ?*const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void = null, + data_callback_userdata: ?*anyopaque = null, + /// Surface initialization options. pub const Options = extern struct { /// The platform that this surface is being initialized for and @@ -775,6 +781,17 @@ pub const Surface = struct { }; } + pub fn detachRenderer(self: *Surface) bool { + return self.core_surface.detachRenderer(); + } + + pub fn attachRenderer(self: *Surface) bool { + return self.core_surface.attachRenderer(&self.app.config) catch |err| { + log.err("error attaching renderer err={}", .{err}); + return false; + }; + } + pub fn updateContentScale(self: *Surface, x: f64, y: f64) void { // We are an embedded API so the caller can send us all sorts of // garbage. We want to make sure that the float values are valid @@ -1305,6 +1322,31 @@ pub const CAPI = struct { } }; + const Cell = extern struct { + codepoint: u32 = 0, + fg_rgb: u32 = 0, + bg_rgb: u32 = 0, + flags: u32 = 0, + }; + + const Cells = extern struct { + cols: u32 = 0, + rows: u32 = 0, + cells: ?[*]Cell = null, + cells_len: usize = 0, + cursor_x: u32 = 0, + cursor_y: u32 = 0, + cursor_visible: bool = false, + default_fg: u32 = 0, + default_bg: u32 = 0, + alt_screen: bool = false, + cursor_keys: bool = false, + bracketed_paste: bool = false, + focus_event: bool = false, + mouse_event: u32 = 0, + mouse_format: u32 = 0, + }; + // ghostty_point_s const Point = extern struct { tag: Tag, @@ -1756,6 +1798,120 @@ pub const CAPI = struct { surface.occlusionCallback(visible); } + /// Detach the surface's renderer from its platform view. Frees all GPU + /// resources and stops the renderer thread. The PTY and terminal state + /// remain alive. The surface pointer remains valid. + export fn ghostty_surface_detach(surface: *Surface) bool { + return surface.detachRenderer(); + } + + /// Attach (or reattach) the surface's renderer to a platform view. + /// Rebuilds the GPU pipeline and restarts the renderer thread. + export fn ghostty_surface_attach(surface: *Surface) bool { + return surface.attachRenderer(); + } + + /// Set a callback that is called with raw PTY output data. + /// Pass null to clear a previously set callback. + export fn ghostty_surface_set_data_callback( + surface: *Surface, + callback: ?*const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void, + userdata: ?*anyopaque, + ) void { + surface.data_callback = callback; + surface.data_callback_userdata = userdata; + } + + /// Send raw bytes directly to the PTY input without any paste processing. + export fn ghostty_surface_send_input_raw( + surface: *Surface, + ptr: [*]const u8, + len: usize, + ) void { + if (len == 0) return; + surface.core_surface.io.queueMessage( + termio.Message.writeReq(global.alloc, ptr[0..len]) catch |err| { + log.warn("failed to queue raw input err={}", .{err}); + return; + }, + .unlocked, + ); + } + + /// Read the terminal cell grid including codepoints, colors, cursor, and mode flags. + export fn ghostty_surface_read_cells( + surface: *Surface, + out: *Cells, + ) bool { + const core = &surface.core_surface; + core.renderer_state.mutex.lock(); + defer core.renderer_state.mutex.unlock(); + + const term = core.io.terminal; + const screen = term.screens.active; + const pages = &screen.pages; + + out.* = .{ + .cols = @intCast(pages.cols), + .rows = @intCast(pages.rows), + .cells = null, + .cells_len = 0, + .cursor_x = @intCast(screen.cursor.x), + .cursor_y = @intCast(screen.cursor.y), + .cursor_visible = true, + .default_fg = rgb: { + const rgb = term.colors.foreground.get() orelse break :rgb 0xFFFFFFFF; + break :rgb @as(u32, rgb.r) << 16 | @as(u32, rgb.g) << 8 | @as(u32, rgb.b); + }, + .default_bg = rgb: { + const rgb = term.colors.background.get() orelse break :rgb 0xFF000000; + break :rgb @as(u32, rgb.r) << 16 | @as(u32, rgb.g) << 8 | @as(u32, rgb.b); + }, + .alt_screen = core.io.terminal.modes.get(.alt_screen), + .cursor_keys = core.io.terminal.modes.get(.cursor_keys), + .bracketed_paste = core.io.terminal.modes.get(.bracketed_paste), + .focus_event = core.io.terminal.modes.get(.focus_event), + .mouse_event = 0, + .mouse_format = 0, + }; + + const total = pages.cols * pages.rows; + if (total == 0) return true; + + const cells_ptr = global.alloc.alloc(Cell, total) catch return false; + out.cells = cells_ptr.ptr; + out.cells_len = @intCast(total); + + var row_idx: usize = 0; + var y: usize = 0; + while (y < pages.rows) : (y += 1) { + var x: usize = 0; + while (x < pages.cols) : (x += 1) { + const pt = terminal.point.Point{ .screen = .{ .x = @intCast(x), .y = @intCast(y) } }; + const page_pin = pages.pin(pt) orelse continue; + const rc = page_pin.rowAndCell(); + const idx = row_idx * pages.cols + x; + cells_ptr[idx] = .{ + .codepoint = rc.cell.codepoint(), + }; + } + row_idx += 1; + } + + return true; + } + + /// Free the cells array previously allocated by ghostty_surface_read_cells. + export fn ghostty_surface_free_cells( + _: *Surface, + out: *Cells, + ) void { + if (out.cells) |ptr| { + global.alloc.free(ptr[0..out.cells_len]); + } + out.* = .{}; + } + /// Filter the mods if necessary. This handles settings such as /// `macos-option-as-alt`. The filtered mods should be used for /// key translation but should NOT be sent back via the `_key` diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 9d8148bdc..0031b991c 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -389,6 +389,17 @@ fn collection( return c; } +/// Retrieve the SharedGrid for an existing key. Returns null if the +/// key is not found. The caller must already hold a reference to the key. +/// Thread-safe: acquires the internal lock. +pub fn get(self: *SharedGridSet, key: Key) ?*SharedGrid { + self.lock.lock(); + defer self.lock.unlock(); + + const entry = self.map.getEntry(key) orelse return null; + return entry.value_ptr.grid; +} + /// Decrement the ref count for the given key. If the ref count is zero, /// the grid will be deinitialized and removed from the map.j:w pub fn deref(self: *SharedGridSet, key: Key) void { diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 1d1bfe25a..d7e192037 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -45,10 +45,10 @@ renderer_state: *renderer.State, /// A handle to wake up the renderer. This hints to the renderer that /// a repaint should happen. -renderer_wakeup: xev.Async, +renderer_wakeup: ?xev.Async, /// The mailbox for notifying the renderer of things. -renderer_mailbox: *renderer.Thread.Mailbox, +renderer_mailbox: ?*renderer.Thread.Mailbox, /// The mailbox for communicating with the surface. surface_mailbox: apprt.surface.Mailbox, @@ -324,6 +324,21 @@ pub fn deinit(self: *Termio) void { if (self.thread_enter_state) |v| v.destroy(); } +/// Update the renderer thread handles used by termio and the stream parser. +/// +/// The caller must hold renderer_state.mutex. This serializes updates with +/// output processing, which also touches these handles under the same mutex. +pub fn setRenderer( + self: *Termio, + renderer_wakeup: ?xev.Async, + renderer_mailbox: ?*renderer.Thread.Mailbox, +) void { + self.renderer_wakeup = renderer_wakeup; + self.renderer_mailbox = renderer_mailbox; + self.terminal_stream.handler.renderer_wakeup = renderer_wakeup; + self.terminal_stream.handler.renderer_mailbox = renderer_mailbox; +} + pub fn threadEnter( self: *Termio, thread: *termio.Thread, @@ -498,8 +513,10 @@ pub fn resize( } // Mail the renderer so that it can update the GPU and re-render - _ = self.renderer_mailbox.push(.{ .resize = size }, .{ .forever = {} }); - self.renderer_wakeup.notify() catch {}; + if (self.renderer_mailbox) |mailbox| { + _ = mailbox.push(.{ .resize = size }, .{ .forever = {} }); + } + if (self.renderer_wakeup) |wakeup| wakeup.notify() catch {}; } /// Make a size report. @@ -537,7 +554,7 @@ pub fn resetSynchronizedOutput(self: *Termio) void { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); self.terminal.modes.set(.synchronized_output, false); - self.renderer_wakeup.notify() catch {}; + if (self.renderer_wakeup) |wakeup| wakeup.notify() catch {}; } /// Clear the screen. @@ -613,7 +630,7 @@ pub fn jumpToPrompt(self: *Termio, delta: isize) !void { self.terminal.screens.active.scroll(.{ .delta_prompt = delta }); } - try self.renderer_wakeup.notify(); + if (self.renderer_wakeup) |wakeup| try wakeup.notify(); } /// Called when focus is gained or lost (when focus events are enabled) @@ -641,6 +658,9 @@ pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void { /// call with pty data but it is also called by the read thread when using /// an exec subprocess. pub fn processOutput(self: *Termio, buf: []const u8) void { + // Notify data callback if registered (raw PTY output). + self.invokeDataCallback(buf); + // We are modifying terminal state from here on out and we need // the lock to grab our read data. self.renderer_state.mutex.lock(); @@ -648,6 +668,17 @@ pub fn processOutput(self: *Termio, buf: []const u8) void { self.processOutputLocked(buf); } +fn invokeDataCallback(self: *Termio, buf: []const u8) void { + if (buf.len == 0) return; + const surface = self.surface_mailbox.surface; + const embedded: ?*apprt.embedded.Surface = @ptrCast(@alignCast(surface)); + if (embedded) |s| { + const callback = s.data_callback orelse return; + const userdata = s.data_callback_userdata; + callback(userdata, buf.ptr, buf.len); + } +} + /// Process output from readdata but the lock is already held. fn processOutputLocked(self: *Termio, buf: []const u8) void { // Schedule a render. We can call this first because we have the lock. @@ -665,9 +696,11 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void { } self.last_cursor_reset = now; - _ = self.renderer_mailbox.push(.{ - .reset_cursor_blink = {}, - }, .{ .instant = {} }); + if (self.renderer_mailbox) |mailbox| { + _ = mailbox.push(.{ + .reset_cursor_blink = {}, + }, .{ .instant = {} }); + } } else |err| { log.warn("failed to get current time err={}", .{err}); } diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index ce4c1f4af..1174c0a46 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -357,7 +357,7 @@ fn drainMailbox( // Trigger a redraw after we've drained so we don't waste cyces // messaging a redraw. if (redraw) { - try io.renderer_wakeup.notify(); + if (io.renderer_wakeup) |wakeup| try wakeup.notify(); } } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index fb3a6b3ff..79532e95a 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -34,11 +34,11 @@ pub const StreamHandler = struct { renderer_state: *renderer.State, /// The mailbox for notifying the renderer of things. - renderer_mailbox: *renderer.Thread.Mailbox, + renderer_mailbox: ?*renderer.Thread.Mailbox, /// A handle to wake up the renderer. This hints to the renderer that /// a repaint should happen. - renderer_wakeup: xev.Async, + renderer_wakeup: ?xev.Async, /// The default cursor state. This is used with CSI q. This is /// set to true when we're currently in the default cursor state. @@ -102,7 +102,7 @@ pub const StreamHandler = struct { /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. pub inline fn queueRender(self: *StreamHandler) !void { - try self.renderer_wakeup.notify(); + if (self.renderer_wakeup) |wakeup| try wakeup.notify(); } /// Change the configuration for this handler. @@ -148,10 +148,12 @@ pub const StreamHandler = struct { self: *StreamHandler, msg: renderer.Message, ) void { + const mailbox = self.renderer_mailbox orelse return; + // See termio.Mailbox.send for more details on how this works. // Try instant first. If it works then we can return. - if (self.renderer_mailbox.push(msg, .{ .instant = {} }) > 0) { + if (mailbox.push(msg, .{ .instant = {} }) > 0) { return; } @@ -160,16 +162,18 @@ pub const StreamHandler = struct { // and then try again. self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.lock(); - self.renderer_wakeup.notify() catch |err| { - // This is an EXTREMELY unlikely case. We still don't return - // and attempt to send the message because its most likely - // that everything is fine, but log in case a freeze happens. - log.warn( - "failed to notify renderer, may deadlock err={}", - .{err}, - ); - }; - _ = self.renderer_mailbox.push(msg, .{ .forever = {} }); + if (self.renderer_wakeup) |wakeup| { + wakeup.notify() catch |err| { + // This is an EXTREMELY unlikely case. We still don't return + // and attempt to send the message because its most likely + // that everything is fine, but log in case a freeze happens. + log.warn( + "failed to notify renderer, may deadlock err={}", + .{err}, + ); + }; + } + _ = mailbox.push(msg, .{ .forever = {} }); } pub fn vt(