feat(wayland): copy committed SHM pixels for mapped windows
- commit
- a8b301e6fd409d9a57460b55dd0307945ea69cad
- author
- brice · · 2026-09-23 15:28 UTC
- parent
- f1e2d50
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +271 -23
M internal/adapters/wayland/globals.go +102 -15
| @@ -1,9 +1,11 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "github.com/bnema/nefertty/internal/ports" | |
| 4 | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 5 | 6 | "github.com/bnema/purego-libwayland/server" | |
| 6 | 7 | "golang.org/x/sys/unix" | |
| 8 | + | "runtime/debug" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 9 | 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 | 22 | func() error { | |
| 21 | 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 | 25 | if e == nil { | |
| 24 | 26 | r.SendFormat(0) | |
| 25 | 27 | r.SendFormat(1) | |
| @@ -119,32 +121,117 @@ func (region) Destroy(*wayland.Region) {} | |||
| 119 | 121 | func (region) Add(*wayland.Region, int32, int32, int32, int32) {} | |
| 120 | 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) { | |
| 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 { | |
| 127 | 128 | 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 | + | } | |
| 130 | 149 | } | |
| 131 | 150 | } | |
| 132 | 151 | func (shm) Release(*wayland.Shm) {} | |
| 133 | 152 | ||
| 134 | 153 | 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 | |
| 137 | 207 | } | |
| 138 | 208 | ||
| 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 | |
| 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 | 236 | type output struct{} | |
| 150 | 237 | ||
| @@ -1,9 +1,11 @@ | |||||
| 1 | package wayland | 1 | 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 int | 154 | + | fd int |
| 136 | - | size int32 | 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 | |||
| 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) { | |||
| 118 | 118 | } | |
| 119 | 119 | pool := c.AllocateID() | |
| 120 | 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 | 122 | t.Fatal(err) | |
| 123 | 123 | } | |
| 124 | 124 | buffer := c.AllocateID() | |
| @@ -194,7 +194,7 @@ func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) { | |||
| 194 | 194 | pool, buffer := c.AllocateID(), c.AllocateID() | |
| 195 | 195 | registerProtocol(t, c, pool) | |
| 196 | 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 | 198 | t.Fatal(err) | |
| 199 | 199 | } | |
| 200 | 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 | 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 | + | } | |
| @@ -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 { | |||
| 24 | 24 | // Channels carries client notifications and commands. Events may be unbuffered. | |
| 25 | 25 | type Channels struct { | |
| 26 | 26 | Events chan<- ports.ClientEvent | |
| 27 | + | Contents chan<- ports.SurfaceContent | |
| 27 | 28 | Commands <-chan ports.ClientCommand | |
| 28 | 29 | } | |
| 29 | 30 | type Server struct { | |
| @@ -36,6 +37,7 @@ type Server struct { | |||
| 36 | 37 | frames uint64 | |
| 37 | 38 | started time.Time | |
| 38 | 39 | surfaces map[*server.Resource]*surface | |
| 40 | + | buffers map[*server.Resource]*buffer | |
| 39 | 41 | serial uint32 | |
| 40 | 42 | ctx context.Context | |
| 41 | 43 | windows map[ports.WindowID]*window | |
| @@ -46,9 +48,14 @@ type Server struct { | |||
| 46 | 48 | // Events, which happens only at shutdown (core owns the receiving end). A | |
| 47 | 49 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | |
| 48 | 50 | // 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{} | |
| 52 | 59 | } | |
| 53 | 60 | ||
| 54 | 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 | 80 | d.Close() | |
| 74 | 81 | 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 | 84 | if err = registerGlobals(d, opts, s); err != nil { | |
| 78 | 85 | cleanup() | |
| 79 | 86 | d.Close() | |
| @@ -88,8 +95,9 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 88 | 95 | s.started = time.Now() | |
| 89 | 96 | s.ctx = ctx | |
| 90 | 97 | var wg sync.WaitGroup | |
| 91 | - | wg.Add(3) | |
| 98 | + | wg.Add(4) | |
| 92 | 99 | go func() { defer wg.Done(); s.forward(ctx) }() | |
| 100 | + | go func() { defer wg.Done(); s.forwardContents(ctx) }() | |
| 93 | 101 | go func() { | |
| 94 | 102 | defer wg.Done() | |
| 95 | 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 | 257 | func (s *Server) apply(cmd ports.ClientCommand) { | |
| 195 | 258 | switch c := cmd.(type) { | |
| 196 | 259 | 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.ClientEvent | 26 | Events chan<- ports.ClientEvent | ||
| 27 | + | Contents chan<- ports.SurfaceContent | |||
| 27 | Commands <-chan ports.ClientCommand | 28 | Commands <-chan ports.ClientCommand | ||
| 28 | } | 29 | } | ||
| 29 | type Server struct { | 30 | type Server struct { | ||
| @@ -36,6 +37,7 @@ type Server struct { | |||||
| 36 | frames uint64 | 37 | frames uint64 | ||
| 37 | started time.Time | 38 | started time.Time | ||
| 38 | surfaces map[*server.Resource]*surface | 39 | surfaces map[*server.Resource]*surface | ||
| 40 | + | buffers map[*server.Resource]*buffer | |||
| 39 | serial uint32 | 41 | serial uint32 | ||
| 40 | ctx context.Context | 42 | ctx context.Context | ||
| 41 | windows map[ports.WindowID]*window | 43 | windows map[ports.WindowID]*window | ||
| @@ -46,9 +48,14 @@ type Server struct { | |||||
| 46 | // Events, which happens only at shutdown (core owns the receiving end). A | 48 | // 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 block | 49 | // 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.Mutex | 51 | + | eventMu sync.Mutex |
| 50 | - | events []ports.ClientEvent | 52 | + | 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, err | 81 | 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 = ctx | 96 | s.ctx = ctx | ||
| 90 | var wg sync.WaitGroup | 97 | 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) { | |||
| 364 | 364 | } | |
| 365 | 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 | + | } | |
| @@ -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 { | |||
| 22 | 22 | callbacks []*wayland.Callback | |
| 23 | 23 | role func(bool) | |
| 24 | 24 | destroyed bool | |
| 25 | + | released bool | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | 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 | 49 | func (s *surface) Commit(*wayland.Surface) { | |
| 50 | + | fresh := s.attached && s.pending != nil | |
| 49 | 51 | if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked { | |
| 50 | 52 | s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack") | |
| 51 | 53 | return | |
| 52 | 54 | } | |
| 53 | 55 | if s.attached { | |
| 54 | 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 | 61 | s.current = s.pending | |
| 62 | + | s.released = false | |
| 58 | 63 | s.pending = nil | |
| 59 | 64 | s.attached = false | |
| 60 | 65 | } | |
| @@ -63,6 +68,20 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 63 | 68 | if s.role != nil { | |
| 64 | 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 | 86 | func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {} | |
| 68 | 87 | func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {} | |
| @@ -22,6 +22,7 @@ type surface struct { | |||||
| 22 | callbacks []*wayland.Callback | 22 | callbacks []*wayland.Callback | ||
| 23 | role func(bool) | 23 | role func(bool) | ||
| 24 | destroyed bool | 24 | 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 | return | 53 | 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.pending | 61 | s.current = s.pending | ||
| 62 | + | s.released = false | |||
| 58 | s.pending = nil | 63 | s.pending = nil | ||
| 59 | s.attached = false | 64 | 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() { | |||
| 88 | 88 | w.xdg.acked = false | |
| 89 | 89 | w.xdg.serials = nil | |
| 90 | 90 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 91 | + | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}) | |
| 91 | 92 | } | |
| 92 | 93 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 93 | 94 | if x.window != nil || x.popup != nil { | |
| @@ -88,6 +88,7 @@ func (w *window) unmap() { | |||||
| 88 | w.xdg.acked = false | 88 | w.xdg.acked = false | ||
| 89 | w.xdg.serials = nil | 89 | 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 { | ||