refactor(wayland): use wlturbo v0.5.0 generated protocol proxies
Bump wlturbo to v0.5.0 and nefergui to v0.3.0, the versions NeferWL pins, and replace the hand-written ext-image-copy-capture, ext-image-capture-source, ext-workspace and xdg-output proxies, opcodes and event signatures with the generated bindings. Only the NeferWL extension keeps hand-written proxies.
- commit
- dbe6e8ae3313c51d164b4675d57baf357c30b6c3
- author
- brice · · 2026-10-01 01:44 UTC
- committer
- brice · · 2026-10-01 02:26 UTC
- parent
- 248b1e3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +212 -320
M go.mod +3 -3
| @@ -3,9 +3,9 @@ module github.com/bnema/nefercap | |||
| 3 | 3 | go 1.27 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | - | github.com/bnema/nefergui v0.2.0 | |
| 6 | + | github.com/bnema/nefergui v0.3.0 | |
| 7 | 7 | github.com/bnema/purego-libwayland v0.8.0 | |
| 8 | - | github.com/bnema/wlturbo v0.3.0 | |
| 8 | + | github.com/bnema/wlturbo v0.5.0 | |
| 9 | 9 | github.com/bnema/zerowrap v1.4.1 | |
| 10 | 10 | github.com/stretchr/testify v1.12.1 | |
| 11 | 11 | golang.org/x/sys v0.48.0 | |
| @@ -14,7 +14,7 @@ require ( | |||
| 14 | 14 | require ( | |
| 15 | 15 | github.com/bnema/purego v0.13.0-bnema.1 // indirect | |
| 16 | 16 | github.com/bnema/purego-vulkan v0.6.0 // indirect | |
| 17 | - | github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 // indirect | |
| 17 | + | github.com/bnema/purego-xkbcommon v0.1.0 // indirect | |
| 18 | 18 | github.com/go-text/typesetting v0.3.5 // indirect | |
| 19 | 19 | github.com/mattn/go-colorable v0.1.15 // indirect | |
| 20 | 20 | github.com/mattn/go-isatty v0.0.22 // indirect | |
| @@ -3,9 +3,9 @@ module github.com/bnema/nefercap | |||||
| 3 | go 1.27 | 3 | go 1.27 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | - | github.com/bnema/nefergui v0.2.0 | 6 | + | github.com/bnema/nefergui v0.3.0 |
| 7 | github.com/bnema/purego-libwayland v0.8.0 | 7 | github.com/bnema/purego-libwayland v0.8.0 | ||
| 8 | - | github.com/bnema/wlturbo v0.3.0 | 8 | + | github.com/bnema/wlturbo v0.5.0 |
| 9 | github.com/bnema/zerowrap v1.4.1 | 9 | github.com/bnema/zerowrap v1.4.1 | ||
| 10 | github.com/stretchr/testify v1.12.1 | 10 | github.com/stretchr/testify v1.12.1 | ||
| 11 | golang.org/x/sys v0.48.0 | 11 | golang.org/x/sys v0.48.0 | ||
| @@ -14,7 +14,7 @@ require ( | |||||
| 14 | require ( | 14 | require ( | ||
| 15 | github.com/bnema/purego v0.13.0-bnema.1 // indirect | 15 | github.com/bnema/purego v0.13.0-bnema.1 // indirect | ||
| 16 | github.com/bnema/purego-vulkan v0.6.0 // indirect | 16 | github.com/bnema/purego-vulkan v0.6.0 // indirect | ||
| 17 | - | github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 // indirect | 17 | + | github.com/bnema/purego-xkbcommon v0.1.0 // indirect |
| 18 | github.com/go-text/typesetting v0.3.5 // indirect | 18 | github.com/go-text/typesetting v0.3.5 // indirect | ||
| 19 | github.com/mattn/go-colorable v0.1.15 // indirect | 19 | github.com/mattn/go-colorable v0.1.15 // indirect | ||
| 20 | github.com/mattn/go-isatty v0.0.22 // indirect | 20 | github.com/mattn/go-isatty v0.0.22 // indirect | ||
M go.sum +6 -6 generated file: collapsed
M internal/adapters/wayland/copycapture.go +59 -111
| @@ -5,7 +5,8 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "time" | |
| 7 | 7 | ||
| 8 | - | "github.com/bnema/wlturbo" | |
| 8 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" | |
| 9 | + | "github.com/bnema/wlturbo/protocol/imagecopycapture" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/nefercap/internal/ports" | |
| 11 | 12 | ) | |
| @@ -16,40 +17,6 @@ const ( | |||
| 16 | 17 | shmXRGB8888 = uint32(ports.XRGB8888) | |
| 17 | 18 | ) | |
| 18 | 19 | ||
| 19 | - | // Hand-written proxies for ext-image-capture-source-v1 and | |
| 20 | - | // ext-image-copy-capture-v1: the wlturbo release in use does not generate | |
| 21 | - | // them. Only the requests and events nefercap uses are declared. | |
| 22 | - | ||
| 23 | - | // requestOnly is a bound global or created object that never sends events. | |
| 24 | - | type requestOnly struct{ wlturbo.BaseProxy } | |
| 25 | - | ||
| 26 | - | func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false } | |
| 27 | - | ||
| 28 | - | func newRequestOnly(ctx *wlturbo.Context) *requestOnly { | |
| 29 | - | p := &requestOnly{} | |
| 30 | - | p.SetContext(ctx) | |
| 31 | - | return p | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | const ( | |
| 35 | - | reqOutputSourceCreate = 0 // ext_output_image_capture_source_manager_v1.create_source | |
| 36 | - | reqSourceDestroy = 0 // ext_image_capture_source_v1.destroy | |
| 37 | - | reqCopyCreateSession = 0 // ext_image_copy_capture_manager_v1.create_session | |
| 38 | - | reqSessionCreateFrame = 0 | |
| 39 | - | reqSessionDestroy = 1 | |
| 40 | - | reqFrameDestroy = 0 | |
| 41 | - | reqFrameAttachBuffer = 1 | |
| 42 | - | reqFrameDamageBuffer = 2 | |
| 43 | - | reqFrameCapture = 3 | |
| 44 | - | ) | |
| 45 | - | ||
| 46 | - | // ext_image_copy_capture_frame_v1.failure_reason | |
| 47 | - | const ( | |
| 48 | - | failUnknown = 0 | |
| 49 | - | failBufferConstraints = 1 | |
| 50 | - | failStopped = 2 | |
| 51 | - | ) | |
| 52 | - | ||
| 53 | 20 | // maxFrameAttempts bounds the retries of one frame after failures that the | |
| 54 | 21 | // protocol calls recoverable. | |
| 55 | 22 | const maxFrameAttempts = 4 | |
| @@ -73,8 +40,8 @@ type constraints struct { | |||
| 73 | 40 | // session is one ext_image_copy_capture_session_v1. Its events are dispatched | |
| 74 | 41 | // on the goroutine that owns the operation. | |
| 75 | 42 | type session struct { | |
| 76 | - | wlturbo.BaseProxy | |
| 77 | - | key sessionKey | |
| 43 | + | proxy *imagecopycapture.ExtImageCopyCaptureSession | |
| 44 | + | key sessionKey | |
| 78 | 45 | ||
| 79 | 46 | pending constraints | |
| 80 | 47 | current constraints | |
| @@ -88,7 +55,8 @@ type session struct { | |||
| 88 | 55 | // lives in buffer cur and stays untouched while the next one is written to | |
| 89 | 56 | // the other buffer, so a Capture that finds the compositor idle (it waits | |
| 90 | 57 | // for damage after the first frame) can repeat the last frame at once. | |
| 91 | - | inflight *frameProxy | |
| 58 | + | frame captureFrame // outcome of the in-flight frame, reset per frame | |
| 59 | + | inflight *captureFrame // &frame while a frame is in flight, else nil | |
| 92 | 60 | inflightBuf int | |
| 93 | 61 | inflightGeom bufGeom // buffer layout the in-flight frame was made for | |
| 94 | 62 | haveLast bool | |
| @@ -96,30 +64,20 @@ type session struct { | |||
| 96 | 64 | lastGeom bufGeom | |
| 97 | 65 | served int // frames handed out by this session | |
| 98 | 66 | fails int // consecutive failed frames | |
| 99 | - | } | |
| 100 | 67 | ||
| 101 | - | func (*session) EventSignature(op uint16) (string, bool) { | |
| 102 | - | switch op { | |
| 103 | - | case 0: | |
| 104 | - | return "uint,uint,", true // buffer_size | |
| 105 | - | case 1: | |
| 106 | - | return "uint,", true // shm_format | |
| 107 | - | case 2: | |
| 108 | - | return "array,", true // dmabuf_device | |
| 109 | - | case 3: | |
| 110 | - | return "uint,array,", true // dmabuf_format | |
| 111 | - | case 4, 5: | |
| 112 | - | return "", true // done, stopped | |
| 113 | - | } | |
| 114 | - | return "", false | |
| 68 | + | // Frame event handlers, built once and shared by every frame proxy. | |
| 69 | + | onTransform func(uint32) | |
| 70 | + | onReady func() | |
| 71 | + | onFailed func(uint32) | |
| 115 | 72 | } | |
| 116 | 73 | ||
| 117 | - | func (c *session) Dispatch(e *wlturbo.Event) { | |
| 118 | - | switch e.Opcode { | |
| 119 | - | case 0: | |
| 120 | - | c.pending.width, c.pending.height, c.pending.haveSize = e.Uint32(), e.Uint32(), true | |
| 121 | - | case 1: | |
| 122 | - | format := e.Uint32() | |
| 74 | + | // newSession wraps a created session proxy and subscribes to its events. | |
| 75 | + | func newSession(proxy *imagecopycapture.ExtImageCopyCaptureSession, key sessionKey) *session { | |
| 76 | + | c := &session{proxy: proxy, key: key} | |
| 77 | + | proxy.OnBufferSize(func(width, height uint32) { | |
| 78 | + | c.pending.width, c.pending.height, c.pending.haveSize = width, height, true | |
| 79 | + | }) | |
| 80 | + | proxy.OnShmFormat(func(format uint32) { | |
| 123 | 81 | c.pending.sawShm = true | |
| 124 | 82 | switch { | |
| 125 | 83 | case format != shmARGB8888 && format != shmXRGB8888: | |
| @@ -127,46 +85,40 @@ func (c *session) Dispatch(e *wlturbo.Event) { | |||
| 127 | 85 | case !c.pending.haveFormat: | |
| 128 | 86 | c.pending.format, c.pending.haveFormat = format, true | |
| 129 | 87 | } | |
| 130 | - | case 4: | |
| 88 | + | }) | |
| 89 | + | proxy.OnDone(func() { | |
| 131 | 90 | c.current, c.pending = c.pending, constraints{} | |
| 132 | 91 | c.gen++ | |
| 133 | - | case 5: | |
| 134 | - | c.stopped = true | |
| 135 | - | } | |
| 92 | + | }) | |
| 93 | + | proxy.OnStopped(func() { c.stopped = true }) | |
| 94 | + | c.onTransform = func(transform uint32) { c.frame.transform = transform } | |
| 95 | + | c.onReady = func() { c.frame.ready = true } | |
| 96 | + | c.onFailed = func(reason uint32) { c.frame.failed, c.frame.reason = true, reason } | |
| 97 | + | return c | |
| 136 | 98 | } | |
| 137 | 99 | ||
| 138 | - | // frameProxy is one ext_image_copy_capture_frame_v1. | |
| 139 | - | type frameProxy struct { | |
| 140 | - | wlturbo.BaseProxy | |
| 100 | + | // captureFrame is one ext_image_copy_capture_frame_v1 and its outcome. The | |
| 101 | + | // session owns a single one, reset for each frame. | |
| 102 | + | type captureFrame struct { | |
| 103 | + | proxy *imagecopycapture.ExtImageCopyCaptureFrame | |
| 141 | 104 | transform uint32 | |
| 142 | 105 | ready bool | |
| 143 | 106 | failed bool | |
| 144 | 107 | reason uint32 | |
| 145 | 108 | } | |
| 146 | 109 | ||
| 147 | - | func (*frameProxy) EventSignature(op uint16) (string, bool) { | |
| 148 | - | switch op { | |
| 149 | - | case 0, 4: | |
| 150 | - | return "uint,", true // transform, failed | |
| 151 | - | case 1: | |
| 152 | - | return "int,int,int,int,", true // damage | |
| 153 | - | case 2: | |
| 154 | - | return "uint,uint,uint,", true // presentation_time | |
| 155 | - | case 3: | |
| 156 | - | return "", true // ready | |
| 157 | - | } | |
| 158 | - | return "", false | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | func (f *frameProxy) Dispatch(e *wlturbo.Event) { | |
| 162 | - | switch e.Opcode { | |
| 163 | - | case 0: | |
| 164 | - | f.transform = e.Uint32() | |
| 165 | - | case 3: | |
| 166 | - | f.ready = true | |
| 167 | - | case 4: | |
| 168 | - | f.failed, f.reason = true, e.Uint32() | |
| 110 | + | // newCaptureFrame creates the next frame of c, resetting its outcome, and | |
| 111 | + | // subscribes the session's shared handlers to its events. | |
| 112 | + | func newCaptureFrame(c *session) (*captureFrame, error) { | |
| 113 | + | proxy, err := c.proxy.CreateFrame() | |
| 114 | + | if err != nil { | |
| 115 | + | return nil, err | |
| 169 | 116 | } | |
| 117 | + | c.frame = captureFrame{proxy: proxy} | |
| 118 | + | proxy.OnTransform(c.onTransform) | |
| 119 | + | proxy.OnReady(c.onReady) | |
| 120 | + | proxy.OnFailed(c.onFailed) | |
| 121 | + | return &c.frame, nil | |
| 170 | 122 | } | |
| 171 | 123 | ||
| 172 | 124 | // sessionKey identifies what a session captures. Crop sessions have a zero | |
| @@ -236,30 +188,27 @@ func (s *Source) sessionFor(key sessionKey, out *output) (*session, error) { | |||
| 236 | 188 | } | |
| 237 | 189 | s.dropSession() | |
| 238 | 190 | } | |
| 239 | - | src := newRequestOnly(s.wl) | |
| 191 | + | var src *imagecapturesource.ExtImageCaptureSource | |
| 240 | 192 | var err error | |
| 241 | 193 | switch { | |
| 242 | 194 | case key.workspace != 0: | |
| 243 | - | err = s.createWorkspaceSource(src, key.workspace) | |
| 195 | + | src, err = s.createWorkspaceSource(key.workspace) | |
| 244 | 196 | case key.region != (ports.Region{}): | |
| 245 | - | r := key.region | |
| 246 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src}, | |
| 247 | - | src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height)) | |
| 197 | + | src, err = s.createRegionSource(out, key.region) | |
| 248 | 198 | default: | |
| 249 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.outSource, Opcode: reqOutputSourceCreate, Name: "ext_output_image_capture_source_manager_v1.create_source", Child: src}, src, out.proxy) | |
| 199 | + | src, err = s.outSource.CreateSource(out.proxy) | |
| 250 | 200 | } | |
| 251 | 201 | if err != nil { | |
| 252 | 202 | s.terminate() | |
| 253 | 203 | return nil, fmt.Errorf("wayland: create capture source: %w", err) | |
| 254 | 204 | } | |
| 255 | - | c := &session{key: key} | |
| 256 | - | c.SetContext(s.wl) | |
| 257 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.copyManager, Opcode: reqCopyCreateSession, Name: "ext_image_copy_capture_manager_v1.create_session", Child: c}, c, src, uint32(0)) | |
| 258 | - | _ = s.wl.Request(wlturbo.Request{Proxy: src, Opcode: reqSourceDestroy, Name: "ext_image_capture_source_v1.destroy", Destructor: true}) | |
| 205 | + | proxy, err := s.copyManager.CreateSession(src, 0) | |
| 206 | + | _ = src.Destroy() | |
| 259 | 207 | if err != nil { | |
| 260 | 208 | s.terminate() | |
| 261 | 209 | return nil, fmt.Errorf("wayland: create capture session: %w", err) | |
| 262 | 210 | } | |
| 211 | + | c := newSession(proxy, key) | |
| 263 | 212 | s.sess = c | |
| 264 | 213 | return c, nil | |
| 265 | 214 | } | |
| @@ -276,7 +225,7 @@ func (s *Source) dropSession() { | |||
| 276 | 225 | if c.exclusion != nil { | |
| 277 | 226 | c.exclusion.destroy(s) | |
| 278 | 227 | } | |
| 279 | - | _ = s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionDestroy, Name: "ext_image_copy_capture_session_v1.destroy", Destructor: true}) | |
| 228 | + | _ = c.proxy.Destroy() | |
| 280 | 229 | } | |
| 281 | 230 | ||
| 282 | 231 | // nextFrame returns the session's current frame. | |
| @@ -341,7 +290,7 @@ func (s *Source) nextFrame(c *session, out *output) (ports.Frame, error) { | |||
| 341 | 290 | if c.stopped { | |
| 342 | 291 | return ports.Frame{}, s.stoppedError(c, out) | |
| 343 | 292 | } | |
| 344 | - | if c.haveLast && f.reason == failUnknown { | |
| 293 | + | if c.haveLast && f.reason == imagecopycapture.FAILURE_REASON_UNKNOWN { | |
| 345 | 294 | return s.lastFrame(c) | |
| 346 | 295 | } | |
| 347 | 296 | default: // still pending after the poll: repeat the last frame | |
| @@ -372,9 +321,9 @@ func (s *Source) awaitFrame(c *session, poll bool) error { | |||
| 372 | 321 | ||
| 373 | 322 | // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a | |
| 374 | 323 | // new constraint batch that follows the failure is seen, then a retry. | |
| 375 | - | func (s *Source) retryAfterFailure(c *session, f *frameProxy) error { | |
| 324 | + | func (s *Source) retryAfterFailure(c *session, f *captureFrame) error { | |
| 376 | 325 | s.destroyFrame(c) | |
| 377 | - | if f.reason == failStopped { | |
| 326 | + | if f.reason == imagecopycapture.FAILURE_REASON_STOPPED { | |
| 378 | 327 | c.stopped = true | |
| 379 | 328 | return nil | |
| 380 | 329 | } | |
| @@ -382,14 +331,14 @@ func (s *Source) retryAfterFailure(c *session, f *frameProxy) error { | |||
| 382 | 331 | return errCompositorFailedCapture | |
| 383 | 332 | } | |
| 384 | 333 | switch f.reason { | |
| 385 | - | case failBufferConstraints, failUnknown: | |
| 334 | + | case imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS, imagecopycapture.FAILURE_REASON_UNKNOWN: | |
| 386 | 335 | return s.roundtrip() | |
| 387 | 336 | } | |
| 388 | 337 | return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason) | |
| 389 | 338 | } | |
| 390 | 339 | ||
| 391 | 340 | // complete hands out a frame the compositor finished. | |
| 392 | - | func (s *Source) complete(c *session, g bufGeom, f *frameProxy) (ports.Frame, error) { | |
| 341 | + | func (s *Source) complete(c *session, g bufGeom, f *captureFrame) (ports.Frame, error) { | |
| 393 | 342 | transform := f.transform | |
| 394 | 343 | idx := c.inflightBuf | |
| 395 | 344 | s.destroyFrame(c) | |
| @@ -423,22 +372,21 @@ func (s *Source) startFrame(c *session, g bufGeom) error { | |||
| 423 | 372 | if err := s.ensureBuffer(idx, g); err != nil { | |
| 424 | 373 | return err | |
| 425 | 374 | } | |
| 426 | - | f := &frameProxy{} | |
| 427 | - | f.SetContext(s.wl) | |
| 428 | - | if err := s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionCreateFrame, Name: "ext_image_copy_capture_session_v1.create_frame", Child: f}, f); err != nil { | |
| 375 | + | f, err := newCaptureFrame(c) | |
| 376 | + | if err != nil { | |
| 429 | 377 | s.terminate() | |
| 430 | 378 | return fmt.Errorf("wayland: create frame: %w", err) | |
| 431 | 379 | } | |
| 432 | 380 | c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g | |
| 433 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameAttachBuffer, Name: "ext_image_copy_capture_frame_v1.attach_buffer"}, s.bufs[idx].buffer); err != nil { | |
| 381 | + | if err := f.proxy.AttachBuffer(s.bufs[idx].buffer); err != nil { | |
| 434 | 382 | s.terminate() | |
| 435 | 383 | return fmt.Errorf("wayland: attach buffer: %w", err) | |
| 436 | 384 | } | |
| 437 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDamageBuffer, Name: "ext_image_copy_capture_frame_v1.damage_buffer"}, int32(0), int32(0), int32(g.width), int32(g.height)); err != nil { | |
| 385 | + | if err := f.proxy.DamageBuffer(0, 0, int32(g.width), int32(g.height)); err != nil { | |
| 438 | 386 | s.terminate() | |
| 439 | 387 | return fmt.Errorf("wayland: damage buffer: %w", err) | |
| 440 | 388 | } | |
| 441 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameCapture, Name: "ext_image_copy_capture_frame_v1.capture"}); err != nil { | |
| 389 | + | if err := f.proxy.Capture(); err != nil { | |
| 442 | 390 | s.terminate() | |
| 443 | 391 | return fmt.Errorf("wayland: capture request: %w", err) | |
| 444 | 392 | } | |
| @@ -449,7 +397,7 @@ func (s *Source) startFrame(c *session, g bufGeom) error { | |||
| 449 | 397 | func (s *Source) destroyFrame(c *session) { | |
| 450 | 398 | if f := c.inflight; f != nil { | |
| 451 | 399 | c.inflight = nil | |
| 452 | - | _ = s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDestroy, Name: "ext_image_copy_capture_frame_v1.destroy", Destructor: true}) | |
| 400 | + | _ = f.proxy.Destroy() | |
| 453 | 401 | } | |
| 454 | 402 | } | |
| 455 | 403 | ||
| @@ -5,7 +5,8 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "time" | 6 | "time" | ||
| 7 | 7 | ||||
| 8 | - | "github.com/bnema/wlturbo" | 8 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" |
| 9 | + | "github.com/bnema/wlturbo/protocol/imagecopycapture" | |||
| 9 | 10 | ||||
| 10 | "github.com/bnema/nefercap/internal/ports" | 11 | "github.com/bnema/nefercap/internal/ports" | ||
| 11 | ) | 12 | ) | ||
| @@ -16,40 +17,6 @@ const ( | |||||
| 16 | shmXRGB8888 = uint32(ports.XRGB8888) | 17 | shmXRGB8888 = uint32(ports.XRGB8888) | ||
| 17 | ) | 18 | ) | ||
| 18 | 19 | ||||
| 19 | - | // Hand-written proxies for ext-image-capture-source-v1 and | |||
| 20 | - | // ext-image-copy-capture-v1: the wlturbo release in use does not generate | |||
| 21 | - | // them. Only the requests and events nefercap uses are declared. | |||
| 22 | - | ||||
| 23 | - | // requestOnly is a bound global or created object that never sends events. | |||
| 24 | - | type requestOnly struct{ wlturbo.BaseProxy } | |||
| 25 | - | ||||
| 26 | - | func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false } | |||
| 27 | - | ||||
| 28 | - | func newRequestOnly(ctx *wlturbo.Context) *requestOnly { | |||
| 29 | - | p := &requestOnly{} | |||
| 30 | - | p.SetContext(ctx) | |||
| 31 | - | return p | |||
| 32 | - | } | |||
| 33 | - | ||||
| 34 | - | const ( | |||
| 35 | - | reqOutputSourceCreate = 0 // ext_output_image_capture_source_manager_v1.create_source | |||
| 36 | - | reqSourceDestroy = 0 // ext_image_capture_source_v1.destroy | |||
| 37 | - | reqCopyCreateSession = 0 // ext_image_copy_capture_manager_v1.create_session | |||
| 38 | - | reqSessionCreateFrame = 0 | |||
| 39 | - | reqSessionDestroy = 1 | |||
| 40 | - | reqFrameDestroy = 0 | |||
| 41 | - | reqFrameAttachBuffer = 1 | |||
| 42 | - | reqFrameDamageBuffer = 2 | |||
| 43 | - | reqFrameCapture = 3 | |||
| 44 | - | ) | |||
| 45 | - | ||||
| 46 | - | // ext_image_copy_capture_frame_v1.failure_reason | |||
| 47 | - | const ( | |||
| 48 | - | failUnknown = 0 | |||
| 49 | - | failBufferConstraints = 1 | |||
| 50 | - | failStopped = 2 | |||
| 51 | - | ) | |||
| 52 | - | ||||
| 53 | // maxFrameAttempts bounds the retries of one frame after failures that the | 20 | // maxFrameAttempts bounds the retries of one frame after failures that the | ||
| 54 | // protocol calls recoverable. | 21 | // protocol calls recoverable. | ||
| 55 | const maxFrameAttempts = 4 | 22 | const maxFrameAttempts = 4 | ||
| @@ -73,8 +40,8 @@ type constraints struct { | |||||
| 73 | // session is one ext_image_copy_capture_session_v1. Its events are dispatched | 40 | // session is one ext_image_copy_capture_session_v1. Its events are dispatched | ||
| 74 | // on the goroutine that owns the operation. | 41 | // on the goroutine that owns the operation. | ||
| 75 | type session struct { | 42 | type session struct { | ||
| 76 | - | wlturbo.BaseProxy | 43 | + | proxy *imagecopycapture.ExtImageCopyCaptureSession |
| 77 | - | key sessionKey | 44 | + | key sessionKey |
| 78 | 45 | ||||
| 79 | pending constraints | 46 | pending constraints | ||
| 80 | current constraints | 47 | current constraints | ||
| @@ -88,7 +55,8 @@ type session struct { | |||||
| 88 | // lives in buffer cur and stays untouched while the next one is written to | 55 | // lives in buffer cur and stays untouched while the next one is written to | ||
| 89 | // the other buffer, so a Capture that finds the compositor idle (it waits | 56 | // the other buffer, so a Capture that finds the compositor idle (it waits | ||
| 90 | // for damage after the first frame) can repeat the last frame at once. | 57 | // for damage after the first frame) can repeat the last frame at once. | ||
| 91 | - | inflight *frameProxy | 58 | + | frame captureFrame // outcome of the in-flight frame, reset per frame |
| 59 | + | inflight *captureFrame // &frame while a frame is in flight, else nil | |||
| 92 | inflightBuf int | 60 | inflightBuf int | ||
| 93 | inflightGeom bufGeom // buffer layout the in-flight frame was made for | 61 | inflightGeom bufGeom // buffer layout the in-flight frame was made for | ||
| 94 | haveLast bool | 62 | haveLast bool | ||
| @@ -96,30 +64,20 @@ type session struct { | |||||
| 96 | lastGeom bufGeom | 64 | lastGeom bufGeom | ||
| 97 | served int // frames handed out by this session | 65 | served int // frames handed out by this session | ||
| 98 | fails int // consecutive failed frames | 66 | fails int // consecutive failed frames | ||
| 99 | - | } | |||
| 100 | 67 | ||||
| 101 | - | func (*session) EventSignature(op uint16) (string, bool) { | 68 | + | // Frame event handlers, built once and shared by every frame proxy. |
| 102 | - | switch op { | 69 | + | onTransform func(uint32) |
| 103 | - | case 0: | 70 | + | onReady func() |
| 104 | - | return "uint,uint,", true // buffer_size | 71 | + | onFailed func(uint32) |
| 105 | - | case 1: | |||
| 106 | - | return "uint,", true // shm_format | |||
| 107 | - | case 2: | |||
| 108 | - | return "array,", true // dmabuf_device | |||
| 109 | - | case 3: | |||
| 110 | - | return "uint,array,", true // dmabuf_format | |||
| 111 | - | case 4, 5: | |||
| 112 | - | return "", true // done, stopped | |||
| 113 | - | } | |||
| 114 | - | return "", false | |||
| 115 | } | 72 | } | ||
| 116 | 73 | ||||
| 117 | - | func (c *session) Dispatch(e *wlturbo.Event) { | 74 | + | // newSession wraps a created session proxy and subscribes to its events. |
| 118 | - | switch e.Opcode { | 75 | + | func newSession(proxy *imagecopycapture.ExtImageCopyCaptureSession, key sessionKey) *session { |
| 119 | - | case 0: | 76 | + | c := &session{proxy: proxy, key: key} |
| 120 | - | c.pending.width, c.pending.height, c.pending.haveSize = e.Uint32(), e.Uint32(), true | 77 | + | proxy.OnBufferSize(func(width, height uint32) { |
| 121 | - | case 1: | 78 | + | c.pending.width, c.pending.height, c.pending.haveSize = width, height, true |
| 122 | - | format := e.Uint32() | 79 | + | }) |
| 80 | + | proxy.OnShmFormat(func(format uint32) { | |||
| 123 | c.pending.sawShm = true | 81 | c.pending.sawShm = true | ||
| 124 | switch { | 82 | switch { | ||
| 125 | case format != shmARGB8888 && format != shmXRGB8888: | 83 | case format != shmARGB8888 && format != shmXRGB8888: | ||
| @@ -127,46 +85,40 @@ func (c *session) Dispatch(e *wlturbo.Event) { | |||||
| 127 | case !c.pending.haveFormat: | 85 | case !c.pending.haveFormat: | ||
| 128 | c.pending.format, c.pending.haveFormat = format, true | 86 | c.pending.format, c.pending.haveFormat = format, true | ||
| 129 | } | 87 | } | ||
| 130 | - | case 4: | 88 | + | }) |
| 89 | + | proxy.OnDone(func() { | |||
| 131 | c.current, c.pending = c.pending, constraints{} | 90 | c.current, c.pending = c.pending, constraints{} | ||
| 132 | c.gen++ | 91 | c.gen++ | ||
| 133 | - | case 5: | 92 | + | }) |
| 134 | - | c.stopped = true | 93 | + | proxy.OnStopped(func() { c.stopped = true }) |
| 135 | - | } | 94 | + | c.onTransform = func(transform uint32) { c.frame.transform = transform } |
| 95 | + | c.onReady = func() { c.frame.ready = true } | |||
| 96 | + | c.onFailed = func(reason uint32) { c.frame.failed, c.frame.reason = true, reason } | |||
| 97 | + | return c | |||
| 136 | } | 98 | } | ||
| 137 | 99 | ||||
| 138 | - | // frameProxy is one ext_image_copy_capture_frame_v1. | 100 | + | // captureFrame is one ext_image_copy_capture_frame_v1 and its outcome. The |
| 139 | - | type frameProxy struct { | 101 | + | // session owns a single one, reset for each frame. |
| 140 | - | wlturbo.BaseProxy | 102 | + | type captureFrame struct { |
| 103 | + | proxy *imagecopycapture.ExtImageCopyCaptureFrame | |||
| 141 | transform uint32 | 104 | transform uint32 | ||
| 142 | ready bool | 105 | ready bool | ||
| 143 | failed bool | 106 | failed bool | ||
| 144 | reason uint32 | 107 | reason uint32 | ||
| 145 | } | 108 | } | ||
| 146 | 109 | ||||
| 147 | - | func (*frameProxy) EventSignature(op uint16) (string, bool) { | 110 | + | // newCaptureFrame creates the next frame of c, resetting its outcome, and |
| 148 | - | switch op { | 111 | + | // subscribes the session's shared handlers to its events. |
| 149 | - | case 0, 4: | 112 | + | func newCaptureFrame(c *session) (*captureFrame, error) { |
| 150 | - | return "uint,", true // transform, failed | 113 | + | proxy, err := c.proxy.CreateFrame() |
| 151 | - | case 1: | 114 | + | if err != nil { |
| 152 | - | return "int,int,int,int,", true // damage | 115 | + | return nil, err |
| 153 | - | case 2: | |||
| 154 | - | return "uint,uint,uint,", true // presentation_time | |||
| 155 | - | case 3: | |||
| 156 | - | return "", true // ready | |||
| 157 | - | } | |||
| 158 | - | return "", false | |||
| 159 | - | } | |||
| 160 | - | ||||
| 161 | - | func (f *frameProxy) Dispatch(e *wlturbo.Event) { | |||
| 162 | - | switch e.Opcode { | |||
| 163 | - | case 0: | |||
| 164 | - | f.transform = e.Uint32() | |||
| 165 | - | case 3: | |||
| 166 | - | f.ready = true | |||
| 167 | - | case 4: | |||
| 168 | - | f.failed, f.reason = true, e.Uint32() | |||
| 169 | } | 116 | } | ||
| 117 | + | c.frame = captureFrame{proxy: proxy} | |||
| 118 | + | proxy.OnTransform(c.onTransform) | |||
| 119 | + | proxy.OnReady(c.onReady) | |||
| 120 | + | proxy.OnFailed(c.onFailed) | |||
| 121 | + | return &c.frame, nil | |||
| 170 | } | 122 | } | ||
| 171 | 123 | ||||
| 172 | // sessionKey identifies what a session captures. Crop sessions have a zero | 124 | // sessionKey identifies what a session captures. Crop sessions have a zero | ||
| @@ -236,30 +188,27 @@ func (s *Source) sessionFor(key sessionKey, out *output) (*session, error) { | |||||
| 236 | } | 188 | } | ||
| 237 | s.dropSession() | 189 | s.dropSession() | ||
| 238 | } | 190 | } | ||
| 239 | - | src := newRequestOnly(s.wl) | 191 | + | var src *imagecapturesource.ExtImageCaptureSource |
| 240 | var err error | 192 | var err error | ||
| 241 | switch { | 193 | switch { | ||
| 242 | case key.workspace != 0: | 194 | case key.workspace != 0: | ||
| 243 | - | err = s.createWorkspaceSource(src, key.workspace) | 195 | + | src, err = s.createWorkspaceSource(key.workspace) |
| 244 | case key.region != (ports.Region{}): | 196 | case key.region != (ports.Region{}): | ||
| 245 | - | r := key.region | 197 | + | src, err = s.createRegionSource(out, key.region) |
| 246 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src}, | |||
| 247 | - | src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height)) | |||
| 248 | default: | 198 | default: | ||
| 249 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.outSource, Opcode: reqOutputSourceCreate, Name: "ext_output_image_capture_source_manager_v1.create_source", Child: src}, src, out.proxy) | 199 | + | src, err = s.outSource.CreateSource(out.proxy) |
| 250 | } | 200 | } | ||
| 251 | if err != nil { | 201 | if err != nil { | ||
| 252 | s.terminate() | 202 | s.terminate() | ||
| 253 | return nil, fmt.Errorf("wayland: create capture source: %w", err) | 203 | return nil, fmt.Errorf("wayland: create capture source: %w", err) | ||
| 254 | } | 204 | } | ||
| 255 | - | c := &session{key: key} | 205 | + | proxy, err := s.copyManager.CreateSession(src, 0) |
| 256 | - | c.SetContext(s.wl) | 206 | + | _ = src.Destroy() |
| 257 | - | err = s.wl.Request(wlturbo.Request{Proxy: s.copyManager, Opcode: reqCopyCreateSession, Name: "ext_image_copy_capture_manager_v1.create_session", Child: c}, c, src, uint32(0)) | |||
| 258 | - | _ = s.wl.Request(wlturbo.Request{Proxy: src, Opcode: reqSourceDestroy, Name: "ext_image_capture_source_v1.destroy", Destructor: true}) | |||
| 259 | if err != nil { | 207 | if err != nil { | ||
| 260 | s.terminate() | 208 | s.terminate() | ||
| 261 | return nil, fmt.Errorf("wayland: create capture session: %w", err) | 209 | return nil, fmt.Errorf("wayland: create capture session: %w", err) | ||
| 262 | } | 210 | } | ||
| 211 | + | c := newSession(proxy, key) | |||
| 263 | s.sess = c | 212 | s.sess = c | ||
| 264 | return c, nil | 213 | return c, nil | ||
| 265 | } | 214 | } | ||
| @@ -276,7 +225,7 @@ func (s *Source) dropSession() { | |||||
| 276 | if c.exclusion != nil { | 225 | if c.exclusion != nil { | ||
| 277 | c.exclusion.destroy(s) | 226 | c.exclusion.destroy(s) | ||
| 278 | } | 227 | } | ||
| 279 | - | _ = s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionDestroy, Name: "ext_image_copy_capture_session_v1.destroy", Destructor: true}) | 228 | + | _ = c.proxy.Destroy() |
| 280 | } | 229 | } | ||
| 281 | 230 | ||||
| 282 | // nextFrame returns the session's current frame. | 231 | // nextFrame returns the session's current frame. | ||
| @@ -341,7 +290,7 @@ func (s *Source) nextFrame(c *session, out *output) (ports.Frame, error) { | |||||
| 341 | if c.stopped { | 290 | if c.stopped { | ||
| 342 | return ports.Frame{}, s.stoppedError(c, out) | 291 | return ports.Frame{}, s.stoppedError(c, out) | ||
| 343 | } | 292 | } | ||
| 344 | - | if c.haveLast && f.reason == failUnknown { | 293 | + | if c.haveLast && f.reason == imagecopycapture.FAILURE_REASON_UNKNOWN { |
| 345 | return s.lastFrame(c) | 294 | return s.lastFrame(c) | ||
| 346 | } | 295 | } | ||
| 347 | default: // still pending after the poll: repeat the last frame | 296 | default: // still pending after the poll: repeat the last frame | ||
| @@ -372,9 +321,9 @@ func (s *Source) awaitFrame(c *session, poll bool) error { | |||||
| 372 | 321 | ||||
| 373 | // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a | 322 | // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a | ||
| 374 | // new constraint batch that follows the failure is seen, then a retry. | 323 | // new constraint batch that follows the failure is seen, then a retry. | ||
| 375 | - | func (s *Source) retryAfterFailure(c *session, f *frameProxy) error { | 324 | + | func (s *Source) retryAfterFailure(c *session, f *captureFrame) error { |
| 376 | s.destroyFrame(c) | 325 | s.destroyFrame(c) | ||
| 377 | - | if f.reason == failStopped { | 326 | + | if f.reason == imagecopycapture.FAILURE_REASON_STOPPED { |
| 378 | c.stopped = true | 327 | c.stopped = true | ||
| 379 | return nil | 328 | return nil | ||
| 380 | } | 329 | } | ||
| @@ -382,14 +331,14 @@ func (s *Source) retryAfterFailure(c *session, f *frameProxy) error { | |||||
| 382 | return errCompositorFailedCapture | 331 | return errCompositorFailedCapture | ||
| 383 | } | 332 | } | ||
| 384 | switch f.reason { | 333 | switch f.reason { | ||
| 385 | - | case failBufferConstraints, failUnknown: | 334 | + | case imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS, imagecopycapture.FAILURE_REASON_UNKNOWN: |
| 386 | return s.roundtrip() | 335 | return s.roundtrip() | ||
| 387 | } | 336 | } | ||
| 388 | return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason) | 337 | return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason) | ||
| 389 | } | 338 | } | ||
| 390 | 339 | ||||
| 391 | // complete hands out a frame the compositor finished. | 340 | // complete hands out a frame the compositor finished. | ||
| 392 | - | func (s *Source) complete(c *session, g bufGeom, f *frameProxy) (ports.Frame, error) { | 341 | + | func (s *Source) complete(c *session, g bufGeom, f *captureFrame) (ports.Frame, error) { |
| 393 | transform := f.transform | 342 | transform := f.transform | ||
| 394 | idx := c.inflightBuf | 343 | idx := c.inflightBuf | ||
| 395 | s.destroyFrame(c) | 344 | s.destroyFrame(c) | ||
| @@ -423,22 +372,21 @@ func (s *Source) startFrame(c *session, g bufGeom) error { | |||||
| 423 | if err := s.ensureBuffer(idx, g); err != nil { | 372 | if err := s.ensureBuffer(idx, g); err != nil { | ||
| 424 | return err | 373 | return err | ||
| 425 | } | 374 | } | ||
| 426 | - | f := &frameProxy{} | 375 | + | f, err := newCaptureFrame(c) |
| 427 | - | f.SetContext(s.wl) | 376 | + | if err != nil { |
| 428 | - | if err := s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionCreateFrame, Name: "ext_image_copy_capture_session_v1.create_frame", Child: f}, f); err != nil { | |||
| 429 | s.terminate() | 377 | s.terminate() | ||
| 430 | return fmt.Errorf("wayland: create frame: %w", err) | 378 | return fmt.Errorf("wayland: create frame: %w", err) | ||
| 431 | } | 379 | } | ||
| 432 | c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g | 380 | c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g | ||
| 433 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameAttachBuffer, Name: "ext_image_copy_capture_frame_v1.attach_buffer"}, s.bufs[idx].buffer); err != nil { | 381 | + | if err := f.proxy.AttachBuffer(s.bufs[idx].buffer); err != nil { |
| 434 | s.terminate() | 382 | s.terminate() | ||
| 435 | return fmt.Errorf("wayland: attach buffer: %w", err) | 383 | return fmt.Errorf("wayland: attach buffer: %w", err) | ||
| 436 | } | 384 | } | ||
| 437 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDamageBuffer, Name: "ext_image_copy_capture_frame_v1.damage_buffer"}, int32(0), int32(0), int32(g.width), int32(g.height)); err != nil { | 385 | + | if err := f.proxy.DamageBuffer(0, 0, int32(g.width), int32(g.height)); err != nil { |
| 438 | s.terminate() | 386 | s.terminate() | ||
| 439 | return fmt.Errorf("wayland: damage buffer: %w", err) | 387 | return fmt.Errorf("wayland: damage buffer: %w", err) | ||
| 440 | } | 388 | } | ||
| 441 | - | if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameCapture, Name: "ext_image_copy_capture_frame_v1.capture"}); err != nil { | 389 | + | if err := f.proxy.Capture(); err != nil { |
| 442 | s.terminate() | 390 | s.terminate() | ||
| 443 | return fmt.Errorf("wayland: capture request: %w", err) | 391 | return fmt.Errorf("wayland: capture request: %w", err) | ||
| 444 | } | 392 | } | ||
| @@ -449,7 +397,7 @@ func (s *Source) startFrame(c *session, g bufGeom) error { | |||||
| 449 | func (s *Source) destroyFrame(c *session) { | 397 | func (s *Source) destroyFrame(c *session) { | ||
| 450 | if f := c.inflight; f != nil { | 398 | if f := c.inflight; f != nil { | ||
| 451 | c.inflight = nil | 399 | c.inflight = nil | ||
| 452 | - | _ = s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDestroy, Name: "ext_image_copy_capture_frame_v1.destroy", Destructor: true}) | 400 | + | _ = f.proxy.Destroy() |
| 453 | } | 401 | } | ||
| 454 | } | 402 | } | ||
| 455 | 403 | ||||
M internal/adapters/wayland/extensions.go +49 -9
| @@ -9,6 +9,8 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/wlturbo" | |
| 11 | 11 | "github.com/bnema/wlturbo/protocol/core" | |
| 12 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" | |
| 13 | + | "github.com/bnema/wlturbo/protocol/xdgoutput" | |
| 12 | 14 | ||
| 13 | 15 | "github.com/bnema/nefercap/internal/ports" | |
| 14 | 16 | ) | |
| @@ -75,6 +77,18 @@ const ( | |||
| 75 | 77 | exclusionSessionStopped = 1 | |
| 76 | 78 | ) | |
| 77 | 79 | ||
| 80 | + | // requestOnly is a bound NeferWL global or created object that never sends | |
| 81 | + | // events: wlturbo has no generated binding for the extension. | |
| 82 | + | type requestOnly struct{ wlturbo.BaseProxy } | |
| 83 | + | ||
| 84 | + | func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false } | |
| 85 | + | ||
| 86 | + | func newRequestOnly(ctx *wlturbo.Context) *requestOnly { | |
| 87 | + | p := &requestOnly{} | |
| 88 | + | p.SetContext(ctx) | |
| 89 | + | return p | |
| 90 | + | } | |
| 91 | + | ||
| 78 | 92 | // extensions holds the bound optional NeferWL globals. | |
| 79 | 93 | type extensions struct { | |
| 80 | 94 | source *requestOnly | |
| @@ -82,28 +96,54 @@ type extensions struct { | |||
| 82 | 96 | } | |
| 83 | 97 | ||
| 84 | 98 | // bindExtras binds every optional global. None is an error when absent. | |
| 85 | - | func (s *Source) bindExtras() (err error) { | |
| 86 | - | if s.xdgOutputs, err = s.bindOptional(xdgOutputIface, xdgOutputVersion); err != nil { | |
| 99 | + | func (s *Source) bindExtras() error { | |
| 100 | + | x := xdgoutput.NewZxdgOutputManager(s.wl) | |
| 101 | + | switch ok, err := s.bindOptional(xdgoutput.ZxdgOutputManagerInterface, xdgOutputVersion, x); { | |
| 102 | + | case err != nil: | |
| 87 | 103 | return err | |
| 104 | + | case ok: | |
| 105 | + | s.xdgOutputs = x | |
| 88 | 106 | } | |
| 89 | 107 | for _, o := range s.outputs { | |
| 90 | 108 | s.watchLogicalSize(o) | |
| 91 | 109 | } | |
| 92 | - | if s.ext.source, err = s.bindOptional(sourceManagerIface, extensionVersion); err != nil { | |
| 110 | + | var err error | |
| 111 | + | if s.ext.source, err = s.bindOptionalExtension(sourceManagerIface); err != nil { | |
| 93 | 112 | return err | |
| 94 | 113 | } | |
| 95 | - | if s.ext.exclusion, err = s.bindOptional(exclusionManagerIface, extensionVersion); err != nil { | |
| 114 | + | if s.ext.exclusion, err = s.bindOptionalExtension(exclusionManagerIface); err != nil { | |
| 96 | 115 | return err | |
| 97 | 116 | } | |
| 98 | 117 | return s.bindWorkspaces() | |
| 99 | 118 | } | |
| 100 | 119 | ||
| 101 | - | func (s *Source) createWorkspaceSource(src *requestOnly, id uint64) error { | |
| 120 | + | // bindOptionalExtension binds a NeferWL global, nil when the compositor has | |
| 121 | + | // none. | |
| 122 | + | func (s *Source) bindOptionalExtension(iface string) (*requestOnly, error) { | |
| 123 | + | p := newRequestOnly(s.wl) | |
| 124 | + | if ok, err := s.bindOptional(iface, extensionVersion, p); err != nil || !ok { | |
| 125 | + | return nil, err | |
| 126 | + | } | |
| 127 | + | return p, nil | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // createWorkspaceSource makes a source that follows the workspace id. | |
| 131 | + | func (s *Source) createWorkspaceSource(id uint64) (*imagecapturesource.ExtImageCaptureSource, error) { | |
| 102 | 132 | h := s.ws.byID[id] | |
| 103 | 133 | if h == nil || h.removed { | |
| 104 | - | return ports.ErrWorkspaceUnavailable | |
| 134 | + | return nil, ports.ErrWorkspaceUnavailable | |
| 105 | 135 | } | |
| 106 | - | return s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h) | |
| 136 | + | src := imagecapturesource.NewExtImageCaptureSource(s.wl) | |
| 137 | + | err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h.proxy) | |
| 138 | + | return src, err | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | // createRegionSource makes a source of a region of out, in logical pixels. | |
| 142 | + | func (s *Source) createRegionSource(out *output, r ports.Region) (*imagecapturesource.ExtImageCaptureSource, error) { | |
| 143 | + | src := imagecapturesource.NewExtImageCaptureSource(s.wl) | |
| 144 | + | err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src}, | |
| 145 | + | src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height)) | |
| 146 | + | return src, err | |
| 107 | 147 | } | |
| 108 | 148 | ||
| 109 | 149 | // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace | |
| @@ -151,7 +191,7 @@ func (s *Source) requestFrames() (bool, error) { | |||
| 151 | 191 | } | |
| 152 | 192 | f := &workspaceFrame{} | |
| 153 | 193 | f.SetContext(s.wl) | |
| 154 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w); err != nil { | |
| 194 | + | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w.proxy); err != nil { | |
| 155 | 195 | s.terminate() | |
| 156 | 196 | return false, fmt.Errorf("wayland: get workspace frame: %w", err) | |
| 157 | 197 | } | |
| @@ -253,7 +293,7 @@ func (s *Source) beginExclusion(t ports.Target) (string, error) { | |||
| 253 | 293 | } | |
| 254 | 294 | e := &exclusion{} | |
| 255 | 295 | e.SetContext(s.wl) | |
| 256 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c); err != nil { | |
| 296 | + | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c.proxy); err != nil { | |
| 257 | 297 | s.terminate() | |
| 258 | 298 | return "", fmt.Errorf("wayland: get exclusion: %w", err) | |
| 259 | 299 | } | |
| @@ -9,6 +9,8 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | "github.com/bnema/wlturbo" | 10 | "github.com/bnema/wlturbo" | ||
| 11 | "github.com/bnema/wlturbo/protocol/core" | 11 | "github.com/bnema/wlturbo/protocol/core" | ||
| 12 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" | |||
| 13 | + | "github.com/bnema/wlturbo/protocol/xdgoutput" | |||
| 12 | 14 | ||||
| 13 | "github.com/bnema/nefercap/internal/ports" | 15 | "github.com/bnema/nefercap/internal/ports" | ||
| 14 | ) | 16 | ) | ||
| @@ -75,6 +77,18 @@ const ( | |||||
| 75 | exclusionSessionStopped = 1 | 77 | exclusionSessionStopped = 1 | ||
| 76 | ) | 78 | ) | ||
| 77 | 79 | ||||
| 80 | + | // requestOnly is a bound NeferWL global or created object that never sends | |||
| 81 | + | // events: wlturbo has no generated binding for the extension. | |||
| 82 | + | type requestOnly struct{ wlturbo.BaseProxy } | |||
| 83 | + | ||||
| 84 | + | func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false } | |||
| 85 | + | ||||
| 86 | + | func newRequestOnly(ctx *wlturbo.Context) *requestOnly { | |||
| 87 | + | p := &requestOnly{} | |||
| 88 | + | p.SetContext(ctx) | |||
| 89 | + | return p | |||
| 90 | + | } | |||
| 91 | + | ||||
| 78 | // extensions holds the bound optional NeferWL globals. | 92 | // extensions holds the bound optional NeferWL globals. | ||
| 79 | type extensions struct { | 93 | type extensions struct { | ||
| 80 | source *requestOnly | 94 | source *requestOnly | ||
| @@ -82,28 +96,54 @@ type extensions struct { | |||||
| 82 | } | 96 | } | ||
| 83 | 97 | ||||
| 84 | // bindExtras binds every optional global. None is an error when absent. | 98 | // bindExtras binds every optional global. None is an error when absent. | ||
| 85 | - | func (s *Source) bindExtras() (err error) { | 99 | + | func (s *Source) bindExtras() error { |
| 86 | - | if s.xdgOutputs, err = s.bindOptional(xdgOutputIface, xdgOutputVersion); err != nil { | 100 | + | x := xdgoutput.NewZxdgOutputManager(s.wl) |
| 101 | + | switch ok, err := s.bindOptional(xdgoutput.ZxdgOutputManagerInterface, xdgOutputVersion, x); { | |||
| 102 | + | case err != nil: | |||
| 87 | return err | 103 | return err | ||
| 104 | + | case ok: | |||
| 105 | + | s.xdgOutputs = x | |||
| 88 | } | 106 | } | ||
| 89 | for _, o := range s.outputs { | 107 | for _, o := range s.outputs { | ||
| 90 | s.watchLogicalSize(o) | 108 | s.watchLogicalSize(o) | ||
| 91 | } | 109 | } | ||
| 92 | - | if s.ext.source, err = s.bindOptional(sourceManagerIface, extensionVersion); err != nil { | 110 | + | var err error |
| 111 | + | if s.ext.source, err = s.bindOptionalExtension(sourceManagerIface); err != nil { | |||
| 93 | return err | 112 | return err | ||
| 94 | } | 113 | } | ||
| 95 | - | if s.ext.exclusion, err = s.bindOptional(exclusionManagerIface, extensionVersion); err != nil { | 114 | + | if s.ext.exclusion, err = s.bindOptionalExtension(exclusionManagerIface); err != nil { |
| 96 | return err | 115 | return err | ||
| 97 | } | 116 | } | ||
| 98 | return s.bindWorkspaces() | 117 | return s.bindWorkspaces() | ||
| 99 | } | 118 | } | ||
| 100 | 119 | ||||
| 101 | - | func (s *Source) createWorkspaceSource(src *requestOnly, id uint64) error { | 120 | + | // bindOptionalExtension binds a NeferWL global, nil when the compositor has |
| 121 | + | // none. | |||
| 122 | + | func (s *Source) bindOptionalExtension(iface string) (*requestOnly, error) { | |||
| 123 | + | p := newRequestOnly(s.wl) | |||
| 124 | + | if ok, err := s.bindOptional(iface, extensionVersion, p); err != nil || !ok { | |||
| 125 | + | return nil, err | |||
| 126 | + | } | |||
| 127 | + | return p, nil | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | // createWorkspaceSource makes a source that follows the workspace id. | |||
| 131 | + | func (s *Source) createWorkspaceSource(id uint64) (*imagecapturesource.ExtImageCaptureSource, error) { | |||
| 102 | h := s.ws.byID[id] | 132 | h := s.ws.byID[id] | ||
| 103 | if h == nil || h.removed { | 133 | if h == nil || h.removed { | ||
| 104 | - | return ports.ErrWorkspaceUnavailable | 134 | + | return nil, ports.ErrWorkspaceUnavailable |
| 105 | } | 135 | } | ||
| 106 | - | return s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h) | 136 | + | src := imagecapturesource.NewExtImageCaptureSource(s.wl) |
| 137 | + | err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h.proxy) | |||
| 138 | + | return src, err | |||
| 139 | + | } | |||
| 140 | + | ||||
| 141 | + | // createRegionSource makes a source of a region of out, in logical pixels. | |||
| 142 | + | func (s *Source) createRegionSource(out *output, r ports.Region) (*imagecapturesource.ExtImageCaptureSource, error) { | |||
| 143 | + | src := imagecapturesource.NewExtImageCaptureSource(s.wl) | |||
| 144 | + | err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src}, | |||
| 145 | + | src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height)) | |||
| 146 | + | return src, err | |||
| 107 | } | 147 | } | ||
| 108 | 148 | ||||
| 109 | // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace | 149 | // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace | ||
| @@ -151,7 +191,7 @@ func (s *Source) requestFrames() (bool, error) { | |||||
| 151 | } | 191 | } | ||
| 152 | f := &workspaceFrame{} | 192 | f := &workspaceFrame{} | ||
| 153 | f.SetContext(s.wl) | 193 | f.SetContext(s.wl) | ||
| 154 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w); err != nil { | 194 | + | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w.proxy); err != nil { |
| 155 | s.terminate() | 195 | s.terminate() | ||
| 156 | return false, fmt.Errorf("wayland: get workspace frame: %w", err) | 196 | return false, fmt.Errorf("wayland: get workspace frame: %w", err) | ||
| 157 | } | 197 | } | ||
| @@ -253,7 +293,7 @@ func (s *Source) beginExclusion(t ports.Target) (string, error) { | |||||
| 253 | } | 293 | } | ||
| 254 | e := &exclusion{} | 294 | e := &exclusion{} | ||
| 255 | e.SetContext(s.wl) | 295 | e.SetContext(s.wl) | ||
| 256 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c); err != nil { | 296 | + | if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c.proxy); err != nil { |
| 257 | s.terminate() | 297 | s.terminate() | ||
| 258 | return "", fmt.Errorf("wayland: get exclusion: %w", err) | 298 | return "", fmt.Errorf("wayland: get exclusion: %w", err) | ||
| 259 | } | 299 | } | ||
M internal/adapters/wayland/source.go +22 -25
| @@ -20,6 +20,9 @@ import ( | |||
| 20 | 20 | ||
| 21 | 21 | "github.com/bnema/wlturbo" | |
| 22 | 22 | "github.com/bnema/wlturbo/protocol/core" | |
| 23 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" | |
| 24 | + | "github.com/bnema/wlturbo/protocol/imagecopycapture" | |
| 25 | + | "github.com/bnema/wlturbo/protocol/xdgoutput" | |
| 23 | 26 | "github.com/bnema/zerowrap" | |
| 24 | 27 | ||
| 25 | 28 | "github.com/bnema/nefercap/internal/logging" | |
| @@ -38,13 +41,6 @@ const ( | |||
| 38 | 41 | defaultPollWait = 2 * time.Millisecond | |
| 39 | 42 | ) | |
| 40 | 43 | ||
| 41 | - | // Globals of the capture protocols. | |
| 42 | - | const ( | |
| 43 | - | outputSourceIface = "ext_output_image_capture_source_manager_v1" | |
| 44 | - | copyManagerIface = "ext_image_copy_capture_manager_v1" | |
| 45 | - | xdgOutputIface = "zxdg_output_manager_v1" | |
| 46 | - | ) | |
| 47 | - | ||
| 48 | 44 | // output is the client-side state of one bound wl_output global. | |
| 49 | 45 | type output struct { | |
| 50 | 46 | proxy *core.Output | |
| @@ -54,7 +50,7 @@ type output struct { | |||
| 54 | 50 | width, height int32 | |
| 55 | 51 | scale int32 | |
| 56 | 52 | lw, lh int32 // logical size from xdg-output, 0 when unknown | |
| 57 | - | xdg *xdgOutput | |
| 53 | + | xdg *xdgoutput.ZxdgOutput | |
| 58 | 54 | removed bool | |
| 59 | 55 | } | |
| 60 | 56 | ||
| @@ -85,9 +81,9 @@ type Source struct { | |||
| 85 | 81 | watchActive atomic.Bool | |
| 86 | 82 | ||
| 87 | 83 | shm *core.Shm | |
| 88 | - | copyManager *requestOnly // ext_image_copy_capture_manager_v1 | |
| 89 | - | outSource *requestOnly // ext_output_image_capture_source_manager_v1 | |
| 90 | - | xdgOutputs *requestOnly // zxdg_output_manager_v1, nil when absent | |
| 84 | + | copyManager *imagecopycapture.ExtImageCopyCaptureManager | |
| 85 | + | outSource *imagecapturesource.ExtOutputImageCaptureSourceManager | |
| 86 | + | xdgOutputs *xdgoutput.ZxdgOutputManager // nil when absent | |
| 91 | 87 | outputs map[uint32]*output | |
| 92 | 88 | byProxy map[uint32]*output // wl_output proxy ID to output | |
| 93 | 89 | bound int // wl_output globals bound so far | |
| @@ -176,10 +172,12 @@ func (s *Source) discover() (err error) { | |||
| 176 | 172 | if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil { | |
| 177 | 173 | return fmt.Errorf("wayland: bind wl_shm: %w", err) | |
| 178 | 174 | } | |
| 179 | - | if s.copyManager, err = s.bindRequired(copyManagerIface, copyVersion); err != nil { | |
| 175 | + | s.copyManager = imagecopycapture.NewExtImageCopyCaptureManager(s.wl) | |
| 176 | + | if err = s.bindRequired(imagecopycapture.ExtImageCopyCaptureManagerInterface, s.copyManager); err != nil { | |
| 180 | 177 | return err | |
| 181 | 178 | } | |
| 182 | - | if s.outSource, err = s.bindRequired(outputSourceIface, copyVersion); err != nil { | |
| 179 | + | s.outSource = imagecapturesource.NewExtOutputImageCaptureSourceManager(s.wl) | |
| 180 | + | if err = s.bindRequired(imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, s.outSource); err != nil { | |
| 183 | 181 | return err | |
| 184 | 182 | } | |
| 185 | 183 | if err = s.bindExtras(); err != nil { | |
| @@ -189,28 +187,27 @@ func (s *Source) discover() (err error) { | |||
| 189 | 187 | } | |
| 190 | 188 | ||
| 191 | 189 | // bindRequired binds a global that capture cannot work without. | |
| 192 | - | func (s *Source) bindRequired(iface string, version uint32) (*requestOnly, error) { | |
| 193 | - | p := newRequestOnly(s.wl) | |
| 194 | - | if _, err := s.display.Registry().BindNegotiated(iface, version, p); err != nil { | |
| 190 | + | func (s *Source) bindRequired(iface string, p wlturbo.Proxy) error { | |
| 191 | + | if _, err := s.display.Registry().BindNegotiated(iface, copyVersion, p); err != nil { | |
| 195 | 192 | if errors.Is(err, wlturbo.ErrGlobalNotFound) { | |
| 196 | - | return nil, fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface) | |
| 193 | + | return fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface) | |
| 197 | 194 | } | |
| 198 | - | return nil, fmt.Errorf("wayland: bind %s: %w", iface, err) | |
| 195 | + | return fmt.Errorf("wayland: bind %s: %w", iface, err) | |
| 199 | 196 | } | |
| 200 | - | return p, nil | |
| 197 | + | return nil | |
| 201 | 198 | } | |
| 202 | 199 | ||
| 203 | - | // bindOptional binds a global when the compositor offers it. | |
| 204 | - | func (s *Source) bindOptional(iface string, version uint32) (*requestOnly, error) { | |
| 205 | - | p := newRequestOnly(s.wl) | |
| 200 | + | // bindOptional binds a global when the compositor offers it, and reports | |
| 201 | + | // whether it did. | |
| 202 | + | func (s *Source) bindOptional(iface string, version uint32, p wlturbo.Proxy) (bool, error) { | |
| 206 | 203 | _, err := s.display.Registry().BindNegotiated(iface, version, p) | |
| 207 | 204 | switch { | |
| 208 | 205 | case err == nil: | |
| 209 | - | return p, nil | |
| 206 | + | return true, nil | |
| 210 | 207 | case errors.Is(err, wlturbo.ErrGlobalNotFound): | |
| 211 | - | return nil, nil | |
| 208 | + | return false, nil | |
| 212 | 209 | } | |
| 213 | - | return nil, fmt.Errorf("wayland: bind %s: %w", iface, err) | |
| 210 | + | return false, fmt.Errorf("wayland: bind %s: %w", iface, err) | |
| 214 | 211 | } | |
| 215 | 212 | ||
| 216 | 213 | func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) { | |
| @@ -20,6 +20,9 @@ import ( | |||||
| 20 | 20 | ||||
| 21 | "github.com/bnema/wlturbo" | 21 | "github.com/bnema/wlturbo" | ||
| 22 | "github.com/bnema/wlturbo/protocol/core" | 22 | "github.com/bnema/wlturbo/protocol/core" | ||
| 23 | + | "github.com/bnema/wlturbo/protocol/imagecapturesource" | |||
| 24 | + | "github.com/bnema/wlturbo/protocol/imagecopycapture" | |||
| 25 | + | "github.com/bnema/wlturbo/protocol/xdgoutput" | |||
| 23 | "github.com/bnema/zerowrap" | 26 | "github.com/bnema/zerowrap" | ||
| 24 | 27 | ||||
| 25 | "github.com/bnema/nefercap/internal/logging" | 28 | "github.com/bnema/nefercap/internal/logging" | ||
| @@ -38,13 +41,6 @@ const ( | |||||
| 38 | defaultPollWait = 2 * time.Millisecond | 41 | defaultPollWait = 2 * time.Millisecond | ||
| 39 | ) | 42 | ) | ||
| 40 | 43 | ||||
| 41 | - | // Globals of the capture protocols. | |||
| 42 | - | const ( | |||
| 43 | - | outputSourceIface = "ext_output_image_capture_source_manager_v1" | |||
| 44 | - | copyManagerIface = "ext_image_copy_capture_manager_v1" | |||
| 45 | - | xdgOutputIface = "zxdg_output_manager_v1" | |||
| 46 | - | ) | |||
| 47 | - | ||||
| 48 | // output is the client-side state of one bound wl_output global. | 44 | // output is the client-side state of one bound wl_output global. | ||
| 49 | type output struct { | 45 | type output struct { | ||
| 50 | proxy *core.Output | 46 | proxy *core.Output | ||
| @@ -54,7 +50,7 @@ type output struct { | |||||
| 54 | width, height int32 | 50 | width, height int32 | ||
| 55 | scale int32 | 51 | scale int32 | ||
| 56 | lw, lh int32 // logical size from xdg-output, 0 when unknown | 52 | lw, lh int32 // logical size from xdg-output, 0 when unknown | ||
| 57 | - | xdg *xdgOutput | 53 | + | xdg *xdgoutput.ZxdgOutput |
| 58 | removed bool | 54 | removed bool | ||
| 59 | } | 55 | } | ||
| 60 | 56 | ||||
| @@ -85,9 +81,9 @@ type Source struct { | |||||
| 85 | watchActive atomic.Bool | 81 | watchActive atomic.Bool | ||
| 86 | 82 | ||||
| 87 | shm *core.Shm | 83 | shm *core.Shm | ||
| 88 | - | copyManager *requestOnly // ext_image_copy_capture_manager_v1 | 84 | + | copyManager *imagecopycapture.ExtImageCopyCaptureManager |
| 89 | - | outSource *requestOnly // ext_output_image_capture_source_manager_v1 | 85 | + | outSource *imagecapturesource.ExtOutputImageCaptureSourceManager |
| 90 | - | xdgOutputs *requestOnly // zxdg_output_manager_v1, nil when absent | 86 | + | xdgOutputs *xdgoutput.ZxdgOutputManager // nil when absent |
| 91 | outputs map[uint32]*output | 87 | outputs map[uint32]*output | ||
| 92 | byProxy map[uint32]*output // wl_output proxy ID to output | 88 | byProxy map[uint32]*output // wl_output proxy ID to output | ||
| 93 | bound int // wl_output globals bound so far | 89 | bound int // wl_output globals bound so far | ||
| @@ -176,10 +172,12 @@ func (s *Source) discover() (err error) { | |||||
| 176 | if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil { | 172 | if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil { | ||
| 177 | return fmt.Errorf("wayland: bind wl_shm: %w", err) | 173 | return fmt.Errorf("wayland: bind wl_shm: %w", err) | ||
| 178 | } | 174 | } | ||
| 179 | - | if s.copyManager, err = s.bindRequired(copyManagerIface, copyVersion); err != nil { | 175 | + | s.copyManager = imagecopycapture.NewExtImageCopyCaptureManager(s.wl) |
| 176 | + | if err = s.bindRequired(imagecopycapture.ExtImageCopyCaptureManagerInterface, s.copyManager); err != nil { | |||
| 180 | return err | 177 | return err | ||
| 181 | } | 178 | } | ||
| 182 | - | if s.outSource, err = s.bindRequired(outputSourceIface, copyVersion); err != nil { | 179 | + | s.outSource = imagecapturesource.NewExtOutputImageCaptureSourceManager(s.wl) |
| 180 | + | if err = s.bindRequired(imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, s.outSource); err != nil { | |||
| 183 | return err | 181 | return err | ||
| 184 | } | 182 | } | ||
| 185 | if err = s.bindExtras(); err != nil { | 183 | if err = s.bindExtras(); err != nil { | ||
| @@ -189,28 +187,27 @@ func (s *Source) discover() (err error) { | |||||
| 189 | } | 187 | } | ||
| 190 | 188 | ||||
| 191 | // bindRequired binds a global that capture cannot work without. | 189 | // bindRequired binds a global that capture cannot work without. | ||
| 192 | - | func (s *Source) bindRequired(iface string, version uint32) (*requestOnly, error) { | 190 | + | func (s *Source) bindRequired(iface string, p wlturbo.Proxy) error { |
| 193 | - | p := newRequestOnly(s.wl) | 191 | + | if _, err := s.display.Registry().BindNegotiated(iface, copyVersion, p); err != nil { |
| 194 | - | if _, err := s.display.Registry().BindNegotiated(iface, version, p); err != nil { | |||
| 195 | if errors.Is(err, wlturbo.ErrGlobalNotFound) { | 192 | if errors.Is(err, wlturbo.ErrGlobalNotFound) { | ||
| 196 | - | return nil, fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface) | 193 | + | return fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface) |
| 197 | } | 194 | } | ||
| 198 | - | return nil, fmt.Errorf("wayland: bind %s: %w", iface, err) | 195 | + | return fmt.Errorf("wayland: bind %s: %w", iface, err) |
| 199 | } | 196 | } | ||
| 200 | - | return p, nil | 197 | + | return nil |
| 201 | } | 198 | } | ||
| 202 | 199 | ||||
| 203 | - | // bindOptional binds a global when the compositor offers it. | 200 | + | // bindOptional binds a global when the compositor offers it, and reports |
| 204 | - | func (s *Source) bindOptional(iface string, version uint32) (*requestOnly, error) { | 201 | + | // whether it did. |
| 205 | - | p := newRequestOnly(s.wl) | 202 | + | func (s *Source) bindOptional(iface string, version uint32, p wlturbo.Proxy) (bool, error) { |
| 206 | _, err := s.display.Registry().BindNegotiated(iface, version, p) | 203 | _, err := s.display.Registry().BindNegotiated(iface, version, p) | ||
| 207 | switch { | 204 | switch { | ||
| 208 | case err == nil: | 205 | case err == nil: | ||
| 209 | - | return p, nil | 206 | + | return true, nil |
| 210 | case errors.Is(err, wlturbo.ErrGlobalNotFound): | 207 | case errors.Is(err, wlturbo.ErrGlobalNotFound): | ||
| 211 | - | return nil, nil | 208 | + | return false, nil |
| 212 | } | 209 | } | ||
| 213 | - | return nil, fmt.Errorf("wayland: bind %s: %w", iface, err) | 210 | + | return false, fmt.Errorf("wayland: bind %s: %w", iface, err) |
| 214 | } | 211 | } | ||
| 215 | 212 | ||||
| 216 | func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) { | 213 | func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) { | ||
M internal/adapters/wayland/source_test.go +3 -2
| @@ -409,8 +409,9 @@ func TestConcurrentCallsAreSerialized(t *testing.T) { | |||
| 409 | 409 | } | |
| 410 | 410 | } | |
| 411 | 411 | ||
| 412 | - | // allocBudget is measured (see the log of TestCaptureAllocations), with | |
| 413 | - | // headroom. | |
| 412 | + | // allocBudget is measured (see the log of TestCaptureAllocations: 17 to 18), | |
| 413 | + | // with a small margin. The session builds the frame handlers once, so a frame | |
| 414 | + | // only allocates its proxy and the handler slices of the generated binding. | |
| 414 | 415 | const allocBudget = 20 | |
| 415 | 416 | ||
| 416 | 417 | // Steady state cost of one capture over the real transport. The measurement | |
| @@ -409,8 +409,9 @@ func TestConcurrentCallsAreSerialized(t *testing.T) { | |||||
| 409 | } | 409 | } | ||
| 410 | } | 410 | } | ||
| 411 | 411 | ||||
| 412 | - | // allocBudget is measured (see the log of TestCaptureAllocations), with | 412 | + | // allocBudget is measured (see the log of TestCaptureAllocations: 17 to 18), |
| 413 | - | // headroom. | 413 | + | // with a small margin. The session builds the frame handlers once, so a frame |
| 414 | + | // only allocates its proxy and the handler slices of the generated binding. | |||
| 414 | const allocBudget = 20 | 415 | const allocBudget = 20 | ||
| 415 | 416 | ||||
| 416 | // Steady state cost of one capture over the real transport. The measurement | 417 | // Steady state cost of one capture over the real transport. The measurement | ||
M internal/adapters/wayland/workspaces.go +61 -123
| @@ -3,13 +3,12 @@ package wayland | |||
| 3 | 3 | import ( | |
| 4 | 4 | "cmp" | |
| 5 | 5 | "context" | |
| 6 | - | "errors" | |
| 7 | 6 | "slices" | |
| 8 | 7 | "strconv" | |
| 9 | 8 | "unicode" | |
| 10 | 9 | "unicode/utf8" | |
| 11 | 10 | ||
| 12 | - | "github.com/bnema/wlturbo" | |
| 11 | + | extworkspace "github.com/bnema/wlturbo/protocol/workspace" | |
| 13 | 12 | ||
| 14 | 13 | "github.com/bnema/nefercap/internal/ports" | |
| 15 | 14 | ) | |
| @@ -19,25 +18,19 @@ import ( | |||
| 19 | 18 | // compositor (no activate, no commit). | |
| 20 | 19 | ||
| 21 | 20 | const ( | |
| 22 | - | workspaceIface = "ext_workspace_manager_v1" | |
| 23 | 21 | workspaceVersion = 1 | |
| 24 | 22 | maxWorkspaces = 256 // bound of the inventory | |
| 25 | 23 | maxNameLen = 256 // bytes, for workspace names | |
| 26 | 24 | maxGroupOutputs = 64 | |
| 27 | 25 | ||
| 28 | - | wsStateActive = 1 | |
| 29 | - | ||
| 30 | - | reqGroupDestroy = 1 | |
| 31 | - | reqHandleDestroy = 0 | |
| 32 | - | ||
| 33 | 26 | maxInterned = 4 * maxWorkspaces // id strings remembered across removals | |
| 34 | 27 | ) | |
| 35 | 28 | ||
| 36 | 29 | // workspaceState is the ext-workspace inventory of a Source. | |
| 37 | 30 | type workspaceState struct { | |
| 38 | - | mgr *workspaceManager // nil when the compositor has no ext-workspace | |
| 39 | - | handles map[uint32]*workspace // by proxy ID | |
| 40 | - | byID map[uint64]*workspace // by Source-local identity | |
| 31 | + | mgr *extworkspace.ExtWorkspaceManager // nil when the compositor has no ext-workspace | |
| 32 | + | handles map[uint32]*workspace // by proxy ID | |
| 33 | + | byID map[uint64]*workspace // by Source-local identity | |
| 41 | 34 | groups map[uint32]*workspaceGroup | |
| 42 | 35 | next uint64 // last identity handed out; identities start at 1 | |
| 43 | 36 | interned map[string]uint64 // id string to identity, bounded by maxInterned | |
| @@ -45,105 +38,67 @@ type workspaceState struct { | |||
| 45 | 38 | shown uint64 // workspace last verified as displayed, without the extension | |
| 46 | 39 | } | |
| 47 | 40 | ||
| 48 | - | // workspaceManager receives the creation events. It is bound once. | |
| 49 | - | type workspaceManager struct { | |
| 50 | - | wlturbo.BaseProxy | |
| 51 | - | s *Source | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | func (*workspaceManager) EventSignature(op uint16) (string, bool) { | |
| 55 | - | switch op { | |
| 56 | - | case 0, 1: | |
| 57 | - | return "new_id,", true // workspace_group, workspace | |
| 58 | - | case 2, 3: | |
| 59 | - | return "", true // done, finished | |
| 60 | - | } | |
| 61 | - | return "", false | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | func (m *workspaceManager) Dispatch(e *wlturbo.Event) { | |
| 65 | - | s := m.s | |
| 66 | - | switch e.Opcode { | |
| 67 | - | case 0: | |
| 68 | - | id := e.Uint32() | |
| 69 | - | g := &workspaceGroup{s: s, outputs: make(map[uint32]bool)} | |
| 70 | - | g.SetContext(s.wl) | |
| 71 | - | g.SetID(id) | |
| 72 | - | g.SetVersion(m.Version()) | |
| 73 | - | s.wl.Register(g) | |
| 41 | + | // watchWorkspaces subscribes to the creation events of the manager. | |
| 42 | + | func (s *Source) watchWorkspaces(m *extworkspace.ExtWorkspaceManager) { | |
| 43 | + | m.OnWorkspaceGroup(func(proxy *extworkspace.ExtWorkspaceGroupHandle) { | |
| 44 | + | // wlturbo registered the proxy already, which absorbs its events. | |
| 74 | 45 | if len(s.ws.groups) >= maxWorkspaces { | |
| 75 | 46 | s.ws.overflow = true | |
| 76 | 47 | return | |
| 77 | 48 | } | |
| 78 | - | s.ws.groups[id] = g | |
| 79 | - | case 1: | |
| 80 | - | id := e.Uint32() | |
| 81 | - | w := &workspace{s: s} | |
| 82 | - | w.SetContext(s.wl) | |
| 83 | - | w.SetID(id) | |
| 84 | - | w.SetVersion(m.Version()) | |
| 85 | - | s.wl.Register(w) | |
| 49 | + | g := &workspaceGroup{proxy: proxy, outputs: make(map[uint32]bool)} | |
| 50 | + | s.ws.groups[proxy.ID()] = g | |
| 51 | + | s.watchGroup(g) | |
| 52 | + | }) | |
| 53 | + | m.OnWorkspace(func(proxy *extworkspace.ExtWorkspaceHandle) { | |
| 86 | 54 | if len(s.ws.handles) >= maxWorkspaces { | |
| 87 | 55 | s.ws.overflow = true | |
| 88 | 56 | return | |
| 89 | 57 | } | |
| 90 | - | s.ws.handles[id] = w | |
| 91 | - | case 2: | |
| 92 | - | s.ws.assignIDs() | |
| 93 | - | } | |
| 58 | + | w := &workspace{proxy: proxy} | |
| 59 | + | s.ws.handles[proxy.ID()] = w | |
| 60 | + | s.watchWorkspace(w) | |
| 61 | + | }) | |
| 62 | + | m.OnDone(s.ws.assignIDs) | |
| 94 | 63 | } | |
| 95 | 64 | ||
| 96 | 65 | type workspaceGroup struct { | |
| 97 | - | wlturbo.BaseProxy | |
| 98 | - | s *Source | |
| 66 | + | proxy *extworkspace.ExtWorkspaceGroupHandle | |
| 99 | 67 | outputs map[uint32]bool // wl_output proxy IDs | |
| 100 | 68 | } | |
| 101 | 69 | ||
| 102 | - | func (*workspaceGroup) EventSignature(op uint16) (string, bool) { | |
| 103 | - | switch op { | |
| 104 | - | case 0: | |
| 105 | - | return "uint,", true // capabilities | |
| 106 | - | case 1, 2, 3, 4: | |
| 107 | - | return "object,", true // output_enter, output_leave, workspace_enter, workspace_leave | |
| 108 | - | case 5: | |
| 109 | - | return "", true // removed | |
| 110 | - | } | |
| 111 | - | return "", false | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | func (g *workspaceGroup) Dispatch(e *wlturbo.Event) { | |
| 115 | - | s := g.s | |
| 116 | - | switch e.Opcode { | |
| 117 | - | case 1: | |
| 118 | - | if id := e.Uint32(); len(g.outputs) < maxGroupOutputs { | |
| 70 | + | func (s *Source) watchGroup(g *workspaceGroup) { | |
| 71 | + | g.proxy.OnOutputEnter(func(id uint32) { | |
| 72 | + | if len(g.outputs) < maxGroupOutputs { | |
| 119 | 73 | g.outputs[id] = true | |
| 120 | 74 | } | |
| 121 | - | case 2: | |
| 122 | - | delete(g.outputs, e.Uint32()) | |
| 123 | - | case 3: | |
| 124 | - | if w := s.ws.handles[e.Uint32()]; w != nil { | |
| 75 | + | }) | |
| 76 | + | g.proxy.OnOutputLeave(func(id uint32) { delete(g.outputs, id) }) | |
| 77 | + | g.proxy.OnWorkspaceEnter(func(id uint32) { | |
| 78 | + | if w := s.ws.handles[id]; w != nil { | |
| 125 | 79 | w.group = g | |
| 126 | 80 | } | |
| 127 | - | case 4: | |
| 128 | - | if w := s.ws.handles[e.Uint32()]; w != nil && w.group == g { | |
| 81 | + | }) | |
| 82 | + | g.proxy.OnWorkspaceLeave(func(id uint32) { | |
| 83 | + | if w := s.ws.handles[id]; w != nil && w.group == g { | |
| 129 | 84 | w.group = nil | |
| 130 | 85 | } | |
| 131 | - | case 5: | |
| 132 | - | delete(s.ws.groups, g.ID()) | |
| 86 | + | }) | |
| 87 | + | g.proxy.OnRemoved(func() { | |
| 88 | + | delete(s.ws.groups, g.proxy.ID()) | |
| 133 | 89 | for _, w := range s.ws.handles { | |
| 134 | 90 | if w.group == g { | |
| 135 | 91 | w.group = nil | |
| 136 | 92 | } | |
| 137 | 93 | } | |
| 138 | - | _ = s.wl.Request(wlturbo.Request{Proxy: g, Opcode: reqGroupDestroy, Name: "ext_workspace_group_handle_v1.destroy", Destructor: true}) | |
| 139 | - | } | |
| 94 | + | _ = g.proxy.Destroy() | |
| 95 | + | }) | |
| 140 | 96 | } | |
| 141 | 97 | ||
| 142 | - | // workspace is one ext_workspace_handle_v1. It is a wlturbo object so that it | |
| 143 | - | // can be the argument of create_workspace_source. | |
| 98 | + | // workspace is one ext_workspace_handle_v1. Its proxy is the argument of | |
| 99 | + | // create_workspace_source. | |
| 144 | 100 | type workspace struct { | |
| 145 | - | wlturbo.BaseProxy | |
| 146 | - | s *Source | |
| 101 | + | proxy *extworkspace.ExtWorkspaceHandle | |
| 147 | 102 | idStr string // the id event: empty for a temporary workspace | |
| 148 | 103 | name string | |
| 149 | 104 | state uint32 | |
| @@ -153,43 +108,29 @@ type workspace struct { | |||
| 153 | 108 | frame *workspaceFrame // NeferWL's extension only; nil until asked for | |
| 154 | 109 | } | |
| 155 | 110 | ||
| 156 | - | func (*workspace) EventSignature(op uint16) (string, bool) { | |
| 157 | - | switch op { | |
| 158 | - | case 0, 1: | |
| 159 | - | return "string,", true // id, name | |
| 160 | - | case 2: | |
| 161 | - | return "array,", true // coordinates | |
| 162 | - | case 3, 4: | |
| 163 | - | return "uint,", true // state, capabilities | |
| 164 | - | case 5: | |
| 165 | - | return "", true // removed | |
| 166 | - | } | |
| 167 | - | return "", false | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | func (w *workspace) Dispatch(e *wlturbo.Event) { | |
| 171 | - | switch e.Opcode { | |
| 172 | - | case 0: | |
| 111 | + | // watchWorkspace subscribes to the events of w. Coordinates are not used: | |
| 112 | + | // nothing here depends on the grid. | |
| 113 | + | func (s *Source) watchWorkspace(w *workspace) { | |
| 114 | + | w.proxy.OnId(func(id string) { | |
| 173 | 115 | if w.idStr == "" { | |
| 174 | - | w.idStr = e.String() | |
| 116 | + | w.idStr = id | |
| 175 | 117 | } | |
| 176 | - | case 1: | |
| 177 | - | if n := e.String(); validName(n) { | |
| 118 | + | }) | |
| 119 | + | w.proxy.OnName(func(n string) { | |
| 120 | + | if validName(n) { | |
| 178 | 121 | w.name = n | |
| 179 | 122 | } | |
| 180 | - | // opcode 2 (coordinates) is not used: nothing here depends on the grid. | |
| 181 | - | case 3: | |
| 182 | - | w.state = e.Uint32() | |
| 183 | - | case 5: | |
| 123 | + | }) | |
| 124 | + | w.proxy.OnState(func(state uint32) { w.state = state }) | |
| 125 | + | w.proxy.OnRemoved(func() { | |
| 184 | 126 | w.removed = true | |
| 185 | - | s := w.s | |
| 186 | 127 | s.releaseWorkspaceFrame(w) | |
| 187 | - | delete(s.ws.handles, w.ID()) | |
| 128 | + | delete(s.ws.handles, w.proxy.ID()) | |
| 188 | 129 | if w.id != 0 && s.ws.byID[w.id] == w { | |
| 189 | 130 | delete(s.ws.byID, w.id) | |
| 190 | 131 | } | |
| 191 | - | _ = s.wl.Request(wlturbo.Request{Proxy: w, Opcode: reqHandleDestroy, Name: "ext_workspace_handle_v1.destroy", Destructor: true}) | |
| 192 | - | } | |
| 132 | + | _ = w.proxy.Destroy() | |
| 133 | + | }) | |
| 193 | 134 | } | |
| 194 | 135 | ||
| 195 | 136 | // assignIDs gives every workspace announced since the last done its | |
| @@ -207,7 +148,7 @@ func (ws *workspaceState) assignIDs() { | |||
| 207 | 148 | } | |
| 208 | 149 | } | |
| 209 | 150 | // Map order is random: numbers follow the announcement order. | |
| 210 | - | slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.ID(), b.ID()) }) | |
| 151 | + | slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.proxy.ID(), b.proxy.ID()) }) | |
| 211 | 152 | for _, w := range fresh { | |
| 212 | 153 | w.id = ws.intern(w.idStr) | |
| 213 | 154 | ws.byID[w.id] = w | |
| @@ -266,17 +207,14 @@ func (s *Source) bindWorkspaces() error { | |||
| 266 | 207 | s.ws.byID = make(map[uint64]*workspace) | |
| 267 | 208 | s.ws.groups = make(map[uint32]*workspaceGroup) | |
| 268 | 209 | s.ws.interned = make(map[string]uint64) | |
| 269 | - | m := &workspaceManager{s: s} | |
| 270 | - | m.SetContext(s.wl) | |
| 271 | - | _, err := s.display.Registry().BindNegotiated(workspaceIface, workspaceVersion, m) | |
| 272 | - | switch { | |
| 273 | - | case err == nil: | |
| 274 | - | s.ws.mgr = m | |
| 275 | - | return nil | |
| 276 | - | case errors.Is(err, wlturbo.ErrGlobalNotFound): | |
| 277 | - | return nil | |
| 210 | + | m := extworkspace.NewExtWorkspaceManager(s.wl) | |
| 211 | + | ok, err := s.bindOptional(extworkspace.ExtWorkspaceManagerInterface, workspaceVersion, m) | |
| 212 | + | if err != nil || !ok { | |
| 213 | + | return err | |
| 278 | 214 | } | |
| 279 | - | return err | |
| 215 | + | s.watchWorkspaces(m) | |
| 216 | + | s.ws.mgr = m | |
| 217 | + | return nil | |
| 280 | 218 | } | |
| 281 | 219 | ||
| 282 | 220 | // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per | |
| @@ -324,7 +262,7 @@ func (s *Source) workspaceList() []ports.Workspace { | |||
| 324 | 262 | list = append(list, ports.Workspace{ | |
| 325 | 263 | ID: w.id, OutputID: o.id, Name: w.name, | |
| 326 | 264 | Region: workspaceRegion(w, o), | |
| 327 | - | Active: w.state&wsStateActive != 0, | |
| 265 | + | Active: w.state&extworkspace.STATE_ACTIVE != 0, | |
| 328 | 266 | }) | |
| 329 | 267 | } | |
| 330 | 268 | } | |
| @@ -372,7 +310,7 @@ func (s *Source) workspaceKey(t ports.Target, out *output) (sessionKey, ports.Re | |||
| 372 | 310 | if err := s.roundtrip(); err != nil { | |
| 373 | 311 | return sessionKey{}, ports.Region{}, err | |
| 374 | 312 | } | |
| 375 | - | if w.removed || w.state&wsStateActive == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] { | |
| 313 | + | if w.removed || w.state&extworkspace.STATE_ACTIVE == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] { | |
| 376 | 314 | return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable | |
| 377 | 315 | } | |
| 378 | 316 | s.ws.shown = w.id | |
| @@ -3,13 +3,12 @@ package wayland | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "cmp" | 4 | "cmp" | ||
| 5 | "context" | 5 | "context" | ||
| 6 | - | "errors" | |||
| 7 | "slices" | 6 | "slices" | ||
| 8 | "strconv" | 7 | "strconv" | ||
| 9 | "unicode" | 8 | "unicode" | ||
| 10 | "unicode/utf8" | 9 | "unicode/utf8" | ||
| 11 | 10 | ||||
| 12 | - | "github.com/bnema/wlturbo" | 11 | + | extworkspace "github.com/bnema/wlturbo/protocol/workspace" |
| 13 | 12 | ||||
| 14 | "github.com/bnema/nefercap/internal/ports" | 13 | "github.com/bnema/nefercap/internal/ports" | ||
| 15 | ) | 14 | ) | ||
| @@ -19,25 +18,19 @@ import ( | |||||
| 19 | // compositor (no activate, no commit). | 18 | // compositor (no activate, no commit). | ||
| 20 | 19 | ||||
| 21 | const ( | 20 | const ( | ||
| 22 | - | workspaceIface = "ext_workspace_manager_v1" | |||
| 23 | workspaceVersion = 1 | 21 | workspaceVersion = 1 | ||
| 24 | maxWorkspaces = 256 // bound of the inventory | 22 | maxWorkspaces = 256 // bound of the inventory | ||
| 25 | maxNameLen = 256 // bytes, for workspace names | 23 | maxNameLen = 256 // bytes, for workspace names | ||
| 26 | maxGroupOutputs = 64 | 24 | maxGroupOutputs = 64 | ||
| 27 | 25 | ||||
| 28 | - | wsStateActive = 1 | |||
| 29 | - | ||||
| 30 | - | reqGroupDestroy = 1 | |||
| 31 | - | reqHandleDestroy = 0 | |||
| 32 | - | ||||
| 33 | maxInterned = 4 * maxWorkspaces // id strings remembered across removals | 26 | maxInterned = 4 * maxWorkspaces // id strings remembered across removals | ||
| 34 | ) | 27 | ) | ||
| 35 | 28 | ||||
| 36 | // workspaceState is the ext-workspace inventory of a Source. | 29 | // workspaceState is the ext-workspace inventory of a Source. | ||
| 37 | type workspaceState struct { | 30 | type workspaceState struct { | ||
| 38 | - | mgr *workspaceManager // nil when the compositor has no ext-workspace | 31 | + | mgr *extworkspace.ExtWorkspaceManager // nil when the compositor has no ext-workspace |
| 39 | - | handles map[uint32]*workspace // by proxy ID | 32 | + | handles map[uint32]*workspace // by proxy ID |
| 40 | - | byID map[uint64]*workspace // by Source-local identity | 33 | + | byID map[uint64]*workspace // by Source-local identity |
| 41 | groups map[uint32]*workspaceGroup | 34 | groups map[uint32]*workspaceGroup | ||
| 42 | next uint64 // last identity handed out; identities start at 1 | 35 | next uint64 // last identity handed out; identities start at 1 | ||
| 43 | interned map[string]uint64 // id string to identity, bounded by maxInterned | 36 | interned map[string]uint64 // id string to identity, bounded by maxInterned | ||
| @@ -45,105 +38,67 @@ type workspaceState struct { | |||||
| 45 | shown uint64 // workspace last verified as displayed, without the extension | 38 | shown uint64 // workspace last verified as displayed, without the extension | ||
| 46 | } | 39 | } | ||
| 47 | 40 | ||||
| 48 | - | // workspaceManager receives the creation events. It is bound once. | 41 | + | // watchWorkspaces subscribes to the creation events of the manager. |
| 49 | - | type workspaceManager struct { | 42 | + | func (s *Source) watchWorkspaces(m *extworkspace.ExtWorkspaceManager) { |
| 50 | - | wlturbo.BaseProxy | 43 | + | m.OnWorkspaceGroup(func(proxy *extworkspace.ExtWorkspaceGroupHandle) { |
| 51 | - | s *Source | 44 | + | // wlturbo registered the proxy already, which absorbs its events. |
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | func (*workspaceManager) EventSignature(op uint16) (string, bool) { | |||
| 55 | - | switch op { | |||
| 56 | - | case 0, 1: | |||
| 57 | - | return "new_id,", true // workspace_group, workspace | |||
| 58 | - | case 2, 3: | |||
| 59 | - | return "", true // done, finished | |||
| 60 | - | } | |||
| 61 | - | return "", false | |||
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | func (m *workspaceManager) Dispatch(e *wlturbo.Event) { | |||
| 65 | - | s := m.s | |||
| 66 | - | switch e.Opcode { | |||
| 67 | - | case 0: | |||
| 68 | - | id := e.Uint32() | |||
| 69 | - | g := &workspaceGroup{s: s, outputs: make(map[uint32]bool)} | |||
| 70 | - | g.SetContext(s.wl) | |||
| 71 | - | g.SetID(id) | |||
| 72 | - | g.SetVersion(m.Version()) | |||
| 73 | - | s.wl.Register(g) | |||
| 74 | if len(s.ws.groups) >= maxWorkspaces { | 45 | if len(s.ws.groups) >= maxWorkspaces { | ||
| 75 | s.ws.overflow = true | 46 | s.ws.overflow = true | ||
| 76 | return | 47 | return | ||
| 77 | } | 48 | } | ||
| 78 | - | s.ws.groups[id] = g | 49 | + | g := &workspaceGroup{proxy: proxy, outputs: make(map[uint32]bool)} |
| 79 | - | case 1: | 50 | + | s.ws.groups[proxy.ID()] = g |
| 80 | - | id := e.Uint32() | 51 | + | s.watchGroup(g) |
| 81 | - | w := &workspace{s: s} | 52 | + | }) |
| 82 | - | w.SetContext(s.wl) | 53 | + | m.OnWorkspace(func(proxy *extworkspace.ExtWorkspaceHandle) { |
| 83 | - | w.SetID(id) | |||
| 84 | - | w.SetVersion(m.Version()) | |||
| 85 | - | s.wl.Register(w) | |||
| 86 | if len(s.ws.handles) >= maxWorkspaces { | 54 | if len(s.ws.handles) >= maxWorkspaces { | ||
| 87 | s.ws.overflow = true | 55 | s.ws.overflow = true | ||
| 88 | return | 56 | return | ||
| 89 | } | 57 | } | ||
| 90 | - | s.ws.handles[id] = w | 58 | + | w := &workspace{proxy: proxy} |
| 91 | - | case 2: | 59 | + | s.ws.handles[proxy.ID()] = w |
| 92 | - | s.ws.assignIDs() | 60 | + | s.watchWorkspace(w) |
| 93 | - | } | 61 | + | }) |
| 62 | + | m.OnDone(s.ws.assignIDs) | |||
| 94 | } | 63 | } | ||
| 95 | 64 | ||||
| 96 | type workspaceGroup struct { | 65 | type workspaceGroup struct { | ||
| 97 | - | wlturbo.BaseProxy | 66 | + | proxy *extworkspace.ExtWorkspaceGroupHandle |
| 98 | - | s *Source | |||
| 99 | outputs map[uint32]bool // wl_output proxy IDs | 67 | outputs map[uint32]bool // wl_output proxy IDs | ||
| 100 | } | 68 | } | ||
| 101 | 69 | ||||
| 102 | - | func (*workspaceGroup) EventSignature(op uint16) (string, bool) { | 70 | + | func (s *Source) watchGroup(g *workspaceGroup) { |
| 103 | - | switch op { | 71 | + | g.proxy.OnOutputEnter(func(id uint32) { |
| 104 | - | case 0: | 72 | + | if len(g.outputs) < maxGroupOutputs { |
| 105 | - | return "uint,", true // capabilities | |||
| 106 | - | case 1, 2, 3, 4: | |||
| 107 | - | return "object,", true // output_enter, output_leave, workspace_enter, workspace_leave | |||
| 108 | - | case 5: | |||
| 109 | - | return "", true // removed | |||
| 110 | - | } | |||
| 111 | - | return "", false | |||
| 112 | - | } | |||
| 113 | - | ||||
| 114 | - | func (g *workspaceGroup) Dispatch(e *wlturbo.Event) { | |||
| 115 | - | s := g.s | |||
| 116 | - | switch e.Opcode { | |||
| 117 | - | case 1: | |||
| 118 | - | if id := e.Uint32(); len(g.outputs) < maxGroupOutputs { | |||
| 119 | g.outputs[id] = true | 73 | g.outputs[id] = true | ||
| 120 | } | 74 | } | ||
| 121 | - | case 2: | 75 | + | }) |
| 122 | - | delete(g.outputs, e.Uint32()) | 76 | + | g.proxy.OnOutputLeave(func(id uint32) { delete(g.outputs, id) }) |
| 123 | - | case 3: | 77 | + | g.proxy.OnWorkspaceEnter(func(id uint32) { |
| 124 | - | if w := s.ws.handles[e.Uint32()]; w != nil { | 78 | + | if w := s.ws.handles[id]; w != nil { |
| 125 | w.group = g | 79 | w.group = g | ||
| 126 | } | 80 | } | ||
| 127 | - | case 4: | 81 | + | }) |
| 128 | - | if w := s.ws.handles[e.Uint32()]; w != nil && w.group == g { | 82 | + | g.proxy.OnWorkspaceLeave(func(id uint32) { |
| 83 | + | if w := s.ws.handles[id]; w != nil && w.group == g { | |||
| 129 | w.group = nil | 84 | w.group = nil | ||
| 130 | } | 85 | } | ||
| 131 | - | case 5: | 86 | + | }) |
| 132 | - | delete(s.ws.groups, g.ID()) | 87 | + | g.proxy.OnRemoved(func() { |
| 88 | + | delete(s.ws.groups, g.proxy.ID()) | |||
| 133 | for _, w := range s.ws.handles { | 89 | for _, w := range s.ws.handles { | ||
| 134 | if w.group == g { | 90 | if w.group == g { | ||
| 135 | w.group = nil | 91 | w.group = nil | ||
| 136 | } | 92 | } | ||
| 137 | } | 93 | } | ||
| 138 | - | _ = s.wl.Request(wlturbo.Request{Proxy: g, Opcode: reqGroupDestroy, Name: "ext_workspace_group_handle_v1.destroy", Destructor: true}) | 94 | + | _ = g.proxy.Destroy() |
| 139 | - | } | 95 | + | }) |
| 140 | } | 96 | } | ||
| 141 | 97 | ||||
| 142 | - | // workspace is one ext_workspace_handle_v1. It is a wlturbo object so that it | 98 | + | // workspace is one ext_workspace_handle_v1. Its proxy is the argument of |
| 143 | - | // can be the argument of create_workspace_source. | 99 | + | // create_workspace_source. |
| 144 | type workspace struct { | 100 | type workspace struct { | ||
| 145 | - | wlturbo.BaseProxy | 101 | + | proxy *extworkspace.ExtWorkspaceHandle |
| 146 | - | s *Source | |||
| 147 | idStr string // the id event: empty for a temporary workspace | 102 | idStr string // the id event: empty for a temporary workspace | ||
| 148 | name string | 103 | name string | ||
| 149 | state uint32 | 104 | state uint32 | ||
| @@ -153,43 +108,29 @@ type workspace struct { | |||||
| 153 | frame *workspaceFrame // NeferWL's extension only; nil until asked for | 108 | frame *workspaceFrame // NeferWL's extension only; nil until asked for | ||
| 154 | } | 109 | } | ||
| 155 | 110 | ||||
| 156 | - | func (*workspace) EventSignature(op uint16) (string, bool) { | 111 | + | // watchWorkspace subscribes to the events of w. Coordinates are not used: |
| 157 | - | switch op { | 112 | + | // nothing here depends on the grid. |
| 158 | - | case 0, 1: | 113 | + | func (s *Source) watchWorkspace(w *workspace) { |
| 159 | - | return "string,", true // id, name | 114 | + | w.proxy.OnId(func(id string) { |
| 160 | - | case 2: | |||
| 161 | - | return "array,", true // coordinates | |||
| 162 | - | case 3, 4: | |||
| 163 | - | return "uint,", true // state, capabilities | |||
| 164 | - | case 5: | |||
| 165 | - | return "", true // removed | |||
| 166 | - | } | |||
| 167 | - | return "", false | |||
| 168 | - | } | |||
| 169 | - | ||||
| 170 | - | func (w *workspace) Dispatch(e *wlturbo.Event) { | |||
| 171 | - | switch e.Opcode { | |||
| 172 | - | case 0: | |||
| 173 | if w.idStr == "" { | 115 | if w.idStr == "" { | ||
| 174 | - | w.idStr = e.String() | 116 | + | w.idStr = id |
| 175 | } | 117 | } | ||
| 176 | - | case 1: | 118 | + | }) |
| 177 | - | if n := e.String(); validName(n) { | 119 | + | w.proxy.OnName(func(n string) { |
| 120 | + | if validName(n) { | |||
| 178 | w.name = n | 121 | w.name = n | ||
| 179 | } | 122 | } | ||
| 180 | - | // opcode 2 (coordinates) is not used: nothing here depends on the grid. | 123 | + | }) |
| 181 | - | case 3: | 124 | + | w.proxy.OnState(func(state uint32) { w.state = state }) |
| 182 | - | w.state = e.Uint32() | 125 | + | w.proxy.OnRemoved(func() { |
| 183 | - | case 5: | |||
| 184 | w.removed = true | 126 | w.removed = true | ||
| 185 | - | s := w.s | |||
| 186 | s.releaseWorkspaceFrame(w) | 127 | s.releaseWorkspaceFrame(w) | ||
| 187 | - | delete(s.ws.handles, w.ID()) | 128 | + | delete(s.ws.handles, w.proxy.ID()) |
| 188 | if w.id != 0 && s.ws.byID[w.id] == w { | 129 | if w.id != 0 && s.ws.byID[w.id] == w { | ||
| 189 | delete(s.ws.byID, w.id) | 130 | delete(s.ws.byID, w.id) | ||
| 190 | } | 131 | } | ||
| 191 | - | _ = s.wl.Request(wlturbo.Request{Proxy: w, Opcode: reqHandleDestroy, Name: "ext_workspace_handle_v1.destroy", Destructor: true}) | 132 | + | _ = w.proxy.Destroy() |
| 192 | - | } | 133 | + | }) |
| 193 | } | 134 | } | ||
| 194 | 135 | ||||
| 195 | // assignIDs gives every workspace announced since the last done its | 136 | // assignIDs gives every workspace announced since the last done its | ||
| @@ -207,7 +148,7 @@ func (ws *workspaceState) assignIDs() { | |||||
| 207 | } | 148 | } | ||
| 208 | } | 149 | } | ||
| 209 | // Map order is random: numbers follow the announcement order. | 150 | // Map order is random: numbers follow the announcement order. | ||
| 210 | - | slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.ID(), b.ID()) }) | 151 | + | slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.proxy.ID(), b.proxy.ID()) }) |
| 211 | for _, w := range fresh { | 152 | for _, w := range fresh { | ||
| 212 | w.id = ws.intern(w.idStr) | 153 | w.id = ws.intern(w.idStr) | ||
| 213 | ws.byID[w.id] = w | 154 | ws.byID[w.id] = w | ||
| @@ -266,17 +207,14 @@ func (s *Source) bindWorkspaces() error { | |||||
| 266 | s.ws.byID = make(map[uint64]*workspace) | 207 | s.ws.byID = make(map[uint64]*workspace) | ||
| 267 | s.ws.groups = make(map[uint32]*workspaceGroup) | 208 | s.ws.groups = make(map[uint32]*workspaceGroup) | ||
| 268 | s.ws.interned = make(map[string]uint64) | 209 | s.ws.interned = make(map[string]uint64) | ||
| 269 | - | m := &workspaceManager{s: s} | 210 | + | m := extworkspace.NewExtWorkspaceManager(s.wl) |
| 270 | - | m.SetContext(s.wl) | 211 | + | ok, err := s.bindOptional(extworkspace.ExtWorkspaceManagerInterface, workspaceVersion, m) |
| 271 | - | _, err := s.display.Registry().BindNegotiated(workspaceIface, workspaceVersion, m) | 212 | + | if err != nil || !ok { |
| 272 | - | switch { | 213 | + | return err |
| 273 | - | case err == nil: | |||
| 274 | - | s.ws.mgr = m | |||
| 275 | - | return nil | |||
| 276 | - | case errors.Is(err, wlturbo.ErrGlobalNotFound): | |||
| 277 | - | return nil | |||
| 278 | } | 214 | } | ||
| 279 | - | return err | 215 | + | s.watchWorkspaces(m) |
| 216 | + | s.ws.mgr = m | |||
| 217 | + | return nil | |||
| 280 | } | 218 | } | ||
| 281 | 219 | ||||
| 282 | // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per | 220 | // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per | ||
| @@ -324,7 +262,7 @@ func (s *Source) workspaceList() []ports.Workspace { | |||||
| 324 | list = append(list, ports.Workspace{ | 262 | list = append(list, ports.Workspace{ | ||
| 325 | ID: w.id, OutputID: o.id, Name: w.name, | 263 | ID: w.id, OutputID: o.id, Name: w.name, | ||
| 326 | Region: workspaceRegion(w, o), | 264 | Region: workspaceRegion(w, o), | ||
| 327 | - | Active: w.state&wsStateActive != 0, | 265 | + | Active: w.state&extworkspace.STATE_ACTIVE != 0, |
| 328 | }) | 266 | }) | ||
| 329 | } | 267 | } | ||
| 330 | } | 268 | } | ||
| @@ -372,7 +310,7 @@ func (s *Source) workspaceKey(t ports.Target, out *output) (sessionKey, ports.Re | |||||
| 372 | if err := s.roundtrip(); err != nil { | 310 | if err := s.roundtrip(); err != nil { | ||
| 373 | return sessionKey{}, ports.Region{}, err | 311 | return sessionKey{}, ports.Region{}, err | ||
| 374 | } | 312 | } | ||
| 375 | - | if w.removed || w.state&wsStateActive == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] { | 313 | + | if w.removed || w.state&extworkspace.STATE_ACTIVE == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] { |
| 376 | return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable | 314 | return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable | ||
| 377 | } | 315 | } | ||
| 378 | s.ws.shown = w.id | 316 | s.ws.shown = w.id | ||
M internal/adapters/wayland/xdgoutput.go +9 -41
| @@ -1,59 +1,27 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | - | import ( | |
| 4 | - | "github.com/bnema/wlturbo" | |
| 5 | - | ||
| 6 | - | "github.com/bnema/nefercap/internal/ports" | |
| 7 | - | ) | |
| 3 | + | import "github.com/bnema/nefercap/internal/ports" | |
| 8 | 4 | ||
| 9 | 5 | // zxdg_output_v1 gives the logical size of an output, which maps a logical | |
| 10 | 6 | // region onto buffer pixels when a region has to be cropped from the output | |
| 11 | 7 | // frame. It is optional: the current mode over the integer scale is the | |
| 12 | 8 | // fallback. | |
| 13 | 9 | ||
| 14 | - | const ( | |
| 15 | - | reqGetXdgOutput = 1 // zxdg_output_manager_v1.get_xdg_output | |
| 16 | - | reqXdgOutputDestroy = 0 // zxdg_output_v1.destroy | |
| 17 | - | ) | |
| 18 | - | ||
| 19 | - | type xdgOutput struct { | |
| 20 | - | wlturbo.BaseProxy | |
| 21 | - | o *output | |
| 22 | - | } | |
| 23 | - | ||
| 24 | - | func (*xdgOutput) EventSignature(op uint16) (string, bool) { | |
| 25 | - | switch op { | |
| 26 | - | case 0, 1: | |
| 27 | - | return "int,int,", true // logical_position, logical_size | |
| 28 | - | case 2: | |
| 29 | - | return "", true // done | |
| 30 | - | case 3, 4: | |
| 31 | - | return "string,", true // name, description | |
| 32 | - | } | |
| 33 | - | return "", false | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | func (x *xdgOutput) Dispatch(e *wlturbo.Event) { | |
| 37 | - | if e.Opcode != 1 { | |
| 38 | - | return | |
| 39 | - | } | |
| 40 | - | w, h := e.Int32(), e.Int32() | |
| 41 | - | if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension { | |
| 42 | - | x.o.lw, x.o.lh = w, h | |
| 43 | - | } | |
| 44 | - | } | |
| 45 | - | ||
| 46 | 10 | // watchLogicalSize subscribes to the logical size of o. | |
| 47 | 11 | func (s *Source) watchLogicalSize(o *output) { | |
| 48 | 12 | if s.xdgOutputs == nil { | |
| 49 | 13 | return | |
| 50 | 14 | } | |
| 51 | - | x := &xdgOutput{o: o} | |
| 52 | - | x.SetContext(s.wl) | |
| 53 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.xdgOutputs, Opcode: reqGetXdgOutput, Name: "zxdg_output_manager_v1.get_xdg_output", Child: x}, x, o.proxy); err != nil { | |
| 15 | + | x, err := s.xdgOutputs.GetXdgOutput(o.proxy) | |
| 16 | + | if err != nil { | |
| 54 | 17 | s.log.Warn().Err(err).Msg("xdg-output request failed") | |
| 55 | 18 | return | |
| 56 | 19 | } | |
| 20 | + | x.OnLogicalSize(func(w, h int32) { | |
| 21 | + | if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension { | |
| 22 | + | o.lw, o.lh = w, h | |
| 23 | + | } | |
| 24 | + | }) | |
| 57 | 25 | o.xdg = x | |
| 58 | 26 | } | |
| 59 | 27 | ||
| @@ -61,6 +29,6 @@ func (s *Source) watchLogicalSize(o *output) { | |||
| 61 | 29 | func (s *Source) releaseXdg(o *output) { | |
| 62 | 30 | if x := o.xdg; x != nil { | |
| 63 | 31 | o.xdg = nil | |
| 64 | - | _ = s.wl.Request(wlturbo.Request{Proxy: x, Opcode: reqXdgOutputDestroy, Name: "zxdg_output_v1.destroy", Destructor: true}) | |
| 32 | + | _ = x.Destroy() | |
| 65 | 33 | } | |
| 66 | 34 | } | |
| @@ -1,59 +1,27 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | - | import ( | 3 | + | import "github.com/bnema/nefercap/internal/ports" |
| 4 | - | "github.com/bnema/wlturbo" | |||
| 5 | - | ||||
| 6 | - | "github.com/bnema/nefercap/internal/ports" | |||
| 7 | - | ) | |||
| 8 | 4 | ||||
| 9 | // zxdg_output_v1 gives the logical size of an output, which maps a logical | 5 | // zxdg_output_v1 gives the logical size of an output, which maps a logical | ||
| 10 | // region onto buffer pixels when a region has to be cropped from the output | 6 | // region onto buffer pixels when a region has to be cropped from the output | ||
| 11 | // frame. It is optional: the current mode over the integer scale is the | 7 | // frame. It is optional: the current mode over the integer scale is the | ||
| 12 | // fallback. | 8 | // fallback. | ||
| 13 | 9 | ||||
| 14 | - | const ( | |||
| 15 | - | reqGetXdgOutput = 1 // zxdg_output_manager_v1.get_xdg_output | |||
| 16 | - | reqXdgOutputDestroy = 0 // zxdg_output_v1.destroy | |||
| 17 | - | ) | |||
| 18 | - | ||||
| 19 | - | type xdgOutput struct { | |||
| 20 | - | wlturbo.BaseProxy | |||
| 21 | - | o *output | |||
| 22 | - | } | |||
| 23 | - | ||||
| 24 | - | func (*xdgOutput) EventSignature(op uint16) (string, bool) { | |||
| 25 | - | switch op { | |||
| 26 | - | case 0, 1: | |||
| 27 | - | return "int,int,", true // logical_position, logical_size | |||
| 28 | - | case 2: | |||
| 29 | - | return "", true // done | |||
| 30 | - | case 3, 4: | |||
| 31 | - | return "string,", true // name, description | |||
| 32 | - | } | |||
| 33 | - | return "", false | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | func (x *xdgOutput) Dispatch(e *wlturbo.Event) { | |||
| 37 | - | if e.Opcode != 1 { | |||
| 38 | - | return | |||
| 39 | - | } | |||
| 40 | - | w, h := e.Int32(), e.Int32() | |||
| 41 | - | if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension { | |||
| 42 | - | x.o.lw, x.o.lh = w, h | |||
| 43 | - | } | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | // watchLogicalSize subscribes to the logical size of o. | 10 | // watchLogicalSize subscribes to the logical size of o. | ||
| 47 | func (s *Source) watchLogicalSize(o *output) { | 11 | func (s *Source) watchLogicalSize(o *output) { | ||
| 48 | if s.xdgOutputs == nil { | 12 | if s.xdgOutputs == nil { | ||
| 49 | return | 13 | return | ||
| 50 | } | 14 | } | ||
| 51 | - | x := &xdgOutput{o: o} | 15 | + | x, err := s.xdgOutputs.GetXdgOutput(o.proxy) |
| 52 | - | x.SetContext(s.wl) | 16 | + | if err != nil { |
| 53 | - | if err := s.wl.Request(wlturbo.Request{Proxy: s.xdgOutputs, Opcode: reqGetXdgOutput, Name: "zxdg_output_manager_v1.get_xdg_output", Child: x}, x, o.proxy); err != nil { | |||
| 54 | s.log.Warn().Err(err).Msg("xdg-output request failed") | 17 | s.log.Warn().Err(err).Msg("xdg-output request failed") | ||
| 55 | return | 18 | return | ||
| 56 | } | 19 | } | ||
| 20 | + | x.OnLogicalSize(func(w, h int32) { | |||
| 21 | + | if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension { | |||
| 22 | + | o.lw, o.lh = w, h | |||
| 23 | + | } | |||
| 24 | + | }) | |||
| 57 | o.xdg = x | 25 | o.xdg = x | ||
| 58 | } | 26 | } | ||
| 59 | 27 | ||||
| @@ -61,6 +29,6 @@ func (s *Source) watchLogicalSize(o *output) { | |||||
| 61 | func (s *Source) releaseXdg(o *output) { | 29 | func (s *Source) releaseXdg(o *output) { | ||
| 62 | if x := o.xdg; x != nil { | 30 | if x := o.xdg; x != nil { | ||
| 63 | o.xdg = nil | 31 | o.xdg = nil | ||
| 64 | - | _ = s.wl.Request(wlturbo.Request{Proxy: x, Opcode: reqXdgOutputDestroy, Name: "zxdg_output_v1.destroy", Destructor: true}) | 32 | + | _ = x.Destroy() |
| 65 | } | 33 | } | ||
| 66 | } | 34 | } | ||