Skip to content

Commit 7866469

Browse files
committed
Stop aether-wp from eating GPU by capping framerate to 30fps, dropping audio decoding, and adding --stop/--cpu flags with proper layer surface cleanup
1 parent 7a75874 commit 7866469

4 files changed

Lines changed: 183 additions & 26 deletions

File tree

cmd/aether-wp/main.go

Lines changed: 155 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@ package main
99
#include <stdlib.h>
1010
1111
static GstElement *pipeline = NULL;
12+
static GtkWindow *window = NULL;
13+
14+
// Tear down pipeline + window inside the running main loop so the
15+
// Wayland compositor receives the surface-destroy before the loop exits.
16+
static gboolean cleanup_and_quit(gpointer data) {
17+
if (pipeline) {
18+
gst_element_set_state(pipeline, GST_STATE_NULL);
19+
gst_object_unref(pipeline);
20+
pipeline = NULL;
21+
}
22+
if (window) {
23+
gtk_widget_destroy(GTK_WIDGET(window));
24+
window = NULL;
25+
}
26+
gtk_main_quit();
27+
return FALSE;
28+
}
29+
30+
// Thread-safe: can be called from a Go goroutine.
31+
static void request_shutdown(void) {
32+
g_idle_add(cleanup_and_quit, NULL);
33+
}
1234
1335
static gboolean bus_callback(GstBus *bus, GstMessage *msg, gpointer data) {
1436
switch (GST_MESSAGE_TYPE(msg)) {
@@ -22,7 +44,7 @@ static gboolean bus_callback(GstBus *bus, GstMessage *msg, gpointer data) {
2244
gst_message_parse_error(msg, &err, NULL);
2345
g_printerr("aether-wp: %s\n", err->message);
2446
g_error_free(err);
25-
gtk_main_quit();
47+
cleanup_and_quit(NULL);
2648
break;
2749
}
2850
default:
@@ -31,13 +53,13 @@ static gboolean bus_callback(GstBus *bus, GstMessage *msg, gpointer data) {
3153
return TRUE;
3254
}
3355
34-
static int run_wallpaper(const char *path) {
56+
static int run_wallpaper(const char *path, int force_cpu) {
3557
int argc = 0;
3658
gtk_init(&argc, NULL);
3759
gst_init(&argc, NULL);
3860
3961
// Create window on the background layer
40-
GtkWindow *window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
62+
window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
4163
gtk_window_set_decorated(window, FALSE);
4264
gtk_layer_init_for_window(window);
4365
gtk_layer_set_layer(window, GTK_LAYER_SHELL_LAYER_BACKGROUND);
@@ -54,34 +76,52 @@ static int run_wallpaper(const char *path) {
5476
return 1;
5577
}
5678
57-
// Try gtkglsink (OpenGL, hardware-accelerated) first, fall back to gtksink (CPU)
58-
GstElement *sink = gst_element_factory_make("gtkglsink", "sink");
79+
// Select video sink: try GPU-accelerated gtkglsink, fall back to CPU gtksink
80+
GstElement *sink = NULL;
5981
GstElement *video_sink = NULL;
60-
if (sink) {
61-
// Wrap gtkglsink in glsinkbin for proper GL pipeline
62-
video_sink = gst_element_factory_make("glsinkbin", "glbin");
63-
if (video_sink) {
64-
g_object_set(video_sink, "sink", sink, NULL);
65-
} else {
66-
gst_object_unref(sink);
67-
sink = NULL;
82+
int using_gpu = 0;
83+
84+
if (!force_cpu) {
85+
sink = gst_element_factory_make("gtkglsink", "sink");
86+
if (sink) {
87+
video_sink = gst_element_factory_make("glsinkbin", "glbin");
88+
if (video_sink) {
89+
g_object_set(video_sink, "sink", sink, NULL);
90+
using_gpu = 1;
91+
} else {
92+
gst_object_unref(sink);
93+
sink = NULL;
94+
}
6895
}
6996
}
97+
98+
// CPU fallback
7099
if (!sink) {
71100
sink = gst_element_factory_make("gtksink", "sink");
72-
video_sink = GST_ELEMENT(gst_object_ref(sink));
101+
if (sink) {
102+
video_sink = GST_ELEMENT(gst_object_ref(sink));
103+
}
73104
}
74105
if (!sink) {
75106
g_printerr("aether-wp: no GTK video sink available (install gst-plugins-good or gst-plugin-gtk)\n");
76107
gst_object_unref(pipeline);
77108
return 1;
78109
}
79110
111+
g_printerr("aether-wp: using %s rendering\n", using_gpu ? "GPU" : "CPU");
112+
80113
g_object_set(pipeline, "video-sink", video_sink, NULL);
81114
g_object_set(pipeline, "volume", 0.0, NULL);
82115
83-
// Enable hardware decoding flags
84-
g_object_set(pipeline, "flags", 0x63, NULL); // video + audio + native-video + deinterlace
116+
// Enable hardware decoding flags (video + native-video + deinterlace, no audio)
117+
g_object_set(pipeline, "flags", 0x61, NULL);
118+
119+
// Cap frame rate to 30fps to reduce GPU load
120+
GstElement *rate = gst_element_factory_make("videorate", "rate");
121+
if (rate) {
122+
g_object_set(rate, "max-rate", 30, NULL);
123+
g_object_set(pipeline, "video-filter", rate, NULL);
124+
}
85125
86126
// Get the GTK widget from the sink and add to window
87127
GtkWidget *video_widget = NULL;
@@ -115,8 +155,6 @@ static int run_wallpaper(const char *path) {
115155
gtk_widget_show_all(GTK_WIDGET(window));
116156
gtk_main();
117157
118-
gst_element_set_state(pipeline, GST_STATE_NULL);
119-
gst_object_unref(pipeline);
120158
return 0;
121159
}
122160
*/
@@ -125,33 +163,129 @@ import "C"
125163
import (
126164
"fmt"
127165
"os"
166+
"os/exec"
128167
"os/signal"
168+
"path/filepath"
169+
"strconv"
170+
"strings"
129171
"syscall"
172+
"time"
130173
"unsafe"
131174
)
132175

176+
func pidFilePath() string {
177+
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
178+
return filepath.Join(dir, "aether-wp.pid")
179+
}
180+
return "/tmp/aether-wp.pid"
181+
}
182+
183+
func writePidFile() {
184+
_ = os.WriteFile(pidFilePath(), []byte(strconv.Itoa(os.Getpid())), 0644)
185+
}
186+
187+
func removePidFile() {
188+
_ = os.Remove(pidFilePath())
189+
}
190+
191+
func isProcessAlive(pid int) bool {
192+
proc, err := os.FindProcess(pid)
193+
if err != nil {
194+
return false
195+
}
196+
return proc.Signal(syscall.Signal(0)) == nil
197+
}
198+
199+
// stopViaPidFile sends SIGTERM to the process in the PID file and waits for it to exit.
200+
func stopViaPidFile() bool {
201+
data, err := os.ReadFile(pidFilePath())
202+
if err != nil {
203+
return false
204+
}
205+
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
206+
if err != nil {
207+
removePidFile()
208+
return false
209+
}
210+
proc, err := os.FindProcess(pid)
211+
if err != nil {
212+
removePidFile()
213+
return false
214+
}
215+
if proc.Signal(syscall.SIGTERM) != nil {
216+
removePidFile()
217+
return false
218+
}
219+
// Wait up to 2 seconds for graceful shutdown
220+
for i := 0; i < 20; i++ {
221+
time.Sleep(100 * time.Millisecond)
222+
if !isProcessAlive(pid) {
223+
removePidFile()
224+
return true
225+
}
226+
}
227+
// Force kill if still alive
228+
_ = proc.Signal(syscall.SIGKILL)
229+
removePidFile()
230+
return true
231+
}
232+
233+
// stopAll stops any running instance using PID file first, then pkill as fallback.
234+
// Only safe to call from --stop (not from a new instance, since pkill would kill itself).
235+
func stopAll() {
236+
if !stopViaPidFile() {
237+
_ = exec.Command("pkill", "-x", "aether-wp").Run()
238+
}
239+
fmt.Fprintln(os.Stderr, "aether-wp: stopped")
240+
}
241+
133242
func main() {
134243
if len(os.Args) < 2 {
135-
fmt.Fprintln(os.Stderr, "Usage: aether-wp <media-file>")
244+
fmt.Fprintln(os.Stderr, "Usage: aether-wp [--cpu] <media-file>")
245+
fmt.Fprintln(os.Stderr, " aether-wp --stop")
136246
os.Exit(1)
137247
}
138248

249+
if os.Args[1] == "--stop" {
250+
stopAll()
251+
os.Exit(0)
252+
}
253+
254+
forceCPU := false
139255
path := os.Args[1]
256+
if path == "--cpu" {
257+
forceCPU = true
258+
if len(os.Args) < 3 {
259+
fmt.Fprintln(os.Stderr, "Usage: aether-wp [--cpu] <media-file>")
260+
os.Exit(1)
261+
}
262+
path = os.Args[2]
263+
}
264+
140265
if _, err := os.Stat(path); os.IsNotExist(err) {
141266
fmt.Fprintf(os.Stderr, "aether-wp: file not found: %s\n", path)
142267
os.Exit(1)
143268
}
144269

270+
// Stop any previous instance via PID file (pkill not safe here — would kill us)
271+
stopViaPidFile()
272+
writePidFile()
273+
145274
// Clean shutdown on SIGTERM/SIGINT
146275
sig := make(chan os.Signal, 1)
147276
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
148277
go func() {
149278
<-sig
150-
C.gtk_main_quit()
279+
C.request_shutdown()
151280
}()
152281

282+
cpu := C.int(0)
283+
if forceCPU {
284+
cpu = 1
285+
}
153286
cpath := C.CString(path)
154287
defer C.free(unsafe.Pointer(cpath))
155-
rc := C.run_wallpaper(cpath)
288+
rc := C.run_wallpaper(cpath, cpu)
289+
removePidFile()
156290
os.Exit(int(rc))
157291
}

docs/cli.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,26 @@ aether --tab <name>
206206

207207
```bash
208208
aether-wp /path/to/video.mp4
209+
aether-wp --cpu /path/to/video.mp4
210+
aether-wp --stop
209211
```
210212

213+
| Flag | Description |
214+
|------|-------------|
215+
| `--stop` | Stop any running aether-wp instance and clean up the layer surface |
216+
| `--cpu` | Force CPU rendering (skip GPU-accelerated OpenGL sink) |
217+
211218
Aether launches `aether-wp` automatically when you apply a theme with an animated wallpaper (`.mp4`, `.webm`, `.gif`). You can also run it standalone.
212219

213220
**How it works:**
214221

215222
- Renders on the background layer via `gtk-layer-shell` (replaces `swaybg`)
216-
- GPU-accelerated playback using `gtkglsink` (OpenGL), falls back to `gtksink` (CPU)
223+
- GPU-accelerated playback using `gtkglsink` (OpenGL), auto-falls back to `gtksink` (CPU)
224+
- Frame rate capped at 30fps to reduce GPU load
217225
- Loops automatically on end-of-stream
218-
- Muted audio by default
219-
- Handles `SIGTERM`/`SIGINT` for clean shutdown
226+
- Muted audio (audio decoding disabled entirely)
227+
- PID file at `$XDG_RUNTIME_DIR/aether-wp.pid` for reliable process management
228+
- Handles `SIGTERM`/`SIGINT` for clean shutdown (tears down layer surface properly)
220229

221230
**Requirements:**
222231

frontend/.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
wailsjs/

internal/theme/applier.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,20 @@ func IsAetherWpAvailable() bool {
152152
return runtime.GOOS == "linux" && aetherWpPath() != ""
153153
}
154154

155+
// stopAetherWp stops any running aether-wp instance using its --stop flag,
156+
// falling back to pkill if the binary is not available.
157+
func stopAetherWp() {
158+
if wpBin := aetherWpPath(); wpBin != "" {
159+
_ = platform.RunAsync(wpBin, "--stop")
160+
} else {
161+
_ = platform.RunAsync("pkill", "-x", "aether-wp")
162+
}
163+
}
164+
155165
// applyWallpaperAetherWp starts aether-wp for animated wallpapers.
156166
func applyWallpaperAetherWp(mediaPath string) error {
157167
_ = platform.RunAsync("pkill", "-x", "swaybg")
158-
_ = platform.RunAsync("pkill", "-x", "aether-wp")
168+
stopAetherWp()
159169
time.Sleep(100 * time.Millisecond)
160170

161171
wpBin := aetherWpPath()
@@ -173,7 +183,7 @@ func applyWallpaperAetherWp(mediaPath string) error {
173183
// applyWallpaperSwaybg sets the wallpaper using swaybg (static images).
174184
func applyWallpaperSwaybg(symlinkPath string) error {
175185
_ = platform.RunAsync("pkill", "-x", "swaybg")
176-
_ = platform.RunAsync("pkill", "-x", "aether-wp")
186+
stopAetherWp()
177187
if err := platform.RunAsync("setsid", "uwsm-app", "--", "swaybg", "-i", symlinkPath, "-m", "fill"); err != nil {
178188
log.Printf("Warning: could not restart swaybg: %v", err)
179189
return err
@@ -215,6 +225,9 @@ func ApplyWallpaper(wallpaperPath string, settings Settings) error {
215225
// ClearTheme removes GTK CSS files, the theme override symlink and CSS,
216226
// and switches to the tokyo-night theme.
217227
func ClearTheme() error {
228+
// Stop any running animated wallpaper
229+
stopAetherWp()
230+
218231
// Delete Aether override CSS file in theme dir (cross-platform)
219232
overrideCss := filepath.Join(platform.ThemeDir(), "aether.override.css")
220233
if err := platform.DeleteFile(overrideCss); err != nil {

0 commit comments

Comments
 (0)