diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:23 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:51 +0300 |
| commit | 5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch) | |
| tree | 672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/apprt/gtk/media.zig | |
| parent | 32abb3776668ee768200019c170956f67fe33a86 (diff) | |
chore: update libghostty vendor
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/media.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/apprt/gtk/media.zig | 91 |
1 files changed, 73 insertions, 18 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/media.zig b/vendor/libghostty-vt/src/apprt/gtk/media.zig index 1015c933..7883bf37 100644 --- a/vendor/libghostty-vt/src/apprt/gtk/media.zig +++ b/vendor/libghostty-vt/src/apprt/gtk/media.zig @@ -44,9 +44,38 @@ pub fn fromResource(path: [:0]const u8) ?*gtk.MediaFile { return gtk.MediaFile.newForResource(path); } -pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) void { - // If the audio file is marked as required, we'll emit an error if - // there was a problem playing it. Otherwise there will be silence. +/// Get-or-create a reusable bell MediaFile targeting `path`. +/// +/// `current` is the surface's currently-cached MediaFile (or null). If it +/// already targets `path` it is returned unchanged; otherwise it is unref'd and +/// a fresh MediaFile is built for `path`. Returns null (after freeing `current`) +/// if `path` is inaccessible, leaving the caller's slot empty. +/// +/// Reusing one MediaFile per surface is what prevents the GStreamer pipeline +/// leak: `gtk.MediaFile.newForFilename` spins up a full pipeline (and, via the +/// GTK4 GStreamer backend's GL sink, gstglcontext/gldisplay-event threads) that +/// is never torn down on the happy path, so allocating one per bell leaked a +/// pipeline + its threads on every ring. See the caller in surface.zig. +pub fn bellMediaFile( + current: ?*gtk.MediaFile, + path: [:0]const u8, + required: bool, +) ?*gtk.MediaFile { + if (current) |media_file| { + if (isForPath(media_file, path)) return media_file; + media_file.unref(); + } + + const media_file = fromFilename(path) orelse return null; + + // If the audio file is marked as required, we'll emit an error if there + // was a problem playing it. Otherwise there will be silence. We connect + // this once, here, because the MediaFile is reused across bells. + // + // NOTE: we intentionally do NOT connect notify::ended to unref. The + // MediaFile is owned by the surface and replayed via `seek(0)` for every + // bell; unref'ing on `ended` is precisely what previously discarded (and + // leaked) a pipeline per ring. if (required) { _ = gobject.Object.signals.notify.connect( media_file, @@ -57,21 +86,27 @@ pub fn playMediaFile(media_file: *gtk.MediaFile, volume: f64, required: bool) vo ); } - // Watch for the "ended" signal so that we can clean up after - // ourselves. - _ = gobject.Object.signals.notify.connect( - media_file, - ?*anyopaque, - mediaFileEnded, - null, - .{ .detail = "ended" }, - ); + return media_file; +} +/// (Re)play `media_file` at `volume`. `seek(0)` rewinds first so that a +/// previously-ended stream plays again; without it playback only ever happens +/// once (see #8957). Safe on a freshly-created stream as well. +pub fn playBell(media_file: *gtk.MediaFile, volume: f64) void { const media_stream = media_file.as(gtk.MediaStream); media_stream.setVolume(volume); + media_stream.seek(0); media_stream.play(); } +/// Whether `media_file` was created for `path`. +fn isForPath(media_file: *gtk.MediaFile, path: [:0]const u8) bool { + const file = media_file.getFile() orelse return false; + const cur = file.getPath() orelse return false; + defer glib.free(cur); + return std.mem.eql(u8, std.mem.span(cur), path); +} + fn mediaFileError( media_file: *gtk.MediaFile, _: *gobject.ParamSpec, @@ -93,10 +128,30 @@ fn mediaFileError( }); } -fn mediaFileEnded( - media_file: *gtk.MediaFile, - _: *gobject.ParamSpec, - _: ?*anyopaque, -) callconv(.c) void { - media_file.unref(); +test "bellMediaFile reuses one MediaFile per path" { + // Regression guard for the audio-bell thread leak: each bell must replay a + // single cached MediaFile, not allocate a fresh GStreamer pipeline (which + // leaked gstglcontext/gldisplay-event threads) per ring. We assert the + // reuse contract of bellMediaFile directly; this needs no display and no + // playback (MediaFile is lazy), only that the path comparison drives reuse. + const testing = std.testing; + + // The files need not exist: MediaFile only records the path until played. + const path_a: [:0]const u8 = "/tmp/ghostty-bell-test-a.oga"; + const path_b: [:0]const u8 = "/tmp/ghostty-bell-test-b.oga"; + + var current = bellMediaFile(null, path_a, false) orelse return error.SkipZigTest; + const first = current; + try testing.expect(isForPath(current, path_a)); + + // Same path => identical object (the leak regression is rebuilding here). + current = bellMediaFile(current, path_a, false).?; + try testing.expectEqual(first, current); + + // Changed path => rebuilt object targeting the new path (old one freed). + current = bellMediaFile(current, path_b, false) orelse return error.SkipZigTest; + try testing.expect(isForPath(current, path_b)); + try testing.expect(!isForPath(current, path_a)); + + current.unref(); } |
