detached a8b301e

feat(wayland): copy committed SHM pixels for mapped windows

commit
a8b301e6fd409d9a57460b55dd0307945ea69cad
author
brice · · 2026-09-23 15:28 UTC
parent
f1e2d50
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +271 -23
M internal/adapters/wayland/globals.go +102 -15
@@ -1,9 +1,11 @@
11 package wayland
22
33 import (
4+ "github.com/bnema/nefertty/internal/ports"
45 "github.com/bnema/purego-libwayland/protocol/wayland"
56 "github.com/bnema/purego-libwayland/server"
67 "golang.org/x/sys/unix"
8+ "runtime/debug"
79 )
810
911 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -19,7 +21,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1921 },
2022 func() error {
2123 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
22- r, e := wayland.NewShm(c, int32(v), id, shm{})
24+ r, e := wayland.NewShm(c, int32(v), id, shm{s})
2325 if e == nil {
2426 r.SendFormat(0)
2527 r.SendFormat(1)
@@ -119,32 +121,117 @@ func (region) Destroy(*wayland.Region) {}
119121 func (region) Add(*wayland.Region, int32, int32, int32, int32) {}
120122 func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
121123
122-type shm struct{}
124+type shm struct{ server *Server }
123125
124-func (shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
125- p, e := wayland.NewShmPool(r.Client(), 1, id, &pool{fd: fd, size: size})
126- if e != nil {
126+func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
127+ if size <= 0 {
127128 unix.Close(fd)
128- } else {
129- p.OnDestroy = func() { unix.Close(fd) }
129+ r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")
130+ return
131+ }
132+ data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
133+ if err != nil {
134+ unix.Close(fd)
135+ r.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot map pool")
136+ return
137+ }
138+ state := &pool{fd: fd, size: size, data: data, shm: r, server: h.server}
139+ p, err := wayland.NewShmPool(r.Client(), 1, id, state)
140+ if err != nil {
141+ state.close()
142+ return
143+ }
144+ p.OnDestroy = func() {
145+ state.destroyed = true
146+ if state.refs == 0 {
147+ state.close()
148+ }
130149 }
131150 }
132151 func (shm) Release(*wayland.Shm) {}
133152
134153 type pool struct {
135- fd int
136- size int32
154+ fd int
155+ size int32
156+ data []byte
157+ shm *wayland.Shm
158+ server *Server
159+ refs int
160+ destroyed bool
161+}
162+
163+func (p *pool) close() {
164+ if p.data != nil {
165+ _ = unix.Munmap(p.data)
166+ p.data = nil
167+ }
168+ _ = unix.Close(p.fd)
169+}
170+func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
171+ if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {
172+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")
173+ return
174+ }
175+ if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(stride)*int64(height) > int64(p.size) {
176+ p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions")
177+ return
178+ }
179+ state := &buffer{pool: p, offset: int(offset), width: int(width), height: int(height), stride: int(stride), format: format}
180+ b, err := wayland.NewBuffer(r.Client(), 1, id, state)
181+ if err != nil {
182+ return
183+ }
184+ p.refs++
185+ p.server.buffers[b.Resource] = state
186+ b.OnDestroy = func() {
187+ delete(p.server.buffers, b.Resource)
188+ p.refs--
189+ if p.destroyed && p.refs == 0 {
190+ p.close()
191+ }
192+ }
193+}
194+func (p *pool) Destroy(*wayland.ShmPool) {}
195+func (p *pool) Resize(r *wayland.ShmPool, size int32) {
196+ if size <= p.size {
197+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")
198+ return
199+ }
200+ data, err := unix.Mmap(p.fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
201+ if err != nil {
202+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")
203+ return
204+ }
205+ _ = unix.Munmap(p.data)
206+ p.data, p.size = data, size
137207 }
138208
139-func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, _ int32, _ int32, _ int32, _ int32, _ uint32) {
140- wayland.NewBuffer(r.Client(), 1, id, buffer{})
209+type buffer struct {
210+ pool *pool
211+ offset, width, height, stride int
212+ format uint32
141213 }
142-func (p *pool) Destroy(*wayland.ShmPool) {}
143-func (p *pool) Resize(_ *wayland.ShmPool, size int32) { p.size = size }
144214
145-type buffer struct{}
215+func (*buffer) Destroy(*wayland.Buffer) {}
146216
147-func (buffer) Destroy(*wayland.Buffer) {}
217+// A malicious client may truncate its fd after mmap; SetPanicOnFault converts
218+// the resulting SIGBUS on this goroutine to a recoverable panic.
219+func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {
220+ defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
221+ defer func() {
222+ if recover() != nil {
223+ content = ports.SurfaceContent{}
224+ ok = false
225+ }
226+ }()
227+ size := b.stride * b.height
228+ pixels := make([]byte, size)
229+ for y := 0; y < b.height; y++ {
230+ start := b.offset + y*b.stride
231+ copy(pixels[y*b.stride:(y+1)*b.stride], b.pool.data[start:start+b.stride])
232+ }
233+ return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true
234+}
148235
149236 type output struct{}
150237
@@ -1,9 +1,11 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "github.com/bnema/nefertty/internal/ports"
4 "github.com/bnema/purego-libwayland/protocol/wayland"5 "github.com/bnema/purego-libwayland/protocol/wayland"
5 "github.com/bnema/purego-libwayland/server"6 "github.com/bnema/purego-libwayland/server"
6 "golang.org/x/sys/unix"7 "golang.org/x/sys/unix"
8+ "runtime/debug"
7 )9 )
8 10
9 func registerGlobals(d *server.Display, o Options, s *Server) error {11 func registerGlobals(d *server.Display, o Options, s *Server) error {
@@ -19,7 +21,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
19 },21 },
20 func() error {22 func() error {
21 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {23 return wayland.NewShmGlobal(d, 1, func(c server.Client, v, id uint32) {
22- r, e := wayland.NewShm(c, int32(v), id, shm{})24+ r, e := wayland.NewShm(c, int32(v), id, shm{s})
23 if e == nil {25 if e == nil {
24 r.SendFormat(0)26 r.SendFormat(0)
25 r.SendFormat(1)27 r.SendFormat(1)
@@ -119,32 +121,117 @@ func (region) Destroy(*wayland.Region) {}
119 func (region) Add(*wayland.Region, int32, int32, int32, int32) {}121 func (region) Add(*wayland.Region, int32, int32, int32, int32) {}
120 func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}122 func (region) Subtract(*wayland.Region, int32, int32, int32, int32) {}
121 123
122-type shm struct{}124+type shm struct{ server *Server }
123 125
124-func (shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {126+func (h shm) CreatePool(r *wayland.Shm, id uint32, fd int, size int32) {
125- p, e := wayland.NewShmPool(r.Client(), 1, id, &pool{fd: fd, size: size})127+ if size <= 0 {
126- if e != nil {
127 unix.Close(fd)128 unix.Close(fd)
128- } else {129+ r.PostError(uint32(wayland.ShmErrorInvalidFd), "invalid pool size")
129- p.OnDestroy = func() { unix.Close(fd) }130+ return
131+ }
132+ data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
133+ if err != nil {
134+ unix.Close(fd)
135+ r.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot map pool")
136+ return
137+ }
138+ state := &pool{fd: fd, size: size, data: data, shm: r, server: h.server}
139+ p, err := wayland.NewShmPool(r.Client(), 1, id, state)
140+ if err != nil {
141+ state.close()
142+ return
143+ }
144+ p.OnDestroy = func() {
145+ state.destroyed = true
146+ if state.refs == 0 {
147+ state.close()
148+ }
130 }149 }
131 }150 }
132 func (shm) Release(*wayland.Shm) {}151 func (shm) Release(*wayland.Shm) {}
133 152
134 type pool struct {153 type pool struct {
135- fd int154+ fd int
136- size int32155+ size int32
156+ data []byte
157+ shm *wayland.Shm
158+ server *Server
159+ refs int
160+ destroyed bool
161+}
162+
163+func (p *pool) close() {
164+ if p.data != nil {
165+ _ = unix.Munmap(p.data)
166+ p.data = nil
167+ }
168+ _ = unix.Close(p.fd)
169+}
170+func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
171+ if format != uint32(wayland.ShmFormatArgb8888) && format != uint32(wayland.ShmFormatXrgb8888) {
172+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFormat), "unsupported format")
173+ return
174+ }
175+ if width <= 0 || height <= 0 || int64(stride) < int64(width)*4 || offset < 0 || stride <= 0 || int64(offset)+int64(stride)*int64(height) > int64(p.size) {
176+ p.shm.PostError(uint32(wayland.ShmErrorInvalidStride), "invalid buffer dimensions")
177+ return
178+ }
179+ state := &buffer{pool: p, offset: int(offset), width: int(width), height: int(height), stride: int(stride), format: format}
180+ b, err := wayland.NewBuffer(r.Client(), 1, id, state)
181+ if err != nil {
182+ return
183+ }
184+ p.refs++
185+ p.server.buffers[b.Resource] = state
186+ b.OnDestroy = func() {
187+ delete(p.server.buffers, b.Resource)
188+ p.refs--
189+ if p.destroyed && p.refs == 0 {
190+ p.close()
191+ }
192+ }
193+}
194+func (p *pool) Destroy(*wayland.ShmPool) {}
195+func (p *pool) Resize(r *wayland.ShmPool, size int32) {
196+ if size <= p.size {
197+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "pool must grow")
198+ return
199+ }
200+ data, err := unix.Mmap(p.fd, 0, int(size), unix.PROT_READ, unix.MAP_SHARED)
201+ if err != nil {
202+ p.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "cannot resize pool")
203+ return
204+ }
205+ _ = unix.Munmap(p.data)
206+ p.data, p.size = data, size
137 }207 }
138 208
139-func (p *pool) CreateBuffer(r *wayland.ShmPool, id uint32, _ int32, _ int32, _ int32, _ int32, _ uint32) {209+type buffer struct {
140- wayland.NewBuffer(r.Client(), 1, id, buffer{})210+ pool *pool
211+ offset, width, height, stride int
212+ format uint32
141 }213 }
142-func (p *pool) Destroy(*wayland.ShmPool) {}
143-func (p *pool) Resize(_ *wayland.ShmPool, size int32) { p.size = size }
144 214
145-type buffer struct{}215+func (*buffer) Destroy(*wayland.Buffer) {}
146 216
147-func (buffer) Destroy(*wayland.Buffer) {}217+// A malicious client may truncate its fd after mmap; SetPanicOnFault converts
218+// the resulting SIGBUS on this goroutine to a recoverable panic.
219+func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {
220+ defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
221+ defer func() {
222+ if recover() != nil {
223+ content = ports.SurfaceContent{}
224+ ok = false
225+ }
226+ }()
227+ size := b.stride * b.height
228+ pixels := make([]byte, size)
229+ for y := 0; y < b.height; y++ {
230+ start := b.offset + y*b.stride
231+ copy(pixels[y*b.stride:(y+1)*b.stride], b.pool.data[start:start+b.stride])
232+ }
233+ return ports.SurfaceContent{ID: id, Width: b.width, Height: b.height, Stride: b.stride, Opaque: b.format == uint32(wayland.ShmFormatXrgb8888), Pixels: pixels}, true
234+}
148 235
149 type output struct{}236 type output struct{}
150 237
M internal/adapters/wayland/protocol_test.go +24 -2
@@ -118,7 +118,7 @@ func TestXDGProtocolErrors(t *testing.T) {
118118 }
119119 pool := c.AllocateID()
120120 registerProtocol(t, c, shm)
121- if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {
121+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
122122 t.Fatal(err)
123123 }
124124 buffer := c.AllocateID()
@@ -194,7 +194,7 @@ func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
194194 pool, buffer := c.AllocateID(), c.AllocateID()
195195 registerProtocol(t, c, pool)
196196 registerProtocol(t, c, buffer)
197- if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {
197+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
198198 t.Fatal(err)
199199 }
200200 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
@@ -258,3 +258,25 @@ func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
258258 }
259259 mapped(t, events, 2*time.Second)
260260 }
261+
262+func TestSHMInvalidStride(t *testing.T) {
263+ s, _, _, dir := lifecycleServer(t)
264+ c := protocolClient(t, s, dir)
265+ shm := bindProtocol(t, c, "wl_shm")
266+ fd, err := unix.MemfdCreate("invalid-stride", 0)
267+ if err != nil {
268+ t.Fatal(err)
269+ }
270+ defer unix.Close(fd)
271+ if err := unix.Ftruncate(fd, 16); err != nil {
272+ t.Fatal(err)
273+ }
274+ pool := c.AllocateID()
275+ registerProtocol(t, c, shm)
276+ registerProtocol(t, c, pool)
277+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
278+ t.Fatal(err)
279+ }
280+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888))
281+ expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
282+}
@@ -118,7 +118,7 @@ func TestXDGProtocolErrors(t *testing.T) {
118 }118 }
119 pool := c.AllocateID()119 pool := c.AllocateID()
120 registerProtocol(t, c, shm)120 registerProtocol(t, c, shm)
121- if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {121+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
122 t.Fatal(err)122 t.Fatal(err)
123 }123 }
124 buffer := c.AllocateID()124 buffer := c.AllocateID()
@@ -194,7 +194,7 @@ func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
194 pool, buffer := c.AllocateID(), c.AllocateID()194 pool, buffer := c.AllocateID(), c.AllocateID()
195 registerProtocol(t, c, pool)195 registerProtocol(t, c, pool)
196 registerProtocol(t, c, buffer)196 registerProtocol(t, c, buffer)
197- if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {197+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
198 t.Fatal(err)198 t.Fatal(err)
199 }199 }
200 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))200 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
@@ -258,3 +258,25 @@ func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
258 }258 }
259 mapped(t, events, 2*time.Second)259 mapped(t, events, 2*time.Second)
260 }260 }
261+
262+func TestSHMInvalidStride(t *testing.T) {
263+ s, _, _, dir := lifecycleServer(t)
264+ c := protocolClient(t, s, dir)
265+ shm := bindProtocol(t, c, "wl_shm")
266+ fd, err := unix.MemfdCreate("invalid-stride", 0)
267+ if err != nil {
268+ t.Fatal(err)
269+ }
270+ defer unix.Close(fd)
271+ if err := unix.Ftruncate(fd, 16); err != nil {
272+ t.Fatal(err)
273+ }
274+ pool := c.AllocateID()
275+ registerProtocol(t, c, shm)
276+ registerProtocol(t, c, pool)
277+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
278+ t.Fatal(err)
279+ }
280+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888))
281+ expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
282+}
M internal/adapters/wayland/server.go +68 -5
@@ -24,6 +24,7 @@ type Options struct {
2424 // Channels carries client notifications and commands. Events may be unbuffered.
2525 type Channels struct {
2626 Events chan<- ports.ClientEvent
27+ Contents chan<- ports.SurfaceContent
2728 Commands <-chan ports.ClientCommand
2829 }
2930 type Server struct {
@@ -36,6 +37,7 @@ type Server struct {
3637 frames uint64
3738 started time.Time
3839 surfaces map[*server.Resource]*surface
40+ buffers map[*server.Resource]*buffer
3941 serial uint32
4042 ctx context.Context
4143 windows map[ports.WindowID]*window
@@ -46,9 +48,14 @@ type Server struct {
4648 // Events, which happens only at shutdown (core owns the receiving end). A
4749 // stalled core is a bug surfaced by ctx cancellation, not a reason to block
4850 // the display goroutine.
49- eventMu sync.Mutex
50- events []ports.ClientEvent
51- eventReady chan struct{}
51+ eventMu sync.Mutex
52+ events []ports.ClientEvent
53+ eventReady chan struct{}
54+ contentMu sync.Mutex
55+ contents map[ports.WindowID]ports.SurfaceContent
56+ contentSeq map[ports.WindowID]uint64
57+ contentNotify chan struct{}
58+ contentReady chan struct{}
5259 }
5360
5461 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
@@ -73,7 +80,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
7380 d.Close()
7481 return nil, err
7582 }
76- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1)}
83+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{})}
7784 if err = registerGlobals(d, opts, s); err != nil {
7885 cleanup()
7986 d.Close()
@@ -88,8 +95,9 @@ func (s *Server) Run(ctx context.Context) error {
8895 s.started = time.Now()
8996 s.ctx = ctx
9097 var wg sync.WaitGroup
91- wg.Add(3)
98+ wg.Add(4)
9299 go func() { defer wg.Done(); s.forward(ctx) }()
100+ go func() { defer wg.Done(); s.forwardContents(ctx) }()
93101 go func() {
94102 defer wg.Done()
95103 for {
@@ -191,6 +199,61 @@ func (s *Server) forward(ctx context.Context) {
191199 }
192200 }
193201
202+func (s *Server) emitContent(c ports.SurfaceContent) {
203+ if s.channels.Contents == nil {
204+ return
205+ }
206+ s.contentMu.Lock()
207+ s.contents[c.ID] = c
208+ s.contentSeq[c.ID]++
209+ close(s.contentNotify)
210+ s.contentNotify = make(chan struct{})
211+ s.contentMu.Unlock()
212+ select {
213+ case s.contentReady <- struct{}{}:
214+ default:
215+ }
216+}
217+
218+func (s *Server) forwardContents(ctx context.Context) {
219+ for {
220+ s.contentMu.Lock()
221+ var id ports.WindowID
222+ var c ports.SurfaceContent
223+ found := false
224+ var seq uint64
225+ var notify <-chan struct{}
226+ for id, c = range s.contents {
227+ found = true
228+ seq = s.contentSeq[id]
229+ break
230+ }
231+ notify = s.contentNotify
232+ s.contentMu.Unlock()
233+ if !found {
234+ select {
235+ case <-ctx.Done():
236+ return
237+ case <-s.contentReady:
238+ continue
239+ }
240+ }
241+ // Do not hold a stale frame while the receiver is busy: wake on newer data.
242+ select {
243+ case <-ctx.Done():
244+ return
245+ case <-notify:
246+ continue
247+ case s.channels.Contents <- c:
248+ s.contentMu.Lock()
249+ if s.contentSeq[id] == seq {
250+ delete(s.contents, id)
251+ }
252+ s.contentMu.Unlock()
253+ }
254+ }
255+}
256+
194257 func (s *Server) apply(cmd ports.ClientCommand) {
195258 switch c := cmd.(type) {
196259 case ports.FocusWindow:
@@ -24,6 +24,7 @@ type Options struct {
24 // Channels carries client notifications and commands. Events may be unbuffered.24 // Channels carries client notifications and commands. Events may be unbuffered.
25 type Channels struct {25 type Channels struct {
26 Events chan<- ports.ClientEvent26 Events chan<- ports.ClientEvent
27+ Contents chan<- ports.SurfaceContent
27 Commands <-chan ports.ClientCommand28 Commands <-chan ports.ClientCommand
28 }29 }
29 type Server struct {30 type Server struct {
@@ -36,6 +37,7 @@ type Server struct {
36 frames uint6437 frames uint64
37 started time.Time38 started time.Time
38 surfaces map[*server.Resource]*surface39 surfaces map[*server.Resource]*surface
40+ buffers map[*server.Resource]*buffer
39 serial uint3241 serial uint32
40 ctx context.Context42 ctx context.Context
41 windows map[ports.WindowID]*window43 windows map[ports.WindowID]*window
@@ -46,9 +48,14 @@ type Server struct {
46 // Events, which happens only at shutdown (core owns the receiving end). A48 // Events, which happens only at shutdown (core owns the receiving end). A
47 // stalled core is a bug surfaced by ctx cancellation, not a reason to block49 // stalled core is a bug surfaced by ctx cancellation, not a reason to block
48 // the display goroutine.50 // the display goroutine.
49- eventMu sync.Mutex51+ eventMu sync.Mutex
50- events []ports.ClientEvent52+ events []ports.ClientEvent
51- eventReady chan struct{}53+ eventReady chan struct{}
54+ contentMu sync.Mutex
55+ contents map[ports.WindowID]ports.SurfaceContent
56+ contentSeq map[ports.WindowID]uint64
57+ contentNotify chan struct{}
58+ contentReady chan struct{}
52 }59 }
53 60
54 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {61 func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
@@ -73,7 +80,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
73 d.Close()80 d.Close()
74 return nil, err81 return nil, err
75 }82 }
76- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1)}83+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{})}
77 if err = registerGlobals(d, opts, s); err != nil {84 if err = registerGlobals(d, opts, s); err != nil {
78 cleanup()85 cleanup()
79 d.Close()86 d.Close()
@@ -88,8 +95,9 @@ func (s *Server) Run(ctx context.Context) error {
88 s.started = time.Now()95 s.started = time.Now()
89 s.ctx = ctx96 s.ctx = ctx
90 var wg sync.WaitGroup97 var wg sync.WaitGroup
91- wg.Add(3)98+ wg.Add(4)
92 go func() { defer wg.Done(); s.forward(ctx) }()99 go func() { defer wg.Done(); s.forward(ctx) }()
100+ go func() { defer wg.Done(); s.forwardContents(ctx) }()
93 go func() {101 go func() {
94 defer wg.Done()102 defer wg.Done()
95 for {103 for {
@@ -191,6 +199,61 @@ func (s *Server) forward(ctx context.Context) {
191 }199 }
192 }200 }
193 201
202+func (s *Server) emitContent(c ports.SurfaceContent) {
203+ if s.channels.Contents == nil {
204+ return
205+ }
206+ s.contentMu.Lock()
207+ s.contents[c.ID] = c
208+ s.contentSeq[c.ID]++
209+ close(s.contentNotify)
210+ s.contentNotify = make(chan struct{})
211+ s.contentMu.Unlock()
212+ select {
213+ case s.contentReady <- struct{}{}:
214+ default:
215+ }
216+}
217+
218+func (s *Server) forwardContents(ctx context.Context) {
219+ for {
220+ s.contentMu.Lock()
221+ var id ports.WindowID
222+ var c ports.SurfaceContent
223+ found := false
224+ var seq uint64
225+ var notify <-chan struct{}
226+ for id, c = range s.contents {
227+ found = true
228+ seq = s.contentSeq[id]
229+ break
230+ }
231+ notify = s.contentNotify
232+ s.contentMu.Unlock()
233+ if !found {
234+ select {
235+ case <-ctx.Done():
236+ return
237+ case <-s.contentReady:
238+ continue
239+ }
240+ }
241+ // Do not hold a stale frame while the receiver is busy: wake on newer data.
242+ select {
243+ case <-ctx.Done():
244+ return
245+ case <-notify:
246+ continue
247+ case s.channels.Contents <- c:
248+ s.contentMu.Lock()
249+ if s.contentSeq[id] == seq {
250+ delete(s.contents, id)
251+ }
252+ s.contentMu.Unlock()
253+ }
254+ }
255+}
256+
194 func (s *Server) apply(cmd ports.ClientCommand) {257 func (s *Server) apply(cmd ports.ClientCommand) {
195 switch c := cmd.(type) {258 switch c := cmd.(type) {
196 case ports.FocusWindow:259 case ports.FocusWindow:
M internal/adapters/wayland/server_test.go +56 -0
@@ -364,3 +364,59 @@ func TestUnbufferedEventsDoNotBlockDisplay(t *testing.T) {
364364 }
365365 mapped(t, events, time.Second)
366366 }
367+
368+func TestWestonContents(t *testing.T) {
369+ tool, err := exec.LookPath("weston-simple-shm")
370+ if err != nil {
371+ t.Skip("weston-simple-shm not installed")
372+ }
373+ dir := t.TempDir()
374+ events := make(chan ports.ClientEvent, 16)
375+ contents := make(chan ports.SurfaceContent, 16)
376+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland"))
377+ if err != nil {
378+ t.Fatal(err)
379+ }
380+ ctx, cancel := context.WithCancel(context.Background())
381+ done := make(chan error, 1)
382+ go func() { done <- s.Run(ctx) }()
383+ t.Cleanup(func() {
384+ cancel()
385+ select {
386+ case err := <-done:
387+ if err != nil {
388+ t.Errorf("Run: %v", err)
389+ }
390+ case <-time.After(2 * time.Second):
391+ t.Error("Run hung")
392+ }
393+ })
394+ _, _, output := lifecycleClient(t, tool, s, dir, nil)
395+ w := mapped(t, events, 5*time.Second)
396+ first := time.After(5 * time.Second)
397+ count := 0
398+ for count < 2 {
399+ select {
400+ case c := <-contents:
401+ if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.Stride < c.Width*4 || len(c.Pixels) != c.Stride*c.Height {
402+ t.Fatalf("invalid content: id=%d dimensions=%dx%d stride=%d length=%d", c.ID, c.Width, c.Height, c.Stride, len(c.Pixels))
403+ }
404+ nonzero := false
405+ for _, b := range c.Pixels {
406+ if b != 0 {
407+ nonzero = true
408+ break
409+ }
410+ }
411+ if !nonzero {
412+ t.Fatal("empty pixels")
413+ }
414+ if count == 0 {
415+ first = time.After(2 * time.Second)
416+ }
417+ count++
418+ case <-first:
419+ t.Fatalf("only %d contents: %s", count, output.String())
420+ }
421+ }
422+}
@@ -364,3 +364,59 @@ func TestUnbufferedEventsDoNotBlockDisplay(t *testing.T) {
364 }364 }
365 mapped(t, events, time.Second)365 mapped(t, events, time.Second)
366 }366 }
367+
368+func TestWestonContents(t *testing.T) {
369+ tool, err := exec.LookPath("weston-simple-shm")
370+ if err != nil {
371+ t.Skip("weston-simple-shm not installed")
372+ }
373+ dir := t.TempDir()
374+ events := make(chan ports.ClientEvent, 16)
375+ contents := make(chan ports.SurfaceContent, 16)
376+ s, err := New(Options{RuntimeDir: dir, OutputWidth: 1920, OutputHeight: 1080}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland"))
377+ if err != nil {
378+ t.Fatal(err)
379+ }
380+ ctx, cancel := context.WithCancel(context.Background())
381+ done := make(chan error, 1)
382+ go func() { done <- s.Run(ctx) }()
383+ t.Cleanup(func() {
384+ cancel()
385+ select {
386+ case err := <-done:
387+ if err != nil {
388+ t.Errorf("Run: %v", err)
389+ }
390+ case <-time.After(2 * time.Second):
391+ t.Error("Run hung")
392+ }
393+ })
394+ _, _, output := lifecycleClient(t, tool, s, dir, nil)
395+ w := mapped(t, events, 5*time.Second)
396+ first := time.After(5 * time.Second)
397+ count := 0
398+ for count < 2 {
399+ select {
400+ case c := <-contents:
401+ if c.ID != w.ID || c.Width <= 0 || c.Height <= 0 || c.Stride < c.Width*4 || len(c.Pixels) != c.Stride*c.Height {
402+ t.Fatalf("invalid content: id=%d dimensions=%dx%d stride=%d length=%d", c.ID, c.Width, c.Height, c.Stride, len(c.Pixels))
403+ }
404+ nonzero := false
405+ for _, b := range c.Pixels {
406+ if b != 0 {
407+ nonzero = true
408+ break
409+ }
410+ }
411+ if !nonzero {
412+ t.Fatal("empty pixels")
413+ }
414+ if count == 0 {
415+ first = time.After(2 * time.Second)
416+ }
417+ count++
418+ case <-first:
419+ t.Fatalf("only %d contents: %s", count, output.String())
420+ }
421+ }
422+}
M internal/adapters/wayland/surface.go +20 -1
@@ -22,6 +22,7 @@ type surface struct {
2222 callbacks []*wayland.Callback
2323 role func(bool)
2424 destroyed bool
25+ released bool
2526 }
2627
2728 func (s *surface) Destroy(*wayland.Surface) {
@@ -46,15 +47,19 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
4647 }
4748 }
4849 func (s *surface) Commit(*wayland.Surface) {
50+ fresh := s.attached && s.pending != nil
4951 if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
5052 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
5153 return
5254 }
5355 if s.attached {
5456 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
55- s.current.SendRelease()
57+ if !s.released && s.current.Resource.Alive() {
58+ s.current.SendRelease()
59+ }
5660 }
5761 s.current = s.pending
62+ s.released = false
5863 s.pending = nil
5964 s.attached = false
6065 }
@@ -63,6 +68,20 @@ func (s *surface) Commit(*wayland.Surface) {
6368 if s.role != nil {
6469 s.role(s.current != nil)
6570 }
71+ if fresh && s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped && s.server.channels.Contents != nil {
72+ b := s.current
73+ if state, ok := s.server.buffers[b.Resource]; ok {
74+ if c, ok := state.content(s.xdg.window.id); ok {
75+ s.server.emitContent(c)
76+ } else {
77+ state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
78+ }
79+ if b.Resource.Alive() {
80+ b.SendRelease()
81+ }
82+ s.released = true
83+ }
84+ }
6685 }
6786 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
6887 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
@@ -22,6 +22,7 @@ type surface struct {
22 callbacks []*wayland.Callback22 callbacks []*wayland.Callback
23 role func(bool)23 role func(bool)
24 destroyed bool24 destroyed bool
25+ released bool
25 }26 }
26 27
27 func (s *surface) Destroy(*wayland.Surface) {28 func (s *surface) Destroy(*wayland.Surface) {
@@ -46,15 +47,19 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
46 }47 }
47 }48 }
48 func (s *surface) Commit(*wayland.Surface) {49 func (s *surface) Commit(*wayland.Surface) {
50+ fresh := s.attached && s.pending != nil
49 if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {51 if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
50 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")52 s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
51 return53 return
52 }54 }
53 if s.attached {55 if s.attached {
54 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {56 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
55- s.current.SendRelease()57+ if !s.released && s.current.Resource.Alive() {
58+ s.current.SendRelease()
59+ }
56 }60 }
57 s.current = s.pending61 s.current = s.pending
62+ s.released = false
58 s.pending = nil63 s.pending = nil
59 s.attached = false64 s.attached = false
60 }65 }
@@ -63,6 +68,20 @@ func (s *surface) Commit(*wayland.Surface) {
63 if s.role != nil {68 if s.role != nil {
64 s.role(s.current != nil)69 s.role(s.current != nil)
65 }70 }
71+ if fresh && s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped && s.server.channels.Contents != nil {
72+ b := s.current
73+ if state, ok := s.server.buffers[b.Resource]; ok {
74+ if c, ok := state.content(s.xdg.window.id); ok {
75+ s.server.emitContent(c)
76+ } else {
77+ state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
78+ }
79+ if b.Resource.Alive() {
80+ b.SendRelease()
81+ }
82+ s.released = true
83+ }
84+ }
66 }85 }
67 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}86 func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {}
68 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}87 func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
M internal/adapters/wayland/xdg.go +1 -0
@@ -88,6 +88,7 @@ func (w *window) unmap() {
8888 w.xdg.acked = false
8989 w.xdg.serials = nil
9090 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
91+ w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id})
9192 }
9293 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
9394 if x.window != nil || x.popup != nil {
@@ -88,6 +88,7 @@ func (w *window) unmap() {
88 w.xdg.acked = false88 w.xdg.acked = false
89 w.xdg.serials = nil89 w.xdg.serials = nil
90 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})90 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
91+ w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id})
91 }92 }
92 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {93 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
93 if x.window != nil || x.popup != nil {94 if x.window != nil || x.popup != nil {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close