perf(wayland): wait for explicit-sync acquire points on the display loop
Explicit acquire eventfds are now display-loop sources (Display.AddFD). A fired point no longer costs a pipe write to the syncWaiter goroutine, its poll wakeup, a Display.Post to the display goroutine and a useless cancel pipe write: the callback runs on the display goroutine and ticks the pacer inline. The waiter goroutine and pipe remain for implicit dma-buf fences only.
- commit
- d8d545e749e04242f58edb6ff66a57839003502f
- author
- brice · · 2026-10-22 18:36 UTC
- parent
- fdd661a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +465 -18
M .mockery.yml +10 -0
| @@ -87,6 +87,16 @@ packages: | |||
| 87 | 87 | dir: internal/adapters/wayland | |
| 88 | 88 | pkgname: wayland | |
| 89 | 89 | filename: acquire_poller_mock_test.go | |
| 90 | + | acquireSource: | |
| 91 | + | config: | |
| 92 | + | dir: internal/adapters/wayland | |
| 93 | + | pkgname: wayland | |
| 94 | + | filename: acquire_source_mock_test.go | |
| 95 | + | acquireRemover: | |
| 96 | + | config: | |
| 97 | + | dir: internal/adapters/wayland | |
| 98 | + | pkgname: wayland | |
| 99 | + | filename: acquire_remover_mock_test.go | |
| 90 | 100 | syncobjDevice: | |
| 91 | 101 | config: | |
| 92 | 102 | dir: internal/adapters/wayland | |
| @@ -87,6 +87,16 @@ packages: | |||||
| 87 | dir: internal/adapters/wayland | 87 | dir: internal/adapters/wayland | ||
| 88 | pkgname: wayland | 88 | pkgname: wayland | ||
| 89 | filename: acquire_poller_mock_test.go | 89 | filename: acquire_poller_mock_test.go | ||
| 90 | + | acquireSource: | |||
| 91 | + | config: | |||
| 92 | + | dir: internal/adapters/wayland | |||
| 93 | + | pkgname: wayland | |||
| 94 | + | filename: acquire_source_mock_test.go | |||
| 95 | + | acquireRemover: | |||
| 96 | + | config: | |||
| 97 | + | dir: internal/adapters/wayland | |||
| 98 | + | pkgname: wayland | |||
| 99 | + | filename: acquire_remover_mock_test.go | |||
| 90 | syncobjDevice: | 100 | syncobjDevice: | ||
| 91 | config: | 101 | config: | ||
| 92 | dir: internal/adapters/wayland | 102 | dir: internal/adapters/wayland | ||
M internal/adapters/wayland/acquire_failure_test.go +0 -4
| @@ -197,10 +197,6 @@ func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) { | |||
| 197 | 197 | if !sw.stopped() { | |
| 198 | 198 | t.Fatal("waiter not terminal") | |
| 199 | 199 | } | |
| 200 | - | dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure | |
| 201 | - | if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed { | |
| 202 | - | t.Fatal("new explicit watch accepted") | |
| 203 | - | } | |
| 204 | 200 | var waits [4]*syncWait | |
| 205 | 201 | if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil { | |
| 206 | 202 | t.Fatal("new implicit watch accepted") | |
| @@ -197,10 +197,6 @@ func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) { | |||||
| 197 | if !sw.stopped() { | 197 | if !sw.stopped() { | ||
| 198 | t.Fatal("waiter not terminal") | 198 | t.Fatal("waiter not terminal") | ||
| 199 | } | 199 | } | ||
| 200 | - | dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure | |||
| 201 | - | if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed { | |||
| 202 | - | t.Fatal("new explicit watch accepted") | |||
| 203 | - | } | |||
| 204 | var waits [4]*syncWait | 200 | var waits [4]*syncWait | ||
| 205 | if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil { | 201 | if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil { | ||
| 206 | t.Fatal("new implicit watch accepted") | 202 | t.Fatal("new implicit watch accepted") | ||
A internal/adapters/wayland/acquire_remover_mock_test.go +78 -0
| @@ -0,0 +1,78 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package wayland | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockacquireRemover creates a new instance of mockacquireRemover. 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 newMockacquireRemover(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockacquireRemover { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockacquireRemover{} | |
| 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 | + | // mockacquireRemover is an autogenerated mock type for the acquireRemover type | |
| 35 | + | type mockacquireRemover struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockacquireRemover_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockacquireRemover) EXPECT() *mockacquireRemover_Expecter { | |
| 44 | + | return &mockacquireRemover_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Remove provides a mock function for the type mockacquireRemover | |
| 48 | + | func (_mock *mockacquireRemover) Remove() { | |
| 49 | + | _mock.Called() | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockacquireRemover_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |
| 54 | + | type mockacquireRemover_Remove_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Remove is a helper method to define mock.On call | |
| 59 | + | func (_e *mockacquireRemover_Expecter) Remove() *mockacquireRemover_Remove_Call { | |
| 60 | + | return &mockacquireRemover_Remove_Call{Call: _e.mock.On("Remove")} | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (_c *mockacquireRemover_Remove_Call) Run(run func()) *mockacquireRemover_Remove_Call { | |
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 65 | + | run() | |
| 66 | + | }) | |
| 67 | + | return _c | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *mockacquireRemover_Remove_Call) Return() *mockacquireRemover_Remove_Call { | |
| 71 | + | _c.Call.Return() | |
| 72 | + | return _c | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (_c *mockacquireRemover_Remove_Call) RunAndReturn(run func()) *mockacquireRemover_Remove_Call { | |
| 76 | + | _c.Run(run) | |
| 77 | + | return _c | |
| 78 | + | } | |
| @@ -0,0 +1,78 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package wayland | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockacquireRemover creates a new instance of mockacquireRemover. 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 newMockacquireRemover(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockacquireRemover { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockacquireRemover{} | |||
| 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 | + | // mockacquireRemover is an autogenerated mock type for the acquireRemover type | |||
| 35 | + | type mockacquireRemover struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockacquireRemover_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockacquireRemover) EXPECT() *mockacquireRemover_Expecter { | |||
| 44 | + | return &mockacquireRemover_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // Remove provides a mock function for the type mockacquireRemover | |||
| 48 | + | func (_mock *mockacquireRemover) Remove() { | |||
| 49 | + | _mock.Called() | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // mockacquireRemover_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |||
| 54 | + | type mockacquireRemover_Remove_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // Remove is a helper method to define mock.On call | |||
| 59 | + | func (_e *mockacquireRemover_Expecter) Remove() *mockacquireRemover_Remove_Call { | |||
| 60 | + | return &mockacquireRemover_Remove_Call{Call: _e.mock.On("Remove")} | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func (_c *mockacquireRemover_Remove_Call) Run(run func()) *mockacquireRemover_Remove_Call { | |||
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 65 | + | run() | |||
| 66 | + | }) | |||
| 67 | + | return _c | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (_c *mockacquireRemover_Remove_Call) Return() *mockacquireRemover_Remove_Call { | |||
| 71 | + | _c.Call.Return() | |||
| 72 | + | return _c | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (_c *mockacquireRemover_Remove_Call) RunAndReturn(run func()) *mockacquireRemover_Remove_Call { | |||
| 76 | + | _c.Run(run) | |||
| 77 | + | return _c | |||
| 78 | + | } | |||
A internal/adapters/wayland/acquire_source_mock_test.go +113 -0
| @@ -0,0 +1,113 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package wayland | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockacquireSource creates a new instance of mockacquireSource. 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 newMockacquireSource(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockacquireSource { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockacquireSource{} | |
| 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 | + | // mockacquireSource is an autogenerated mock type for the acquireSource type | |
| 35 | + | type mockacquireSource struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockacquireSource_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockacquireSource) EXPECT() *mockacquireSource_Expecter { | |
| 44 | + | return &mockacquireSource_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // add provides a mock function for the type mockacquireSource | |
| 48 | + | func (_mock *mockacquireSource) add(fd int, onReadable func()) (acquireRemover, error) { | |
| 49 | + | ret := _mock.Called(fd, onReadable) | |
| 50 | + | ||
| 51 | + | if len(ret) == 0 { | |
| 52 | + | panic("no return value specified for add") | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | var r0 acquireRemover | |
| 56 | + | var r1 error | |
| 57 | + | if returnFunc, ok := ret.Get(0).(func(int, func()) (acquireRemover, error)); ok { | |
| 58 | + | return returnFunc(fd, onReadable) | |
| 59 | + | } | |
| 60 | + | if returnFunc, ok := ret.Get(0).(func(int, func()) acquireRemover); ok { | |
| 61 | + | r0 = returnFunc(fd, onReadable) | |
| 62 | + | } else { | |
| 63 | + | if ret.Get(0) != nil { | |
| 64 | + | r0 = ret.Get(0).(acquireRemover) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | if returnFunc, ok := ret.Get(1).(func(int, func()) error); ok { | |
| 68 | + | r1 = returnFunc(fd, onReadable) | |
| 69 | + | } else { | |
| 70 | + | r1 = ret.Error(1) | |
| 71 | + | } | |
| 72 | + | return r0, r1 | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // mockacquireSource_add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'add' | |
| 76 | + | type mockacquireSource_add_Call struct { | |
| 77 | + | *mock.Call | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // add is a helper method to define mock.On call | |
| 81 | + | // - fd int | |
| 82 | + | // - onReadable func() | |
| 83 | + | func (_e *mockacquireSource_Expecter) add(fd any, onReadable any) *mockacquireSource_add_Call { | |
| 84 | + | return &mockacquireSource_add_Call{Call: _e.mock.On("add", fd, onReadable)} | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (_c *mockacquireSource_add_Call) Run(run func(fd int, onReadable func())) *mockacquireSource_add_Call { | |
| 88 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 89 | + | var arg0 int | |
| 90 | + | if args[0] != nil { | |
| 91 | + | arg0 = args[0].(int) | |
| 92 | + | } | |
| 93 | + | var arg1 func() | |
| 94 | + | if args[1] != nil { | |
| 95 | + | arg1 = args[1].(func()) | |
| 96 | + | } | |
| 97 | + | run( | |
| 98 | + | arg0, | |
| 99 | + | arg1, | |
| 100 | + | ) | |
| 101 | + | }) | |
| 102 | + | return _c | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (_c *mockacquireSource_add_Call) Return(acquireRemoverMoqParam acquireRemover, err error) *mockacquireSource_add_Call { | |
| 106 | + | _c.Call.Return(acquireRemoverMoqParam, err) | |
| 107 | + | return _c | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | func (_c *mockacquireSource_add_Call) RunAndReturn(run func(fd int, onReadable func()) (acquireRemover, error)) *mockacquireSource_add_Call { | |
| 111 | + | _c.Call.Return(run) | |
| 112 | + | return _c | |
| 113 | + | } | |
| @@ -0,0 +1,113 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package wayland | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockacquireSource creates a new instance of mockacquireSource. 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 newMockacquireSource(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockacquireSource { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockacquireSource{} | |||
| 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 | + | // mockacquireSource is an autogenerated mock type for the acquireSource type | |||
| 35 | + | type mockacquireSource struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockacquireSource_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockacquireSource) EXPECT() *mockacquireSource_Expecter { | |||
| 44 | + | return &mockacquireSource_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // add provides a mock function for the type mockacquireSource | |||
| 48 | + | func (_mock *mockacquireSource) add(fd int, onReadable func()) (acquireRemover, error) { | |||
| 49 | + | ret := _mock.Called(fd, onReadable) | |||
| 50 | + | ||||
| 51 | + | if len(ret) == 0 { | |||
| 52 | + | panic("no return value specified for add") | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | var r0 acquireRemover | |||
| 56 | + | var r1 error | |||
| 57 | + | if returnFunc, ok := ret.Get(0).(func(int, func()) (acquireRemover, error)); ok { | |||
| 58 | + | return returnFunc(fd, onReadable) | |||
| 59 | + | } | |||
| 60 | + | if returnFunc, ok := ret.Get(0).(func(int, func()) acquireRemover); ok { | |||
| 61 | + | r0 = returnFunc(fd, onReadable) | |||
| 62 | + | } else { | |||
| 63 | + | if ret.Get(0) != nil { | |||
| 64 | + | r0 = ret.Get(0).(acquireRemover) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | if returnFunc, ok := ret.Get(1).(func(int, func()) error); ok { | |||
| 68 | + | r1 = returnFunc(fd, onReadable) | |||
| 69 | + | } else { | |||
| 70 | + | r1 = ret.Error(1) | |||
| 71 | + | } | |||
| 72 | + | return r0, r1 | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // mockacquireSource_add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'add' | |||
| 76 | + | type mockacquireSource_add_Call struct { | |||
| 77 | + | *mock.Call | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // add is a helper method to define mock.On call | |||
| 81 | + | // - fd int | |||
| 82 | + | // - onReadable func() | |||
| 83 | + | func (_e *mockacquireSource_Expecter) add(fd any, onReadable any) *mockacquireSource_add_Call { | |||
| 84 | + | return &mockacquireSource_add_Call{Call: _e.mock.On("add", fd, onReadable)} | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func (_c *mockacquireSource_add_Call) Run(run func(fd int, onReadable func())) *mockacquireSource_add_Call { | |||
| 88 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 89 | + | var arg0 int | |||
| 90 | + | if args[0] != nil { | |||
| 91 | + | arg0 = args[0].(int) | |||
| 92 | + | } | |||
| 93 | + | var arg1 func() | |||
| 94 | + | if args[1] != nil { | |||
| 95 | + | arg1 = args[1].(func()) | |||
| 96 | + | } | |||
| 97 | + | run( | |||
| 98 | + | arg0, | |||
| 99 | + | arg1, | |||
| 100 | + | ) | |||
| 101 | + | }) | |||
| 102 | + | return _c | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func (_c *mockacquireSource_add_Call) Return(acquireRemoverMoqParam acquireRemover, err error) *mockacquireSource_add_Call { | |||
| 106 | + | _c.Call.Return(acquireRemoverMoqParam, err) | |||
| 107 | + | return _c | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | func (_c *mockacquireSource_add_Call) RunAndReturn(run func(fd int, onReadable func()) (acquireRemover, error)) *mockacquireSource_add_Call { | |||
| 111 | + | _c.Call.Return(run) | |||
| 112 | + | return _c | |||
| 113 | + | } | |||
M internal/adapters/wayland/server.go +2 -0
| @@ -459,6 +459,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 459 | 459 | s.tokens = map[string]activationToken{} | |
| 460 | 460 | s.foreignExports = map[string]*foreignExport{} | |
| 461 | 461 | s.syncWait.log = log | |
| 462 | + | s.syncWait.src, s.syncWait.pace = displayAcquireSource{d}, s.acquireSignalled | |
| 462 | 463 | s.cleanup = func() { | |
| 463 | 464 | if s.paceReady { | |
| 464 | 465 | unix.Close(s.paceFD) | |
| @@ -585,6 +586,7 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 585 | 586 | } | |
| 586 | 587 | s.castInventoryChanged() | |
| 587 | 588 | err := s.display.Run(ctx) | |
| 589 | + | s.syncWait.releasePending() | |
| 588 | 590 | s.stopSecurityContexts() | |
| 589 | 591 | wg.Wait() | |
| 590 | 592 | return err | |
| @@ -459,6 +459,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 459 | s.tokens = map[string]activationToken{} | 459 | s.tokens = map[string]activationToken{} | ||
| 460 | s.foreignExports = map[string]*foreignExport{} | 460 | s.foreignExports = map[string]*foreignExport{} | ||
| 461 | s.syncWait.log = log | 461 | s.syncWait.log = log | ||
| 462 | + | s.syncWait.src, s.syncWait.pace = displayAcquireSource{d}, s.acquireSignalled | |||
| 462 | s.cleanup = func() { | 463 | s.cleanup = func() { | ||
| 463 | if s.paceReady { | 464 | if s.paceReady { | ||
| 464 | unix.Close(s.paceFD) | 465 | unix.Close(s.paceFD) | ||
| @@ -585,6 +586,7 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 585 | } | 586 | } | ||
| 586 | s.castInventoryChanged() | 587 | s.castInventoryChanged() | ||
| 587 | err := s.display.Run(ctx) | 588 | err := s.display.Run(ctx) | ||
| 589 | + | s.syncWait.releasePending() | |||
| 588 | s.stopSecurityContexts() | 590 | s.stopSecurityContexts() | ||
| 589 | wg.Wait() | 591 | wg.Wait() | ||
| 590 | return err | 592 | return err | ||
M internal/adapters/wayland/session_lock_surface_test.go +2 -2
| @@ -470,8 +470,8 @@ func TestLockSurfaceDestroyRetiresFencedChildDependencies(t *testing.T) { | |||
| 470 | 470 | if destroy == "surface" { | |
| 471 | 471 | remaining, refs = 0, 1 // parent destruction detaches the child | |
| 472 | 472 | } | |
| 473 | - | if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.cancelled.Load() || tl.uses != 0 { | |
| 474 | - | t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d cancelled=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.cancelled.Load()) | |
| 473 | + | if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.done || wait.efd >= 0 || tl.uses != 0 { | |
| 474 | + | t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d done=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.done) | |
| 475 | 475 | } | |
| 476 | 476 | dep.applyGraph() | |
| 477 | 477 | parent.applyGraph() | |
| @@ -470,8 +470,8 @@ func TestLockSurfaceDestroyRetiresFencedChildDependencies(t *testing.T) { | |||||
| 470 | if destroy == "surface" { | 470 | if destroy == "surface" { | ||
| 471 | remaining, refs = 0, 1 // parent destruction detaches the child | 471 | remaining, refs = 0, 1 // parent destruction detaches the child | ||
| 472 | } | 472 | } | ||
| 473 | - | if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.cancelled.Load() || tl.uses != 0 { | 473 | + | if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.done || wait.efd >= 0 || tl.uses != 0 { |
| 474 | - | t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d cancelled=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.cancelled.Load()) | 474 | + | t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d done=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.done) |
| 475 | } | 475 | } | ||
| 476 | dep.applyGraph() | 476 | dep.applyGraph() | ||
| 477 | parent.applyGraph() | 477 | parent.applyGraph() | ||
M internal/adapters/wayland/syncobj.go +108 -12
| @@ -191,9 +191,6 @@ func (s *surface) pendingNotDMABuf() bool { | |||
| 191 | 191 | return !dma | |
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | - | // syncWaiter watches acquire points: each gets an eventfd the kernel | |
| 195 | - | // writes once the point is signalled, and the pacer is woken to apply the | |
| 196 | - | // commits that waited for it. | |
| 197 | 194 | type acquirePoller interface { | |
| 198 | 195 | poll([]unix.PollFd, int) (int, error) | |
| 199 | 196 | } | |
| @@ -204,7 +201,38 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) { | |||
| 204 | 201 | return p.waiter.Poll(fds, timeout) | |
| 205 | 202 | } | |
| 206 | 203 | ||
| 204 | + | // acquireSource registers an acquire eventfd with the display loop: onReadable | |
| 205 | + | // runs on the display goroutine while fd is readable. Display goroutine only. | |
| 206 | + | type acquireSource interface { | |
| 207 | + | add(fd int, onReadable func()) (acquireRemover, error) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // acquireRemover unregisters a source added by acquireSource. | |
| 211 | + | type acquireRemover interface { | |
| 212 | + | Remove() | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | type displayAcquireSource struct{ d *server.Display } | |
| 216 | + | ||
| 217 | + | func (s displayAcquireSource) add(fd int, onReadable func()) (acquireRemover, error) { | |
| 218 | + | src, err := s.d.AddFD(fd, onReadable) | |
| 219 | + | if err != nil { | |
| 220 | + | return nil, err | |
| 221 | + | } | |
| 222 | + | return src, nil | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // syncWaiter watches acquire points: each gets an eventfd the kernel | |
| 226 | + | // writes once the point is signalled, and the pacer is woken to apply the | |
| 227 | + | // commits that waited for it. Explicit waits are display-loop sources; the | |
| 228 | + | // goroutine (run) with its pipe polls implicit dma-buf fences only. | |
| 207 | 229 | type syncWaiter struct { | |
| 230 | + | // src and pace serve explicit waits, which run on the display loop: | |
| 231 | + | // src registers the eventfd there, pace applies what the fired wait | |
| 232 | + | // unblocked (display goroutine only). | |
| 233 | + | src acquireSource | |
| 234 | + | pace func() | |
| 235 | + | pending map[*syncWait]struct{} // explicit waits still registered (display goroutine, then Run's exit) | |
| 208 | 236 | poller acquirePoller | |
| 209 | 237 | waiter *fdwait.Poller | |
| 210 | 238 | terminal bool // polling ended in failure; protected by mu | |
| @@ -218,9 +246,12 @@ type syncWaiter struct { | |||
| 218 | 246 | pollError sync.Once | |
| 219 | 247 | } | |
| 220 | 248 | ||
| 221 | - | // syncWait is one acquire point a queued commit waits for. | |
| 249 | + | // syncWait is one acquire point a queued commit waits for. An explicit wait | |
| 250 | + | // (src set) lives on the display loop only; an implicit one is polled by run. | |
| 222 | 251 | type syncWait struct { | |
| 223 | 252 | efd int | |
| 253 | + | src acquireRemover // explicit: the display-loop registration, nil once removed | |
| 254 | + | explicit bool | |
| 224 | 255 | done bool // the eventfd fired or the wait was cancelled (display goroutine) | |
| 225 | 256 | failed bool // observation failed; done must not authorize publication | |
| 226 | 257 | cancelled atomic.Bool // run stops polling it and closes efd | |
| @@ -240,11 +271,9 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||
| 240 | 271 | return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil | |
| 241 | 272 | } | |
| 242 | 273 | ||
| 243 | - | // watch registers an eventfd for p; the wait is ready once it fires. | |
| 274 | + | // watch registers an eventfd for p on the display loop; the wait is ready once | |
| 275 | + | // it fires. Display goroutine only. | |
| 244 | 276 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |
| 245 | - | if sw.stopped() { | |
| 246 | - | return &syncWait{efd: -1, done: true, failed: true}, unix.EIO | |
| 247 | - | } | |
| 248 | 277 | ready, err := p.tl.dev.signalled(p.tl.handle, p.point) | |
| 249 | 278 | if err != nil || ready { | |
| 250 | 279 | return &syncWait{efd: -1, done: true, failed: err != nil}, err | |
| @@ -257,13 +286,50 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||
| 257 | 286 | unix.Close(efd) | |
| 258 | 287 | return &syncWait{efd: -1, done: true, failed: true}, err | |
| 259 | 288 | } | |
| 260 | - | w := sw.add(efd) | |
| 261 | - | if w.failed { | |
| 262 | - | return w, unix.EIO | |
| 289 | + | w := &syncWait{efd: efd, explicit: true} | |
| 290 | + | src, err := sw.src.add(efd, func() { sw.onAcquire(w) }) | |
| 291 | + | if err != nil { | |
| 292 | + | unix.Close(efd) | |
| 293 | + | return &syncWait{efd: -1, done: true, failed: true}, err | |
| 263 | 294 | } | |
| 295 | + | w.src = src | |
| 296 | + | if sw.pending == nil { | |
| 297 | + | sw.pending = map[*syncWait]struct{}{} | |
| 298 | + | } | |
| 299 | + | sw.pending[w] = struct{}{} | |
| 264 | 300 | return w, nil | |
| 265 | 301 | } | |
| 266 | 302 | ||
| 303 | + | // onAcquire is the eventfd's callback, on the display goroutine. | |
| 304 | + | func (sw *syncWaiter) onAcquire(w *syncWait) { | |
| 305 | + | if w.done { | |
| 306 | + | return | |
| 307 | + | } | |
| 308 | + | var b [8]byte | |
| 309 | + | if _, err := unix.Read(w.efd, b[:]); err != nil { | |
| 310 | + | if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { | |
| 311 | + | return // not signalled yet: keep waiting | |
| 312 | + | } | |
| 313 | + | w.failed = true | |
| 314 | + | } | |
| 315 | + | sw.finish(w) | |
| 316 | + | sw.pace() | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | // finish unregisters then closes an explicit wait's eventfd and marks it done. | |
| 320 | + | func (sw *syncWaiter) finish(w *syncWait) { | |
| 321 | + | delete(sw.pending, w) | |
| 322 | + | if w.src != nil { | |
| 323 | + | w.src.Remove() | |
| 324 | + | w.src = nil | |
| 325 | + | } | |
| 326 | + | if w.efd >= 0 { | |
| 327 | + | unix.Close(w.efd) | |
| 328 | + | w.efd = -1 | |
| 329 | + | } | |
| 330 | + | w.done = true | |
| 331 | + | } | |
| 332 | + | ||
| 267 | 333 | // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch. | |
| 268 | 334 | func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error { | |
| 269 | 335 | if sw.stopped() { | |
| @@ -336,6 +402,9 @@ func (sw *syncWaiter) add(fd int) *syncWait { | |||
| 336 | 402 | ||
| 337 | 403 | // fired reports whether w's point is signalled, and forgets w once it has. | |
| 338 | 404 | func (sw *syncWaiter) fired(w *syncWait) bool { | |
| 405 | + | if w.explicit { | |
| 406 | + | return w.done // display goroutine only | |
| 407 | + | } | |
| 339 | 408 | sw.mu.Lock() | |
| 340 | 409 | defer sw.mu.Unlock() | |
| 341 | 410 | if ready, ok := sw.ready[w]; ok { | |
| @@ -347,7 +416,14 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||
| 347 | 416 | ||
| 348 | 417 | // cancel stops watching w (the commit was dropped). | |
| 349 | 418 | func (sw *syncWaiter) cancel(w *syncWait) { | |
| 350 | - | if w == nil || w.efd < 0 { | |
| 419 | + | if w == nil { | |
| 420 | + | return | |
| 421 | + | } | |
| 422 | + | if w.explicit { | |
| 423 | + | sw.finish(w) | |
| 424 | + | return | |
| 425 | + | } | |
| 426 | + | if w.efd < 0 { | |
| 351 | 427 | return | |
| 352 | 428 | } | |
| 353 | 429 | w.cancelled.Store(true) | |
| @@ -593,3 +669,23 @@ func (s *surface) applySync(cs *commitSync) { | |||
| 593 | 669 | s.server.dropTimeline(cs.acquire.tl) | |
| 594 | 670 | s.hold = syncHold{acquire: f, release: cs.release} | |
| 595 | 671 | } | |
| 672 | + | ||
| 673 | + | // acquireSignalled applies what a fired acquire point unblocked: like a | |
| 674 | + | // commit, it ticks inline instead of posting a tick to the display loop. | |
| 675 | + | func (s *Server) acquireSignalled() { | |
| 676 | + | s.paceDepth++ | |
| 677 | + | s.wakePacer() | |
| 678 | + | s.paceDepth-- | |
| 679 | + | if s.paceDepth == 0 && s.paceDirty { | |
| 680 | + | s.paceDirty = false | |
| 681 | + | s.paceNow() | |
| 682 | + | } | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | // releasePending unregisters and closes the explicit waits still pending at | |
| 686 | + | // shutdown. Called once the display loop has stopped. | |
| 687 | + | func (sw *syncWaiter) releasePending() { | |
| 688 | + | for w := range sw.pending { | |
| 689 | + | sw.finish(w) | |
| 690 | + | } | |
| 691 | + | } | |
| @@ -191,9 +191,6 @@ func (s *surface) pendingNotDMABuf() bool { | |||||
| 191 | return !dma | 191 | return !dma | ||
| 192 | } | 192 | } | ||
| 193 | 193 | ||||
| 194 | - | // syncWaiter watches acquire points: each gets an eventfd the kernel | |||
| 195 | - | // writes once the point is signalled, and the pacer is woken to apply the | |||
| 196 | - | // commits that waited for it. | |||
| 197 | type acquirePoller interface { | 194 | type acquirePoller interface { | ||
| 198 | poll([]unix.PollFd, int) (int, error) | 195 | poll([]unix.PollFd, int) (int, error) | ||
| 199 | } | 196 | } | ||
| @@ -204,7 +201,38 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) { | |||||
| 204 | return p.waiter.Poll(fds, timeout) | 201 | return p.waiter.Poll(fds, timeout) | ||
| 205 | } | 202 | } | ||
| 206 | 203 | ||||
| 204 | + | // acquireSource registers an acquire eventfd with the display loop: onReadable | |||
| 205 | + | // runs on the display goroutine while fd is readable. Display goroutine only. | |||
| 206 | + | type acquireSource interface { | |||
| 207 | + | add(fd int, onReadable func()) (acquireRemover, error) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // acquireRemover unregisters a source added by acquireSource. | |||
| 211 | + | type acquireRemover interface { | |||
| 212 | + | Remove() | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | type displayAcquireSource struct{ d *server.Display } | |||
| 216 | + | ||||
| 217 | + | func (s displayAcquireSource) add(fd int, onReadable func()) (acquireRemover, error) { | |||
| 218 | + | src, err := s.d.AddFD(fd, onReadable) | |||
| 219 | + | if err != nil { | |||
| 220 | + | return nil, err | |||
| 221 | + | } | |||
| 222 | + | return src, nil | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // syncWaiter watches acquire points: each gets an eventfd the kernel | |||
| 226 | + | // writes once the point is signalled, and the pacer is woken to apply the | |||
| 227 | + | // commits that waited for it. Explicit waits are display-loop sources; the | |||
| 228 | + | // goroutine (run) with its pipe polls implicit dma-buf fences only. | |||
| 207 | type syncWaiter struct { | 229 | type syncWaiter struct { | ||
| 230 | + | // src and pace serve explicit waits, which run on the display loop: | |||
| 231 | + | // src registers the eventfd there, pace applies what the fired wait | |||
| 232 | + | // unblocked (display goroutine only). | |||
| 233 | + | src acquireSource | |||
| 234 | + | pace func() | |||
| 235 | + | pending map[*syncWait]struct{} // explicit waits still registered (display goroutine, then Run's exit) | |||
| 208 | poller acquirePoller | 236 | poller acquirePoller | ||
| 209 | waiter *fdwait.Poller | 237 | waiter *fdwait.Poller | ||
| 210 | terminal bool // polling ended in failure; protected by mu | 238 | terminal bool // polling ended in failure; protected by mu | ||
| @@ -218,9 +246,12 @@ type syncWaiter struct { | |||||
| 218 | pollError sync.Once | 246 | pollError sync.Once | ||
| 219 | } | 247 | } | ||
| 220 | 248 | ||||
| 221 | - | // syncWait is one acquire point a queued commit waits for. | 249 | + | // syncWait is one acquire point a queued commit waits for. An explicit wait |
| 250 | + | // (src set) lives on the display loop only; an implicit one is polled by run. | |||
| 222 | type syncWait struct { | 251 | type syncWait struct { | ||
| 223 | efd int | 252 | efd int | ||
| 253 | + | src acquireRemover // explicit: the display-loop registration, nil once removed | |||
| 254 | + | explicit bool | |||
| 224 | done bool // the eventfd fired or the wait was cancelled (display goroutine) | 255 | done bool // the eventfd fired or the wait was cancelled (display goroutine) | ||
| 225 | failed bool // observation failed; done must not authorize publication | 256 | failed bool // observation failed; done must not authorize publication | ||
| 226 | cancelled atomic.Bool // run stops polling it and closes efd | 257 | cancelled atomic.Bool // run stops polling it and closes efd | ||
| @@ -240,11 +271,9 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) { | |||||
| 240 | return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil | 271 | return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil | ||
| 241 | } | 272 | } | ||
| 242 | 273 | ||||
| 243 | - | // watch registers an eventfd for p; the wait is ready once it fires. | 274 | + | // watch registers an eventfd for p on the display loop; the wait is ready once |
| 275 | + | // it fires. Display goroutine only. | |||
| 244 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | 276 | func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | ||
| 245 | - | if sw.stopped() { | |||
| 246 | - | return &syncWait{efd: -1, done: true, failed: true}, unix.EIO | |||
| 247 | - | } | |||
| 248 | ready, err := p.tl.dev.signalled(p.tl.handle, p.point) | 277 | ready, err := p.tl.dev.signalled(p.tl.handle, p.point) | ||
| 249 | if err != nil || ready { | 278 | if err != nil || ready { | ||
| 250 | return &syncWait{efd: -1, done: true, failed: err != nil}, err | 279 | return &syncWait{efd: -1, done: true, failed: err != nil}, err | ||
| @@ -257,13 +286,50 @@ func (sw *syncWaiter) watch(p syncPoint) (*syncWait, error) { | |||||
| 257 | unix.Close(efd) | 286 | unix.Close(efd) | ||
| 258 | return &syncWait{efd: -1, done: true, failed: true}, err | 287 | return &syncWait{efd: -1, done: true, failed: true}, err | ||
| 259 | } | 288 | } | ||
| 260 | - | w := sw.add(efd) | 289 | + | w := &syncWait{efd: efd, explicit: true} |
| 261 | - | if w.failed { | 290 | + | src, err := sw.src.add(efd, func() { sw.onAcquire(w) }) |
| 262 | - | return w, unix.EIO | 291 | + | if err != nil { |
| 292 | + | unix.Close(efd) | |||
| 293 | + | return &syncWait{efd: -1, done: true, failed: true}, err | |||
| 263 | } | 294 | } | ||
| 295 | + | w.src = src | |||
| 296 | + | if sw.pending == nil { | |||
| 297 | + | sw.pending = map[*syncWait]struct{}{} | |||
| 298 | + | } | |||
| 299 | + | sw.pending[w] = struct{}{} | |||
| 264 | return w, nil | 300 | return w, nil | ||
| 265 | } | 301 | } | ||
| 266 | 302 | ||||
| 303 | + | // onAcquire is the eventfd's callback, on the display goroutine. | |||
| 304 | + | func (sw *syncWaiter) onAcquire(w *syncWait) { | |||
| 305 | + | if w.done { | |||
| 306 | + | return | |||
| 307 | + | } | |||
| 308 | + | var b [8]byte | |||
| 309 | + | if _, err := unix.Read(w.efd, b[:]); err != nil { | |||
| 310 | + | if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { | |||
| 311 | + | return // not signalled yet: keep waiting | |||
| 312 | + | } | |||
| 313 | + | w.failed = true | |||
| 314 | + | } | |||
| 315 | + | sw.finish(w) | |||
| 316 | + | sw.pace() | |||
| 317 | + | } | |||
| 318 | + | ||||
| 319 | + | // finish unregisters then closes an explicit wait's eventfd and marks it done. | |||
| 320 | + | func (sw *syncWaiter) finish(w *syncWait) { | |||
| 321 | + | delete(sw.pending, w) | |||
| 322 | + | if w.src != nil { | |||
| 323 | + | w.src.Remove() | |||
| 324 | + | w.src = nil | |||
| 325 | + | } | |||
| 326 | + | if w.efd >= 0 { | |||
| 327 | + | unix.Close(w.efd) | |||
| 328 | + | w.efd = -1 | |||
| 329 | + | } | |||
| 330 | + | w.done = true | |||
| 331 | + | } | |||
| 332 | + | ||||
| 267 | // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch. | 333 | // watchImplicit waits for a dma-buf's exclusive fences without blocking dispatch. | ||
| 268 | func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error { | 334 | func (sw *syncWaiter) watchImplicit(b *ports.DMABuf, waits *[4]*syncWait) error { | ||
| 269 | if sw.stopped() { | 335 | if sw.stopped() { | ||
| @@ -336,6 +402,9 @@ func (sw *syncWaiter) add(fd int) *syncWait { | |||||
| 336 | 402 | ||||
| 337 | // fired reports whether w's point is signalled, and forgets w once it has. | 403 | // fired reports whether w's point is signalled, and forgets w once it has. | ||
| 338 | func (sw *syncWaiter) fired(w *syncWait) bool { | 404 | func (sw *syncWaiter) fired(w *syncWait) bool { | ||
| 405 | + | if w.explicit { | |||
| 406 | + | return w.done // display goroutine only | |||
| 407 | + | } | |||
| 339 | sw.mu.Lock() | 408 | sw.mu.Lock() | ||
| 340 | defer sw.mu.Unlock() | 409 | defer sw.mu.Unlock() | ||
| 341 | if ready, ok := sw.ready[w]; ok { | 410 | if ready, ok := sw.ready[w]; ok { | ||
| @@ -347,7 +416,14 @@ func (sw *syncWaiter) fired(w *syncWait) bool { | |||||
| 347 | 416 | ||||
| 348 | // cancel stops watching w (the commit was dropped). | 417 | // cancel stops watching w (the commit was dropped). | ||
| 349 | func (sw *syncWaiter) cancel(w *syncWait) { | 418 | func (sw *syncWaiter) cancel(w *syncWait) { | ||
| 350 | - | if w == nil || w.efd < 0 { | 419 | + | if w == nil { |
| 420 | + | return | |||
| 421 | + | } | |||
| 422 | + | if w.explicit { | |||
| 423 | + | sw.finish(w) | |||
| 424 | + | return | |||
| 425 | + | } | |||
| 426 | + | if w.efd < 0 { | |||
| 351 | return | 427 | return | ||
| 352 | } | 428 | } | ||
| 353 | w.cancelled.Store(true) | 429 | w.cancelled.Store(true) | ||
| @@ -593,3 +669,23 @@ func (s *surface) applySync(cs *commitSync) { | |||||
| 593 | s.server.dropTimeline(cs.acquire.tl) | 669 | s.server.dropTimeline(cs.acquire.tl) | ||
| 594 | s.hold = syncHold{acquire: f, release: cs.release} | 670 | s.hold = syncHold{acquire: f, release: cs.release} | ||
| 595 | } | 671 | } | ||
| 672 | + | ||||
| 673 | + | // acquireSignalled applies what a fired acquire point unblocked: like a | |||
| 674 | + | // commit, it ticks inline instead of posting a tick to the display loop. | |||
| 675 | + | func (s *Server) acquireSignalled() { | |||
| 676 | + | s.paceDepth++ | |||
| 677 | + | s.wakePacer() | |||
| 678 | + | s.paceDepth-- | |||
| 679 | + | if s.paceDepth == 0 && s.paceDirty { | |||
| 680 | + | s.paceDirty = false | |||
| 681 | + | s.paceNow() | |||
| 682 | + | } | |||
| 683 | + | } | |||
| 684 | + | ||||
| 685 | + | // releasePending unregisters and closes the explicit waits still pending at | |||
| 686 | + | // shutdown. Called once the display loop has stopped. | |||
| 687 | + | func (sw *syncWaiter) releasePending() { | |||
| 688 | + | for w := range sw.pending { | |||
| 689 | + | sw.finish(w) | |||
| 690 | + | } | |||
| 691 | + | } | |||
A internal/adapters/wayland/syncwait_explicit_test.go +152 -0
| @@ -0,0 +1,152 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/mock" | |
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // explicitWaiter is a waiter whose explicit waits register with a mocked | |
| 12 | + | // display-loop source; the callback and fd are captured. | |
| 13 | + | type explicitWaiter struct { | |
| 14 | + | sw *syncWaiter | |
| 15 | + | src *mockacquireSource | |
| 16 | + | rm *mockacquireRemover | |
| 17 | + | paced int | |
| 18 | + | fd int | |
| 19 | + | fire func() | |
| 20 | + | point syncPoint | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func newExplicitWaiter(t *testing.T, addErr error) *explicitWaiter { | |
| 24 | + | t.Helper() | |
| 25 | + | e := &explicitWaiter{src: newMockacquireSource(t), rm: newMockacquireRemover(t)} | |
| 26 | + | dev := newMocksyncobjDevice(t) | |
| 27 | + | dev.EXPECT().signalled(uint32(7), uint64(4)).Return(false, nil).Once() | |
| 28 | + | dev.EXPECT().eventfd(uint32(7), uint64(4), mock.Anything).Return(nil).Once() | |
| 29 | + | e.point = syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4} | |
| 30 | + | sw, err := newSyncWaiter(func() { t.Error("explicit wait posted a tick") }) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | t.Cleanup(func() { sw.discard(); sw.close() }) | |
| 35 | + | sw.src, sw.pace = e.src, func() { e.paced++ } | |
| 36 | + | e.sw = sw | |
| 37 | + | e.src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(fd int, f func()) (acquireRemover, error) { | |
| 38 | + | e.fd, e.fire = fd, f | |
| 39 | + | if addErr != nil { | |
| 40 | + | return nil, addErr | |
| 41 | + | } | |
| 42 | + | return e.rm, nil | |
| 43 | + | }).Once() | |
| 44 | + | return e | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func fdClosed(fd int) bool { | |
| 48 | + | _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0) | |
| 49 | + | return err == unix.EBADF | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // A fired eventfd removes the source, closes the fd and wakes the pacer | |
| 53 | + | // inline, never through a posted tick. | |
| 54 | + | func TestExplicitWaitFiresOnDisplayLoop(t *testing.T) { | |
| 55 | + | e := newExplicitWaiter(t, nil) | |
| 56 | + | w, err := e.sw.watch(e.point) | |
| 57 | + | if err != nil || e.sw.fired(w) { | |
| 58 | + | t.Fatalf("watch: %v fired=%v", err, e.sw.fired(w)) | |
| 59 | + | } | |
| 60 | + | e.rm.EXPECT().Remove().Once() | |
| 61 | + | one := [8]byte{1} | |
| 62 | + | if _, err := unix.Write(e.fd, one[:]); err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | fd := e.fd | |
| 66 | + | e.fire() | |
| 67 | + | if !e.sw.fired(w) || w.failed || e.paced != 1 || !fdClosed(fd) { | |
| 68 | + | t.Fatalf("fired=%v failed=%v paced=%d closed=%v", e.sw.fired(w), w.failed, e.paced, fdClosed(fd)) | |
| 69 | + | } | |
| 70 | + | e.sw.cancel(w) // applySync cancels after the fire: nothing more to release | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Cancelling before the fire removes the source, then closes the fd. | |
| 74 | + | func TestExplicitWaitCancelRemovesThenCloses(t *testing.T) { | |
| 75 | + | e := newExplicitWaiter(t, nil) | |
| 76 | + | w, err := e.sw.watch(e.point) | |
| 77 | + | if err != nil { | |
| 78 | + | t.Fatal(err) | |
| 79 | + | } | |
| 80 | + | e.rm.EXPECT().Remove().RunAndReturn(func() { | |
| 81 | + | if fdClosed(e.fd) { | |
| 82 | + | t.Error("fd closed before Remove") | |
| 83 | + | } | |
| 84 | + | }).Once() | |
| 85 | + | e.sw.cancel(w) | |
| 86 | + | if !e.sw.fired(w) || !fdClosed(e.fd) || e.paced != 0 { | |
| 87 | + | t.Fatal("cancel left the wait registered") | |
| 88 | + | } | |
| 89 | + | e.sw.cancel(w) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | func TestExplicitWaitAddFailureFails(t *testing.T) { | |
| 93 | + | e := newExplicitWaiter(t, unix.ENOMEM) | |
| 94 | + | w, err := e.sw.watch(e.point) | |
| 95 | + | if err == nil || !e.sw.fired(w) || !w.failed || !fdClosed(e.fd) { | |
| 96 | + | t.Fatalf("add failure: err=%v", err) | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // A callback with nothing to read keeps the wait registered. | |
| 101 | + | func TestExplicitWaitSpuriousCallbackKeepsWaiting(t *testing.T) { | |
| 102 | + | e := newExplicitWaiter(t, nil) | |
| 103 | + | w, err := e.sw.watch(e.point) | |
| 104 | + | if err != nil { | |
| 105 | + | t.Fatal(err) | |
| 106 | + | } | |
| 107 | + | e.fire() | |
| 108 | + | if e.sw.fired(w) || e.paced != 0 || fdClosed(e.fd) { | |
| 109 | + | t.Fatal("spurious callback ended the wait") | |
| 110 | + | } | |
| 111 | + | e.rm.EXPECT().Remove().Once() | |
| 112 | + | e.sw.cancel(w) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // Shutdown releases waits still registered, Remove before Close. | |
| 116 | + | func TestExplicitWaitReleasePending(t *testing.T) { | |
| 117 | + | e := newExplicitWaiter(t, nil) | |
| 118 | + | if _, err := e.sw.watch(e.point); err != nil { | |
| 119 | + | t.Fatal(err) | |
| 120 | + | } | |
| 121 | + | e.rm.EXPECT().Remove().Once() | |
| 122 | + | e.sw.releasePending() | |
| 123 | + | if !fdClosed(e.fd) { | |
| 124 | + | t.Fatal("pending wait leaked its eventfd") | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // End to end: a queued commit stays queued until the callback runs, then | |
| 129 | + | // is applied by that same tick. | |
| 130 | + | func TestSyncobjCommitAppliedByAcquireCallback(t *testing.T) { | |
| 131 | + | h := newSyncHarness(t) | |
| 132 | + | h.releases = map[uint32]*syncReleaseProxy{} | |
| 133 | + | src, rm := newMockacquireSource(t), newMockacquireRemover(t) | |
| 134 | + | var fire func() | |
| 135 | + | src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(_ int, f func()) (acquireRemover, error) { | |
| 136 | + | fire = f | |
| 137 | + | return rm, nil | |
| 138 | + | }).Once() | |
| 139 | + | rm.EXPECT().Remove().Maybe() | |
| 140 | + | h.s.display.Do(func() { h.s.syncWait.src = src }) | |
| 141 | + | a := h.dmabuf() | |
| 142 | + | h.commit(a, 1, 2) | |
| 143 | + | if _, ok := h.content(50 * time.Millisecond); ok { | |
| 144 | + | t.Fatal("content before the acquire point fired") | |
| 145 | + | } | |
| 146 | + | h.fire(1) // the kernel signals: efd readable (display loop is mocked, so nothing reads it) | |
| 147 | + | h.s.display.Do(func() { fire() }) | |
| 148 | + | got, ok := h.content(2 * time.Second) | |
| 149 | + | if !ok || got.Acquire == nil { | |
| 150 | + | t.Fatalf("content %+v", got) | |
| 151 | + | } | |
| 152 | + | } | |
| @@ -0,0 +1,152 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/mock" | |||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // explicitWaiter is a waiter whose explicit waits register with a mocked | |||
| 12 | + | // display-loop source; the callback and fd are captured. | |||
| 13 | + | type explicitWaiter struct { | |||
| 14 | + | sw *syncWaiter | |||
| 15 | + | src *mockacquireSource | |||
| 16 | + | rm *mockacquireRemover | |||
| 17 | + | paced int | |||
| 18 | + | fd int | |||
| 19 | + | fire func() | |||
| 20 | + | point syncPoint | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func newExplicitWaiter(t *testing.T, addErr error) *explicitWaiter { | |||
| 24 | + | t.Helper() | |||
| 25 | + | e := &explicitWaiter{src: newMockacquireSource(t), rm: newMockacquireRemover(t)} | |||
| 26 | + | dev := newMocksyncobjDevice(t) | |||
| 27 | + | dev.EXPECT().signalled(uint32(7), uint64(4)).Return(false, nil).Once() | |||
| 28 | + | dev.EXPECT().eventfd(uint32(7), uint64(4), mock.Anything).Return(nil).Once() | |||
| 29 | + | e.point = syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4} | |||
| 30 | + | sw, err := newSyncWaiter(func() { t.Error("explicit wait posted a tick") }) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | t.Cleanup(func() { sw.discard(); sw.close() }) | |||
| 35 | + | sw.src, sw.pace = e.src, func() { e.paced++ } | |||
| 36 | + | e.sw = sw | |||
| 37 | + | e.src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(fd int, f func()) (acquireRemover, error) { | |||
| 38 | + | e.fd, e.fire = fd, f | |||
| 39 | + | if addErr != nil { | |||
| 40 | + | return nil, addErr | |||
| 41 | + | } | |||
| 42 | + | return e.rm, nil | |||
| 43 | + | }).Once() | |||
| 44 | + | return e | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func fdClosed(fd int) bool { | |||
| 48 | + | _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0) | |||
| 49 | + | return err == unix.EBADF | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // A fired eventfd removes the source, closes the fd and wakes the pacer | |||
| 53 | + | // inline, never through a posted tick. | |||
| 54 | + | func TestExplicitWaitFiresOnDisplayLoop(t *testing.T) { | |||
| 55 | + | e := newExplicitWaiter(t, nil) | |||
| 56 | + | w, err := e.sw.watch(e.point) | |||
| 57 | + | if err != nil || e.sw.fired(w) { | |||
| 58 | + | t.Fatalf("watch: %v fired=%v", err, e.sw.fired(w)) | |||
| 59 | + | } | |||
| 60 | + | e.rm.EXPECT().Remove().Once() | |||
| 61 | + | one := [8]byte{1} | |||
| 62 | + | if _, err := unix.Write(e.fd, one[:]); err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | fd := e.fd | |||
| 66 | + | e.fire() | |||
| 67 | + | if !e.sw.fired(w) || w.failed || e.paced != 1 || !fdClosed(fd) { | |||
| 68 | + | t.Fatalf("fired=%v failed=%v paced=%d closed=%v", e.sw.fired(w), w.failed, e.paced, fdClosed(fd)) | |||
| 69 | + | } | |||
| 70 | + | e.sw.cancel(w) // applySync cancels after the fire: nothing more to release | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // Cancelling before the fire removes the source, then closes the fd. | |||
| 74 | + | func TestExplicitWaitCancelRemovesThenCloses(t *testing.T) { | |||
| 75 | + | e := newExplicitWaiter(t, nil) | |||
| 76 | + | w, err := e.sw.watch(e.point) | |||
| 77 | + | if err != nil { | |||
| 78 | + | t.Fatal(err) | |||
| 79 | + | } | |||
| 80 | + | e.rm.EXPECT().Remove().RunAndReturn(func() { | |||
| 81 | + | if fdClosed(e.fd) { | |||
| 82 | + | t.Error("fd closed before Remove") | |||
| 83 | + | } | |||
| 84 | + | }).Once() | |||
| 85 | + | e.sw.cancel(w) | |||
| 86 | + | if !e.sw.fired(w) || !fdClosed(e.fd) || e.paced != 0 { | |||
| 87 | + | t.Fatal("cancel left the wait registered") | |||
| 88 | + | } | |||
| 89 | + | e.sw.cancel(w) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | func TestExplicitWaitAddFailureFails(t *testing.T) { | |||
| 93 | + | e := newExplicitWaiter(t, unix.ENOMEM) | |||
| 94 | + | w, err := e.sw.watch(e.point) | |||
| 95 | + | if err == nil || !e.sw.fired(w) || !w.failed || !fdClosed(e.fd) { | |||
| 96 | + | t.Fatalf("add failure: err=%v", err) | |||
| 97 | + | } | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // A callback with nothing to read keeps the wait registered. | |||
| 101 | + | func TestExplicitWaitSpuriousCallbackKeepsWaiting(t *testing.T) { | |||
| 102 | + | e := newExplicitWaiter(t, nil) | |||
| 103 | + | w, err := e.sw.watch(e.point) | |||
| 104 | + | if err != nil { | |||
| 105 | + | t.Fatal(err) | |||
| 106 | + | } | |||
| 107 | + | e.fire() | |||
| 108 | + | if e.sw.fired(w) || e.paced != 0 || fdClosed(e.fd) { | |||
| 109 | + | t.Fatal("spurious callback ended the wait") | |||
| 110 | + | } | |||
| 111 | + | e.rm.EXPECT().Remove().Once() | |||
| 112 | + | e.sw.cancel(w) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | // Shutdown releases waits still registered, Remove before Close. | |||
| 116 | + | func TestExplicitWaitReleasePending(t *testing.T) { | |||
| 117 | + | e := newExplicitWaiter(t, nil) | |||
| 118 | + | if _, err := e.sw.watch(e.point); err != nil { | |||
| 119 | + | t.Fatal(err) | |||
| 120 | + | } | |||
| 121 | + | e.rm.EXPECT().Remove().Once() | |||
| 122 | + | e.sw.releasePending() | |||
| 123 | + | if !fdClosed(e.fd) { | |||
| 124 | + | t.Fatal("pending wait leaked its eventfd") | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // End to end: a queued commit stays queued until the callback runs, then | |||
| 129 | + | // is applied by that same tick. | |||
| 130 | + | func TestSyncobjCommitAppliedByAcquireCallback(t *testing.T) { | |||
| 131 | + | h := newSyncHarness(t) | |||
| 132 | + | h.releases = map[uint32]*syncReleaseProxy{} | |||
| 133 | + | src, rm := newMockacquireSource(t), newMockacquireRemover(t) | |||
| 134 | + | var fire func() | |||
| 135 | + | src.EXPECT().add(mock.Anything, mock.Anything).RunAndReturn(func(_ int, f func()) (acquireRemover, error) { | |||
| 136 | + | fire = f | |||
| 137 | + | return rm, nil | |||
| 138 | + | }).Once() | |||
| 139 | + | rm.EXPECT().Remove().Maybe() | |||
| 140 | + | h.s.display.Do(func() { h.s.syncWait.src = src }) | |||
| 141 | + | a := h.dmabuf() | |||
| 142 | + | h.commit(a, 1, 2) | |||
| 143 | + | if _, ok := h.content(50 * time.Millisecond); ok { | |||
| 144 | + | t.Fatal("content before the acquire point fired") | |||
| 145 | + | } | |||
| 146 | + | h.fire(1) // the kernel signals: efd readable (display loop is mocked, so nothing reads it) | |||
| 147 | + | h.s.display.Do(func() { fire() }) | |||
| 148 | + | got, ok := h.content(2 * time.Second) | |||
| 149 | + | if !ok || got.Acquire == nil { | |||
| 150 | + | t.Fatalf("content %+v", got) | |||
| 151 | + | } | |||
| 152 | + | } | |||