fix(examples): gate first present, shutdown order, SIGTERM, timeline cleanup in layer
- commit
- 1988d1edfc30509bec290f11afdaa2c7f9576c8c
- author
- brice · · 2026-10-01 19:23 UTC
- parent
- 7c37286
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +26 -5
M examples/layer/main.go +26 -5
| @@ -19,6 +19,7 @@ import ( | |||
| 19 | 19 | "fmt" | |
| 20 | 20 | "os" | |
| 21 | 21 | "os/signal" | |
| 22 | + | "syscall" | |
| 22 | 23 | "time" | |
| 23 | 24 | ||
| 24 | 25 | "github.com/bnema/neferclient" | |
| @@ -30,6 +31,10 @@ import ( | |||
| 30 | 31 | const ( | |
| 31 | 32 | surfaceW, surfaceH = 240, 90 | |
| 32 | 33 | buttonColor = "#2060c0" | |
| 34 | + | ||
| 35 | + | // guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI | |
| 36 | + | // use the same bit order for them, so the plain bits can be copied. | |
| 37 | + | guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper | |
| 33 | 38 | ) | |
| 34 | 39 | ||
| 35 | 40 | type model struct{ count int } | |
| @@ -71,7 +76,7 @@ type app struct { | |||
| 71 | 76 | func main() { | |
| 72 | 77 | frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)") | |
| 73 | 78 | flag.Parse() | |
| 74 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | |
| 79 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 75 | 80 | defer stop() | |
| 76 | 81 | if err := run(ctx, *frames); err != nil { | |
| 77 | 82 | fmt.Fprintln(os.Stderr, "layer:", err) | |
| @@ -85,11 +90,14 @@ func run(ctx context.Context, frames int) (err error) { | |||
| 85 | 90 | return fmt.Errorf("connect: %w", err) | |
| 86 | 91 | } | |
| 87 | 92 | a := &app{conn: conn, frames: frames, cursor: neferclient.CursorDefault} | |
| 93 | + | // Shutdown order: close the connection first, which drops its descriptor | |
| 94 | + | // watches and destroys every Wayland object, then free the renderer and | |
| 95 | + | // its GPU resources (including the release eventfds). | |
| 88 | 96 | defer func() { | |
| 97 | + | err = errors.Join(err, conn.Close()) | |
| 89 | 98 | if a.r != nil { | |
| 90 | 99 | err = errors.Join(err, a.r.Close()) | |
| 91 | 100 | } | |
| 92 | - | err = errors.Join(err, conn.Close()) | |
| 93 | 101 | }() | |
| 94 | 102 | a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{ | |
| 95 | 103 | Level: neferclient.LayerTop, | |
| @@ -107,7 +115,7 @@ func run(ctx context.Context, frames int) (err error) { | |||
| 107 | 115 | for !a.done { | |
| 108 | 116 | select { | |
| 109 | 117 | case <-ctx.Done(): | |
| 110 | - | return nil | |
| 118 | + | return nil // interrupt or SIGTERM: a clean exit | |
| 111 | 119 | case <-conn.Wake(): | |
| 112 | 120 | if err = conn.Dispatch(a); err != nil { | |
| 113 | 121 | return fmt.Errorf("dispatch: %w", err) | |
| @@ -131,10 +139,19 @@ func run(ctx context.Context, frames int) (err error) { | |||
| 131 | 139 | ||
| 132 | 140 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens | |
| 133 | 141 | // the compositor's main device and picks a format and modifier from the list. | |
| 142 | + | // | |
| 143 | + | // Only the first complete feedback is used. A later one (the compositor | |
| 144 | + | // changing its preferred device or formats) is ignored: this demo assumes a | |
| 145 | + | // single GPU, and rebuilding the renderer is application policy that would | |
| 146 | + | // obscure the glue. A real application would create a new Renderer and | |
| 147 | + | // re-import its buffers here. | |
| 134 | 148 | func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() } | |
| 135 | 149 | ||
| 136 | 150 | func (a *app) Configure(neferclient.SurfaceID, int32, int32) { | |
| 137 | - | a.configured, a.canPresent = true, true | |
| 151 | + | if !a.configured { // later configures must not lift the frame-callback gate | |
| 152 | + | a.canPresent = true | |
| 153 | + | } | |
| 154 | + | a.configured = true | |
| 138 | 155 | a.resize() | |
| 139 | 156 | a.setup() | |
| 140 | 157 | } | |
| @@ -240,7 +257,7 @@ func (a *app) Key(ev *neferclient.KeyEvent) { | |||
| 240 | 257 | Text: ev.Text, // valid only during the call, as for Input | |
| 241 | 258 | Pressed: ev.Pressed, | |
| 242 | 259 | Repeat: ev.Repeat, | |
| 243 | - | Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super | |
| 260 | + | Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods), | |
| 244 | 261 | }) | |
| 245 | 262 | } | |
| 246 | 263 | ||
| @@ -284,6 +301,10 @@ func (a *app) present() error { | |||
| 284 | 301 | if err := a.surf.DestroyBuffer(rt.Buffer); err != nil { | |
| 285 | 302 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | |
| 286 | 303 | } | |
| 304 | + | // The release timeline of a buffer is imported under the buffer's id. | |
| 305 | + | if err := a.surf.DestroyTimeline(rt.Buffer); err != nil { | |
| 306 | + | return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err) | |
| 307 | + | } | |
| 287 | 308 | } | |
| 288 | 309 | if out.NewBuffer { | |
| 289 | 310 | buf := neferclient.Buffer{ | |
| @@ -19,6 +19,7 @@ import ( | |||||
| 19 | "fmt" | 19 | "fmt" | ||
| 20 | "os" | 20 | "os" | ||
| 21 | "os/signal" | 21 | "os/signal" | ||
| 22 | + | "syscall" | |||
| 22 | "time" | 23 | "time" | ||
| 23 | 24 | ||||
| 24 | "github.com/bnema/neferclient" | 25 | "github.com/bnema/neferclient" | ||
| @@ -30,6 +31,10 @@ import ( | |||||
| 30 | const ( | 31 | const ( | ||
| 31 | surfaceW, surfaceH = 240, 90 | 32 | surfaceW, surfaceH = 240, 90 | ||
| 32 | buttonColor = "#2060c0" | 33 | buttonColor = "#2060c0" | ||
| 34 | + | ||||
| 35 | + | // guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI | |||
| 36 | + | // use the same bit order for them, so the plain bits can be copied. | |||
| 37 | + | guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper | |||
| 33 | ) | 38 | ) | ||
| 34 | 39 | ||||
| 35 | type model struct{ count int } | 40 | type model struct{ count int } | ||
| @@ -71,7 +76,7 @@ type app struct { | |||||
| 71 | func main() { | 76 | func main() { | ||
| 72 | frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)") | 77 | frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)") | ||
| 73 | flag.Parse() | 78 | flag.Parse() | ||
| 74 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | 79 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 75 | defer stop() | 80 | defer stop() | ||
| 76 | if err := run(ctx, *frames); err != nil { | 81 | if err := run(ctx, *frames); err != nil { | ||
| 77 | fmt.Fprintln(os.Stderr, "layer:", err) | 82 | fmt.Fprintln(os.Stderr, "layer:", err) | ||
| @@ -85,11 +90,14 @@ func run(ctx context.Context, frames int) (err error) { | |||||
| 85 | return fmt.Errorf("connect: %w", err) | 90 | return fmt.Errorf("connect: %w", err) | ||
| 86 | } | 91 | } | ||
| 87 | a := &app{conn: conn, frames: frames, cursor: neferclient.CursorDefault} | 92 | a := &app{conn: conn, frames: frames, cursor: neferclient.CursorDefault} | ||
| 93 | + | // Shutdown order: close the connection first, which drops its descriptor | |||
| 94 | + | // watches and destroys every Wayland object, then free the renderer and | |||
| 95 | + | // its GPU resources (including the release eventfds). | |||
| 88 | defer func() { | 96 | defer func() { | ||
| 97 | + | err = errors.Join(err, conn.Close()) | |||
| 89 | if a.r != nil { | 98 | if a.r != nil { | ||
| 90 | err = errors.Join(err, a.r.Close()) | 99 | err = errors.Join(err, a.r.Close()) | ||
| 91 | } | 100 | } | ||
| 92 | - | err = errors.Join(err, conn.Close()) | |||
| 93 | }() | 101 | }() | ||
| 94 | a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{ | 102 | a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{ | ||
| 95 | Level: neferclient.LayerTop, | 103 | Level: neferclient.LayerTop, | ||
| @@ -107,7 +115,7 @@ func run(ctx context.Context, frames int) (err error) { | |||||
| 107 | for !a.done { | 115 | for !a.done { | ||
| 108 | select { | 116 | select { | ||
| 109 | case <-ctx.Done(): | 117 | case <-ctx.Done(): | ||
| 110 | - | return nil | 118 | + | return nil // interrupt or SIGTERM: a clean exit |
| 111 | case <-conn.Wake(): | 119 | case <-conn.Wake(): | ||
| 112 | if err = conn.Dispatch(a); err != nil { | 120 | if err = conn.Dispatch(a); err != nil { | ||
| 113 | return fmt.Errorf("dispatch: %w", err) | 121 | return fmt.Errorf("dispatch: %w", err) | ||
| @@ -131,10 +139,19 @@ func run(ctx context.Context, frames int) (err error) { | |||||
| 131 | 139 | ||||
| 132 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens | 140 | // FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens | ||
| 133 | // the compositor's main device and picks a format and modifier from the list. | 141 | // the compositor's main device and picks a format and modifier from the list. | ||
| 142 | + | // | |||
| 143 | + | // Only the first complete feedback is used. A later one (the compositor | |||
| 144 | + | // changing its preferred device or formats) is ignored: this demo assumes a | |||
| 145 | + | // single GPU, and rebuilding the renderer is application policy that would | |||
| 146 | + | // obscure the glue. A real application would create a new Renderer and | |||
| 147 | + | // re-import its buffers here. | |||
| 134 | func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() } | 148 | func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() } | ||
| 135 | 149 | ||||
| 136 | func (a *app) Configure(neferclient.SurfaceID, int32, int32) { | 150 | func (a *app) Configure(neferclient.SurfaceID, int32, int32) { | ||
| 137 | - | a.configured, a.canPresent = true, true | 151 | + | if !a.configured { // later configures must not lift the frame-callback gate |
| 152 | + | a.canPresent = true | |||
| 153 | + | } | |||
| 154 | + | a.configured = true | |||
| 138 | a.resize() | 155 | a.resize() | ||
| 139 | a.setup() | 156 | a.setup() | ||
| 140 | } | 157 | } | ||
| @@ -240,7 +257,7 @@ func (a *app) Key(ev *neferclient.KeyEvent) { | |||||
| 240 | Text: ev.Text, // valid only during the call, as for Input | 257 | Text: ev.Text, // valid only during the call, as for Input | ||
| 241 | Pressed: ev.Pressed, | 258 | Pressed: ev.Pressed, | ||
| 242 | Repeat: ev.Repeat, | 259 | Repeat: ev.Repeat, | ||
| 243 | - | Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super | 260 | + | Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods), |
| 244 | }) | 261 | }) | ||
| 245 | } | 262 | } | ||
| 246 | 263 | ||||
| @@ -284,6 +301,10 @@ func (a *app) present() error { | |||||
| 284 | if err := a.surf.DestroyBuffer(rt.Buffer); err != nil { | 301 | if err := a.surf.DestroyBuffer(rt.Buffer); err != nil { | ||
| 285 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | 302 | return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err) | ||
| 286 | } | 303 | } | ||
| 304 | + | // The release timeline of a buffer is imported under the buffer's id. | |||
| 305 | + | if err := a.surf.DestroyTimeline(rt.Buffer); err != nil { | |||
| 306 | + | return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err) | |||
| 307 | + | } | |||
| 287 | } | 308 | } | ||
| 288 | if out.NewBuffer { | 309 | if out.NewBuffer { | ||
| 289 | buf := neferclient.Buffer{ | 310 | buf := neferclient.Buffer{ | ||