detached d08c2b9

feat: add dmabuf import, explicit-sync present and input region

commit
d08c2b9c67187594558f94e72b591f4797bb1786
author
brice · · 2026-10-01 16:32 UTC
parent
143cbf8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +226 -0
A present.go +226 -0
@@ -0,0 +1,226 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/wlturbo/wl"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller.
12+type Plane struct {
13+ FD int
14+ Offset uint32
15+ Stride uint32
16+}
17+
18+// Buffer describes a DMA-BUF to import as a wl_buffer.
19+type Buffer struct {
20+ Width, Height int32 // physical pixels
21+ FourCC uint32
22+ Modifier uint64
23+ Planes [4]Plane
24+ PlaneCount int
25+}
26+
27+// Present describes one commit: which imported buffer, the explicit-sync
28+// timelines and points, and the damage in buffer pixels.
29+type Present struct {
30+ Buffer uint64
31+ AcquireTimeline uint64
32+ ReleaseTimeline uint64
33+ AcquirePoint uint64
34+ ReleasePoint uint64
35+ Damage []Rect // nil or empty damages the whole buffer
36+ Opaque bool // the whole surface is opaque
37+}
38+
39+// 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.
41+func (s *Surface) ImportBuffer(id uint64, b *Buffer) error {
42+ if s.closed {
43+ return ErrClosed
44+ }
45+ if _, ok := s.buffers[id]; ok {
46+ return fmt.Errorf("neferclient: buffer %d already imported", id)
47+ }
48+ 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")
50+ }
51+ var dups [4]int
52+ sent := 0
53+ defer func() {
54+ for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request
55+ if fd > 0 {
56+ _ = unix.Close(fd)
57+ }
58+ }
59+ }()
60+ for i := range b.PlaneCount {
61+ fd, err := unix.FcntlInt(uintptr(b.Planes[i].FD), unix.F_DUPFD_CLOEXEC, 0)
62+ if err != nil {
63+ return fmt.Errorf("neferclient: dup plane %d: %w", i, err)
64+ }
65+ dups[i] = fd
66+ }
67+ params, err := s.c.g.dmabuf.CreateParams()
68+ if err != nil {
69+ return err
70+ }
71+ defer params.Destroy()
72+ hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier)
73+ for i := range b.PlaneCount {
74+ // On success the request closes the duplicate; on failure it stays ours.
75+ if err = params.Add(dups[i], uint32(i), b.Planes[i].Offset, b.Planes[i].Stride, hi, lo); err != nil {
76+ return err
77+ }
78+ sent++
79+ }
80+ buf, err := params.CreateImmed(b.Width, b.Height, b.FourCC, 0)
81+ if err != nil {
82+ return err
83+ }
84+ s.buffers[id] = buf
85+ return nil
86+}
87+
88+// DestroyBuffer destroys an imported wl_buffer. The compositor may still read
89+// it until its release point signals; see the explicit-sync rules.
90+func (s *Surface) DestroyBuffer(id uint64) error {
91+ buf, ok := s.buffers[id]
92+ if !ok {
93+ return fmt.Errorf("neferclient: unknown buffer %d", id)
94+ }
95+ delete(s.buffers, id)
96+ return buf.Destroy()
97+}
98+
99+// ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally.
100+func (s *Surface) ImportTimeline(id uint64, fd int) error {
101+ if s.closed {
102+ return ErrClosed
103+ }
104+ if _, ok := s.timelines[id]; ok {
105+ return fmt.Errorf("neferclient: timeline %d already imported", id)
106+ }
107+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
108+ if err != nil {
109+ return fmt.Errorf("neferclient: dup timeline: %w", err)
110+ }
111+ t, err := s.c.g.syncobj.ImportTimeline(dup)
112+ if err != nil {
113+ _ = unix.Close(dup) // not consumed on error
114+ return err
115+ }
116+ s.timelines[id] = t
117+ return nil
118+}
119+
120+// Present commits an imported buffer. It refuses before the first configure,
121+// before the first complete dmabuf feedback and while the previous frame
122+// callback is pending.
123+func (s *Surface) Present(p *Present) error {
124+ if s.closed {
125+ return ErrClosed
126+ }
127+ if !s.configured {
128+ return errors.New("neferclient: present before configure")
129+ }
130+ if !s.fbDone {
131+ return errors.New("neferclient: dmabuf feedback incomplete")
132+ }
133+ if !s.frameReady {
134+ return errors.New("neferclient: frame callback pending")
135+ }
136+ buf, ok := s.buffers[p.Buffer]
137+ if !ok {
138+ return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
139+ }
140+ acq, ok := s.timelines[p.AcquireTimeline]
141+ if !ok {
142+ return fmt.Errorf("neferclient: unknown timeline %d", p.AcquireTimeline)
143+ }
144+ rel, ok := s.timelines[p.ReleaseTimeline]
145+ if !ok {
146+ return fmt.Errorf("neferclient: unknown timeline %d", p.ReleaseTimeline)
147+ }
148+ pw, ph, err := s.PhysicalSize()
149+ if err != nil {
150+ return err
151+ }
152+ if s.viewport != nil {
153+ if s.sentDestW != s.width || s.sentDestH != s.height {
154+ if err = s.viewport.SetDestination(s.width, s.height); err != nil {
155+ return err
156+ }
157+ s.sentDestW, s.sentDestH = s.width, s.height
158+ }
159+ } else if s.scale > 1 && s.sentBufScale != int32(s.scale) {
160+ if err = s.surf.SetBufferScale(int32(s.scale)); err != nil {
161+ return err
162+ }
163+ s.sentBufScale = int32(s.scale)
164+ }
165+ if err = s.setOpaque(p.Opaque); err != nil {
166+ return err
167+ }
168+ if err = s.sync.SetAcquirePoint(acq, uint32(p.AcquirePoint>>32), uint32(p.AcquirePoint)); err != nil {
169+ return err
170+ }
171+ if err = s.sync.SetReleasePoint(rel, uint32(p.ReleasePoint>>32), uint32(p.ReleasePoint)); err != nil {
172+ return err
173+ }
174+ if err = s.surf.Attach(buf, 0, 0); err != nil {
175+ return err
176+ }
177+ if len(p.Damage) == 0 {
178+ err = s.surf.DamageBuffer(0, 0, pw, ph)
179+ }
180+ for _, r := range p.Damage {
181+ if err != nil {
182+ break
183+ }
184+ err = s.surf.DamageBuffer(r.X, r.Y, r.Width, r.Height)
185+ }
186+ if err != nil {
187+ return err
188+ }
189+ // The one frame callback object is reused: only one is ever pending.
190+ if err = s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb)); err != nil {
191+ return err
192+ }
193+ if err = s.surf.Commit(); err != nil {
194+ return err
195+ }
196+ s.frameReady = false
197+ return nil
198+}
199+
200+// setOpaque keeps the opaque region equal to the full logical surface when
201+// opaque, and empty otherwise, sending requests only on change.
202+func (s *Surface) setOpaque(opaque bool) error {
203+ if !opaque {
204+ if s.opaqueSet {
205+ s.opaqueSet = false
206+ return s.surf.SetOpaqueRegion(nil)
207+ }
208+ return nil
209+ }
210+ if s.opaqueSet && s.opaqueW == s.width && s.opaqueH == s.height {
211+ return nil
212+ }
213+ region, err := s.c.g.compositor.CreateRegion()
214+ if err != nil {
215+ return err
216+ }
217+ if err = region.Add(0, 0, s.width, s.height); err == nil {
218+ err = s.surf.SetOpaqueRegion(region)
219+ }
220+ _ = region.Destroy()
221+ if err != nil {
222+ return err
223+ }
224+ s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
225+ return nil
226+}
@@ -0,0 +1,226 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/wlturbo/wl"
8+ "golang.org/x/sys/unix"
9+)
10+
11+// Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller.
12+type Plane struct {
13+ FD int
14+ Offset uint32
15+ Stride uint32
16+}
17+
18+// Buffer describes a DMA-BUF to import as a wl_buffer.
19+type Buffer struct {
20+ Width, Height int32 // physical pixels
21+ FourCC uint32
22+ Modifier uint64
23+ Planes [4]Plane
24+ PlaneCount int
25+}
26+
27+// Present describes one commit: which imported buffer, the explicit-sync
28+// timelines and points, and the damage in buffer pixels.
29+type Present struct {
30+ Buffer uint64
31+ AcquireTimeline uint64
32+ ReleaseTimeline uint64
33+ AcquirePoint uint64
34+ ReleasePoint uint64
35+ Damage []Rect // nil or empty damages the whole buffer
36+ Opaque bool // the whole surface is opaque
37+}
38+
39+// 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.
41+func (s *Surface) ImportBuffer(id uint64, b *Buffer) error {
42+ if s.closed {
43+ return ErrClosed
44+ }
45+ if _, ok := s.buffers[id]; ok {
46+ return fmt.Errorf("neferclient: buffer %d already imported", id)
47+ }
48+ 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")
50+ }
51+ var dups [4]int
52+ sent := 0
53+ defer func() {
54+ for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request
55+ if fd > 0 {
56+ _ = unix.Close(fd)
57+ }
58+ }
59+ }()
60+ for i := range b.PlaneCount {
61+ fd, err := unix.FcntlInt(uintptr(b.Planes[i].FD), unix.F_DUPFD_CLOEXEC, 0)
62+ if err != nil {
63+ return fmt.Errorf("neferclient: dup plane %d: %w", i, err)
64+ }
65+ dups[i] = fd
66+ }
67+ params, err := s.c.g.dmabuf.CreateParams()
68+ if err != nil {
69+ return err
70+ }
71+ defer params.Destroy()
72+ hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier)
73+ for i := range b.PlaneCount {
74+ // On success the request closes the duplicate; on failure it stays ours.
75+ if err = params.Add(dups[i], uint32(i), b.Planes[i].Offset, b.Planes[i].Stride, hi, lo); err != nil {
76+ return err
77+ }
78+ sent++
79+ }
80+ buf, err := params.CreateImmed(b.Width, b.Height, b.FourCC, 0)
81+ if err != nil {
82+ return err
83+ }
84+ s.buffers[id] = buf
85+ return nil
86+}
87+
88+// DestroyBuffer destroys an imported wl_buffer. The compositor may still read
89+// it until its release point signals; see the explicit-sync rules.
90+func (s *Surface) DestroyBuffer(id uint64) error {
91+ buf, ok := s.buffers[id]
92+ if !ok {
93+ return fmt.Errorf("neferclient: unknown buffer %d", id)
94+ }
95+ delete(s.buffers, id)
96+ return buf.Destroy()
97+}
98+
99+// ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally.
100+func (s *Surface) ImportTimeline(id uint64, fd int) error {
101+ if s.closed {
102+ return ErrClosed
103+ }
104+ if _, ok := s.timelines[id]; ok {
105+ return fmt.Errorf("neferclient: timeline %d already imported", id)
106+ }
107+ dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
108+ if err != nil {
109+ return fmt.Errorf("neferclient: dup timeline: %w", err)
110+ }
111+ t, err := s.c.g.syncobj.ImportTimeline(dup)
112+ if err != nil {
113+ _ = unix.Close(dup) // not consumed on error
114+ return err
115+ }
116+ s.timelines[id] = t
117+ return nil
118+}
119+
120+// Present commits an imported buffer. It refuses before the first configure,
121+// before the first complete dmabuf feedback and while the previous frame
122+// callback is pending.
123+func (s *Surface) Present(p *Present) error {
124+ if s.closed {
125+ return ErrClosed
126+ }
127+ if !s.configured {
128+ return errors.New("neferclient: present before configure")
129+ }
130+ if !s.fbDone {
131+ return errors.New("neferclient: dmabuf feedback incomplete")
132+ }
133+ if !s.frameReady {
134+ return errors.New("neferclient: frame callback pending")
135+ }
136+ buf, ok := s.buffers[p.Buffer]
137+ if !ok {
138+ return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
139+ }
140+ acq, ok := s.timelines[p.AcquireTimeline]
141+ if !ok {
142+ return fmt.Errorf("neferclient: unknown timeline %d", p.AcquireTimeline)
143+ }
144+ rel, ok := s.timelines[p.ReleaseTimeline]
145+ if !ok {
146+ return fmt.Errorf("neferclient: unknown timeline %d", p.ReleaseTimeline)
147+ }
148+ pw, ph, err := s.PhysicalSize()
149+ if err != nil {
150+ return err
151+ }
152+ if s.viewport != nil {
153+ if s.sentDestW != s.width || s.sentDestH != s.height {
154+ if err = s.viewport.SetDestination(s.width, s.height); err != nil {
155+ return err
156+ }
157+ s.sentDestW, s.sentDestH = s.width, s.height
158+ }
159+ } else if s.scale > 1 && s.sentBufScale != int32(s.scale) {
160+ if err = s.surf.SetBufferScale(int32(s.scale)); err != nil {
161+ return err
162+ }
163+ s.sentBufScale = int32(s.scale)
164+ }
165+ if err = s.setOpaque(p.Opaque); err != nil {
166+ return err
167+ }
168+ if err = s.sync.SetAcquirePoint(acq, uint32(p.AcquirePoint>>32), uint32(p.AcquirePoint)); err != nil {
169+ return err
170+ }
171+ if err = s.sync.SetReleasePoint(rel, uint32(p.ReleasePoint>>32), uint32(p.ReleasePoint)); err != nil {
172+ return err
173+ }
174+ if err = s.surf.Attach(buf, 0, 0); err != nil {
175+ return err
176+ }
177+ if len(p.Damage) == 0 {
178+ err = s.surf.DamageBuffer(0, 0, pw, ph)
179+ }
180+ for _, r := range p.Damage {
181+ if err != nil {
182+ break
183+ }
184+ err = s.surf.DamageBuffer(r.X, r.Y, r.Width, r.Height)
185+ }
186+ if err != nil {
187+ return err
188+ }
189+ // The one frame callback object is reused: only one is ever pending.
190+ if err = s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb)); err != nil {
191+ return err
192+ }
193+ if err = s.surf.Commit(); err != nil {
194+ return err
195+ }
196+ s.frameReady = false
197+ return nil
198+}
199+
200+// setOpaque keeps the opaque region equal to the full logical surface when
201+// opaque, and empty otherwise, sending requests only on change.
202+func (s *Surface) setOpaque(opaque bool) error {
203+ if !opaque {
204+ if s.opaqueSet {
205+ s.opaqueSet = false
206+ return s.surf.SetOpaqueRegion(nil)
207+ }
208+ return nil
209+ }
210+ if s.opaqueSet && s.opaqueW == s.width && s.opaqueH == s.height {
211+ return nil
212+ }
213+ region, err := s.c.g.compositor.CreateRegion()
214+ if err != nil {
215+ return err
216+ }
217+ if err = region.Add(0, 0, s.width, s.height); err == nil {
218+ err = s.surf.SetOpaqueRegion(region)
219+ }
220+ _ = region.Destroy()
221+ if err != nil {
222+ return err
223+ }
224+ s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
225+ return nil
226+}

Keyboard shortcuts

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

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

Branches 1

Tags 4

↑↓ moveenter openesc close