fix: harden surface present, input region, lock close and fd ownership
Transfer the feedback table fd before reading it (no double close), reject inconsistent Present requests (nil, size mismatch, point order), reset the buffer scale to 1, stage input regions until the surface is mapped and refuse them on lock surfaces, bound lock configure sizes, allow one lock surface per output, make Lock.Close notice an in-flight locked event, close duplicated plane fds exactly once and join params.Destroy errors. Remove dead code. Tests: one regression test per finding, the feedback spy replaced by the Mockery Handler mock, and a skipped TestCloseWithPendingFrame documenting the wlturbo delete_id limitation.
- commit
- 7e0d12d63ffdab57842efc111dd9ef5486572151
- author
- brice · · 2026-10-01 17:51 UTC
- parent
- 71357f8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +908 -67
M .mockery.yml +7 -0
| @@ -13,6 +13,13 @@ packages: | |||
| 13 | 13 | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | |
| 14 | 14 | interfaces: | |
| 15 | 15 | Handler: | |
| 16 | + | configs: | |
| 17 | + | - dir: mocks | |
| 18 | + | pkgname: neferclientmocks | |
| 19 | + | filename: handler_mock.go | |
| 20 | + | - dir: . | |
| 21 | + | pkgname: neferclient | |
| 22 | + | filename: handler_mock_test.go | |
| 16 | 23 | surfaceRequests: | |
| 17 | 24 | config: | |
| 18 | 25 | dir: . | |
| @@ -13,6 +13,13 @@ packages: | |||||
| 13 | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | 13 | filename: '{{.InterfaceFile | base | trimSuffix ".go"}}_mock.go' | ||
| 14 | interfaces: | 14 | interfaces: | ||
| 15 | Handler: | 15 | Handler: | ||
| 16 | + | configs: | |||
| 17 | + | - dir: mocks | |||
| 18 | + | pkgname: neferclientmocks | |||
| 19 | + | filename: handler_mock.go | |||
| 20 | + | - dir: . | |||
| 21 | + | pkgname: neferclient | |||
| 22 | + | filename: handler_mock_test.go | |||
| 16 | surfaceRequests: | 23 | surfaceRequests: | ||
| 17 | config: | 24 | config: | ||
| 18 | dir: . | 25 | dir: . | ||
M export_test.go +6 -0
| @@ -33,3 +33,9 @@ func (s *Surface) ResetFrame() { s.frameReady = true } | |||
| 33 | 33 | ||
| 34 | 34 | // UseWireRequests restores the full request path. | |
| 35 | 35 | func (s *Surface) UseWireRequests() { s.req = wireRequests{s: s} } | |
| 36 | + | ||
| 37 | + | // MarkPresented stands in for a committed Present. | |
| 38 | + | func (s *Surface) MarkPresented() { s.presented = true } | |
| 39 | + | ||
| 40 | + | // DropFrameCallback marks the pending frame callback as answered. | |
| 41 | + | func (s *Surface) PendingFrame() bool { return !s.frameReady } | |
| @@ -33,3 +33,9 @@ func (s *Surface) ResetFrame() { s.frameReady = true } | |||||
| 33 | 33 | ||||
| 34 | // UseWireRequests restores the full request path. | 34 | // UseWireRequests restores the full request path. | ||
| 35 | func (s *Surface) UseWireRequests() { s.req = wireRequests{s: s} } | 35 | func (s *Surface) UseWireRequests() { s.req = wireRequests{s: s} } | ||
| 36 | + | ||||
| 37 | + | // MarkPresented stands in for a committed Present. | |||
| 38 | + | func (s *Surface) MarkPresented() { s.presented = true } | |||
| 39 | + | ||||
| 40 | + | // DropFrameCallback marks the pending frame callback as answered. | |||
| 41 | + | func (s *Surface) PendingFrame() bool { return !s.frameReady } | |||
M globals.go +0 -1
| @@ -51,7 +51,6 @@ func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newP | |||
| 51 | 51 | return zero, 0, &CapabilityError{Name: iface, Cause: err} | |
| 52 | 52 | } | |
| 53 | 53 | if v < minimum { | |
| 54 | - | _ = c.wlctx // the proxy stays bound; it is unusable below minimum | |
| 55 | 54 | var zero P | |
| 56 | 55 | return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)} | |
| 57 | 56 | } | |
| @@ -51,7 +51,6 @@ func (c *Conn) require[P wl.Proxy](iface string, supported, minimum uint32, newP | |||||
| 51 | return zero, 0, &CapabilityError{Name: iface, Cause: err} | 51 | return zero, 0, &CapabilityError{Name: iface, Cause: err} | ||
| 52 | } | 52 | } | ||
| 53 | if v < minimum { | 53 | if v < minimum { | ||
| 54 | - | _ = c.wlctx // the proxy stays bound; it is unusable below minimum | |||
| 55 | var zero P | 54 | var zero P | ||
| 56 | return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)} | 55 | return zero, 0, &CapabilityError{Name: iface, Cause: fmt.Errorf("requires version %d, negotiated %d", minimum, v)} | ||
| 57 | } | 56 | } | ||
A handler_mock_test.go +489 -0
| @@ -0,0 +1,489 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package neferclient | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func NewMockHandler(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *MockHandler { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &MockHandler{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // MockHandler is an autogenerated mock type for the Handler type | |
| 35 | + | type MockHandler struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type MockHandler_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *MockHandler) EXPECT() *MockHandler_Expecter { | |
| 44 | + | return &MockHandler_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Closed provides a mock function for the type MockHandler | |
| 48 | + | func (_mock *MockHandler) Closed(id SurfaceID) { | |
| 49 | + | _mock.Called(id) | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed' | |
| 54 | + | type MockHandler_Closed_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Closed is a helper method to define mock.On call | |
| 59 | + | // - id SurfaceID | |
| 60 | + | func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call { | |
| 61 | + | return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)} | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func (_c *MockHandler_Closed_Call) Run(run func(id SurfaceID)) *MockHandler_Closed_Call { | |
| 65 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 66 | + | var arg0 SurfaceID | |
| 67 | + | if args[0] != nil { | |
| 68 | + | arg0 = args[0].(SurfaceID) | |
| 69 | + | } | |
| 70 | + | run( | |
| 71 | + | arg0, | |
| 72 | + | ) | |
| 73 | + | }) | |
| 74 | + | return _c | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call { | |
| 78 | + | _c.Call.Return() | |
| 79 | + | return _c | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Closed_Call { | |
| 83 | + | _c.Run(run) | |
| 84 | + | return _c | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // Configure provides a mock function for the type MockHandler | |
| 88 | + | func (_mock *MockHandler) Configure(id SurfaceID, width int32, height int32) { | |
| 89 | + | _mock.Called(id, width, height) | |
| 90 | + | return | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure' | |
| 94 | + | type MockHandler_Configure_Call struct { | |
| 95 | + | *mock.Call | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // Configure is a helper method to define mock.On call | |
| 99 | + | // - id SurfaceID | |
| 100 | + | // - width int32 | |
| 101 | + | // - height int32 | |
| 102 | + | func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call { | |
| 103 | + | return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)} | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func (_c *MockHandler_Configure_Call) Run(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call { | |
| 107 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 108 | + | var arg0 SurfaceID | |
| 109 | + | if args[0] != nil { | |
| 110 | + | arg0 = args[0].(SurfaceID) | |
| 111 | + | } | |
| 112 | + | var arg1 int32 | |
| 113 | + | if args[1] != nil { | |
| 114 | + | arg1 = args[1].(int32) | |
| 115 | + | } | |
| 116 | + | var arg2 int32 | |
| 117 | + | if args[2] != nil { | |
| 118 | + | arg2 = args[2].(int32) | |
| 119 | + | } | |
| 120 | + | run( | |
| 121 | + | arg0, | |
| 122 | + | arg1, | |
| 123 | + | arg2, | |
| 124 | + | ) | |
| 125 | + | }) | |
| 126 | + | return _c | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call { | |
| 130 | + | _c.Call.Return() | |
| 131 | + | return _c | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call { | |
| 135 | + | _c.Run(run) | |
| 136 | + | return _c | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // Error provides a mock function for the type MockHandler | |
| 140 | + | func (_mock *MockHandler) Error(err error) { | |
| 141 | + | _mock.Called(err) | |
| 142 | + | return | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' | |
| 146 | + | type MockHandler_Error_Call struct { | |
| 147 | + | *mock.Call | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | // Error is a helper method to define mock.On call | |
| 151 | + | // - err error | |
| 152 | + | func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call { | |
| 153 | + | return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)} | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call { | |
| 157 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 158 | + | var arg0 error | |
| 159 | + | if args[0] != nil { | |
| 160 | + | arg0 = args[0].(error) | |
| 161 | + | } | |
| 162 | + | run( | |
| 163 | + | arg0, | |
| 164 | + | ) | |
| 165 | + | }) | |
| 166 | + | return _c | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call { | |
| 170 | + | _c.Call.Return() | |
| 171 | + | return _c | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call { | |
| 175 | + | _c.Run(run) | |
| 176 | + | return _c | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // FDReady provides a mock function for the type MockHandler | |
| 180 | + | func (_mock *MockHandler) FDReady(id uint64) { | |
| 181 | + | _mock.Called(id) | |
| 182 | + | return | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady' | |
| 186 | + | type MockHandler_FDReady_Call struct { | |
| 187 | + | *mock.Call | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // FDReady is a helper method to define mock.On call | |
| 191 | + | // - id uint64 | |
| 192 | + | func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call { | |
| 193 | + | return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)} | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call { | |
| 197 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 198 | + | var arg0 uint64 | |
| 199 | + | if args[0] != nil { | |
| 200 | + | arg0 = args[0].(uint64) | |
| 201 | + | } | |
| 202 | + | run( | |
| 203 | + | arg0, | |
| 204 | + | ) | |
| 205 | + | }) | |
| 206 | + | return _c | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call { | |
| 210 | + | _c.Call.Return() | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call { | |
| 215 | + | _c.Run(run) | |
| 216 | + | return _c | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // FeedbackDone provides a mock function for the type MockHandler | |
| 220 | + | func (_mock *MockHandler) FeedbackDone(id SurfaceID) { | |
| 221 | + | _mock.Called(id) | |
| 222 | + | return | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone' | |
| 226 | + | type MockHandler_FeedbackDone_Call struct { | |
| 227 | + | *mock.Call | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // FeedbackDone is a helper method to define mock.On call | |
| 231 | + | // - id SurfaceID | |
| 232 | + | func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call { | |
| 233 | + | return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)} | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | func (_c *MockHandler_FeedbackDone_Call) Run(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call { | |
| 237 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 238 | + | var arg0 SurfaceID | |
| 239 | + | if args[0] != nil { | |
| 240 | + | arg0 = args[0].(SurfaceID) | |
| 241 | + | } | |
| 242 | + | run( | |
| 243 | + | arg0, | |
| 244 | + | ) | |
| 245 | + | }) | |
| 246 | + | return _c | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call { | |
| 250 | + | _c.Call.Return() | |
| 251 | + | return _c | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call { | |
| 255 | + | _c.Run(run) | |
| 256 | + | return _c | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | // Frame provides a mock function for the type MockHandler | |
| 260 | + | func (_mock *MockHandler) Frame(id SurfaceID) { | |
| 261 | + | _mock.Called(id) | |
| 262 | + | return | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | // MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame' | |
| 266 | + | type MockHandler_Frame_Call struct { | |
| 267 | + | *mock.Call | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // Frame is a helper method to define mock.On call | |
| 271 | + | // - id SurfaceID | |
| 272 | + | func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call { | |
| 273 | + | return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)} | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | func (_c *MockHandler_Frame_Call) Run(run func(id SurfaceID)) *MockHandler_Frame_Call { | |
| 277 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 278 | + | var arg0 SurfaceID | |
| 279 | + | if args[0] != nil { | |
| 280 | + | arg0 = args[0].(SurfaceID) | |
| 281 | + | } | |
| 282 | + | run( | |
| 283 | + | arg0, | |
| 284 | + | ) | |
| 285 | + | }) | |
| 286 | + | return _c | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call { | |
| 290 | + | _c.Call.Return() | |
| 291 | + | return _c | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Frame_Call { | |
| 295 | + | _c.Run(run) | |
| 296 | + | return _c | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | // LockFinished provides a mock function for the type MockHandler | |
| 300 | + | func (_mock *MockHandler) LockFinished() { | |
| 301 | + | _mock.Called() | |
| 302 | + | return | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | // MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished' | |
| 306 | + | type MockHandler_LockFinished_Call struct { | |
| 307 | + | *mock.Call | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | // LockFinished is a helper method to define mock.On call | |
| 311 | + | func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call { | |
| 312 | + | return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")} | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call { | |
| 316 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 317 | + | run() | |
| 318 | + | }) | |
| 319 | + | return _c | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call { | |
| 323 | + | _c.Call.Return() | |
| 324 | + | return _c | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call { | |
| 328 | + | _c.Run(run) | |
| 329 | + | return _c | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | // Locked provides a mock function for the type MockHandler | |
| 333 | + | func (_mock *MockHandler) Locked() { | |
| 334 | + | _mock.Called() | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | // MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked' | |
| 339 | + | type MockHandler_Locked_Call struct { | |
| 340 | + | *mock.Call | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | // Locked is a helper method to define mock.On call | |
| 344 | + | func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call { | |
| 345 | + | return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")} | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call { | |
| 349 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 350 | + | run() | |
| 351 | + | }) | |
| 352 | + | return _c | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call { | |
| 356 | + | _c.Call.Return() | |
| 357 | + | return _c | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call { | |
| 361 | + | _c.Run(run) | |
| 362 | + | return _c | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | // OutputAdded provides a mock function for the type MockHandler | |
| 366 | + | func (_mock *MockHandler) OutputAdded(out *Output) { | |
| 367 | + | _mock.Called(out) | |
| 368 | + | return | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | // MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded' | |
| 372 | + | type MockHandler_OutputAdded_Call struct { | |
| 373 | + | *mock.Call | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | // OutputAdded is a helper method to define mock.On call | |
| 377 | + | // - out *Output | |
| 378 | + | func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call { | |
| 379 | + | return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)} | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | func (_c *MockHandler_OutputAdded_Call) Run(run func(out *Output)) *MockHandler_OutputAdded_Call { | |
| 383 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 384 | + | var arg0 *Output | |
| 385 | + | if args[0] != nil { | |
| 386 | + | arg0 = args[0].(*Output) | |
| 387 | + | } | |
| 388 | + | run( | |
| 389 | + | arg0, | |
| 390 | + | ) | |
| 391 | + | }) | |
| 392 | + | return _c | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call { | |
| 396 | + | _c.Call.Return() | |
| 397 | + | return _c | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *Output)) *MockHandler_OutputAdded_Call { | |
| 401 | + | _c.Run(run) | |
| 402 | + | return _c | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | // OutputRemoved provides a mock function for the type MockHandler | |
| 406 | + | func (_mock *MockHandler) OutputRemoved(global uint32) { | |
| 407 | + | _mock.Called(global) | |
| 408 | + | return | |
| 409 | + | } | |
| 410 | + | ||
| 411 | + | // MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved' | |
| 412 | + | type MockHandler_OutputRemoved_Call struct { | |
| 413 | + | *mock.Call | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | // OutputRemoved is a helper method to define mock.On call | |
| 417 | + | // - global uint32 | |
| 418 | + | func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call { | |
| 419 | + | return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)} | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |
| 423 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 424 | + | var arg0 uint32 | |
| 425 | + | if args[0] != nil { | |
| 426 | + | arg0 = args[0].(uint32) | |
| 427 | + | } | |
| 428 | + | run( | |
| 429 | + | arg0, | |
| 430 | + | ) | |
| 431 | + | }) | |
| 432 | + | return _c | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call { | |
| 436 | + | _c.Call.Return() | |
| 437 | + | return _c | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |
| 441 | + | _c.Run(run) | |
| 442 | + | return _c | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | // Scale provides a mock function for the type MockHandler | |
| 446 | + | func (_mock *MockHandler) Scale(id SurfaceID, scale float64) { | |
| 447 | + | _mock.Called(id, scale) | |
| 448 | + | return | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | // MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale' | |
| 452 | + | type MockHandler_Scale_Call struct { | |
| 453 | + | *mock.Call | |
| 454 | + | } | |
| 455 | + | ||
| 456 | + | // Scale is a helper method to define mock.On call | |
| 457 | + | // - id SurfaceID | |
| 458 | + | // - scale float64 | |
| 459 | + | func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call { | |
| 460 | + | return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)} | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | func (_c *MockHandler_Scale_Call) Run(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call { | |
| 464 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 465 | + | var arg0 SurfaceID | |
| 466 | + | if args[0] != nil { | |
| 467 | + | arg0 = args[0].(SurfaceID) | |
| 468 | + | } | |
| 469 | + | var arg1 float64 | |
| 470 | + | if args[1] != nil { | |
| 471 | + | arg1 = args[1].(float64) | |
| 472 | + | } | |
| 473 | + | run( | |
| 474 | + | arg0, | |
| 475 | + | arg1, | |
| 476 | + | ) | |
| 477 | + | }) | |
| 478 | + | return _c | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call { | |
| 482 | + | _c.Call.Return() | |
| 483 | + | return _c | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call { | |
| 487 | + | _c.Run(run) | |
| 488 | + | return _c | |
| 489 | + | } | |
| @@ -0,0 +1,489 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package neferclient | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // NewMockHandler creates a new instance of MockHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 12 | + | // The first argument is typically a *testing.T value. | |||
| 13 | + | func NewMockHandler(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *MockHandler { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &MockHandler{} | |||
| 22 | + | mock.Mock.Test(t) | |||
| 23 | + | ||||
| 24 | + | t.Cleanup(func() { | |||
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 26 | + | helper.Helper() | |||
| 27 | + | } | |||
| 28 | + | mock.AssertExpectations(t) | |||
| 29 | + | }) | |||
| 30 | + | ||||
| 31 | + | return mock | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // MockHandler is an autogenerated mock type for the Handler type | |||
| 35 | + | type MockHandler struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type MockHandler_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *MockHandler) EXPECT() *MockHandler_Expecter { | |||
| 44 | + | return &MockHandler_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Closed provides a mock function for the type MockHandler | |||
| 48 | + | func (_mock *MockHandler) Closed(id SurfaceID) { | |||
| 49 | + | _mock.Called(id) | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // MockHandler_Closed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Closed' | |||
| 54 | + | type MockHandler_Closed_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Closed is a helper method to define mock.On call | |||
| 59 | + | // - id SurfaceID | |||
| 60 | + | func (_e *MockHandler_Expecter) Closed(id any) *MockHandler_Closed_Call { | |||
| 61 | + | return &MockHandler_Closed_Call{Call: _e.mock.On("Closed", id)} | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func (_c *MockHandler_Closed_Call) Run(run func(id SurfaceID)) *MockHandler_Closed_Call { | |||
| 65 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 66 | + | var arg0 SurfaceID | |||
| 67 | + | if args[0] != nil { | |||
| 68 | + | arg0 = args[0].(SurfaceID) | |||
| 69 | + | } | |||
| 70 | + | run( | |||
| 71 | + | arg0, | |||
| 72 | + | ) | |||
| 73 | + | }) | |||
| 74 | + | return _c | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func (_c *MockHandler_Closed_Call) Return() *MockHandler_Closed_Call { | |||
| 78 | + | _c.Call.Return() | |||
| 79 | + | return _c | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func (_c *MockHandler_Closed_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Closed_Call { | |||
| 83 | + | _c.Run(run) | |||
| 84 | + | return _c | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // Configure provides a mock function for the type MockHandler | |||
| 88 | + | func (_mock *MockHandler) Configure(id SurfaceID, width int32, height int32) { | |||
| 89 | + | _mock.Called(id, width, height) | |||
| 90 | + | return | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // MockHandler_Configure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Configure' | |||
| 94 | + | type MockHandler_Configure_Call struct { | |||
| 95 | + | *mock.Call | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // Configure is a helper method to define mock.On call | |||
| 99 | + | // - id SurfaceID | |||
| 100 | + | // - width int32 | |||
| 101 | + | // - height int32 | |||
| 102 | + | func (_e *MockHandler_Expecter) Configure(id any, width any, height any) *MockHandler_Configure_Call { | |||
| 103 | + | return &MockHandler_Configure_Call{Call: _e.mock.On("Configure", id, width, height)} | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func (_c *MockHandler_Configure_Call) Run(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call { | |||
| 107 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 108 | + | var arg0 SurfaceID | |||
| 109 | + | if args[0] != nil { | |||
| 110 | + | arg0 = args[0].(SurfaceID) | |||
| 111 | + | } | |||
| 112 | + | var arg1 int32 | |||
| 113 | + | if args[1] != nil { | |||
| 114 | + | arg1 = args[1].(int32) | |||
| 115 | + | } | |||
| 116 | + | var arg2 int32 | |||
| 117 | + | if args[2] != nil { | |||
| 118 | + | arg2 = args[2].(int32) | |||
| 119 | + | } | |||
| 120 | + | run( | |||
| 121 | + | arg0, | |||
| 122 | + | arg1, | |||
| 123 | + | arg2, | |||
| 124 | + | ) | |||
| 125 | + | }) | |||
| 126 | + | return _c | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func (_c *MockHandler_Configure_Call) Return() *MockHandler_Configure_Call { | |||
| 130 | + | _c.Call.Return() | |||
| 131 | + | return _c | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | func (_c *MockHandler_Configure_Call) RunAndReturn(run func(id SurfaceID, width int32, height int32)) *MockHandler_Configure_Call { | |||
| 135 | + | _c.Run(run) | |||
| 136 | + | return _c | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // Error provides a mock function for the type MockHandler | |||
| 140 | + | func (_mock *MockHandler) Error(err error) { | |||
| 141 | + | _mock.Called(err) | |||
| 142 | + | return | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // MockHandler_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' | |||
| 146 | + | type MockHandler_Error_Call struct { | |||
| 147 | + | *mock.Call | |||
| 148 | + | } | |||
| 149 | + | ||||
| 150 | + | // Error is a helper method to define mock.On call | |||
| 151 | + | // - err error | |||
| 152 | + | func (_e *MockHandler_Expecter) Error(err any) *MockHandler_Error_Call { | |||
| 153 | + | return &MockHandler_Error_Call{Call: _e.mock.On("Error", err)} | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func (_c *MockHandler_Error_Call) Run(run func(err error)) *MockHandler_Error_Call { | |||
| 157 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 158 | + | var arg0 error | |||
| 159 | + | if args[0] != nil { | |||
| 160 | + | arg0 = args[0].(error) | |||
| 161 | + | } | |||
| 162 | + | run( | |||
| 163 | + | arg0, | |||
| 164 | + | ) | |||
| 165 | + | }) | |||
| 166 | + | return _c | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func (_c *MockHandler_Error_Call) Return() *MockHandler_Error_Call { | |||
| 170 | + | _c.Call.Return() | |||
| 171 | + | return _c | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | func (_c *MockHandler_Error_Call) RunAndReturn(run func(err error)) *MockHandler_Error_Call { | |||
| 175 | + | _c.Run(run) | |||
| 176 | + | return _c | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // FDReady provides a mock function for the type MockHandler | |||
| 180 | + | func (_mock *MockHandler) FDReady(id uint64) { | |||
| 181 | + | _mock.Called(id) | |||
| 182 | + | return | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // MockHandler_FDReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FDReady' | |||
| 186 | + | type MockHandler_FDReady_Call struct { | |||
| 187 | + | *mock.Call | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // FDReady is a helper method to define mock.On call | |||
| 191 | + | // - id uint64 | |||
| 192 | + | func (_e *MockHandler_Expecter) FDReady(id any) *MockHandler_FDReady_Call { | |||
| 193 | + | return &MockHandler_FDReady_Call{Call: _e.mock.On("FDReady", id)} | |||
| 194 | + | } | |||
| 195 | + | ||||
| 196 | + | func (_c *MockHandler_FDReady_Call) Run(run func(id uint64)) *MockHandler_FDReady_Call { | |||
| 197 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 198 | + | var arg0 uint64 | |||
| 199 | + | if args[0] != nil { | |||
| 200 | + | arg0 = args[0].(uint64) | |||
| 201 | + | } | |||
| 202 | + | run( | |||
| 203 | + | arg0, | |||
| 204 | + | ) | |||
| 205 | + | }) | |||
| 206 | + | return _c | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func (_c *MockHandler_FDReady_Call) Return() *MockHandler_FDReady_Call { | |||
| 210 | + | _c.Call.Return() | |||
| 211 | + | return _c | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | func (_c *MockHandler_FDReady_Call) RunAndReturn(run func(id uint64)) *MockHandler_FDReady_Call { | |||
| 215 | + | _c.Run(run) | |||
| 216 | + | return _c | |||
| 217 | + | } | |||
| 218 | + | ||||
| 219 | + | // FeedbackDone provides a mock function for the type MockHandler | |||
| 220 | + | func (_mock *MockHandler) FeedbackDone(id SurfaceID) { | |||
| 221 | + | _mock.Called(id) | |||
| 222 | + | return | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // MockHandler_FeedbackDone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FeedbackDone' | |||
| 226 | + | type MockHandler_FeedbackDone_Call struct { | |||
| 227 | + | *mock.Call | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // FeedbackDone is a helper method to define mock.On call | |||
| 231 | + | // - id SurfaceID | |||
| 232 | + | func (_e *MockHandler_Expecter) FeedbackDone(id any) *MockHandler_FeedbackDone_Call { | |||
| 233 | + | return &MockHandler_FeedbackDone_Call{Call: _e.mock.On("FeedbackDone", id)} | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | func (_c *MockHandler_FeedbackDone_Call) Run(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call { | |||
| 237 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 238 | + | var arg0 SurfaceID | |||
| 239 | + | if args[0] != nil { | |||
| 240 | + | arg0 = args[0].(SurfaceID) | |||
| 241 | + | } | |||
| 242 | + | run( | |||
| 243 | + | arg0, | |||
| 244 | + | ) | |||
| 245 | + | }) | |||
| 246 | + | return _c | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func (_c *MockHandler_FeedbackDone_Call) Return() *MockHandler_FeedbackDone_Call { | |||
| 250 | + | _c.Call.Return() | |||
| 251 | + | return _c | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (_c *MockHandler_FeedbackDone_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_FeedbackDone_Call { | |||
| 255 | + | _c.Run(run) | |||
| 256 | + | return _c | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | // Frame provides a mock function for the type MockHandler | |||
| 260 | + | func (_mock *MockHandler) Frame(id SurfaceID) { | |||
| 261 | + | _mock.Called(id) | |||
| 262 | + | return | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | // MockHandler_Frame_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frame' | |||
| 266 | + | type MockHandler_Frame_Call struct { | |||
| 267 | + | *mock.Call | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // Frame is a helper method to define mock.On call | |||
| 271 | + | // - id SurfaceID | |||
| 272 | + | func (_e *MockHandler_Expecter) Frame(id any) *MockHandler_Frame_Call { | |||
| 273 | + | return &MockHandler_Frame_Call{Call: _e.mock.On("Frame", id)} | |||
| 274 | + | } | |||
| 275 | + | ||||
| 276 | + | func (_c *MockHandler_Frame_Call) Run(run func(id SurfaceID)) *MockHandler_Frame_Call { | |||
| 277 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 278 | + | var arg0 SurfaceID | |||
| 279 | + | if args[0] != nil { | |||
| 280 | + | arg0 = args[0].(SurfaceID) | |||
| 281 | + | } | |||
| 282 | + | run( | |||
| 283 | + | arg0, | |||
| 284 | + | ) | |||
| 285 | + | }) | |||
| 286 | + | return _c | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | func (_c *MockHandler_Frame_Call) Return() *MockHandler_Frame_Call { | |||
| 290 | + | _c.Call.Return() | |||
| 291 | + | return _c | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHandler_Frame_Call { | |||
| 295 | + | _c.Run(run) | |||
| 296 | + | return _c | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | // LockFinished provides a mock function for the type MockHandler | |||
| 300 | + | func (_mock *MockHandler) LockFinished() { | |||
| 301 | + | _mock.Called() | |||
| 302 | + | return | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | // MockHandler_LockFinished_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LockFinished' | |||
| 306 | + | type MockHandler_LockFinished_Call struct { | |||
| 307 | + | *mock.Call | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | // LockFinished is a helper method to define mock.On call | |||
| 311 | + | func (_e *MockHandler_Expecter) LockFinished() *MockHandler_LockFinished_Call { | |||
| 312 | + | return &MockHandler_LockFinished_Call{Call: _e.mock.On("LockFinished")} | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | func (_c *MockHandler_LockFinished_Call) Run(run func()) *MockHandler_LockFinished_Call { | |||
| 316 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 317 | + | run() | |||
| 318 | + | }) | |||
| 319 | + | return _c | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | func (_c *MockHandler_LockFinished_Call) Return() *MockHandler_LockFinished_Call { | |||
| 323 | + | _c.Call.Return() | |||
| 324 | + | return _c | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | func (_c *MockHandler_LockFinished_Call) RunAndReturn(run func()) *MockHandler_LockFinished_Call { | |||
| 328 | + | _c.Run(run) | |||
| 329 | + | return _c | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | // Locked provides a mock function for the type MockHandler | |||
| 333 | + | func (_mock *MockHandler) Locked() { | |||
| 334 | + | _mock.Called() | |||
| 335 | + | return | |||
| 336 | + | } | |||
| 337 | + | ||||
| 338 | + | // MockHandler_Locked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Locked' | |||
| 339 | + | type MockHandler_Locked_Call struct { | |||
| 340 | + | *mock.Call | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | // Locked is a helper method to define mock.On call | |||
| 344 | + | func (_e *MockHandler_Expecter) Locked() *MockHandler_Locked_Call { | |||
| 345 | + | return &MockHandler_Locked_Call{Call: _e.mock.On("Locked")} | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | func (_c *MockHandler_Locked_Call) Run(run func()) *MockHandler_Locked_Call { | |||
| 349 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 350 | + | run() | |||
| 351 | + | }) | |||
| 352 | + | return _c | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | func (_c *MockHandler_Locked_Call) Return() *MockHandler_Locked_Call { | |||
| 356 | + | _c.Call.Return() | |||
| 357 | + | return _c | |||
| 358 | + | } | |||
| 359 | + | ||||
| 360 | + | func (_c *MockHandler_Locked_Call) RunAndReturn(run func()) *MockHandler_Locked_Call { | |||
| 361 | + | _c.Run(run) | |||
| 362 | + | return _c | |||
| 363 | + | } | |||
| 364 | + | ||||
| 365 | + | // OutputAdded provides a mock function for the type MockHandler | |||
| 366 | + | func (_mock *MockHandler) OutputAdded(out *Output) { | |||
| 367 | + | _mock.Called(out) | |||
| 368 | + | return | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | // MockHandler_OutputAdded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputAdded' | |||
| 372 | + | type MockHandler_OutputAdded_Call struct { | |||
| 373 | + | *mock.Call | |||
| 374 | + | } | |||
| 375 | + | ||||
| 376 | + | // OutputAdded is a helper method to define mock.On call | |||
| 377 | + | // - out *Output | |||
| 378 | + | func (_e *MockHandler_Expecter) OutputAdded(out any) *MockHandler_OutputAdded_Call { | |||
| 379 | + | return &MockHandler_OutputAdded_Call{Call: _e.mock.On("OutputAdded", out)} | |||
| 380 | + | } | |||
| 381 | + | ||||
| 382 | + | func (_c *MockHandler_OutputAdded_Call) Run(run func(out *Output)) *MockHandler_OutputAdded_Call { | |||
| 383 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 384 | + | var arg0 *Output | |||
| 385 | + | if args[0] != nil { | |||
| 386 | + | arg0 = args[0].(*Output) | |||
| 387 | + | } | |||
| 388 | + | run( | |||
| 389 | + | arg0, | |||
| 390 | + | ) | |||
| 391 | + | }) | |||
| 392 | + | return _c | |||
| 393 | + | } | |||
| 394 | + | ||||
| 395 | + | func (_c *MockHandler_OutputAdded_Call) Return() *MockHandler_OutputAdded_Call { | |||
| 396 | + | _c.Call.Return() | |||
| 397 | + | return _c | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | func (_c *MockHandler_OutputAdded_Call) RunAndReturn(run func(out *Output)) *MockHandler_OutputAdded_Call { | |||
| 401 | + | _c.Run(run) | |||
| 402 | + | return _c | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | // OutputRemoved provides a mock function for the type MockHandler | |||
| 406 | + | func (_mock *MockHandler) OutputRemoved(global uint32) { | |||
| 407 | + | _mock.Called(global) | |||
| 408 | + | return | |||
| 409 | + | } | |||
| 410 | + | ||||
| 411 | + | // MockHandler_OutputRemoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OutputRemoved' | |||
| 412 | + | type MockHandler_OutputRemoved_Call struct { | |||
| 413 | + | *mock.Call | |||
| 414 | + | } | |||
| 415 | + | ||||
| 416 | + | // OutputRemoved is a helper method to define mock.On call | |||
| 417 | + | // - global uint32 | |||
| 418 | + | func (_e *MockHandler_Expecter) OutputRemoved(global any) *MockHandler_OutputRemoved_Call { | |||
| 419 | + | return &MockHandler_OutputRemoved_Call{Call: _e.mock.On("OutputRemoved", global)} | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | func (_c *MockHandler_OutputRemoved_Call) Run(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |||
| 423 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 424 | + | var arg0 uint32 | |||
| 425 | + | if args[0] != nil { | |||
| 426 | + | arg0 = args[0].(uint32) | |||
| 427 | + | } | |||
| 428 | + | run( | |||
| 429 | + | arg0, | |||
| 430 | + | ) | |||
| 431 | + | }) | |||
| 432 | + | return _c | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | func (_c *MockHandler_OutputRemoved_Call) Return() *MockHandler_OutputRemoved_Call { | |||
| 436 | + | _c.Call.Return() | |||
| 437 | + | return _c | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32)) *MockHandler_OutputRemoved_Call { | |||
| 441 | + | _c.Run(run) | |||
| 442 | + | return _c | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | // Scale provides a mock function for the type MockHandler | |||
| 446 | + | func (_mock *MockHandler) Scale(id SurfaceID, scale float64) { | |||
| 447 | + | _mock.Called(id, scale) | |||
| 448 | + | return | |||
| 449 | + | } | |||
| 450 | + | ||||
| 451 | + | // MockHandler_Scale_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Scale' | |||
| 452 | + | type MockHandler_Scale_Call struct { | |||
| 453 | + | *mock.Call | |||
| 454 | + | } | |||
| 455 | + | ||||
| 456 | + | // Scale is a helper method to define mock.On call | |||
| 457 | + | // - id SurfaceID | |||
| 458 | + | // - scale float64 | |||
| 459 | + | func (_e *MockHandler_Expecter) Scale(id any, scale any) *MockHandler_Scale_Call { | |||
| 460 | + | return &MockHandler_Scale_Call{Call: _e.mock.On("Scale", id, scale)} | |||
| 461 | + | } | |||
| 462 | + | ||||
| 463 | + | func (_c *MockHandler_Scale_Call) Run(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call { | |||
| 464 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 465 | + | var arg0 SurfaceID | |||
| 466 | + | if args[0] != nil { | |||
| 467 | + | arg0 = args[0].(SurfaceID) | |||
| 468 | + | } | |||
| 469 | + | var arg1 float64 | |||
| 470 | + | if args[1] != nil { | |||
| 471 | + | arg1 = args[1].(float64) | |||
| 472 | + | } | |||
| 473 | + | run( | |||
| 474 | + | arg0, | |||
| 475 | + | arg1, | |||
| 476 | + | ) | |||
| 477 | + | }) | |||
| 478 | + | return _c | |||
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | func (_c *MockHandler_Scale_Call) Return() *MockHandler_Scale_Call { | |||
| 482 | + | _c.Call.Return() | |||
| 483 | + | return _c | |||
| 484 | + | } | |||
| 485 | + | ||||
| 486 | + | func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale float64)) *MockHandler_Scale_Call { | |||
| 487 | + | _c.Run(run) | |||
| 488 | + | return _c | |||
| 489 | + | } | |||
M lock.go +20 -5
| @@ -77,6 +77,11 @@ func (l *Lock) NewSurface(output uint32) (*Surface, error) { | |||
| 77 | 77 | if e == nil || !e.done { | |
| 78 | 78 | return nil, fmt.Errorf("neferclient: lock output %d unavailable", output) | |
| 79 | 79 | } | |
| 80 | + | for _, other := range c.surfaces { | |
| 81 | + | if other.lock == l && other.output == output { | |
| 82 | + | return nil, fmt.Errorf("neferclient: output %d already has a lock surface", output) | |
| 83 | + | } | |
| 84 | + | } | |
| 80 | 85 | c.pause() | |
| 81 | 86 | defer c.resume() | |
| 82 | 87 | s, err := c.newSurface(roleLock, 0, 0) | |
| @@ -92,10 +97,10 @@ func (l *Lock) NewSurface(output uint32) (*Surface, error) { | |||
| 92 | 97 | if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil { | |
| 93 | 98 | return nil, err | |
| 94 | 99 | } | |
| 95 | - | s.lock = l | |
| 100 | + | s.lock, s.output = l, output | |
| 96 | 101 | q, sid := c.q, uint32(s.id) | |
| 97 | 102 | s.lockSurf.OnConfigure(func(serial, w, h uint32) { | |
| 98 | - | ev := event{kind: evLockConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)} | |
| 103 | + | ev := configureEvent(evLockConfigure, sid, serial, w, h) | |
| 99 | 104 | q.post(&ev) | |
| 100 | 105 | }) | |
| 101 | 106 | // ext-session-lock forbids the initial empty commit: the configure comes | |
| @@ -125,13 +130,23 @@ func (l *Lock) Unlock() error { | |||
| 125 | 130 | return l.c.Roundtrip() | |
| 126 | 131 | } | |
| 127 | 132 | ||
| 128 | - | // Close destroys a lock that never reached Locked (refused or abandoned). It | |
| 129 | - | // is an error after Locked: a locked session is only ended by Unlock, and | |
| 130 | - | // leaving the connection closed keeps the session locked. | |
| 133 | + | // Close destroys a lock that never reached Locked (refused or abandoned). A | |
| 134 | + | // locked session is only ended by Unlock, so Close fails once Locked was | |
| 135 | + | // applied. Because the compositor may have sent locked before it processes the | |
| 136 | + | // destroy request (which would then be a protocol error), Close first | |
| 137 | + | // roundtrips and looks through the queued events: if a locked event for this | |
| 138 | + | // lock is already queued it is treated as applied (Handler.Locked is still | |
| 139 | + | // delivered by the next Dispatch) and Close fails with the same error. | |
| 131 | 140 | func (l *Lock) Close() error { | |
| 132 | 141 | if l.done { | |
| 133 | 142 | return nil | |
| 134 | 143 | } | |
| 144 | + | if !l.locked && !l.finished { | |
| 145 | + | if err := l.c.Roundtrip(); err != nil { | |
| 146 | + | return err | |
| 147 | + | } | |
| 148 | + | l.locked = l.locked || l.c.q.hasLocked(l.gen) | |
| 149 | + | } | |
| 135 | 150 | if l.locked && !l.finished { | |
| 136 | 151 | return errors.New("neferclient: lock is locked: use Unlock") | |
| 137 | 152 | } | |
| @@ -77,6 +77,11 @@ func (l *Lock) NewSurface(output uint32) (*Surface, error) { | |||||
| 77 | if e == nil || !e.done { | 77 | if e == nil || !e.done { | ||
| 78 | return nil, fmt.Errorf("neferclient: lock output %d unavailable", output) | 78 | return nil, fmt.Errorf("neferclient: lock output %d unavailable", output) | ||
| 79 | } | 79 | } | ||
| 80 | + | for _, other := range c.surfaces { | |||
| 81 | + | if other.lock == l && other.output == output { | |||
| 82 | + | return nil, fmt.Errorf("neferclient: output %d already has a lock surface", output) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 80 | c.pause() | 85 | c.pause() | ||
| 81 | defer c.resume() | 86 | defer c.resume() | ||
| 82 | s, err := c.newSurface(roleLock, 0, 0) | 87 | s, err := c.newSurface(roleLock, 0, 0) | ||
| @@ -92,10 +97,10 @@ func (l *Lock) NewSurface(output uint32) (*Surface, error) { | |||||
| 92 | if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil { | 97 | if s.lockSurf, err = l.obj.GetLockSurface(s.surf, e.proxy); err != nil { | ||
| 93 | return nil, err | 98 | return nil, err | ||
| 94 | } | 99 | } | ||
| 95 | - | s.lock = l | 100 | + | s.lock, s.output = l, output |
| 96 | q, sid := c.q, uint32(s.id) | 101 | q, sid := c.q, uint32(s.id) | ||
| 97 | s.lockSurf.OnConfigure(func(serial, w, h uint32) { | 102 | s.lockSurf.OnConfigure(func(serial, w, h uint32) { | ||
| 98 | - | ev := event{kind: evLockConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)} | 103 | + | ev := configureEvent(evLockConfigure, sid, serial, w, h) |
| 99 | q.post(&ev) | 104 | q.post(&ev) | ||
| 100 | }) | 105 | }) | ||
| 101 | // ext-session-lock forbids the initial empty commit: the configure comes | 106 | // ext-session-lock forbids the initial empty commit: the configure comes | ||
| @@ -125,13 +130,23 @@ func (l *Lock) Unlock() error { | |||||
| 125 | return l.c.Roundtrip() | 130 | return l.c.Roundtrip() | ||
| 126 | } | 131 | } | ||
| 127 | 132 | ||||
| 128 | - | // Close destroys a lock that never reached Locked (refused or abandoned). It | 133 | + | // Close destroys a lock that never reached Locked (refused or abandoned). A |
| 129 | - | // is an error after Locked: a locked session is only ended by Unlock, and | 134 | + | // locked session is only ended by Unlock, so Close fails once Locked was |
| 130 | - | // leaving the connection closed keeps the session locked. | 135 | + | // applied. Because the compositor may have sent locked before it processes the |
| 136 | + | // destroy request (which would then be a protocol error), Close first | |||
| 137 | + | // roundtrips and looks through the queued events: if a locked event for this | |||
| 138 | + | // lock is already queued it is treated as applied (Handler.Locked is still | |||
| 139 | + | // delivered by the next Dispatch) and Close fails with the same error. | |||
| 131 | func (l *Lock) Close() error { | 140 | func (l *Lock) Close() error { | ||
| 132 | if l.done { | 141 | if l.done { | ||
| 133 | return nil | 142 | return nil | ||
| 134 | } | 143 | } | ||
| 144 | + | if !l.locked && !l.finished { | |||
| 145 | + | if err := l.c.Roundtrip(); err != nil { | |||
| 146 | + | return err | |||
| 147 | + | } | |||
| 148 | + | l.locked = l.locked || l.c.q.hasLocked(l.gen) | |||
| 149 | + | } | |||
| 135 | if l.locked && !l.finished { | 150 | if l.locked && !l.finished { | ||
| 136 | return errors.New("neferclient: lock is locked: use Unlock") | 151 | return errors.New("neferclient: lock is locked: use Unlock") | ||
| 137 | } | 152 | } | ||
M present.go +31 -10
| @@ -4,10 +4,17 @@ import ( | |||
| 4 | 4 | "errors" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | ||
| 7 | + | "github.com/bnema/go-wayland-bindings/client/wayland" | |
| 7 | 8 | "github.com/bnema/wlturbo/wl" | |
| 8 | 9 | "golang.org/x/sys/unix" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 12 | + | // importedBuffer is a wl_buffer with the size it was imported at. | |
| 13 | + | type importedBuffer struct { | |
| 14 | + | buf *wayland.Buffer | |
| 15 | + | w, h int32 | |
| 16 | + | } | |
| 17 | + | ||
| 11 | 18 | // Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller. | |
| 12 | 19 | type Plane struct { | |
| 13 | 20 | FD int | |
| @@ -38,7 +45,7 @@ type Present struct { | |||
| 38 | 45 | ||
| 39 | 46 | // ImportBuffer creates a wl_buffer from b under the caller's id. The plane | |
| 40 | 47 | // descriptors are duplicated internally; the caller keeps ownership of its own. | |
| 41 | - | func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |
| 48 | + | func (s *Surface) ImportBuffer(id uint64, b *Buffer) (err error) { | |
| 42 | 49 | if s.closed { | |
| 43 | 50 | return ErrClosed | |
| 44 | 51 | } | |
| @@ -48,11 +55,11 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||
| 48 | 55 | if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) { | |
| 49 | 56 | return errors.New("neferclient: invalid buffer description") | |
| 50 | 57 | } | |
| 51 | - | var dups [4]int | |
| 58 | + | dups := [4]int{-1, -1, -1, -1} | |
| 52 | 59 | sent := 0 | |
| 53 | 60 | defer func() { | |
| 54 | 61 | for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request | |
| 55 | - | if fd > 0 { | |
| 62 | + | if fd >= 0 { | |
| 56 | 63 | _ = unix.Close(fd) | |
| 57 | 64 | } | |
| 58 | 65 | } | |
| @@ -68,7 +75,7 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||
| 68 | 75 | if err != nil { | |
| 69 | 76 | return err | |
| 70 | 77 | } | |
| 71 | - | defer params.Destroy() | |
| 78 | + | defer func() { err = errors.Join(err, params.Destroy()) }() | |
| 72 | 79 | hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier) | |
| 73 | 80 | for i := range b.PlaneCount { | |
| 74 | 81 | // On success the request closes the duplicate; on failure it stays ours. | |
| @@ -81,7 +88,7 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||
| 81 | 88 | if err != nil { | |
| 82 | 89 | return err | |
| 83 | 90 | } | |
| 84 | - | s.buffers[id] = buf | |
| 91 | + | s.buffers[id] = importedBuffer{buf: buf, w: b.Width, h: b.Height} | |
| 85 | 92 | return nil | |
| 86 | 93 | } | |
| 87 | 94 | ||
| @@ -93,7 +100,7 @@ func (s *Surface) DestroyBuffer(id uint64) error { | |||
| 93 | 100 | return fmt.Errorf("neferclient: unknown buffer %d", id) | |
| 94 | 101 | } | |
| 95 | 102 | delete(s.buffers, id) | |
| 96 | - | return buf.Destroy() | |
| 103 | + | return buf.buf.Destroy() | |
| 97 | 104 | } | |
| 98 | 105 | ||
| 99 | 106 | // ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally. | |
| @@ -124,6 +131,9 @@ func (s *Surface) Present(p *Present) error { | |||
| 124 | 131 | if s.closed { | |
| 125 | 132 | return ErrClosed | |
| 126 | 133 | } | |
| 134 | + | if p == nil { | |
| 135 | + | return errors.New("neferclient: nil present") | |
| 136 | + | } | |
| 127 | 137 | if !s.configured { | |
| 128 | 138 | return errors.New("neferclient: present before configure") | |
| 129 | 139 | } | |
| @@ -133,9 +143,13 @@ func (s *Surface) Present(p *Present) error { | |||
| 133 | 143 | if !s.frameReady { | |
| 134 | 144 | return errors.New("neferclient: frame callback pending") | |
| 135 | 145 | } | |
| 136 | - | if _, ok := s.buffers[p.Buffer]; !ok { | |
| 146 | + | buf, ok := s.buffers[p.Buffer] | |
| 147 | + | if !ok { | |
| 137 | 148 | return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer) | |
| 138 | 149 | } | |
| 150 | + | if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint { | |
| 151 | + | return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint) | |
| 152 | + | } | |
| 139 | 153 | for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} { | |
| 140 | 154 | if _, ok := s.timelines[id]; !ok { | |
| 141 | 155 | return fmt.Errorf("neferclient: unknown timeline %d", id) | |
| @@ -145,6 +159,11 @@ func (s *Surface) Present(p *Present) error { | |||
| 145 | 159 | if err != nil { | |
| 146 | 160 | return err | |
| 147 | 161 | } | |
| 162 | + | // Without a viewport (and always on lock surfaces) the compositor | |
| 163 | + | // requires the buffer to match the surface exactly. | |
| 164 | + | if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) { | |
| 165 | + | return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph) | |
| 166 | + | } | |
| 148 | 167 | r := s.req | |
| 149 | 168 | if s.viewport != nil { | |
| 150 | 169 | if s.sentDestW != s.width || s.sentDestH != s.height { | |
| @@ -153,7 +172,7 @@ func (s *Surface) Present(p *Present) error { | |||
| 153 | 172 | } | |
| 154 | 173 | s.sentDestW, s.sentDestH = s.width, s.height | |
| 155 | 174 | } | |
| 156 | - | } else if s.scale > 1 && s.sentBufScale != int32(s.scale) { | |
| 175 | + | } else if s.sentBufScale != int32(s.scale) { | |
| 157 | 176 | if err = r.setBufferScale(int32(s.scale)); err != nil { | |
| 158 | 177 | return err | |
| 159 | 178 | } | |
| @@ -199,7 +218,7 @@ func (s *Surface) Present(p *Present) error { | |||
| 199 | 218 | if err = r.commit(); err != nil { | |
| 200 | 219 | return err | |
| 201 | 220 | } | |
| 202 | - | s.frameReady = false | |
| 221 | + | s.frameReady, s.presented = false, true | |
| 203 | 222 | return nil | |
| 204 | 223 | } | |
| 205 | 224 | ||
| @@ -251,7 +270,9 @@ func (r wireRequests) setRelease(timeline, point uint64) error { | |||
| 251 | 270 | return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point)) | |
| 252 | 271 | } | |
| 253 | 272 | ||
| 254 | - | func (r wireRequests) attach(buffer uint64) error { return r.s.surf.Attach(r.s.buffers[buffer], 0, 0) } | |
| 273 | + | func (r wireRequests) attach(buffer uint64) error { | |
| 274 | + | return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0) | |
| 275 | + | } | |
| 255 | 276 | ||
| 256 | 277 | func (r wireRequests) damageBuffer(d Rect) error { | |
| 257 | 278 | return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height) | |
| @@ -4,10 +4,17 @@ import ( | |||||
| 4 | "errors" | 4 | "errors" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | 6 | ||||
| 7 | + | "github.com/bnema/go-wayland-bindings/client/wayland" | |||
| 7 | "github.com/bnema/wlturbo/wl" | 8 | "github.com/bnema/wlturbo/wl" | ||
| 8 | "golang.org/x/sys/unix" | 9 | "golang.org/x/sys/unix" | ||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 12 | + | // importedBuffer is a wl_buffer with the size it was imported at. | |||
| 13 | + | type importedBuffer struct { | |||
| 14 | + | buf *wayland.Buffer | |||
| 15 | + | w, h int32 | |||
| 16 | + | } | |||
| 17 | + | ||||
| 11 | // Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller. | 18 | // Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller. | ||
| 12 | type Plane struct { | 19 | type Plane struct { | ||
| 13 | FD int | 20 | FD int | ||
| @@ -38,7 +45,7 @@ type Present struct { | |||||
| 38 | 45 | ||||
| 39 | // ImportBuffer creates a wl_buffer from b under the caller's id. The plane | 46 | // ImportBuffer creates a wl_buffer from b under the caller's id. The plane | ||
| 40 | // descriptors are duplicated internally; the caller keeps ownership of its own. | 47 | // descriptors are duplicated internally; the caller keeps ownership of its own. | ||
| 41 | - | func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | 48 | + | func (s *Surface) ImportBuffer(id uint64, b *Buffer) (err error) { |
| 42 | if s.closed { | 49 | if s.closed { | ||
| 43 | return ErrClosed | 50 | return ErrClosed | ||
| 44 | } | 51 | } | ||
| @@ -48,11 +55,11 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||||
| 48 | if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) { | 55 | if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) { | ||
| 49 | return errors.New("neferclient: invalid buffer description") | 56 | return errors.New("neferclient: invalid buffer description") | ||
| 50 | } | 57 | } | ||
| 51 | - | var dups [4]int | 58 | + | dups := [4]int{-1, -1, -1, -1} |
| 52 | sent := 0 | 59 | sent := 0 | ||
| 53 | defer func() { | 60 | defer func() { | ||
| 54 | for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request | 61 | for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request | ||
| 55 | - | if fd > 0 { | 62 | + | if fd >= 0 { |
| 56 | _ = unix.Close(fd) | 63 | _ = unix.Close(fd) | ||
| 57 | } | 64 | } | ||
| 58 | } | 65 | } | ||
| @@ -68,7 +75,7 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||||
| 68 | if err != nil { | 75 | if err != nil { | ||
| 69 | return err | 76 | return err | ||
| 70 | } | 77 | } | ||
| 71 | - | defer params.Destroy() | 78 | + | defer func() { err = errors.Join(err, params.Destroy()) }() |
| 72 | hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier) | 79 | hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier) | ||
| 73 | for i := range b.PlaneCount { | 80 | for i := range b.PlaneCount { | ||
| 74 | // On success the request closes the duplicate; on failure it stays ours. | 81 | // On success the request closes the duplicate; on failure it stays ours. | ||
| @@ -81,7 +88,7 @@ func (s *Surface) ImportBuffer(id uint64, b *Buffer) error { | |||||
| 81 | if err != nil { | 88 | if err != nil { | ||
| 82 | return err | 89 | return err | ||
| 83 | } | 90 | } | ||
| 84 | - | s.buffers[id] = buf | 91 | + | s.buffers[id] = importedBuffer{buf: buf, w: b.Width, h: b.Height} |
| 85 | return nil | 92 | return nil | ||
| 86 | } | 93 | } | ||
| 87 | 94 | ||||
| @@ -93,7 +100,7 @@ func (s *Surface) DestroyBuffer(id uint64) error { | |||||
| 93 | return fmt.Errorf("neferclient: unknown buffer %d", id) | 100 | return fmt.Errorf("neferclient: unknown buffer %d", id) | ||
| 94 | } | 101 | } | ||
| 95 | delete(s.buffers, id) | 102 | delete(s.buffers, id) | ||
| 96 | - | return buf.Destroy() | 103 | + | return buf.buf.Destroy() |
| 97 | } | 104 | } | ||
| 98 | 105 | ||||
| 99 | // ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally. | 106 | // ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally. | ||
| @@ -124,6 +131,9 @@ func (s *Surface) Present(p *Present) error { | |||||
| 124 | if s.closed { | 131 | if s.closed { | ||
| 125 | return ErrClosed | 132 | return ErrClosed | ||
| 126 | } | 133 | } | ||
| 134 | + | if p == nil { | |||
| 135 | + | return errors.New("neferclient: nil present") | |||
| 136 | + | } | |||
| 127 | if !s.configured { | 137 | if !s.configured { | ||
| 128 | return errors.New("neferclient: present before configure") | 138 | return errors.New("neferclient: present before configure") | ||
| 129 | } | 139 | } | ||
| @@ -133,9 +143,13 @@ func (s *Surface) Present(p *Present) error { | |||||
| 133 | if !s.frameReady { | 143 | if !s.frameReady { | ||
| 134 | return errors.New("neferclient: frame callback pending") | 144 | return errors.New("neferclient: frame callback pending") | ||
| 135 | } | 145 | } | ||
| 136 | - | if _, ok := s.buffers[p.Buffer]; !ok { | 146 | + | buf, ok := s.buffers[p.Buffer] |
| 147 | + | if !ok { | |||
| 137 | return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer) | 148 | return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer) | ||
| 138 | } | 149 | } | ||
| 150 | + | if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint { | |||
| 151 | + | return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint) | |||
| 152 | + | } | |||
| 139 | for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} { | 153 | for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} { | ||
| 140 | if _, ok := s.timelines[id]; !ok { | 154 | if _, ok := s.timelines[id]; !ok { | ||
| 141 | return fmt.Errorf("neferclient: unknown timeline %d", id) | 155 | return fmt.Errorf("neferclient: unknown timeline %d", id) | ||
| @@ -145,6 +159,11 @@ func (s *Surface) Present(p *Present) error { | |||||
| 145 | if err != nil { | 159 | if err != nil { | ||
| 146 | return err | 160 | return err | ||
| 147 | } | 161 | } | ||
| 162 | + | // Without a viewport (and always on lock surfaces) the compositor | |||
| 163 | + | // requires the buffer to match the surface exactly. | |||
| 164 | + | if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) { | |||
| 165 | + | return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph) | |||
| 166 | + | } | |||
| 148 | r := s.req | 167 | r := s.req | ||
| 149 | if s.viewport != nil { | 168 | if s.viewport != nil { | ||
| 150 | if s.sentDestW != s.width || s.sentDestH != s.height { | 169 | if s.sentDestW != s.width || s.sentDestH != s.height { | ||
| @@ -153,7 +172,7 @@ func (s *Surface) Present(p *Present) error { | |||||
| 153 | } | 172 | } | ||
| 154 | s.sentDestW, s.sentDestH = s.width, s.height | 173 | s.sentDestW, s.sentDestH = s.width, s.height | ||
| 155 | } | 174 | } | ||
| 156 | - | } else if s.scale > 1 && s.sentBufScale != int32(s.scale) { | 175 | + | } else if s.sentBufScale != int32(s.scale) { |
| 157 | if err = r.setBufferScale(int32(s.scale)); err != nil { | 176 | if err = r.setBufferScale(int32(s.scale)); err != nil { | ||
| 158 | return err | 177 | return err | ||
| 159 | } | 178 | } | ||
| @@ -199,7 +218,7 @@ func (s *Surface) Present(p *Present) error { | |||||
| 199 | if err = r.commit(); err != nil { | 218 | if err = r.commit(); err != nil { | ||
| 200 | return err | 219 | return err | ||
| 201 | } | 220 | } | ||
| 202 | - | s.frameReady = false | 221 | + | s.frameReady, s.presented = false, true |
| 203 | return nil | 222 | return nil | ||
| 204 | } | 223 | } | ||
| 205 | 224 | ||||
| @@ -251,7 +270,9 @@ func (r wireRequests) setRelease(timeline, point uint64) error { | |||||
| 251 | return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point)) | 270 | return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point)) | ||
| 252 | } | 271 | } | ||
| 253 | 272 | ||||
| 254 | - | func (r wireRequests) attach(buffer uint64) error { return r.s.surf.Attach(r.s.buffers[buffer], 0, 0) } | 273 | + | func (r wireRequests) attach(buffer uint64) error { |
| 274 | + | return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0) | |||
| 275 | + | } | |||
| 255 | 276 | ||||
| 256 | func (r wireRequests) damageBuffer(d Rect) error { | 277 | func (r wireRequests) damageBuffer(d Rect) error { | ||
| 257 | return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height) | 278 | return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height) | ||
M queue.go +18 -0
| @@ -280,6 +280,24 @@ func (q *queue) takeHeld() []event { | |||
| 280 | 280 | return held | |
| 281 | 281 | } | |
| 282 | 282 | ||
| 283 | + | // hasLocked reports whether an evLocked for lock generation gen is queued or | |
| 284 | + | // parked. | |
| 285 | + | func (q *queue) hasLocked(gen uint32) bool { | |
| 286 | + | q.mu.Lock() | |
| 287 | + | defer q.mu.Unlock() | |
| 288 | + | for i := range q.n { | |
| 289 | + | if e := &q.ring[(q.head+i)%ringSize]; e.kind == evLocked && e.id == gen { | |
| 290 | + | return true | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | for i := range q.held { | |
| 294 | + | if q.held[i].kind == evLocked && q.held[i].id == gen { | |
| 295 | + | return true | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | return false | |
| 299 | + | } | |
| 300 | + | ||
| 283 | 301 | // closeFDs closes the descriptors owned by events nobody will drain. Only | |
| 284 | 302 | // valid once both producers have stopped. | |
| 285 | 303 | func (q *queue) closeFDs() { | |
| @@ -280,6 +280,24 @@ func (q *queue) takeHeld() []event { | |||||
| 280 | return held | 280 | return held | ||
| 281 | } | 281 | } | ||
| 282 | 282 | ||||
| 283 | + | // hasLocked reports whether an evLocked for lock generation gen is queued or | |||
| 284 | + | // parked. | |||
| 285 | + | func (q *queue) hasLocked(gen uint32) bool { | |||
| 286 | + | q.mu.Lock() | |||
| 287 | + | defer q.mu.Unlock() | |||
| 288 | + | for i := range q.n { | |||
| 289 | + | if e := &q.ring[(q.head+i)%ringSize]; e.kind == evLocked && e.id == gen { | |||
| 290 | + | return true | |||
| 291 | + | } | |||
| 292 | + | } | |||
| 293 | + | for i := range q.held { | |||
| 294 | + | if q.held[i].kind == evLocked && q.held[i].id == gen { | |||
| 295 | + | return true | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | return false | |||
| 299 | + | } | |||
| 300 | + | ||||
| 283 | // closeFDs closes the descriptors owned by events nobody will drain. Only | 301 | // closeFDs closes the descriptors owned by events nobody will drain. Only | ||
| 284 | // valid once both producers have stopped. | 302 | // valid once both producers have stopped. | ||
| 285 | func (q *queue) closeFDs() { | 303 | func (q *queue) closeFDs() { | ||
M surface.go +37 -23
| @@ -113,9 +113,10 @@ type Surface struct { | |||
| 113 | 113 | // destination/opaque state last sent; they are double-buffered surface | |
| 114 | 114 | // state that persists across commits, so unchanged values are not resent. | |
| 115 | 115 | sentDestW, sentDestH int32 | |
| 116 | - | sentBufScale int32 | |
| 116 | + | sentBufScale int32 // starts at 1, the protocol default | |
| 117 | + | presented bool // a Present committed: the surface is mapped | |
| 118 | + | output uint32 // lock surfaces: the covered wl_output global | |
| 117 | 119 | opaqueSet bool | |
| 118 | - | opaque *wayland.Region | |
| 119 | 120 | opaqueW, opaqueH int32 | |
| 120 | 121 | ||
| 121 | 122 | cb *frameCallback | |
| @@ -124,7 +125,7 @@ type Surface struct { | |||
| 124 | 125 | inputRects []Rect | |
| 125 | 126 | inputCustom bool | |
| 126 | 127 | ||
| 127 | - | buffers map[uint64]*wayland.Buffer | |
| 128 | + | buffers map[uint64]importedBuffer | |
| 128 | 129 | timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline | |
| 129 | 130 | ||
| 130 | 131 | fb Feedback | |
| @@ -161,8 +162,8 @@ func (f *frameCallback) Dispatch(ev *wl.Event) { | |||
| 161 | 162 | // ID returns the surface's identifier used in Handler callbacks. | |
| 162 | 163 | func (s *Surface) ID() SurfaceID { return s.id } | |
| 163 | 164 | ||
| 164 | - | // Size returns the current logical size and scale (1 until announced). It is | |
| 165 | - | // zero until the first configure. | |
| 165 | + | // Size returns the current logical size and scale (1 until announced). Before | |
| 166 | + | // the first configure the size is the requested one (zero for lock surfaces). | |
| 166 | 167 | func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale } | |
| 167 | 168 | ||
| 168 | 169 | // Feedback returns the latest complete dmabuf feedback, or nil before the | |
| @@ -237,8 +238,8 @@ func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) { | |||
| 237 | 238 | } | |
| 238 | 239 | g := &c.g | |
| 239 | 240 | c.nextSurf++ | |
| 240 | - | s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, | |
| 241 | - | buffers: map[uint64]*wayland.Buffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}} | |
| 241 | + | s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1, | |
| 242 | + | buffers: map[uint64]importedBuffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}} | |
| 242 | 243 | ok := false | |
| 243 | 244 | defer func() { | |
| 244 | 245 | if !ok { | |
| @@ -345,7 +346,16 @@ var badEventErrors = [...]string{ | |||
| 345 | 346 | badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable", | |
| 346 | 347 | badMainDevice: "neferclient: invalid dmabuf main_device length", | |
| 347 | 348 | badTranche: "neferclient: odd dmabuf tranche format list", | |
| 348 | - | badConfigure: "neferclient: layer configure size out of range", | |
| 349 | + | badConfigure: "neferclient: configure size out of range", | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // configureEvent builds a layer or lock configure event, or a badConfigure | |
| 353 | + | // event when the compositor announced an out-of-range size. | |
| 354 | + | func configureEvent(kind eventKind, sid, serial, w, h uint32) event { | |
| 355 | + | if w > maxLayerDimension || h > maxLayerDimension { | |
| 356 | + | return event{kind: evBadEvent, id: sid, a: badConfigure} | |
| 357 | + | } | |
| 358 | + | return event{kind: kind, id: sid, serial: serial, a: int32(w), b: int32(h)} | |
| 349 | 359 | } | |
| 350 | 360 | ||
| 351 | 361 | // NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable | |
| @@ -402,12 +412,7 @@ func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) { | |||
| 402 | 412 | q.post(&e) | |
| 403 | 413 | }) | |
| 404 | 414 | s.layer.OnConfigure(func(serial, w, h uint32) { | |
| 405 | - | if w > maxLayerDimension || h > maxLayerDimension { | |
| 406 | - | e := event{kind: evBadEvent, id: sid, a: badConfigure} | |
| 407 | - | q.post(&e) | |
| 408 | - | return | |
| 409 | - | } | |
| 410 | - | e := event{kind: evLayerConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)} | |
| 415 | + | e := configureEvent(evLayerConfigure, sid, serial, w, h) | |
| 411 | 416 | q.post(&e) | |
| 412 | 417 | }) | |
| 413 | 418 | // Opposite anchors let the compositor assign that axis (size 0). | |
| @@ -574,7 +579,9 @@ func (c *Conn) applySurface(ev *event, h Handler) { | |||
| 574 | 579 | s.setScale(float64(ev.a), h) | |
| 575 | 580 | } | |
| 576 | 581 | case evFeedbackTable: | |
| 577 | - | if err := s.readTable(int(ev.fd), ev.flags); err != nil { | |
| 582 | + | fd := int(ev.fd) | |
| 583 | + | ev.fd = -1 // readTable consumes fd; the deferred closeFD must not touch the number again | |
| 584 | + | if err := s.readTable(fd, ev.flags); err != nil { | |
| 578 | 585 | h.Error(err) | |
| 579 | 586 | } | |
| 580 | 587 | case evFeedbackMainDevice: | |
| @@ -645,15 +652,22 @@ func (s *Surface) readTable(fd int, size uint32) error { | |||
| 645 | 652 | return nil | |
| 646 | 653 | } | |
| 647 | 654 | ||
| 648 | - | // SetInputRegion sets the input region (logical pixels) and commits when it | |
| 649 | - | // changed. nil restores the whole surface; a non-nil empty slice passes all | |
| 650 | - | // input through. Before the first configure the next commit applies it. | |
| 655 | + | // SetInputRegion sets the input region (logical pixels). nil restores the | |
| 656 | + | // whole surface; a non-nil empty slice passes all input through. The region is | |
| 657 | + | // double-buffered state: it is committed immediately only once the surface | |
| 658 | + | // presented a buffer; before that the next Present applies it (committing | |
| 659 | + | // earlier would map a surface without a buffer, which lock surfaces forbid and | |
| 660 | + | // layer surfaces answer with an error). Lock surfaces always take all input | |
| 661 | + | // and refuse it. | |
| 651 | 662 | func (s *Surface) SetInputRegion(rects []Rect) error { | |
| 652 | 663 | if s.closed { | |
| 653 | 664 | return ErrClosed | |
| 654 | 665 | } | |
| 666 | + | if s.role == roleLock { | |
| 667 | + | return errors.New("neferclient: lock surfaces take all input") | |
| 668 | + | } | |
| 655 | 669 | changed, err := s.stageInputRects(rects) | |
| 656 | - | if err != nil || !changed || !s.configured { | |
| 670 | + | if err != nil || !changed || !s.presented { | |
| 657 | 671 | return err | |
| 658 | 672 | } | |
| 659 | 673 | return s.surf.Commit() | |
| @@ -709,6 +723,9 @@ func (s *Surface) Close() error { | |||
| 709 | 723 | } | |
| 710 | 724 | ||
| 711 | 725 | func (s *Surface) destroy() error { | |
| 726 | + | // TODO(wlturbo): a frame callback pending here is unregistered locally, so | |
| 727 | + | // its later done/delete_id reaches an unknown object. Fixing it needs | |
| 728 | + | // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame). | |
| 712 | 729 | s.closed = true | |
| 713 | 730 | delete(s.c.surfaces, s.id) | |
| 714 | 731 | var errs []error | |
| @@ -718,16 +735,13 @@ func (s *Surface) destroy() error { | |||
| 718 | 735 | } | |
| 719 | 736 | } | |
| 720 | 737 | for id, b := range s.buffers { | |
| 721 | - | add(b.Destroy()) | |
| 738 | + | add(b.buf.Destroy()) | |
| 722 | 739 | delete(s.buffers, id) | |
| 723 | 740 | } | |
| 724 | 741 | for id, t := range s.timelines { | |
| 725 | 742 | add(t.Destroy()) | |
| 726 | 743 | delete(s.timelines, id) | |
| 727 | 744 | } | |
| 728 | - | if s.opaque != nil { | |
| 729 | - | add(s.opaque.Destroy()) | |
| 730 | - | } | |
| 731 | 745 | if s.feedback != nil { | |
| 732 | 746 | add(s.feedback.Destroy()) | |
| 733 | 747 | } | |
| @@ -113,9 +113,10 @@ type Surface struct { | |||||
| 113 | // destination/opaque state last sent; they are double-buffered surface | 113 | // destination/opaque state last sent; they are double-buffered surface | ||
| 114 | // state that persists across commits, so unchanged values are not resent. | 114 | // state that persists across commits, so unchanged values are not resent. | ||
| 115 | sentDestW, sentDestH int32 | 115 | sentDestW, sentDestH int32 | ||
| 116 | - | sentBufScale int32 | 116 | + | sentBufScale int32 // starts at 1, the protocol default |
| 117 | + | presented bool // a Present committed: the surface is mapped | |||
| 118 | + | output uint32 // lock surfaces: the covered wl_output global | |||
| 117 | opaqueSet bool | 119 | opaqueSet bool | ||
| 118 | - | opaque *wayland.Region | |||
| 119 | opaqueW, opaqueH int32 | 120 | opaqueW, opaqueH int32 | ||
| 120 | 121 | ||||
| 121 | cb *frameCallback | 122 | cb *frameCallback | ||
| @@ -124,7 +125,7 @@ type Surface struct { | |||||
| 124 | inputRects []Rect | 125 | inputRects []Rect | ||
| 125 | inputCustom bool | 126 | inputCustom bool | ||
| 126 | 127 | ||||
| 127 | - | buffers map[uint64]*wayland.Buffer | 128 | + | buffers map[uint64]importedBuffer |
| 128 | timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline | 129 | timelines map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline | ||
| 129 | 130 | ||||
| 130 | fb Feedback | 131 | fb Feedback | ||
| @@ -161,8 +162,8 @@ func (f *frameCallback) Dispatch(ev *wl.Event) { | |||||
| 161 | // ID returns the surface's identifier used in Handler callbacks. | 162 | // ID returns the surface's identifier used in Handler callbacks. | ||
| 162 | func (s *Surface) ID() SurfaceID { return s.id } | 163 | func (s *Surface) ID() SurfaceID { return s.id } | ||
| 163 | 164 | ||||
| 164 | - | // Size returns the current logical size and scale (1 until announced). It is | 165 | + | // Size returns the current logical size and scale (1 until announced). Before |
| 165 | - | // zero until the first configure. | 166 | + | // the first configure the size is the requested one (zero for lock surfaces). |
| 166 | func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale } | 167 | func (s *Surface) Size() (w, h int32, scale float64) { return s.width, s.height, s.scale } | ||
| 167 | 168 | ||||
| 168 | // Feedback returns the latest complete dmabuf feedback, or nil before the | 169 | // Feedback returns the latest complete dmabuf feedback, or nil before the | ||
| @@ -237,8 +238,8 @@ func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) { | |||||
| 237 | } | 238 | } | ||
| 238 | g := &c.g | 239 | g := &c.g | ||
| 239 | c.nextSurf++ | 240 | c.nextSurf++ | ||
| 240 | - | s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, | 241 | + | s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1, |
| 241 | - | buffers: map[uint64]*wayland.Buffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}} | 242 | + | buffers: map[uint64]importedBuffer{}, timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{}} |
| 242 | ok := false | 243 | ok := false | ||
| 243 | defer func() { | 244 | defer func() { | ||
| 244 | if !ok { | 245 | if !ok { | ||
| @@ -345,7 +346,16 @@ var badEventErrors = [...]string{ | |||||
| 345 | badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable", | 346 | badFeedbackFD: "neferclient: dmabuf format table descriptor unavailable", | ||
| 346 | badMainDevice: "neferclient: invalid dmabuf main_device length", | 347 | badMainDevice: "neferclient: invalid dmabuf main_device length", | ||
| 347 | badTranche: "neferclient: odd dmabuf tranche format list", | 348 | badTranche: "neferclient: odd dmabuf tranche format list", | ||
| 348 | - | badConfigure: "neferclient: layer configure size out of range", | 349 | + | badConfigure: "neferclient: configure size out of range", |
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | // configureEvent builds a layer or lock configure event, or a badConfigure | |||
| 353 | + | // event when the compositor announced an out-of-range size. | |||
| 354 | + | func configureEvent(kind eventKind, sid, serial, w, h uint32) event { | |||
| 355 | + | if w > maxLayerDimension || h > maxLayerDimension { | |||
| 356 | + | return event{kind: evBadEvent, id: sid, a: badConfigure} | |||
| 357 | + | } | |||
| 358 | + | return event{kind: kind, id: sid, serial: serial, a: int32(w), b: int32(h)} | |||
| 349 | } | 359 | } | ||
| 350 | 360 | ||||
| 351 | // NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable | 361 | // NewLayerSurface creates a wlr-layer-shell surface. It becomes presentable | ||
| @@ -402,12 +412,7 @@ func (c *Conn) NewLayerSurface(cfg LayerConfig) (*Surface, error) { | |||||
| 402 | q.post(&e) | 412 | q.post(&e) | ||
| 403 | }) | 413 | }) | ||
| 404 | s.layer.OnConfigure(func(serial, w, h uint32) { | 414 | s.layer.OnConfigure(func(serial, w, h uint32) { | ||
| 405 | - | if w > maxLayerDimension || h > maxLayerDimension { | 415 | + | e := configureEvent(evLayerConfigure, sid, serial, w, h) |
| 406 | - | e := event{kind: evBadEvent, id: sid, a: badConfigure} | |||
| 407 | - | q.post(&e) | |||
| 408 | - | return | |||
| 409 | - | } | |||
| 410 | - | e := event{kind: evLayerConfigure, id: sid, serial: serial, a: int32(w), b: int32(h)} | |||
| 411 | q.post(&e) | 416 | q.post(&e) | ||
| 412 | }) | 417 | }) | ||
| 413 | // Opposite anchors let the compositor assign that axis (size 0). | 418 | // Opposite anchors let the compositor assign that axis (size 0). | ||
| @@ -574,7 +579,9 @@ func (c *Conn) applySurface(ev *event, h Handler) { | |||||
| 574 | s.setScale(float64(ev.a), h) | 579 | s.setScale(float64(ev.a), h) | ||
| 575 | } | 580 | } | ||
| 576 | case evFeedbackTable: | 581 | case evFeedbackTable: | ||
| 577 | - | if err := s.readTable(int(ev.fd), ev.flags); err != nil { | 582 | + | fd := int(ev.fd) |
| 583 | + | ev.fd = -1 // readTable consumes fd; the deferred closeFD must not touch the number again | |||
| 584 | + | if err := s.readTable(fd, ev.flags); err != nil { | |||
| 578 | h.Error(err) | 585 | h.Error(err) | ||
| 579 | } | 586 | } | ||
| 580 | case evFeedbackMainDevice: | 587 | case evFeedbackMainDevice: | ||
| @@ -645,15 +652,22 @@ func (s *Surface) readTable(fd int, size uint32) error { | |||||
| 645 | return nil | 652 | return nil | ||
| 646 | } | 653 | } | ||
| 647 | 654 | ||||
| 648 | - | // SetInputRegion sets the input region (logical pixels) and commits when it | 655 | + | // SetInputRegion sets the input region (logical pixels). nil restores the |
| 649 | - | // changed. nil restores the whole surface; a non-nil empty slice passes all | 656 | + | // whole surface; a non-nil empty slice passes all input through. The region is |
| 650 | - | // input through. Before the first configure the next commit applies it. | 657 | + | // double-buffered state: it is committed immediately only once the surface |
| 658 | + | // presented a buffer; before that the next Present applies it (committing | |||
| 659 | + | // earlier would map a surface without a buffer, which lock surfaces forbid and | |||
| 660 | + | // layer surfaces answer with an error). Lock surfaces always take all input | |||
| 661 | + | // and refuse it. | |||
| 651 | func (s *Surface) SetInputRegion(rects []Rect) error { | 662 | func (s *Surface) SetInputRegion(rects []Rect) error { | ||
| 652 | if s.closed { | 663 | if s.closed { | ||
| 653 | return ErrClosed | 664 | return ErrClosed | ||
| 654 | } | 665 | } | ||
| 666 | + | if s.role == roleLock { | |||
| 667 | + | return errors.New("neferclient: lock surfaces take all input") | |||
| 668 | + | } | |||
| 655 | changed, err := s.stageInputRects(rects) | 669 | changed, err := s.stageInputRects(rects) | ||
| 656 | - | if err != nil || !changed || !s.configured { | 670 | + | if err != nil || !changed || !s.presented { |
| 657 | return err | 671 | return err | ||
| 658 | } | 672 | } | ||
| 659 | return s.surf.Commit() | 673 | return s.surf.Commit() | ||
| @@ -709,6 +723,9 @@ func (s *Surface) Close() error { | |||||
| 709 | } | 723 | } | ||
| 710 | 724 | ||||
| 711 | func (s *Surface) destroy() error { | 725 | func (s *Surface) destroy() error { | ||
| 726 | + | // TODO(wlturbo): a frame callback pending here is unregistered locally, so | |||
| 727 | + | // its later done/delete_id reaches an unknown object. Fixing it needs | |||
| 728 | + | // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame). | |||
| 712 | s.closed = true | 729 | s.closed = true | ||
| 713 | delete(s.c.surfaces, s.id) | 730 | delete(s.c.surfaces, s.id) | ||
| 714 | var errs []error | 731 | var errs []error | ||
| @@ -718,16 +735,13 @@ func (s *Surface) destroy() error { | |||||
| 718 | } | 735 | } | ||
| 719 | } | 736 | } | ||
| 720 | for id, b := range s.buffers { | 737 | for id, b := range s.buffers { | ||
| 721 | - | add(b.Destroy()) | 738 | + | add(b.buf.Destroy()) |
| 722 | delete(s.buffers, id) | 739 | delete(s.buffers, id) | ||
| 723 | } | 740 | } | ||
| 724 | for id, t := range s.timelines { | 741 | for id, t := range s.timelines { | ||
| 725 | add(t.Destroy()) | 742 | add(t.Destroy()) | ||
| 726 | delete(s.timelines, id) | 743 | delete(s.timelines, id) | ||
| 727 | } | 744 | } | ||
| 728 | - | if s.opaque != nil { | |||
| 729 | - | add(s.opaque.Destroy()) | |||
| 730 | - | } | |||
| 731 | if s.feedback != nil { | 745 | if s.feedback != nil { | ||
| 732 | add(s.feedback.Destroy()) | 746 | add(s.feedback.Destroy()) | ||
| 733 | } | 747 | } | ||
M surface_internal_test.go +96 -21
| @@ -2,11 +2,11 @@ package neferclient | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | + | "strings" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj" | |
| 8 | 9 | "github.com/bnema/go-wayland-bindings/client/viewporter" | |
| 9 | - | "github.com/bnema/go-wayland-bindings/client/wayland" | |
| 10 | 10 | "github.com/stretchr/testify/mock" | |
| 11 | 11 | "github.com/stretchr/testify/require" | |
| 12 | 12 | "golang.org/x/sys/unix" | |
| @@ -14,8 +14,9 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | // presentable returns a configured surface that sends through req. | |
| 16 | 16 | func presentable(req surfaceRequests, scale float64) *Surface { | |
| 17 | - | return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, configured: true, frameReady: true, fbDone: true, | |
| 18 | - | buffers: map[uint64]*wayland.Buffer{1: nil}, | |
| 17 | + | return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, sentBufScale: 1, role: roleToplevel, | |
| 18 | + | configured: true, frameReady: true, fbDone: true, | |
| 19 | + | buffers: map[uint64]importedBuffer{1: {w: 100, h: 50}}, | |
| 19 | 20 | timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil}, | |
| 20 | 21 | viewport: &viewporter.WpViewport{}} | |
| 21 | 22 | } | |
| @@ -84,17 +85,6 @@ func TestPhysicalSize(t *testing.T) { | |||
| 84 | 85 | require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up | |
| 85 | 86 | } | |
| 86 | 87 | ||
| 87 | - | // feedbackHandler is the measured input, not a double: it keeps what the | |
| 88 | - | // owner-side feedback parsing produced. | |
| 89 | - | type feedbackHandler struct { | |
| 90 | - | NopHandler | |
| 91 | - | done int | |
| 92 | - | errs []error | |
| 93 | - | } | |
| 94 | - | ||
| 95 | - | func (h *feedbackHandler) FeedbackDone(SurfaceID) { h.done++ } | |
| 96 | - | func (h *feedbackHandler) Error(err error) { h.errs = append(h.errs, err) } | |
| 97 | - | ||
| 98 | 88 | func tableFD(t *testing.T, entries []Format) int { | |
| 99 | 89 | t.Helper() | |
| 100 | 90 | fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC) | |
| @@ -134,16 +124,16 @@ func TestFeedbackParsedOnOwner(t *testing.T) { | |||
| 134 | 124 | c, s := newOwnerConn() | |
| 135 | 125 | table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}} | |
| 136 | 126 | postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0) | |
| 137 | - | h := &feedbackHandler{} | |
| 127 | + | h := NewMockHandler(t) | |
| 128 | + | h.EXPECT().FeedbackDone(s.id).Return().Once() | |
| 138 | 129 | require.NoError(t, c.Dispatch(h)) | |
| 139 | - | require.Empty(t, h.errs) | |
| 140 | - | require.Equal(t, 1, h.done) | |
| 141 | 130 | fb := s.Feedback() | |
| 142 | 131 | require.NotNil(t, fb) | |
| 143 | 132 | require.Equal(t, uint64(0xe280), fb.MainDevice) | |
| 144 | 133 | require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order") | |
| 145 | 134 | ||
| 146 | 135 | // A second round replaces the first without leaking the first one's formats. | |
| 136 | + | h.EXPECT().FeedbackDone(s.id).Return().Once() | |
| 147 | 137 | postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1) | |
| 148 | 138 | require.NoError(t, c.Dispatch(h)) | |
| 149 | 139 | require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback()) | |
| @@ -153,11 +143,11 @@ func TestFeedbackBadIndexAndSize(t *testing.T) { | |||
| 153 | 143 | c, s := newOwnerConn() | |
| 154 | 144 | postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3) | |
| 155 | 145 | postRound(c, s, tableFD(t, nil), 15, 5) | |
| 156 | - | h := &feedbackHandler{} | |
| 146 | + | h := NewMockHandler(t) | |
| 147 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "out of range") })).Return().Once() | |
| 148 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "invalid dmabuf format table size") })).Return().Once() | |
| 149 | + | h.EXPECT().FeedbackDone(s.id).Return().Twice() | |
| 157 | 150 | require.NoError(t, c.Dispatch(h)) | |
| 158 | - | require.Len(t, h.errs, 2) | |
| 159 | - | require.ErrorContains(t, h.errs[0], "out of range") | |
| 160 | - | require.ErrorContains(t, h.errs[1], "invalid dmabuf format table size") | |
| 161 | 151 | } | |
| 162 | 152 | ||
| 163 | 153 | func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil } | |
| @@ -198,6 +188,22 @@ func TestEventForClosedSurfaceDropsFD(t *testing.T) { | |||
| 198 | 188 | require.False(t, fdOpen(fd)) | |
| 199 | 189 | } | |
| 200 | 190 | ||
| 191 | + | // GO-001: the descriptor is consumed by readTable; the event must not close | |
| 192 | + | // the number a second time, which by then may belong to someone else. | |
| 193 | + | func TestFeedbackTableFDNotClosedTwice(t *testing.T) { | |
| 194 | + | c, s := newOwnerConn() | |
| 195 | + | fd := tableFD(t, []Format{{1, 1}}) | |
| 196 | + | ev := event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd), flags: 16} | |
| 197 | + | c.applySurface(&ev, NewMockHandler(t)) | |
| 198 | + | require.False(t, fdOpen(fd), "readTable consumed it") | |
| 199 | + | require.Equal(t, int32(-1), ev.fd, "ownership left the event") | |
| 200 | + | other := tableFD(t, nil) // the kernel hands out the freed number again | |
| 201 | + | require.Equal(t, fd, other) | |
| 202 | + | ev.closeFD() // what a second close of the event would do | |
| 203 | + | require.True(t, fdOpen(other), "an unrelated descriptor that reuses the number stays open") | |
| 204 | + | _ = unix.Close(other) | |
| 205 | + | } | |
| 206 | + | ||
| 201 | 207 | func TestLockRules(t *testing.T) { | |
| 202 | 208 | c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}} | |
| 203 | 209 | l := &Lock{c: c, gen: 1} | |
| @@ -215,3 +221,72 @@ func TestLockRules(t *testing.T) { | |||
| 215 | 221 | require.False(t, l.Locked()) | |
| 216 | 222 | require.ErrorContains(t, l.Unlock(), "before the locked event") | |
| 217 | 223 | } | |
| 224 | + | ||
| 225 | + | // GO-004: the buffer scale returns to 1 when the output scale drops, with no | |
| 226 | + | // viewport to carry the size. | |
| 227 | + | func TestPresentResetsBufferScale(t *testing.T) { | |
| 228 | + | m := newMocksurfaceRequests(t) | |
| 229 | + | s := presentable(m, 2) | |
| 230 | + | s.viewport = nil | |
| 231 | + | s.buffers[1] = importedBuffer{w: 200, h: 100} | |
| 232 | + | m.EXPECT().setBufferScale(int32(2)).Return(nil).Once() | |
| 233 | + | m.EXPECT().setBufferScale(int32(1)).Return(nil).Once() | |
| 234 | + | m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice() | |
| 235 | + | m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice() | |
| 236 | + | m.EXPECT().attach(uint64(1)).Return(nil).Twice() | |
| 237 | + | m.EXPECT().damageBuffer(mock.Anything).Return(nil).Twice() | |
| 238 | + | m.EXPECT().frame().Return(nil).Twice() | |
| 239 | + | m.EXPECT().commit().Return(nil).Twice() | |
| 240 | + | p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2} | |
| 241 | + | require.NoError(t, s.Present(p)) | |
| 242 | + | s.scale, s.frameReady = 1, true | |
| 243 | + | s.buffers[1] = importedBuffer{w: 100, h: 50} | |
| 244 | + | require.NoError(t, s.Present(p)) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // GO-005: size mismatches, timeline point order and a nil request are refused | |
| 248 | + | // before anything is sent (the mock is strict). | |
| 249 | + | func TestPresentRefusesInconsistentRequests(t *testing.T) { | |
| 250 | + | m := newMocksurfaceRequests(t) | |
| 251 | + | good := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2} | |
| 252 | + | for name, tc := range map[string]struct { | |
| 253 | + | mutate func(*Surface) | |
| 254 | + | p *Present | |
| 255 | + | }{ | |
| 256 | + | "nil": {func(*Surface) {}, nil}, | |
| 257 | + | "mismatch no viewport": {func(s *Surface) { s.viewport = nil; s.buffers[1] = importedBuffer{w: 99, h: 50} }, good}, | |
| 258 | + | "mismatch lock role": {func(s *Surface) { s.role = roleLock; s.buffers[1] = importedBuffer{w: 100, h: 49} }, good}, | |
| 259 | + | "same timeline acquire not before release": {func(*Surface) {}, | |
| 260 | + | &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 1, AcquirePoint: 5, ReleasePoint: 5}}, | |
| 261 | + | } { | |
| 262 | + | s := presentable(m, 1) | |
| 263 | + | tc.mutate(s) | |
| 264 | + | require.Error(t, s.Present(tc.p), name) | |
| 265 | + | } | |
| 266 | + | // With a viewport a different buffer size is legal (scaled by the compositor). | |
| 267 | + | s := presentable(m, 1) | |
| 268 | + | s.buffers[1] = importedBuffer{w: 50, h: 25} | |
| 269 | + | m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once() | |
| 270 | + | m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Once() | |
| 271 | + | m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Once() | |
| 272 | + | m.EXPECT().attach(uint64(1)).Return(nil).Once() | |
| 273 | + | m.EXPECT().damageBuffer(mock.Anything).Return(nil).Once() | |
| 274 | + | m.EXPECT().frame().Return(nil).Once() | |
| 275 | + | m.EXPECT().commit().Return(nil).Once() | |
| 276 | + | require.NoError(t, s.Present(good)) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | // GO-008: lock and layer configure sizes share the bounds check. | |
| 280 | + | func TestConfigureEventBounds(t *testing.T) { | |
| 281 | + | for _, kind := range []eventKind{evLayerConfigure, evLockConfigure} { | |
| 282 | + | ev := configureEvent(kind, 3, 9, maxLayerDimension, 1) | |
| 283 | + | require.Equal(t, event{kind: kind, id: 3, serial: 9, a: maxLayerDimension, b: 1}, ev) | |
| 284 | + | require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, maxLayerDimension+1, 1).kind) | |
| 285 | + | require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, 1, 1<<31).kind, "no wrap to a negative int32") | |
| 286 | + | } | |
| 287 | + | c, _ := newOwnerConn() | |
| 288 | + | c.q.post(&event{kind: evBadEvent, id: 1, a: badConfigure}) | |
| 289 | + | h := NewMockHandler(t) | |
| 290 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "configure size") })).Return().Once() | |
| 291 | + | require.NoError(t, c.Dispatch(h)) | |
| 292 | + | } | |
| @@ -2,11 +2,11 @@ package neferclient | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | + | "strings" | |||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj" | 8 | "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj" | ||
| 8 | "github.com/bnema/go-wayland-bindings/client/viewporter" | 9 | "github.com/bnema/go-wayland-bindings/client/viewporter" | ||
| 9 | - | "github.com/bnema/go-wayland-bindings/client/wayland" | |||
| 10 | "github.com/stretchr/testify/mock" | 10 | "github.com/stretchr/testify/mock" | ||
| 11 | "github.com/stretchr/testify/require" | 11 | "github.com/stretchr/testify/require" | ||
| 12 | "golang.org/x/sys/unix" | 12 | "golang.org/x/sys/unix" | ||
| @@ -14,8 +14,9 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | // presentable returns a configured surface that sends through req. | 15 | // presentable returns a configured surface that sends through req. | ||
| 16 | func presentable(req surfaceRequests, scale float64) *Surface { | 16 | func presentable(req surfaceRequests, scale float64) *Surface { | ||
| 17 | - | return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, configured: true, frameReady: true, fbDone: true, | 17 | + | return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, sentBufScale: 1, role: roleToplevel, |
| 18 | - | buffers: map[uint64]*wayland.Buffer{1: nil}, | 18 | + | configured: true, frameReady: true, fbDone: true, |
| 19 | + | buffers: map[uint64]importedBuffer{1: {w: 100, h: 50}}, | |||
| 19 | timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil}, | 20 | timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil}, | ||
| 20 | viewport: &viewporter.WpViewport{}} | 21 | viewport: &viewporter.WpViewport{}} | ||
| 21 | } | 22 | } | ||
| @@ -84,17 +85,6 @@ func TestPhysicalSize(t *testing.T) { | |||||
| 84 | require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up | 85 | require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up | ||
| 85 | } | 86 | } | ||
| 86 | 87 | ||||
| 87 | - | // feedbackHandler is the measured input, not a double: it keeps what the | |||
| 88 | - | // owner-side feedback parsing produced. | |||
| 89 | - | type feedbackHandler struct { | |||
| 90 | - | NopHandler | |||
| 91 | - | done int | |||
| 92 | - | errs []error | |||
| 93 | - | } | |||
| 94 | - | ||||
| 95 | - | func (h *feedbackHandler) FeedbackDone(SurfaceID) { h.done++ } | |||
| 96 | - | func (h *feedbackHandler) Error(err error) { h.errs = append(h.errs, err) } | |||
| 97 | - | ||||
| 98 | func tableFD(t *testing.T, entries []Format) int { | 88 | func tableFD(t *testing.T, entries []Format) int { | ||
| 99 | t.Helper() | 89 | t.Helper() | ||
| 100 | fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC) | 90 | fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC) | ||
| @@ -134,16 +124,16 @@ func TestFeedbackParsedOnOwner(t *testing.T) { | |||||
| 134 | c, s := newOwnerConn() | 124 | c, s := newOwnerConn() | ||
| 135 | table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}} | 125 | table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}} | ||
| 136 | postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0) | 126 | postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0) | ||
| 137 | - | h := &feedbackHandler{} | 127 | + | h := NewMockHandler(t) |
| 128 | + | h.EXPECT().FeedbackDone(s.id).Return().Once() | |||
| 138 | require.NoError(t, c.Dispatch(h)) | 129 | require.NoError(t, c.Dispatch(h)) | ||
| 139 | - | require.Empty(t, h.errs) | |||
| 140 | - | require.Equal(t, 1, h.done) | |||
| 141 | fb := s.Feedback() | 130 | fb := s.Feedback() | ||
| 142 | require.NotNil(t, fb) | 131 | require.NotNil(t, fb) | ||
| 143 | require.Equal(t, uint64(0xe280), fb.MainDevice) | 132 | require.Equal(t, uint64(0xe280), fb.MainDevice) | ||
| 144 | require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order") | 133 | require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order") | ||
| 145 | 134 | ||||
| 146 | // A second round replaces the first without leaking the first one's formats. | 135 | // A second round replaces the first without leaking the first one's formats. | ||
| 136 | + | h.EXPECT().FeedbackDone(s.id).Return().Once() | |||
| 147 | postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1) | 137 | postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1) | ||
| 148 | require.NoError(t, c.Dispatch(h)) | 138 | require.NoError(t, c.Dispatch(h)) | ||
| 149 | require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback()) | 139 | require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback()) | ||
| @@ -153,11 +143,11 @@ func TestFeedbackBadIndexAndSize(t *testing.T) { | |||||
| 153 | c, s := newOwnerConn() | 143 | c, s := newOwnerConn() | ||
| 154 | postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3) | 144 | postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3) | ||
| 155 | postRound(c, s, tableFD(t, nil), 15, 5) | 145 | postRound(c, s, tableFD(t, nil), 15, 5) | ||
| 156 | - | h := &feedbackHandler{} | 146 | + | h := NewMockHandler(t) |
| 147 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "out of range") })).Return().Once() | |||
| 148 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "invalid dmabuf format table size") })).Return().Once() | |||
| 149 | + | h.EXPECT().FeedbackDone(s.id).Return().Twice() | |||
| 157 | require.NoError(t, c.Dispatch(h)) | 150 | require.NoError(t, c.Dispatch(h)) | ||
| 158 | - | require.Len(t, h.errs, 2) | |||
| 159 | - | require.ErrorContains(t, h.errs[0], "out of range") | |||
| 160 | - | require.ErrorContains(t, h.errs[1], "invalid dmabuf format table size") | |||
| 161 | } | 151 | } | ||
| 162 | 152 | ||||
| 163 | func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil } | 153 | func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil } | ||
| @@ -198,6 +188,22 @@ func TestEventForClosedSurfaceDropsFD(t *testing.T) { | |||||
| 198 | require.False(t, fdOpen(fd)) | 188 | require.False(t, fdOpen(fd)) | ||
| 199 | } | 189 | } | ||
| 200 | 190 | ||||
| 191 | + | // GO-001: the descriptor is consumed by readTable; the event must not close | |||
| 192 | + | // the number a second time, which by then may belong to someone else. | |||
| 193 | + | func TestFeedbackTableFDNotClosedTwice(t *testing.T) { | |||
| 194 | + | c, s := newOwnerConn() | |||
| 195 | + | fd := tableFD(t, []Format{{1, 1}}) | |||
| 196 | + | ev := event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd), flags: 16} | |||
| 197 | + | c.applySurface(&ev, NewMockHandler(t)) | |||
| 198 | + | require.False(t, fdOpen(fd), "readTable consumed it") | |||
| 199 | + | require.Equal(t, int32(-1), ev.fd, "ownership left the event") | |||
| 200 | + | other := tableFD(t, nil) // the kernel hands out the freed number again | |||
| 201 | + | require.Equal(t, fd, other) | |||
| 202 | + | ev.closeFD() // what a second close of the event would do | |||
| 203 | + | require.True(t, fdOpen(other), "an unrelated descriptor that reuses the number stays open") | |||
| 204 | + | _ = unix.Close(other) | |||
| 205 | + | } | |||
| 206 | + | ||||
| 201 | func TestLockRules(t *testing.T) { | 207 | func TestLockRules(t *testing.T) { | ||
| 202 | c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}} | 208 | c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}} | ||
| 203 | l := &Lock{c: c, gen: 1} | 209 | l := &Lock{c: c, gen: 1} | ||
| @@ -215,3 +221,72 @@ func TestLockRules(t *testing.T) { | |||||
| 215 | require.False(t, l.Locked()) | 221 | require.False(t, l.Locked()) | ||
| 216 | require.ErrorContains(t, l.Unlock(), "before the locked event") | 222 | require.ErrorContains(t, l.Unlock(), "before the locked event") | ||
| 217 | } | 223 | } | ||
| 224 | + | ||||
| 225 | + | // GO-004: the buffer scale returns to 1 when the output scale drops, with no | |||
| 226 | + | // viewport to carry the size. | |||
| 227 | + | func TestPresentResetsBufferScale(t *testing.T) { | |||
| 228 | + | m := newMocksurfaceRequests(t) | |||
| 229 | + | s := presentable(m, 2) | |||
| 230 | + | s.viewport = nil | |||
| 231 | + | s.buffers[1] = importedBuffer{w: 200, h: 100} | |||
| 232 | + | m.EXPECT().setBufferScale(int32(2)).Return(nil).Once() | |||
| 233 | + | m.EXPECT().setBufferScale(int32(1)).Return(nil).Once() | |||
| 234 | + | m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice() | |||
| 235 | + | m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice() | |||
| 236 | + | m.EXPECT().attach(uint64(1)).Return(nil).Twice() | |||
| 237 | + | m.EXPECT().damageBuffer(mock.Anything).Return(nil).Twice() | |||
| 238 | + | m.EXPECT().frame().Return(nil).Twice() | |||
| 239 | + | m.EXPECT().commit().Return(nil).Twice() | |||
| 240 | + | p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2} | |||
| 241 | + | require.NoError(t, s.Present(p)) | |||
| 242 | + | s.scale, s.frameReady = 1, true | |||
| 243 | + | s.buffers[1] = importedBuffer{w: 100, h: 50} | |||
| 244 | + | require.NoError(t, s.Present(p)) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // GO-005: size mismatches, timeline point order and a nil request are refused | |||
| 248 | + | // before anything is sent (the mock is strict). | |||
| 249 | + | func TestPresentRefusesInconsistentRequests(t *testing.T) { | |||
| 250 | + | m := newMocksurfaceRequests(t) | |||
| 251 | + | good := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2} | |||
| 252 | + | for name, tc := range map[string]struct { | |||
| 253 | + | mutate func(*Surface) | |||
| 254 | + | p *Present | |||
| 255 | + | }{ | |||
| 256 | + | "nil": {func(*Surface) {}, nil}, | |||
| 257 | + | "mismatch no viewport": {func(s *Surface) { s.viewport = nil; s.buffers[1] = importedBuffer{w: 99, h: 50} }, good}, | |||
| 258 | + | "mismatch lock role": {func(s *Surface) { s.role = roleLock; s.buffers[1] = importedBuffer{w: 100, h: 49} }, good}, | |||
| 259 | + | "same timeline acquire not before release": {func(*Surface) {}, | |||
| 260 | + | &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 1, AcquirePoint: 5, ReleasePoint: 5}}, | |||
| 261 | + | } { | |||
| 262 | + | s := presentable(m, 1) | |||
| 263 | + | tc.mutate(s) | |||
| 264 | + | require.Error(t, s.Present(tc.p), name) | |||
| 265 | + | } | |||
| 266 | + | // With a viewport a different buffer size is legal (scaled by the compositor). | |||
| 267 | + | s := presentable(m, 1) | |||
| 268 | + | s.buffers[1] = importedBuffer{w: 50, h: 25} | |||
| 269 | + | m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once() | |||
| 270 | + | m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Once() | |||
| 271 | + | m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Once() | |||
| 272 | + | m.EXPECT().attach(uint64(1)).Return(nil).Once() | |||
| 273 | + | m.EXPECT().damageBuffer(mock.Anything).Return(nil).Once() | |||
| 274 | + | m.EXPECT().frame().Return(nil).Once() | |||
| 275 | + | m.EXPECT().commit().Return(nil).Once() | |||
| 276 | + | require.NoError(t, s.Present(good)) | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | // GO-008: lock and layer configure sizes share the bounds check. | |||
| 280 | + | func TestConfigureEventBounds(t *testing.T) { | |||
| 281 | + | for _, kind := range []eventKind{evLayerConfigure, evLockConfigure} { | |||
| 282 | + | ev := configureEvent(kind, 3, 9, maxLayerDimension, 1) | |||
| 283 | + | require.Equal(t, event{kind: kind, id: 3, serial: 9, a: maxLayerDimension, b: 1}, ev) | |||
| 284 | + | require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, maxLayerDimension+1, 1).kind) | |||
| 285 | + | require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, 1, 1<<31).kind, "no wrap to a negative int32") | |||
| 286 | + | } | |||
| 287 | + | c, _ := newOwnerConn() | |||
| 288 | + | c.q.post(&event{kind: evBadEvent, id: 1, a: badConfigure}) | |||
| 289 | + | h := NewMockHandler(t) | |||
| 290 | + | h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "configure size") })).Return().Once() | |||
| 291 | + | require.NoError(t, c.Dispatch(h)) | |||
| 292 | + | } | |||
A surface_wire_test.go +200 -0
| @@ -0,0 +1,200 @@ | |||
| 1 | + | package neferclient_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "os" | |
| 6 | + | "sync/atomic" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/neferclient" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | var surfaceGlobals = []wireGlobal{{1, "wl_compositor", 6}, {2, "zwp_linux_dmabuf_v1", 4}, | |
| 16 | + | {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}, | |
| 17 | + | {6, "ext_session_lock_manager_v1", 1}, {7, outputIface, 4}} | |
| 18 | + | ||
| 19 | + | var surfaceOutputs = map[uint32]outputSpec{7: {"DP-1", 1, 640, 480}} | |
| 20 | + | ||
| 21 | + | func openFDs(t *testing.T) int { | |
| 22 | + | t.Helper() | |
| 23 | + | ents, err := os.ReadDir("/proc/self/fd") | |
| 24 | + | require.NoError(t, err) | |
| 25 | + | return len(ents) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // GO-009 and GO-012: failure paths close each duplicated descriptor exactly | |
| 29 | + | // once, and descriptor 0 is never touched. | |
| 30 | + | func TestImportErrorPathsCloseDupsOnce(t *testing.T) { | |
| 31 | + | c, _ := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 32 | + | s, err := c.NewToplevel("fd", 64, 48) | |
| 33 | + | require.NoError(t, err) | |
| 34 | + | memfd, err := unix.MemfdCreate("fd", unix.MFD_CLOEXEC) | |
| 35 | + | require.NoError(t, err) | |
| 36 | + | defer unix.Close(memfd) | |
| 37 | + | _, err = unix.FcntlInt(0, unix.F_GETFD, 0) | |
| 38 | + | require.NoError(t, err, "stdin is open for the test") | |
| 39 | + | ||
| 40 | + | before := openFDs(t) | |
| 41 | + | // Plane 0 duplicates, plane 1 cannot: the first duplicate must be closed. | |
| 42 | + | err = s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 2, | |
| 43 | + | Planes: [4]neferclient.Plane{{FD: memfd}, {FD: 9999}}}) | |
| 44 | + | require.Error(t, err) | |
| 45 | + | require.Equal(t, before, openFDs(t), "no descriptor leaked") | |
| 46 | + | _, err = unix.FcntlInt(0, unix.F_GETFD, 0) | |
| 47 | + | require.NoError(t, err, "descriptor 0 untouched") | |
| 48 | + | require.Error(t, s.ImportTimeline(1, 9999)) | |
| 49 | + | require.Equal(t, before, openFDs(t)) | |
| 50 | + | ||
| 51 | + | // Success paths: the request consumes the duplicates. | |
| 52 | + | require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |
| 53 | + | Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}})) | |
| 54 | + | require.NoError(t, s.ImportTimeline(1, memfd)) | |
| 55 | + | require.Equal(t, before, openFDs(t), "sent descriptors are closed, the caller's stays open") | |
| 56 | + | require.Error(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |
| 57 | + | Planes: [4]neferclient.Plane{{FD: memfd}}}), "duplicate id") | |
| 58 | + | require.Equal(t, before, openFDs(t)) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // requestLog counts the commits the wire peer sees. | |
| 62 | + | type requestLog struct{ commits atomic.Int32 } | |
| 63 | + | ||
| 64 | + | // GO-003: before the first Present the region is staged without a commit; a | |
| 65 | + | // lock surface refuses it. | |
| 66 | + | func TestSetInputRegionCommitsOnlyOnceMapped(t *testing.T) { | |
| 67 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 68 | + | s, err := c.NewToplevel("input", 64, 48) | |
| 69 | + | require.NoError(t, err) | |
| 70 | + | require.NoError(t, c.Roundtrip()) // the peer consumed the initial commit | |
| 71 | + | var log requestLog | |
| 72 | + | surfaceID := s.SurfaceObjectID() | |
| 73 | + | srv.mu.Lock() | |
| 74 | + | srv.hook = func(obj uint32, op uint16, _ []byte) { | |
| 75 | + | if obj == surfaceID && op == 6 { | |
| 76 | + | log.commits.Add(1) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | srv.mu.Unlock() | |
| 80 | + | s.MarkPresentable(64, 48) | |
| 81 | + | ||
| 82 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 4, Height: 4}})) | |
| 83 | + | require.NoError(t, c.Roundtrip()) | |
| 84 | + | require.Zero(t, log.commits.Load(), "unmapped surface: staged, not committed") | |
| 85 | + | ||
| 86 | + | s.MarkPresented() | |
| 87 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}})) | |
| 88 | + | require.NoError(t, c.Roundtrip()) | |
| 89 | + | require.Equal(t, int32(1), log.commits.Load(), "mapped surface: committed") | |
| 90 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}})) | |
| 91 | + | require.NoError(t, c.Roundtrip()) | |
| 92 | + | require.Equal(t, int32(1), log.commits.Load(), "unchanged region sends nothing") | |
| 93 | + | ||
| 94 | + | l, err := c.Lock() | |
| 95 | + | require.NoError(t, err) | |
| 96 | + | ls, err := l.NewSurface(7) | |
| 97 | + | require.NoError(t, err) | |
| 98 | + | require.Error(t, ls.SetInputRegion(nil), "lock surfaces take all input") | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // GO-007: one lock surface per output. | |
| 102 | + | func TestLockRefusesSecondSurfacePerOutput(t *testing.T) { | |
| 103 | + | c, _ := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 104 | + | l, err := c.Lock() | |
| 105 | + | require.NoError(t, err) | |
| 106 | + | first, err := l.NewSurface(7) | |
| 107 | + | require.NoError(t, err) | |
| 108 | + | _, err = l.NewSurface(7) | |
| 109 | + | require.ErrorContains(t, err, "already has a lock surface") | |
| 110 | + | require.NoError(t, first.Close()) | |
| 111 | + | again, err := l.NewSurface(7) | |
| 112 | + | require.NoError(t, err, "after closing the first one the output is free again") | |
| 113 | + | require.NoError(t, again.Close()) | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // watchLock records the ext_session_lock_v1 id the peer saw requested and | |
| 117 | + | // counts its destroy requests. | |
| 118 | + | func watchLock(t *testing.T, srv *wireServer) (id func() uint32, destroys *atomic.Int32) { | |
| 119 | + | t.Helper() | |
| 120 | + | var lockID atomic.Uint32 | |
| 121 | + | destroys = new(atomic.Int32) | |
| 122 | + | srv.mu.Lock() | |
| 123 | + | srv.hook = func(obj uint32, op uint16, body []byte) { | |
| 124 | + | srv.mu.Lock() | |
| 125 | + | manager := srv.bound[6].id // the lock manager is bound by the first Lock call | |
| 126 | + | srv.mu.Unlock() | |
| 127 | + | switch { | |
| 128 | + | case obj == manager && op == 1 && len(body) == 4: // lock(new_id) | |
| 129 | + | lockID.Store(binary.LittleEndian.Uint32(body)) | |
| 130 | + | case obj == lockID.Load() && obj != 0 && op == 0: // destroy | |
| 131 | + | destroys.Add(1) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | srv.mu.Unlock() | |
| 135 | + | return lockID.Load, destroys | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // GO-006: a locked event already on the wire when Close runs wins: destroying | |
| 139 | + | // a locked lock is a protocol error, so Close reports it instead. | |
| 140 | + | func TestLockCloseSeesLockedInFlight(t *testing.T) { | |
| 141 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 142 | + | lockID, destroys := watchLock(t, srv) | |
| 143 | + | ||
| 144 | + | l, err := c.Lock() | |
| 145 | + | require.NoError(t, err) | |
| 146 | + | require.NoError(t, c.Roundtrip()) // the peer saw the lock request | |
| 147 | + | srv.write(frame(lockID(), 0, nil)) // ext_session_lock_v1.locked, not yet dispatched | |
| 148 | + | ||
| 149 | + | require.ErrorContains(t, l.Close(), "use Unlock") | |
| 150 | + | require.Zero(t, destroys.Load(), "no destroy request was sent") | |
| 151 | + | require.True(t, l.Locked(), "treated as locked") | |
| 152 | + | require.NoError(t, c.Roundtrip()) | |
| 153 | + | require.Zero(t, destroys.Load()) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func TestLockCloseDestroysUnlockedLock(t *testing.T) { | |
| 157 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 158 | + | _, destroys := watchLock(t, srv) | |
| 159 | + | l, err := c.Lock() | |
| 160 | + | require.NoError(t, err) | |
| 161 | + | require.NoError(t, l.Close()) | |
| 162 | + | require.NoError(t, c.Roundtrip()) | |
| 163 | + | require.Equal(t, int32(1), destroys.Load()) | |
| 164 | + | require.NoError(t, l.Close(), "idempotent") | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | // GO-002 reproduction, enabled once wlturbo handles delete_id for objects the | |
| 168 | + | // server creates: closing a surface with a pending frame callback must not turn | |
| 169 | + | // the compositor's later callback done and delete_id into a transport error. | |
| 170 | + | func TestCloseWithPendingFrame(t *testing.T) { | |
| 171 | + | t.Skip("needs wlturbo server-side delete_id handling") | |
| 172 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |
| 173 | + | s, err := c.NewToplevel("close", 64, 48) | |
| 174 | + | require.NoError(t, err) | |
| 175 | + | s.MarkPresentable(64, 48) | |
| 176 | + | surfaceID := s.SurfaceObjectID() | |
| 177 | + | var callback atomic.Uint32 | |
| 178 | + | srv.mu.Lock() | |
| 179 | + | srv.hook = func(obj uint32, op uint16, body []byte) { | |
| 180 | + | if obj == surfaceID && op == 3 && len(body) == 4 { | |
| 181 | + | callback.Store(binary.LittleEndian.Uint32(body)) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | srv.mu.Unlock() | |
| 185 | + | memfd, err := unix.MemfdCreate("close", unix.MFD_CLOEXEC) | |
| 186 | + | require.NoError(t, err) | |
| 187 | + | defer unix.Close(memfd) | |
| 188 | + | require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |
| 189 | + | Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}})) | |
| 190 | + | require.NoError(t, s.ImportTimeline(1, memfd)) | |
| 191 | + | require.NoError(t, s.ImportTimeline(2, memfd)) | |
| 192 | + | require.NoError(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 1, ReleasePoint: 2})) | |
| 193 | + | require.NoError(t, c.Roundtrip()) | |
| 194 | + | require.NoError(t, s.Close()) | |
| 195 | + | ||
| 196 | + | srv.write(frame(callback.Load(), 0, appendU32(nil, 1))) | |
| 197 | + | srv.write(frame(1, 1, appendU32(nil, callback.Load()))) | |
| 198 | + | require.NoError(t, c.Roundtrip()) | |
| 199 | + | require.NoError(t, c.Dispatch(nil)) | |
| 200 | + | } | |
| @@ -0,0 +1,200 @@ | |||||
| 1 | + | package neferclient_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "os" | |||
| 6 | + | "sync/atomic" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/neferclient" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | var surfaceGlobals = []wireGlobal{{1, "wl_compositor", 6}, {2, "zwp_linux_dmabuf_v1", 4}, | |||
| 16 | + | {3, "wp_linux_drm_syncobj_manager_v1", 1}, {4, "wp_viewporter", 1}, {5, "xdg_wm_base", 6}, | |||
| 17 | + | {6, "ext_session_lock_manager_v1", 1}, {7, outputIface, 4}} | |||
| 18 | + | ||||
| 19 | + | var surfaceOutputs = map[uint32]outputSpec{7: {"DP-1", 1, 640, 480}} | |||
| 20 | + | ||||
| 21 | + | func openFDs(t *testing.T) int { | |||
| 22 | + | t.Helper() | |||
| 23 | + | ents, err := os.ReadDir("/proc/self/fd") | |||
| 24 | + | require.NoError(t, err) | |||
| 25 | + | return len(ents) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // GO-009 and GO-012: failure paths close each duplicated descriptor exactly | |||
| 29 | + | // once, and descriptor 0 is never touched. | |||
| 30 | + | func TestImportErrorPathsCloseDupsOnce(t *testing.T) { | |||
| 31 | + | c, _ := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 32 | + | s, err := c.NewToplevel("fd", 64, 48) | |||
| 33 | + | require.NoError(t, err) | |||
| 34 | + | memfd, err := unix.MemfdCreate("fd", unix.MFD_CLOEXEC) | |||
| 35 | + | require.NoError(t, err) | |||
| 36 | + | defer unix.Close(memfd) | |||
| 37 | + | _, err = unix.FcntlInt(0, unix.F_GETFD, 0) | |||
| 38 | + | require.NoError(t, err, "stdin is open for the test") | |||
| 39 | + | ||||
| 40 | + | before := openFDs(t) | |||
| 41 | + | // Plane 0 duplicates, plane 1 cannot: the first duplicate must be closed. | |||
| 42 | + | err = s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 2, | |||
| 43 | + | Planes: [4]neferclient.Plane{{FD: memfd}, {FD: 9999}}}) | |||
| 44 | + | require.Error(t, err) | |||
| 45 | + | require.Equal(t, before, openFDs(t), "no descriptor leaked") | |||
| 46 | + | _, err = unix.FcntlInt(0, unix.F_GETFD, 0) | |||
| 47 | + | require.NoError(t, err, "descriptor 0 untouched") | |||
| 48 | + | require.Error(t, s.ImportTimeline(1, 9999)) | |||
| 49 | + | require.Equal(t, before, openFDs(t)) | |||
| 50 | + | ||||
| 51 | + | // Success paths: the request consumes the duplicates. | |||
| 52 | + | require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |||
| 53 | + | Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}})) | |||
| 54 | + | require.NoError(t, s.ImportTimeline(1, memfd)) | |||
| 55 | + | require.Equal(t, before, openFDs(t), "sent descriptors are closed, the caller's stays open") | |||
| 56 | + | require.Error(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |||
| 57 | + | Planes: [4]neferclient.Plane{{FD: memfd}}}), "duplicate id") | |||
| 58 | + | require.Equal(t, before, openFDs(t)) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // requestLog counts the commits the wire peer sees. | |||
| 62 | + | type requestLog struct{ commits atomic.Int32 } | |||
| 63 | + | ||||
| 64 | + | // GO-003: before the first Present the region is staged without a commit; a | |||
| 65 | + | // lock surface refuses it. | |||
| 66 | + | func TestSetInputRegionCommitsOnlyOnceMapped(t *testing.T) { | |||
| 67 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 68 | + | s, err := c.NewToplevel("input", 64, 48) | |||
| 69 | + | require.NoError(t, err) | |||
| 70 | + | require.NoError(t, c.Roundtrip()) // the peer consumed the initial commit | |||
| 71 | + | var log requestLog | |||
| 72 | + | surfaceID := s.SurfaceObjectID() | |||
| 73 | + | srv.mu.Lock() | |||
| 74 | + | srv.hook = func(obj uint32, op uint16, _ []byte) { | |||
| 75 | + | if obj == surfaceID && op == 6 { | |||
| 76 | + | log.commits.Add(1) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | srv.mu.Unlock() | |||
| 80 | + | s.MarkPresentable(64, 48) | |||
| 81 | + | ||||
| 82 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 0, Y: 0, Width: 4, Height: 4}})) | |||
| 83 | + | require.NoError(t, c.Roundtrip()) | |||
| 84 | + | require.Zero(t, log.commits.Load(), "unmapped surface: staged, not committed") | |||
| 85 | + | ||||
| 86 | + | s.MarkPresented() | |||
| 87 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}})) | |||
| 88 | + | require.NoError(t, c.Roundtrip()) | |||
| 89 | + | require.Equal(t, int32(1), log.commits.Load(), "mapped surface: committed") | |||
| 90 | + | require.NoError(t, s.SetInputRegion([]neferclient.Rect{{X: 1, Y: 1, Width: 4, Height: 4}})) | |||
| 91 | + | require.NoError(t, c.Roundtrip()) | |||
| 92 | + | require.Equal(t, int32(1), log.commits.Load(), "unchanged region sends nothing") | |||
| 93 | + | ||||
| 94 | + | l, err := c.Lock() | |||
| 95 | + | require.NoError(t, err) | |||
| 96 | + | ls, err := l.NewSurface(7) | |||
| 97 | + | require.NoError(t, err) | |||
| 98 | + | require.Error(t, ls.SetInputRegion(nil), "lock surfaces take all input") | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // GO-007: one lock surface per output. | |||
| 102 | + | func TestLockRefusesSecondSurfacePerOutput(t *testing.T) { | |||
| 103 | + | c, _ := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 104 | + | l, err := c.Lock() | |||
| 105 | + | require.NoError(t, err) | |||
| 106 | + | first, err := l.NewSurface(7) | |||
| 107 | + | require.NoError(t, err) | |||
| 108 | + | _, err = l.NewSurface(7) | |||
| 109 | + | require.ErrorContains(t, err, "already has a lock surface") | |||
| 110 | + | require.NoError(t, first.Close()) | |||
| 111 | + | again, err := l.NewSurface(7) | |||
| 112 | + | require.NoError(t, err, "after closing the first one the output is free again") | |||
| 113 | + | require.NoError(t, again.Close()) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // watchLock records the ext_session_lock_v1 id the peer saw requested and | |||
| 117 | + | // counts its destroy requests. | |||
| 118 | + | func watchLock(t *testing.T, srv *wireServer) (id func() uint32, destroys *atomic.Int32) { | |||
| 119 | + | t.Helper() | |||
| 120 | + | var lockID atomic.Uint32 | |||
| 121 | + | destroys = new(atomic.Int32) | |||
| 122 | + | srv.mu.Lock() | |||
| 123 | + | srv.hook = func(obj uint32, op uint16, body []byte) { | |||
| 124 | + | srv.mu.Lock() | |||
| 125 | + | manager := srv.bound[6].id // the lock manager is bound by the first Lock call | |||
| 126 | + | srv.mu.Unlock() | |||
| 127 | + | switch { | |||
| 128 | + | case obj == manager && op == 1 && len(body) == 4: // lock(new_id) | |||
| 129 | + | lockID.Store(binary.LittleEndian.Uint32(body)) | |||
| 130 | + | case obj == lockID.Load() && obj != 0 && op == 0: // destroy | |||
| 131 | + | destroys.Add(1) | |||
| 132 | + | } | |||
| 133 | + | } | |||
| 134 | + | srv.mu.Unlock() | |||
| 135 | + | return lockID.Load, destroys | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // GO-006: a locked event already on the wire when Close runs wins: destroying | |||
| 139 | + | // a locked lock is a protocol error, so Close reports it instead. | |||
| 140 | + | func TestLockCloseSeesLockedInFlight(t *testing.T) { | |||
| 141 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 142 | + | lockID, destroys := watchLock(t, srv) | |||
| 143 | + | ||||
| 144 | + | l, err := c.Lock() | |||
| 145 | + | require.NoError(t, err) | |||
| 146 | + | require.NoError(t, c.Roundtrip()) // the peer saw the lock request | |||
| 147 | + | srv.write(frame(lockID(), 0, nil)) // ext_session_lock_v1.locked, not yet dispatched | |||
| 148 | + | ||||
| 149 | + | require.ErrorContains(t, l.Close(), "use Unlock") | |||
| 150 | + | require.Zero(t, destroys.Load(), "no destroy request was sent") | |||
| 151 | + | require.True(t, l.Locked(), "treated as locked") | |||
| 152 | + | require.NoError(t, c.Roundtrip()) | |||
| 153 | + | require.Zero(t, destroys.Load()) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func TestLockCloseDestroysUnlockedLock(t *testing.T) { | |||
| 157 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 158 | + | _, destroys := watchLock(t, srv) | |||
| 159 | + | l, err := c.Lock() | |||
| 160 | + | require.NoError(t, err) | |||
| 161 | + | require.NoError(t, l.Close()) | |||
| 162 | + | require.NoError(t, c.Roundtrip()) | |||
| 163 | + | require.Equal(t, int32(1), destroys.Load()) | |||
| 164 | + | require.NoError(t, l.Close(), "idempotent") | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | // GO-002 reproduction, enabled once wlturbo handles delete_id for objects the | |||
| 168 | + | // server creates: closing a surface with a pending frame callback must not turn | |||
| 169 | + | // the compositor's later callback done and delete_id into a transport error. | |||
| 170 | + | func TestCloseWithPendingFrame(t *testing.T) { | |||
| 171 | + | t.Skip("needs wlturbo server-side delete_id handling") | |||
| 172 | + | c, srv := connectWire(t, surfaceGlobals, surfaceOutputs) | |||
| 173 | + | s, err := c.NewToplevel("close", 64, 48) | |||
| 174 | + | require.NoError(t, err) | |||
| 175 | + | s.MarkPresentable(64, 48) | |||
| 176 | + | surfaceID := s.SurfaceObjectID() | |||
| 177 | + | var callback atomic.Uint32 | |||
| 178 | + | srv.mu.Lock() | |||
| 179 | + | srv.hook = func(obj uint32, op uint16, body []byte) { | |||
| 180 | + | if obj == surfaceID && op == 3 && len(body) == 4 { | |||
| 181 | + | callback.Store(binary.LittleEndian.Uint32(body)) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | srv.mu.Unlock() | |||
| 185 | + | memfd, err := unix.MemfdCreate("close", unix.MFD_CLOEXEC) | |||
| 186 | + | require.NoError(t, err) | |||
| 187 | + | defer unix.Close(memfd) | |||
| 188 | + | require.NoError(t, s.ImportBuffer(1, &neferclient.Buffer{Width: 64, Height: 48, FourCC: 1, PlaneCount: 1, | |||
| 189 | + | Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}})) | |||
| 190 | + | require.NoError(t, s.ImportTimeline(1, memfd)) | |||
| 191 | + | require.NoError(t, s.ImportTimeline(2, memfd)) | |||
| 192 | + | require.NoError(t, s.Present(&neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 1, ReleasePoint: 2})) | |||
| 193 | + | require.NoError(t, c.Roundtrip()) | |||
| 194 | + | require.NoError(t, s.Close()) | |||
| 195 | + | ||||
| 196 | + | srv.write(frame(callback.Load(), 0, appendU32(nil, 1))) | |||
| 197 | + | srv.write(frame(1, 1, appendU32(nil, callback.Load()))) | |||
| 198 | + | require.NoError(t, c.Roundtrip()) | |||
| 199 | + | require.NoError(t, c.Dispatch(nil)) | |||
| 200 | + | } | |||
M wire_test.go +4 -7
| @@ -138,7 +138,11 @@ func (s *wireServer) serve() { | |||
| 138 | 138 | func (s *wireServer) handle(obj uint32, op uint16, body []byte) { | |
| 139 | 139 | s.mu.Lock() | |
| 140 | 140 | reg := s.registry | |
| 141 | + | hook := s.hook | |
| 141 | 142 | s.mu.Unlock() | |
| 143 | + | if hook != nil && obj != 1 && obj != reg { | |
| 144 | + | hook(obj, op, body) // every request on a client-created or bound object | |
| 145 | + | } | |
| 142 | 146 | switch { | |
| 143 | 147 | case obj == 1 && op == 1: // get_registry | |
| 144 | 148 | id := binary.LittleEndian.Uint32(body) | |
| @@ -172,13 +176,6 @@ func (s *wireServer) handle(obj uint32, op uint16, body []byte) { | |||
| 172 | 176 | } | |
| 173 | 177 | case op == 0 && len(body) == 0: // wl_output.release (destructor, no args) | |
| 174 | 178 | s.write(frame(1, 1, appendU32(nil, obj))) | |
| 175 | - | default: | |
| 176 | - | s.mu.Lock() | |
| 177 | - | hook := s.hook | |
| 178 | - | s.mu.Unlock() | |
| 179 | - | if hook != nil { | |
| 180 | - | hook(obj, op, body) | |
| 181 | - | } | |
| 182 | 179 | } | |
| 183 | 180 | } | |
| 184 | 181 | ||
| @@ -138,7 +138,11 @@ func (s *wireServer) serve() { | |||||
| 138 | func (s *wireServer) handle(obj uint32, op uint16, body []byte) { | 138 | func (s *wireServer) handle(obj uint32, op uint16, body []byte) { | ||
| 139 | s.mu.Lock() | 139 | s.mu.Lock() | ||
| 140 | reg := s.registry | 140 | reg := s.registry | ||
| 141 | + | hook := s.hook | |||
| 141 | s.mu.Unlock() | 142 | s.mu.Unlock() | ||
| 143 | + | if hook != nil && obj != 1 && obj != reg { | |||
| 144 | + | hook(obj, op, body) // every request on a client-created or bound object | |||
| 145 | + | } | |||
| 142 | switch { | 146 | switch { | ||
| 143 | case obj == 1 && op == 1: // get_registry | 147 | case obj == 1 && op == 1: // get_registry | ||
| 144 | id := binary.LittleEndian.Uint32(body) | 148 | id := binary.LittleEndian.Uint32(body) | ||
| @@ -172,13 +176,6 @@ func (s *wireServer) handle(obj uint32, op uint16, body []byte) { | |||||
| 172 | } | 176 | } | ||
| 173 | case op == 0 && len(body) == 0: // wl_output.release (destructor, no args) | 177 | case op == 0 && len(body) == 0: // wl_output.release (destructor, no args) | ||
| 174 | s.write(frame(1, 1, appendU32(nil, obj))) | 178 | s.write(frame(1, 1, appendU32(nil, obj))) | ||
| 175 | - | default: | |||
| 176 | - | s.mu.Lock() | |||
| 177 | - | hook := s.hook | |||
| 178 | - | s.mu.Unlock() | |||
| 179 | - | if hook != nil { | |||
| 180 | - | hook(obj, op, body) | |||
| 181 | - | } | |||
| 182 | } | 179 | } | ||
| 183 | } | 180 | } | ||
| 184 | 181 | ||||