From f0f3f4d8d816836e8e527dc1938841462913a1fa Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 5 Aug 2026 20:10:53 -0500 Subject: [PATCH] GTK: move audio bell processing to the application This fixes #13647 by using at most one GStreamer thread per application. This was previously addressed in #12815 which used at most one GStreamer thread per surface. Originally discussed in #12808. --- src/apprt/gtk/class/application.zig | 47 +++++++++++++++++++++++++++++ src/apprt/gtk/class/surface.zig | 42 +------------------------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 55d7f4055..50d173bbd 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -45,6 +45,7 @@ const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseCo const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog; const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts; const OpenURI = @import("../portal.zig").OpenURI; +const media = @import("../media.zig"); const log = std.log.scoped(.gtk_ghostty_application); @@ -223,6 +224,12 @@ pub const Application = extern struct { open_uri: OpenURI = undefined, + // The audio bell's MediaFile, reused across bells so we don't leak a + // GStreamer pipeline (and its GL threads) on every ring. Built lazily + // on the first audio bell and rebuilt when `bell-audio-path` changes; + // unref'd on dispose. See ringBell and media.zig. + bell_media: ?*gtk.MediaFile = null, + pub var offset: c_int = 0; }; @@ -1478,6 +1485,7 @@ pub const Application = extern struct { .init("quit", actionQuit, null), .init("reload-config", actionReloadConfig, null), .init("toggle-quick-terminal", actionToggleQuickTerminal, null), + .init("ring-bell", actionRingBell, null), }; ext.actions.add(Self, self, &actions); @@ -1549,6 +1557,11 @@ pub const Application = extern struct { priv.signal_source = null; } + if (priv.bell_media) |v| { + v.unref(); + priv.bell_media = null; + } + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), @@ -1948,6 +1961,40 @@ pub const Application = extern struct { ); } + pub fn actionRingBell( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Self, + ) callconv(.c) void { + const priv: *Private = self.private(); + const config = priv.config.get(); + + // Do our sound + if (config.@"bell-features".audio) audio: { + const config_path = config.@"bell-audio-path" orelse break :audio; + const path, const required = switch (config_path) { + .optional => |path| .{ path, false }, + .required => |path| .{ path, true }, + }; + + const volume = std.math.clamp( + config.@"bell-audio-volume", + 0.0, + 1.0, + ); + + // Reuse one MediaFile per application (rebuilt only when the path + // changes) so each bell replays the same pipeline instead of + // leaking a fresh one. Assign unconditionally: bellMediaFile frees + // any stale MediaFile and returns the current slot value (possibly + // null if the path is now inaccessible), so priv.bell_media never + // dangles. + priv.bell_media = media.bellMediaFile(priv.bell_media, path, required); + const media_file = priv.bell_media orelse break :audio; + media.playBell(media_file, volume); + } + } + //---------------------------------------------------------------- // Boilerplate/Noise diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index b9f896c93..e9aea6d51 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -36,7 +36,6 @@ const Window = @import("window.zig").Window; const InspectorWindow = @import("inspector_window.zig").InspectorWindow; const SplitTree = @import("split_tree.zig").SplitTree; const i18n = @import("../../../os/i18n.zig"); -const media = @import("../media.zig"); const global = @import("../../../global.zig"); const gtk_version = @import("../gtk_version.zig"); @@ -677,12 +676,6 @@ pub const Surface = extern struct { // false by a parent widget. bell_ringing: bool = false, - // The audio bell's MediaFile, reused across bells so we don't leak a - // GStreamer pipeline (and its GL threads) on every ring. Built lazily - // on the first audio bell and rebuilt when `bell-audio-path` changes; - // unref'd on dispose. See ringBell and media.zig. - bell_media: ?*gtk.MediaFile = null, - /// True if this surface is in an error state. This is currently /// a simple boolean with no additional information on WHAT the /// error state is, because we don't yet need it or use it. For now, @@ -1880,11 +1873,6 @@ pub const Surface = extern struct { priv.config = null; } - if (priv.bell_media) |v| { - v.unref(); - priv.bell_media = null; - } - if (priv.vadj_signal_group) |group| { group.setTarget(null); group.as(gobject.Object).unref(); @@ -2544,8 +2532,6 @@ pub const Surface = extern struct { /// Handle bell features that need to happen every time a BEL is received /// Currently this is audio and system but this could change in the future. fn ringBell(self: *Self) void { - const priv = self.private(); - // Emit the signal signals.bell.impl.emit( self, @@ -2557,33 +2543,7 @@ pub const Surface = extern struct { // Activate actions if they exist _ = self.as(gtk.Widget).activateAction("tab.ring-bell", null); _ = self.as(gtk.Widget).activateAction("win.ring-bell", null); - - const config = if (priv.config) |c| c.get() else return; - - // Do our sound - if (config.@"bell-features".audio) audio: { - const config_path = config.@"bell-audio-path" orelse break :audio; - const path, const required = switch (config_path) { - .optional => |path| .{ path, false }, - .required => |path| .{ path, true }, - }; - - const volume = std.math.clamp( - config.@"bell-audio-volume", - 0.0, - 1.0, - ); - - // Reuse one MediaFile per surface (rebuilt only when the path - // changes) so each bell replays the same pipeline instead of - // leaking a fresh one. Assign unconditionally: bellMediaFile frees - // any stale MediaFile and returns the current slot value (possibly - // null if the path is now inaccessible), so priv.bell_media never - // dangles. - priv.bell_media = media.bellMediaFile(priv.bell_media, path, required); - const media_file = priv.bell_media orelse break :audio; - media.playBell(media_file, volume); - } + _ = self.as(gtk.Widget).activateAction("app.ring-bell", null); } //---------------------------------------------------------------