v0.1.0 f503a24

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// Present commits an imported buffer. It refuses before the first configure,
144// before the first complete dmabuf feedback and while the previous frame
145// callback is pending.
146func (s *Surface) Present(p *Present) error {
147	if s.closed {
148		return ErrClosed
149	}
150	if p == nil {
151		return errors.New("neferclient: nil present")
152	}
153	if !s.configured {
154		return errors.New("neferclient: present before configure")
155	}
156	if !s.fbDone {
157		return errors.New("neferclient: dmabuf feedback incomplete")
158	}
159	if !s.frameReady {
160		return errors.New("neferclient: frame callback pending")
161	}
162	buf, ok := s.buffers[p.Buffer]
163	if !ok {
164		return fmt.Errorf("neferclient: unknown buffer %d", p.Buffer)
165	}
166	if p.AcquireTimeline == p.ReleaseTimeline && p.AcquirePoint >= p.ReleasePoint {
167		return fmt.Errorf("neferclient: acquire point %d must precede release point %d on one timeline", p.AcquirePoint, p.ReleasePoint)
168	}
169	for _, id := range [...]uint64{p.AcquireTimeline, p.ReleaseTimeline} {
170		if _, ok := s.timelines[id]; !ok {
171			return fmt.Errorf("neferclient: unknown timeline %d", id)
172		}
173	}
174	pw, ph, err := s.PhysicalSize()
175	if err != nil {
176		return err
177	}
178	// Without a viewport (and always on lock surfaces) the compositor
179	// requires the buffer to match the surface exactly.
180	if (s.viewport == nil || s.role == roleLock) && (buf.w != pw || buf.h != ph) {
181		return fmt.Errorf("neferclient: buffer %dx%d does not match surface %dx%d", buf.w, buf.h, pw, ph)
182	}
183	r := s.req
184	if s.viewport != nil {
185		if s.sentDestW != s.width || s.sentDestH != s.height {
186			if err = r.setDestination(s.width, s.height); err != nil {
187				return err
188			}
189			s.sentDestW, s.sentDestH = s.width, s.height
190		}
191	} else if s.sentBufScale != int32(s.scale) {
192		if err = r.setBufferScale(int32(s.scale)); err != nil {
193			return err
194		}
195		s.sentBufScale = int32(s.scale)
196	}
197	if p.Opaque {
198		if !s.opaqueSet || s.opaqueW != s.width || s.opaqueH != s.height {
199			if err = r.setOpaqueRegion(s.width, s.height, true); err != nil {
200				return err
201			}
202			s.opaqueSet, s.opaqueW, s.opaqueH = true, s.width, s.height
203		}
204	} else if s.opaqueSet {
205		if err = r.setOpaqueRegion(0, 0, false); err != nil {
206			return err
207		}
208		s.opaqueSet = false
209	}
210	if err = r.setAcquire(p.AcquireTimeline, p.AcquirePoint); err != nil {
211		return err
212	}
213	if err = r.setRelease(p.ReleaseTimeline, p.ReleasePoint); err != nil {
214		return err
215	}
216	if err = r.attach(p.Buffer); err != nil {
217		return err
218	}
219	if len(p.Damage) == 0 {
220		err = r.damageBuffer(Rect{0, 0, pw, ph})
221	}
222	for _, d := range p.Damage {
223		if err != nil {
224			break
225		}
226		err = r.damageBuffer(d)
227	}
228	if err != nil {
229		return err
230	}
231	if err = r.frame(); err != nil {
232		return err
233	}
234	if err = r.commit(); err != nil {
235		return err
236	}
237	s.frameReady, s.presented = false, true
238	return nil
239}
240
241// surfaceRequests is the seam between the Present ordering rules and the
242// wire: the production implementation sends the protocol requests.
243type surfaceRequests interface {
244	setDestination(w, h int32) error
245	setBufferScale(n int32) error
246	setOpaqueRegion(w, h int32, opaque bool) error
247	setAcquire(timeline, point uint64) error
248	setRelease(timeline, point uint64) error
249	attach(buffer uint64) error
250	damageBuffer(r Rect) error
251	frame() error
252	commit() error
253}
254
255// wireRequests sends the requests. skipFrame is set by tests that isolate the
256// request path from the frame callback registration.
257type wireRequests struct {
258	s         *Surface
259	skipFrame bool
260}
261
262func (r wireRequests) setDestination(w, h int32) error { return r.s.viewport.SetDestination(w, h) }
263func (r wireRequests) setBufferScale(n int32) error    { return r.s.surf.SetBufferScale(n) }
264
265func (r wireRequests) setOpaqueRegion(w, h int32, opaque bool) error {
266	s := r.s
267	if !opaque {
268		return s.surf.SetOpaqueRegion(nil)
269	}
270	region, err := s.c.g.compositor.CreateRegion()
271	if err != nil {
272		return err
273	}
274	if err = region.Add(0, 0, w, h); err == nil {
275		err = s.surf.SetOpaqueRegion(region)
276	}
277	_ = region.Destroy()
278	return err
279}
280
281func (r wireRequests) setAcquire(timeline, point uint64) error {
282	return r.s.sync.SetAcquirePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
283}
284
285func (r wireRequests) setRelease(timeline, point uint64) error {
286	return r.s.sync.SetReleasePoint(r.s.timelines[timeline], uint32(point>>32), uint32(point))
287}
288
289func (r wireRequests) attach(buffer uint64) error {
290	return r.s.surf.Attach(r.s.buffers[buffer].buf, 0, 0)
291}
292
293func (r wireRequests) damageBuffer(d Rect) error {
294	return r.s.surf.DamageBuffer(d.X, d.Y, d.Width, d.Height)
295}
296
297// frame requests the frame callback on the surface's single reusable
298// callback object: only one is ever pending.
299func (r wireRequests) frame() error {
300	s := r.s
301	if r.skipFrame {
302		return nil
303	}
304	return s.c.wlctx.RequestArgs(wl.Request{Proxy: s.surf, Opcode: 3, Name: "wl_surface.frame", Child: s.cb}, wl.ArgObject(s.cb))
305}
306
307func (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