v0.4.0 5f1ad00

present.go RawHistoryFolder

1package neferclient
2
3import (
4	"errors"
5	"fmt"
6
7	"github.com/bnema/go-wayland-bindings/client/wayland"
8	"github.com/bnema/wlturbo/wl"
9	"golang.org/x/sys/unix"
10)
11
12// importedBuffer is a wl_buffer with the size it was imported at.
13type importedBuffer struct {
14	buf  *wayland.Buffer
15	w, h int32
16}
17
18// Plane is one DMA-BUF plane of a [Buffer]. FD stays owned by the caller.
19type Plane struct {
20	FD     int
21	Offset uint32
22	Stride uint32
23}
24
25// Buffer describes a DMA-BUF to import as a wl_buffer.
26type Buffer struct {
27	Width, Height int32 // physical pixels
28	FourCC        uint32
29	Modifier      uint64
30	Planes        [4]Plane
31	PlaneCount    int
32}
33
34// Present describes one commit: which imported buffer, the explicit-sync
35// timelines and points, and the damage in buffer pixels.
36type Present struct {
37	Buffer          uint64
38	AcquireTimeline uint64
39	ReleaseTimeline uint64
40	AcquirePoint    uint64
41	ReleasePoint    uint64
42	Damage          []Rect // nil or empty damages the whole buffer
43	Opaque          bool   // the whole surface is opaque
44}
45
46// ImportBuffer creates a wl_buffer from b under the caller's id. The plane
47// descriptors are duplicated internally; the caller keeps ownership of its own.
48func (s *Surface) ImportBuffer(id uint64, b *Buffer) (err error) {
49	if s.closed {
50		return ErrClosed
51	}
52	if _, ok := s.buffers[id]; ok {
53		return fmt.Errorf("neferclient: buffer %d already imported", id)
54	}
55	if b == nil || b.Width <= 0 || b.Height <= 0 || b.PlaneCount < 1 || b.PlaneCount > len(b.Planes) {
56		return errors.New("neferclient: invalid buffer description")
57	}
58	dups := [4]int{-1, -1, -1, -1}
59	sent := 0
60	defer func() {
61		for _, fd := range dups[sent:b.PlaneCount] { // not consumed by a request
62			if fd >= 0 {
63				_ = unix.Close(fd)
64			}
65		}
66	}()
67	for i := range b.PlaneCount {
68		fd, err := unix.FcntlInt(uintptr(b.Planes[i].FD), unix.F_DUPFD_CLOEXEC, 0)
69		if err != nil {
70			return fmt.Errorf("neferclient: dup plane %d: %w", i, err)
71		}
72		dups[i] = fd
73	}
74	params, err := s.c.g.dmabuf.CreateParams()
75	if err != nil {
76		return err
77	}
78	defer func() { err = errors.Join(err, params.Destroy()) }()
79	hi, lo := uint32(b.Modifier>>32), uint32(b.Modifier)
80	for i := range b.PlaneCount {
81		// On success the request closes the duplicate; on failure it stays ours.
82		if err = params.Add(dups[i], uint32(i), b.Planes[i].Offset, b.Planes[i].Stride, hi, lo); err != nil {
83			return err
84		}
85		sent++
86	}
87	buf, err := params.CreateImmed(b.Width, b.Height, b.FourCC, 0)
88	if err != nil {
89		return err
90	}
91	s.buffers[id] = importedBuffer{buf: buf, w: b.Width, h: b.Height}
92	return nil
93}
94
95// DestroyBuffer destroys an imported wl_buffer. The compositor may still read
96// it until its release point signals; see the explicit-sync rules.
97func (s *Surface) DestroyBuffer(id uint64) error {
98	buf, ok := s.buffers[id]
99	if !ok {
100		return fmt.Errorf("neferclient: unknown buffer %d", id)
101	}
102	delete(s.buffers, id)
103	return buf.buf.Destroy()
104}
105
106// ImportTimeline imports a DRM syncobj timeline. fd is duplicated internally.
107func (s *Surface) ImportTimeline(id uint64, fd int) error {
108	if s.closed {
109		return ErrClosed
110	}
111	if _, ok := s.timelines[id]; ok {
112		return fmt.Errorf("neferclient: timeline %d already imported", id)
113	}
114	dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
115	if err != nil {
116		return fmt.Errorf("neferclient: dup timeline: %w", err)
117	}
118	t, err := s.c.g.syncobj.ImportTimeline(dup)
119	if err != nil {
120		_ = unix.Close(dup) // not consumed on error
121		return err
122	}
123	s.timelines[id] = t
124	return nil
125}
126
127// DestroyTimeline destroys an imported timeline object. The id is the one given
128// to [Surface.ImportTimeline]; it may be imported again afterwards. Do not
129// destroy a timeline that a pending commit still references. Surface.Close
130// destroys every remaining timeline.
131func (s *Surface) DestroyTimeline(id uint64) error {
132	t, ok := s.timelines[id]
133	if !ok {
134		return fmt.Errorf("neferclient: unknown timeline %d", id)
135	}
136	delete(s.timelines, id)
137	if err := t.Destroy(); err != nil {
138		return fmt.Errorf("neferclient: destroy timeline %d: %w", id, err)
139	}
140	return nil
141}
142
143// DestroyImports destroys every imported buffer and timeline of the surface,
144// so a renderer built from older dmabuf feedback can be replaced by one that
145// imports its buffers again under any ids (the old ids are free afterwards).
146// It is safe while a frame is pending and when nothing is imported.
147//
148// Present refuses the old ids from now on, so nothing is sent on a destroyed
149// object. A frame callback already requested still fires and is delivered as
150// usual; the next Present waits for it as always. The compositor keeps the
151// content and the DMA-BUF references of a commit it already received: destroying
152// a wl_buffer or a timeline never withdraws a commit, and the descriptors were
153// duplicated at import, so none is closed twice. The caller may drop its own
154// references to the storage (close the renderer) at once, but must not write
155// to or recycle it before the release points of the commits that used it
156// signal (the explicit-sync rule).
157//
158// Every object is destroyed exactly once, even if a request fails; the errors
159// are joined. It returns [ErrClosed] after Surface.Close or Conn.Close. See
160// [Feedback.Equal] and the package documentation.
161func (s *Surface) DestroyImports() error {
162	if s.closed || s.c.closed {
163		return ErrClosed
164	}
165	var errs []error
166	for id, b := range s.buffers {
167		delete(s.buffers, id)
168		if err := b.buf.Destroy(); err != nil {
169			errs = append(errs, fmt.Errorf("neferclient: destroy buffer %d: %w", id, err))
170		}
171	}
172	for id, t := range s.timelines {
173		delete(s.timelines, id)
174		if err := t.Destroy(); err != nil {
175			errs = append(errs, fmt.Errorf("neferclient: destroy timeline %d: %w", id, err))
176		}
177	}
178	return errors.Join(errs...)
179}
180
181// Present commits an imported buffer. It refuses before the first configure,
182// before the first complete dmabuf feedback and while the previous frame
183// callback is pending.
184func (s *Surface) Present(p *Present) error {
185	if s.closed {
186		return ErrClosed
187	}
188	if p == nil {
189		return errors.New("neferclient: nil present")
190	}
191	if !s.configured {
192		return errors.New("neferclient: present before configure")
193	}
194	if !s.fbDone {
195		return errors.New("neferclient: dmabuf feedback incomplete")
196	}
197	if !s.frameReady {
198		return errors.New("neferclient: frame callback pending")
199	}
200	buf, ok := s.buffers[p.Buffer]
201	if !ok {
202		return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
203	}
204	if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint {
205		return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint)
206	}
207	for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} {
208		if _, ok := s.timelines[id]; !ok {
209			return fmt.Errorf("neferclient: unknown timeline %d", id)
210		}
211	}
212	pw, ph, err := s.PhysicalSize()
213	if err != nil {
214		return err
215	}
216	// Without a viewport (and always on lock surfaces) the compositor
217	// requires the buffer to match the surface exactly.
218	if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) {
219		return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph)
220	}
221	r := s.req
222	if s.viewport != nil {
223		if s.sentDestW != s.width || s.sentDestH != s.height {
224			if err = r.setDestination(s.width, s.height); err != nil {
225				return err
226			}
227			s.sentDestW, s.sentDestH = s.width, s.height
228		}
229	} else if s.sentBufScale != int32(s.scale) {
230		if err = r.setBufferScale(int32(s.scale)); err != nil {
231			return err
232		}
233		s.sentBufScale = int32(s.scale)
234	}
235	if p.Opaque {
236		if !s.opaqueSet || s.opaqueW != s.width || s.opaqueH != s.height {
237			if err = r.setOpaqueRegion(s.width, s.height, true); err != nil {
238				return err
239			}
240			s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
241		}
242	} else if s.opaqueSet {
243		if err = r.setOpaqueRegion(0, 0, false); err != nil {
244			return err
245		}
246		s.opaqueSet = false
247	}
248	if err = r.setAcquire(p.AcquireTimeline, p.AcquirePoint); err != nil {
249		return err
250	}
251	if err = r.setRelease(p.ReleaseTimeline, p.ReleasePoint); err != nil {
252		return err
253	}
254	if err = r.attach(p.Buffer); err != nil {
255		return err
256	}
257	if len(p.Damage) == 0 {
258		err = r.damageBuffer(Rect{0, 0, pw, ph})
259	}
260	for _, d := range p.Damage {
261		if err != nil {
262			break
263		}
264		err = r.damageBuffer(d)
265	}
266	if err != nil {
267		return err
268	}
269	if err = r.frame(); err != nil {
270		return err
271	}
272	if err = r.commit(); err != nil {
273		return err
274	}
275	s.frameReady, s.presented = false, true
276	return nil
277}
278
279// surfaceRequests is the seam between the Present ordering rules and the
280// wire: the production implementation sends the protocol requests.
281type surfaceRequests interface {
282	setDestination(w, h int32) error
283	setBufferScale(n int32) error
284	setOpaqueRegion(w, h int32, opaque bool) error
285	setAcquire(timeline, point uint64) error
286	setRelease(timeline, point uint64) error
287	attach(buffer uint64) error
288	damageBuffer(r Rect) error
289	frame() error
290	commit() error
291}
292
293// wireRequests sends the requests. skipFrame is set by tests that isolate the
294// request path from the frame callback registration.
295type wireRequests struct {
296	s         *Surface
297	skipFrame bool
298}
299
300func (r wireRequests) setDestination(w, h int32) error { return r.s.viewport.SetDestination(w, h) }
301func (r wireRequests) setBufferScale(n int32) error    { return r.s.surf.SetBufferScale(n) }
302
303func (r wireRequests) setOpaqueRegion(w, h int32, opaque bool) error {
304	s := r.s
305	if !opaque {
306		return s.surf.SetOpaqueRegion(nil)
307	}
308	region, err := s.c.g.compositor.CreateRegion()
309	if err != nil {
310		return err
311	}
312	if err = region.Add(0, 0, w, h); err == nil {
313		err = s.surf.SetOpaqueRegion(region)
314	}
315	_ = region.Destroy()
316	return err
317}
318
319func (r wireRequests) setAcquire(timeline, point uint64) error {
320	return r.s.sync.SetAcquirePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
321}
322
323func (r wireRequests) setRelease(timeline, point uint64) error {
324	return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
325}
326
327func (r wireRequests) attach(buffer uint64) error {
328	return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0)
329}
330
331func (r wireRequests) damageBuffer(d Rect) error {
332	return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height)
333}
334
335// frame requests the frame callback on the surface's single reusable
336// callback object: only one is ever pending.
337func (r wireRequests) frame() error {
338	s := r.s
339	if r.skipFrame {
340		return nil
341	}
342	return s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb))
343}
344
345func (r wireRequests) commit() error { return r.s.surf.Commit() }

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