@@ -9,6 +9,28 @@ package main
99#include <stdlib.h>
1010
1111static 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
1335static 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"
125163import (
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+
133242func 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}
0 commit comments