detached b633319

feat(drm): tearing page flips for scanned-out surfaces (wp_tearing_control_v1) and VRR

commit
b633319e4581d80e1e14eeacb33fac68d0103e85
author
brice · · 2026-09-25 17:45 UTC
parent
2102161
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
17 files · +315 -17
M internal/adapters/config/config.go +14 -0
@@ -106,6 +106,8 @@ func Defaults() ports.Config {
106106 c.Binds[move] = fmt.Sprintf("move-to-workspace %d", n)
107107 }
108108 c.Render.DirectScanout = true
109+ c.Render.Tearing = true
110+ c.Render.VRR = true
109111 c.Log.Level = "info"
110112 c.Log.Debug = []string{}
111113 return c
@@ -451,6 +453,18 @@ func set(c *ports.Config, key, v string) error {
451453 return err
452454 }
453455 c.Render.DirectScanout = b
456+ case "render.tearing":
457+ b, err := onOff(v)
458+ if err != nil {
459+ return err
460+ }
461+ c.Render.Tearing = b
462+ case "render.vrr":
463+ b, err := onOff(v)
464+ if err != nil {
465+ return err
466+ }
467+ c.Render.VRR = b
454468 case "log.level":
455469 if v != "debug" && v != "info" && v != "warn" && v != "error" {
456470 return fmt.Errorf("must be debug, info, warn or error")
@@ -106,6 +106,8 @@ func Defaults() ports.Config {
106 c.Binds[move] = fmt.Sprintf("move-to-workspace %d", n)106 c.Binds[move] = fmt.Sprintf("move-to-workspace %d", n)
107 }107 }
108 c.Render.DirectScanout = true108 c.Render.DirectScanout = true
109+ c.Render.Tearing = true
110+ c.Render.VRR = true
109 c.Log.Level = "info"111 c.Log.Level = "info"
110 c.Log.Debug = []string{}112 c.Log.Debug = []string{}
111 return c113 return c
@@ -451,6 +453,18 @@ func set(c *ports.Config, key, v string) error {
451 return err453 return err
452 }454 }
453 c.Render.DirectScanout = b455 c.Render.DirectScanout = b
456+ case "render.tearing":
457+ b, err := onOff(v)
458+ if err != nil {
459+ return err
460+ }
461+ c.Render.Tearing = b
462+ case "render.vrr":
463+ b, err := onOff(v)
464+ if err != nil {
465+ return err
466+ }
467+ c.Render.VRR = b
454 case "log.level":468 case "log.level":
455 if v != "debug" && v != "info" && v != "warn" && v != "error" {469 if v != "debug" && v != "info" && v != "warn" && v != "error" {
456 return fmt.Errorf("must be debug, info, warn or error")470 return fmt.Errorf("must be debug, info, warn or error")
M internal/adapters/config/config_test.go +3 -1
@@ -62,6 +62,8 @@ output.DP-1.scale = 1.5
6262 output.DP-1.primary = on
6363 bind.cmd+code:30 = quit
6464 render.direct-scanout = off
65+render.tearing = off
66+render.vrr = off
6567 startup = wl-paste --watch cliphist store
6668 startup = wl-paste --primary --watch cliphist store
6769 `)
@@ -71,7 +73,7 @@ startup = wl-paste --primary --watch cliphist store
7173 if !reflect.DeepEqual(c.Startup, [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"wl-paste", "--primary", "--watch", "cliphist", "store"}}) {
7274 t.Fatalf("startup: %q", c.Startup)
7375 }
74- if c.Keyboard.Layout != "fr" || c.Keyboard.RepeatRate != 40 || !reflect.DeepEqual(c.Terminal.Command, []string{"foot", "--server"}) || c.Background.Color != "#000000" || c.Render.DirectScanout {
76+ if c.Keyboard.Layout != "fr" || c.Keyboard.RepeatRate != 40 || !reflect.DeepEqual(c.Terminal.Command, []string{"foot", "--server"}) || c.Background.Color != "#000000" || c.Render.DirectScanout || c.Render.Tearing || c.Render.VRR {
7577 t.Fatalf("%+v", c)
7678 }
7779 if !reflect.DeepEqual(c.Layout.Presets, []string{"1/3", "1/2", "1"}) || !reflect.DeepEqual(c.Log.Debug, []string{"core", "input"}) {
@@ -62,6 +62,8 @@ output.DP-1.scale = 1.5
62 output.DP-1.primary = on62 output.DP-1.primary = on
63 bind.cmd+code:30 = quit63 bind.cmd+code:30 = quit
64 render.direct-scanout = off64 render.direct-scanout = off
65+render.tearing = off
66+render.vrr = off
65 startup = wl-paste --watch cliphist store67 startup = wl-paste --watch cliphist store
66 startup = wl-paste --primary --watch cliphist store68 startup = wl-paste --primary --watch cliphist store
67 `)69 `)
@@ -71,7 +73,7 @@ startup = wl-paste --primary --watch cliphist store
71 if !reflect.DeepEqual(c.Startup, [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"wl-paste", "--primary", "--watch", "cliphist", "store"}}) {73 if !reflect.DeepEqual(c.Startup, [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"wl-paste", "--primary", "--watch", "cliphist", "store"}}) {
72 t.Fatalf("startup: %q", c.Startup)74 t.Fatalf("startup: %q", c.Startup)
73 }75 }
74- if c.Keyboard.Layout != "fr" || c.Keyboard.RepeatRate != 40 || !reflect.DeepEqual(c.Terminal.Command, []string{"foot", "--server"}) || c.Background.Color != "#000000" || c.Render.DirectScanout {76+ if c.Keyboard.Layout != "fr" || c.Keyboard.RepeatRate != 40 || !reflect.DeepEqual(c.Terminal.Command, []string{"foot", "--server"}) || c.Background.Color != "#000000" || c.Render.DirectScanout || c.Render.Tearing || c.Render.VRR {
75 t.Fatalf("%+v", c)77 t.Fatalf("%+v", c)
76 }78 }
77 if !reflect.DeepEqual(c.Layout.Presets, []string{"1/3", "1/2", "1"}) || !reflect.DeepEqual(c.Log.Debug, []string{"core", "input"}) {79 if !reflect.DeepEqual(c.Layout.Presets, []string{"1/3", "1/2", "1"}) || !reflect.DeepEqual(c.Log.Debug, []string{"core", "input"}) {
M internal/adapters/drm/card.go +5 -1
@@ -29,6 +29,8 @@ type Card struct {
2929 // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS.
3030 gemMu sync.Mutex
3131 modifiers bool
32+ // async is DRM_CAP_ASYNC_PAGE_FLIP.
33+ async bool
3234 }
3335
3436 // OpenCard reads the card's CRTCs. fd stays owned by the caller.
@@ -43,7 +45,9 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
4345 }
4446 cp := getCap{capability: capAddFB2Modifiers}
4547 mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1
46- return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods}, nil
48+ ap := getCap{capability: capAsyncPageFlip}
49+ async := ioctl(fd, ioctlGetCap, unsafe.Pointer(&ap)) == nil && ap.value == 1
50+ return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods, async: async}, nil
4751 }
4852
4953 // Path is the device path, e.g. /dev/dri/card1.
@@ -29,6 +29,8 @@ type Card struct {
29 // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS.29 // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS.
30 gemMu sync.Mutex30 gemMu sync.Mutex
31 modifiers bool31 modifiers bool
32+ // async is DRM_CAP_ASYNC_PAGE_FLIP.
33+ async bool
32 }34 }
33 35
34 // OpenCard reads the card's CRTCs. fd stays owned by the caller.36 // OpenCard reads the card's CRTCs. fd stays owned by the caller.
@@ -43,7 +45,9 @@ func OpenCard(fd int, path string, want Want, log zerowrap.Logger) (*Card, error
43 }45 }
44 cp := getCap{capability: capAddFB2Modifiers}46 cp := getCap{capability: capAddFB2Modifiers}
45 mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 147 mods := ioctl(fd, ioctlGetCap, unsafe.Pointer(&cp)) == nil && cp.value == 1
46- return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods}, nil48+ ap := getCap{capability: capAsyncPageFlip}
49+ async := ioctl(fd, ioctlGetCap, unsafe.Pointer(&ap)) == nil && ap.value == 1
50+ return &Card{fd: fd, path: path, want: want, log: log, crtcs: crtcs, outputs: map[string]*Output{}, flips: flips, modifiers: mods, async: async}, nil
47 }51 }
48 52
49 // Path is the device path, e.g. /dev/dri/card1.53 // Path is the device path, e.g. /dev/dri/card1.
M internal/adapters/drm/kms.go +9 -1
@@ -25,6 +25,7 @@ const (
2525 ioctlDestroyDumb = 0xC00464B4
2626
2727 pageFlipEvent = 1
28+ pageFlipAsync = 2
2829 eventFlipDone = 2
2930 modeTypePrefered = 1 << 3
3031 connected = 1
@@ -121,6 +122,10 @@ type Want struct {
121122 Modes map[string][3]float64
122123 // NoScanout disables direct scanout (render.direct-scanout = off).
123124 NoScanout bool
125+ // NoTearing ignores tearing requests (render.tearing = off).
126+ NoTearing bool
127+ // NoVRR keeps variable refresh off (render.vrr = off).
128+ NoVRR bool
124129 }
125130
126131 // usable reports whether a connector should be driven.
@@ -316,8 +321,11 @@ func getCrtc(fd int, crtc uint32) (modeCrtc, error) {
316321 return c, err
317322 }
318323
319-func flip(fd int, crtc, fb uint32) error {
324+func flip(fd int, crtc, fb uint32, async bool) error {
320325 p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent}
326+ if async {
327+ p.flags |= pageFlipAsync
328+ }
321329 return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p))
322330 }
323331
@@ -25,6 +25,7 @@ const (
25 ioctlDestroyDumb = 0xC00464B425 ioctlDestroyDumb = 0xC00464B4
26 26
27 pageFlipEvent = 127 pageFlipEvent = 1
28+ pageFlipAsync = 2
28 eventFlipDone = 229 eventFlipDone = 2
29 modeTypePrefered = 1 << 330 modeTypePrefered = 1 << 3
30 connected = 131 connected = 1
@@ -121,6 +122,10 @@ type Want struct {
121 Modes map[string][3]float64122 Modes map[string][3]float64
122 // NoScanout disables direct scanout (render.direct-scanout = off).123 // NoScanout disables direct scanout (render.direct-scanout = off).
123 NoScanout bool124 NoScanout bool
125+ // NoTearing ignores tearing requests (render.tearing = off).
126+ NoTearing bool
127+ // NoVRR keeps variable refresh off (render.vrr = off).
128+ NoVRR bool
124 }129 }
125 130
126 // usable reports whether a connector should be driven.131 // usable reports whether a connector should be driven.
@@ -316,8 +321,11 @@ func getCrtc(fd int, crtc uint32) (modeCrtc, error) {
316 return c, err321 return c, err
317 }322 }
318 323
319-func flip(fd int, crtc, fb uint32) error {324+func flip(fd int, crtc, fb uint32, async bool) error {
320 p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent}325 p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent}
326+ if async {
327+ p.flags |= pageFlipAsync
328+ }
321 return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p))329 return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p))
322 }330 }
323 331
M internal/adapters/drm/kms_test.go +3 -0
@@ -23,6 +23,9 @@ func TestStructSizesMatchIoctls(t *testing.T) {
2323 {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2},
2424 {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle},
2525 {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose},
26+ {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps},
27+ {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp},
28+ {"obj_set_prop", unsafe.Sizeof(objSetProp{}), ioctlObjSetProp},
2629 } {
2730 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
2831 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
@@ -23,6 +23,9 @@ func TestStructSizesMatchIoctls(t *testing.T) {
23 {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2},23 {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2},
24 {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle},24 {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle},
25 {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose},25 {"gem_close", unsafe.Sizeof(gemClose{}), ioctlGemClose},
26+ {"obj_get_props", unsafe.Sizeof(objGetProps{}), ioctlObjGetProps},
27+ {"get_prop", unsafe.Sizeof(getProp{}), ioctlGetProp},
28+ {"obj_set_prop", unsafe.Sizeof(objSetProp{}), ioctlObjSetProp},
26 } {29 } {
27 if want := (tc.req >> 16) & 0x3fff; tc.got != want {30 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
28 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)31 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
M internal/adapters/drm/output.go +57 -3
@@ -44,6 +44,12 @@ type Output struct {
4444 shown, queued uint64
4545 reason string // why the last frame was composed ("" = scanout)
4646 unsent ports.OutputPresented
47+ // Tearing (ADR 006): async flips of scanned-out buffers that ask for
48+ // them, while the card accepts them. vrrProp is the CRTC's VRR_ENABLED
49+ // property (0: no VRR), turned on while a buffer is scanned out.
50+ tearing bool
51+ vrrProp uint32
52+ vrrOn, async bool
4753 // kind is how the images were made; validated once a modeset took them.
4854 kind imageKind
4955 validated bool
@@ -61,10 +67,14 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
6167 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
6268 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
6369 }
70+ o.tearing = card.async && !card.want.NoTearing
71+ if !card.want.NoVRR {
72+ o.vrrProp = vrrProperty(card.fd, c.id, crtc)
73+ }
6474 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
6575 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
6676 }
67- log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Msg("output")
77+ log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output")
6878 return o, nil
6979 }
7080
@@ -93,6 +103,9 @@ func (o *Output) modeset() error {
93103 return fmt.Errorf("set crtc: %w", err)
94104 }
95105 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
106+ // Another master may have left VRR on: start from off.
107+ o.vrrOn = true
108+ o.setVRR(false)
96109 return nil
97110 }
98111
@@ -102,7 +115,9 @@ func (o *Output) present(r ports.Renderer) error {
102115 if b := o.bufs[o.back]; b != nil {
103116 r.CopyBGRX(b.mem, int(b.pitch))
104117 }
105- if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {
118+ o.setVRR(false)
119+ o.setAsync(false)
120+ if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil {
106121 return err
107122 }
108123 o.pending, o.flipStart = true, time.Now()
@@ -134,7 +149,19 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports
134149 // Already on screen: nothing to flip.
135150 return true, nil
136151 }
137- if err := flip(o.fd, o.crtc, fb); err != nil {
152+ o.setVRR(true)
153+ // Drivers refuse async flips that change the format or modifier: the
154+ // flip from the composed image into scanout waits for vblank.
155+ async := c.Async && o.tearing && o.shown != 0
156+ o.setAsync(async)
157+ err := flip(o.fd, o.crtc, fb, async)
158+ if err != nil && async && errors.Is(err, unix.EINVAL) {
159+ // Refused anyway (e.g. the client changed buffer layout): flip
160+ // at vblank this time.
161+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
162+ err = flip(o.fd, o.crtc, fb, false)
163+ }
164+ if err != nil {
138165 if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) {
139166 // KMS refuses this buffer on the plane: compose it instead.
140167 o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
@@ -150,12 +177,39 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports
150177 return true, nil
151178 }
152179
180+// setVRR turns variable refresh on or off, when the output has it.
181+func (o *Output) setVRR(on bool) {
182+ if o.vrrProp == 0 || on == o.vrrOn {
183+ return
184+ }
185+ v := uint64(0)
186+ if on {
187+ v = 1
188+ }
189+ if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil {
190+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled")
191+ o.vrrProp = 0
192+ return
193+ }
194+ o.vrrOn = on
195+ o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Msg("vrr")
196+}
197+
198+// setAsync logs when the output starts or stops tearing.
199+func (o *Output) setAsync(on bool) {
200+ if on != o.async {
201+ o.async = on
202+ o.log.Info().Bool("tearing", on).Str("connector", o.conn.name).Msg("tearing")
203+ }
204+}
205+
153206 // Close restores the CRTC state found at startup, when it had a mode, and
154207 // frees buffers.
155208 func (o *Output) Close() {
156209 if o.cursor != nil {
157210 o.cursor.close()
158211 }
212+ o.setVRR(false)
159213 if o.saved.crtcID != 0 {
160214 ids := []uint32{o.conn.id}
161215 s := o.saved
@@ -44,6 +44,12 @@ type Output struct {
44 shown, queued uint6444 shown, queued uint64
45 reason string // why the last frame was composed ("" = scanout)45 reason string // why the last frame was composed ("" = scanout)
46 unsent ports.OutputPresented46 unsent ports.OutputPresented
47+ // Tearing (ADR 006): async flips of scanned-out buffers that ask for
48+ // them, while the card accepts them. vrrProp is the CRTC's VRR_ENABLED
49+ // property (0: no VRR), turned on while a buffer is scanned out.
50+ tearing bool
51+ vrrProp uint32
52+ vrrOn, async bool
47 // kind is how the images were made; validated once a modeset took them.53 // kind is how the images were made; validated once a modeset took them.
48 kind imageKind54 kind imageKind
49 validated bool55 validated bool
@@ -61,10 +67,14 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er
61 if o.saved, err = getCrtc(card.fd, crtc); err != nil {67 if o.saved, err = getCrtc(card.fd, crtc); err != nil {
62 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")68 log.Warn().Err(err).Uint32("crtc", crtc).Msg("save crtc; it will not be restored on exit")
63 }69 }
70+ o.tearing = card.async && !card.want.NoTearing
71+ if !card.want.NoVRR {
72+ o.vrrProp = vrrProperty(card.fd, c.id, crtc)
73+ }
64 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {74 if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil {
65 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")75 log.Warn().Err(err).Str("connector", c.name).Msg("no hardware cursor")
66 }76 }
67- log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Msg("output")77+ log.Info().Str("card", card.path).Str("connector", c.name).Str("mode", mode.String()).Str("make", o.monitor.Make).Str("model", o.monitor.Model).Uint32("crtc", crtc).Bool("tearing", o.tearing).Bool("vrr", o.vrrProp != 0).Msg("output")
68 return o, nil78 return o, nil
69 }79 }
70 80
@@ -93,6 +103,9 @@ func (o *Output) modeset() error {
93 return fmt.Errorf("set crtc: %w", err)103 return fmt.Errorf("set crtc: %w", err)
94 }104 }
95 o.log.Info().Str("connector", o.conn.name).Msg("modeset")105 o.log.Info().Str("connector", o.conn.name).Msg("modeset")
106+ // Another master may have left VRR on: start from off.
107+ o.vrrOn = true
108+ o.setVRR(false)
96 return nil109 return nil
97 }110 }
98 111
@@ -102,7 +115,9 @@ func (o *Output) present(r ports.Renderer) error {
102 if b := o.bufs[o.back]; b != nil {115 if b := o.bufs[o.back]; b != nil {
103 r.CopyBGRX(b.mem, int(b.pitch))116 r.CopyBGRX(b.mem, int(b.pitch))
104 }117 }
105- if err := flip(o.fd, o.crtc, o.fbs[o.back]); err != nil {118+ o.setVRR(false)
119+ o.setAsync(false)
120+ if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil {
106 return err121 return err
107 }122 }
108 o.pending, o.flipStart = true, time.Now()123 o.pending, o.flipStart = true, time.Now()
@@ -134,7 +149,19 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports
134 // Already on screen: nothing to flip.149 // Already on screen: nothing to flip.
135 return true, nil150 return true, nil
136 }151 }
137- if err := flip(o.fd, o.crtc, fb); err != nil {152+ o.setVRR(true)
153+ // Drivers refuse async flips that change the format or modifier: the
154+ // flip from the composed image into scanout waits for vblank.
155+ async := c.Async && o.tearing && o.shown != 0
156+ o.setAsync(async)
157+ err := flip(o.fd, o.crtc, fb, async)
158+ if err != nil && async && errors.Is(err, unix.EINVAL) {
159+ // Refused anyway (e.g. the client changed buffer layout): flip
160+ // at vblank this time.
161+ o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused")
162+ err = flip(o.fd, o.crtc, fb, false)
163+ }
164+ if err != nil {
138 if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) {165 if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) {
139 // KMS refuses this buffer on the plane: compose it instead.166 // KMS refuses this buffer on the plane: compose it instead.
140 o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")167 o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused")
@@ -150,12 +177,39 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports
150 return true, nil177 return true, nil
151 }178 }
152 179
180+// setVRR turns variable refresh on or off, when the output has it.
181+func (o *Output) setVRR(on bool) {
182+ if o.vrrProp == 0 || on == o.vrrOn {
183+ return
184+ }
185+ v := uint64(0)
186+ if on {
187+ v = 1
188+ }
189+ if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil {
190+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled")
191+ o.vrrProp = 0
192+ return
193+ }
194+ o.vrrOn = on
195+ o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Msg("vrr")
196+}
197+
198+// setAsync logs when the output starts or stops tearing.
199+func (o *Output) setAsync(on bool) {
200+ if on != o.async {
201+ o.async = on
202+ o.log.Info().Bool("tearing", on).Str("connector", o.conn.name).Msg("tearing")
203+ }
204+}
205+
153 // Close restores the CRTC state found at startup, when it had a mode, and206 // Close restores the CRTC state found at startup, when it had a mode, and
154 // frees buffers.207 // frees buffers.
155 func (o *Output) Close() {208 func (o *Output) Close() {
156 if o.cursor != nil {209 if o.cursor != nil {
157 o.cursor.close()210 o.cursor.close()
158 }211 }
212+ o.setVRR(false)
159 if o.saved.crtcID != 0 {213 if o.saved.crtcID != 0 {
160 ids := []uint32{o.conn.id}214 ids := []uint32{o.conn.id}
161 s := o.saved215 s := o.saved
A internal/adapters/drm/props.go +82 -0
@@ -0,0 +1,82 @@
1+package drm
2+
3+import (
4+ "unsafe"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+// KMS object properties, read and set with the legacy property ioctls.
10+
11+const (
12+ ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties)
13+ ioctlGetProp = 0xC04064AA // DRM_IOWR('d', 0xAA, struct drm_mode_get_property)
14+ ioctlObjSetProp = 0xC01864BA // DRM_IOWR('d', 0xBA, struct drm_mode_obj_set_property)
15+
16+ objCrtc = 0xcccccccc
17+ objConnector = 0xc0c0c0c0
18+)
19+
20+type objGetProps struct {
21+ props, values uint64
22+ count, objID uint32
23+ objType, _ uint32
24+}
25+
26+type getProp struct {
27+ values, enumBlobs uint64
28+ propID, flags uint32
29+ name [32]byte
30+ countValues, countEn uint32
31+}
32+
33+type objSetProp struct {
34+ value uint64
35+ propID, objID, objType uint32
36+ _ uint32
37+}
38+
39+// objProps returns an object's property IDs and values by name.
40+func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) {
41+ q := objGetProps{objID: obj, objType: typ}
42+ if err := ioctl(fd, ioctlObjGetProps, unsafe.Pointer(&q)); err != nil {
43+ return nil, err
44+ }
45+ out := map[string][2]uint64{}
46+ if q.count == 0 {
47+ return out, nil
48+ }
49+ ids := make([]uint32, q.count)
50+ vals := make([]uint64, q.count)
51+ q.props, q.values = uint64(uintptr(unsafe.Pointer(&ids[0]))), uint64(uintptr(unsafe.Pointer(&vals[0])))
52+ if err := ioctl(fd, ioctlObjGetProps, unsafe.Pointer(&q)); err != nil {
53+ return nil, err
54+ }
55+ for i := range min(int(q.count), len(ids)) {
56+ p := getProp{propID: ids[i]}
57+ if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil {
58+ continue
59+ }
60+ out[unix.ByteSliceToString(p.name[:])] = [2]uint64{uint64(ids[i]), vals[i]}
61+ }
62+ return out, nil
63+}
64+
65+// vrrProperty returns the CRTC's VRR_ENABLED property when the connector
66+// is VRR capable, else 0.
67+func vrrProperty(fd int, conn, crtc uint32) uint32 {
68+ cp, err := objProps(fd, conn, objConnector)
69+ if err != nil || cp["vrr_capable"][1] != 1 {
70+ return 0
71+ }
72+ rp, err := objProps(fd, crtc, objCrtc)
73+ if err != nil {
74+ return 0
75+ }
76+ return uint32(rp["VRR_ENABLED"][0])
77+}
78+
79+func setProp(fd int, obj, typ, prop uint32, v uint64) error {
80+ s := objSetProp{value: v, propID: prop, objID: obj, objType: typ}
81+ return ioctl(fd, ioctlObjSetProp, unsafe.Pointer(&s))
82+}
@@ -0,0 +1,82 @@
1+package drm
2+
3+import (
4+ "unsafe"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+// KMS object properties, read and set with the legacy property ioctls.
10+
11+const (
12+ ioctlObjGetProps = 0xC02064B9 // DRM_IOWR('d', 0xB9, struct drm_mode_obj_get_properties)
13+ ioctlGetProp = 0xC04064AA // DRM_IOWR('d', 0xAA, struct drm_mode_get_property)
14+ ioctlObjSetProp = 0xC01864BA // DRM_IOWR('d', 0xBA, struct drm_mode_obj_set_property)
15+
16+ objCrtc = 0xcccccccc
17+ objConnector = 0xc0c0c0c0
18+)
19+
20+type objGetProps struct {
21+ props, values uint64
22+ count, objID uint32
23+ objType, _ uint32
24+}
25+
26+type getProp struct {
27+ values, enumBlobs uint64
28+ propID, flags uint32
29+ name [32]byte
30+ countValues, countEn uint32
31+}
32+
33+type objSetProp struct {
34+ value uint64
35+ propID, objID, objType uint32
36+ _ uint32
37+}
38+
39+// objProps returns an object's property IDs and values by name.
40+func objProps(fd int, obj, typ uint32) (map[string][2]uint64, error) {
41+ q := objGetProps{objID: obj, objType: typ}
42+ if err := ioctl(fd, ioctlObjGetProps, unsafe.Pointer(&q)); err != nil {
43+ return nil, err
44+ }
45+ out := map[string][2]uint64{}
46+ if q.count == 0 {
47+ return out, nil
48+ }
49+ ids := make([]uint32, q.count)
50+ vals := make([]uint64, q.count)
51+ q.props, q.values = uint64(uintptr(unsafe.Pointer(&ids[0]))), uint64(uintptr(unsafe.Pointer(&vals[0])))
52+ if err := ioctl(fd, ioctlObjGetProps, unsafe.Pointer(&q)); err != nil {
53+ return nil, err
54+ }
55+ for i := range min(int(q.count), len(ids)) {
56+ p := getProp{propID: ids[i]}
57+ if ioctl(fd, ioctlGetProp, unsafe.Pointer(&p)) != nil {
58+ continue
59+ }
60+ out[unix.ByteSliceToString(p.name[:])] = [2]uint64{uint64(ids[i]), vals[i]}
61+ }
62+ return out, nil
63+}
64+
65+// vrrProperty returns the CRTC's VRR_ENABLED property when the connector
66+// is VRR capable, else 0.
67+func vrrProperty(fd int, conn, crtc uint32) uint32 {
68+ cp, err := objProps(fd, conn, objConnector)
69+ if err != nil || cp["vrr_capable"][1] != 1 {
70+ return 0
71+ }
72+ rp, err := objProps(fd, crtc, objCrtc)
73+ if err != nil {
74+ return 0
75+ }
76+ return uint32(rp["VRR_ENABLED"][0])
77+}
78+
79+func setProp(fd int, obj, typ, prop uint32, v uint64) error {
80+ s := objSetProp{value: v, propID: prop, objID: obj, objType: typ}
81+ return ioctl(fd, ioctlObjSetProp, unsafe.Pointer(&s))
82+}
M internal/adapters/drm/scanout.go +1 -0
@@ -23,6 +23,7 @@ const (
2323
2424 fbModifiers = 1 << 1
2525 capAddFB2Modifiers = 0x10
26+ capAsyncPageFlip = 0x7
2627 modInvalid = 0x00ffffffffffffff
2728 scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused
2829 )
@@ -23,6 +23,7 @@ const (
23 23
24 fbModifiers = 1 << 124 fbModifiers = 1 << 1
25 capAddFB2Modifiers = 0x1025 capAddFB2Modifiers = 0x10
26+ capAsyncPageFlip = 0x7
26 modInvalid = 0x00ffffffffffffff27 modInvalid = 0x00ffffffffffffff
27 scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused28 scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused
28 )29 )
M internal/adapters/wayland/globals.go +1 -0
@@ -17,6 +17,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1717 func() error { return registerCursorShape(d, s) },
1818 func() error { return registerDMABuf(d, s, o.DMABuf) },
1919 func() error { return registerPointerConstraints(d, s) },
20+ func() error { return registerTearing(d, s) },
2021 func() error {
2122 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
2223 },
@@ -17,6 +17,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
17 func() error { return registerCursorShape(d, s) },17 func() error { return registerCursorShape(d, s) },
18 func() error { return registerDMABuf(d, s, o.DMABuf) },18 func() error { return registerDMABuf(d, s, o.DMABuf) },
19 func() error { return registerPointerConstraints(d, s) },19 func() error { return registerPointerConstraints(d, s) },
20+ func() error { return registerTearing(d, s) },
20 func() error {21 func() error {
21 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })22 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
22 },23 },
M internal/adapters/wayland/surface.go +8 -1
@@ -42,6 +42,10 @@ type surface struct {
4242 content ports.SurfaceContent
4343 has bool
4444 sub subState
45+ // tearing is the surface's wp_tearing_control_v1; async is the
46+ // committed hint, pendingAsync the requested one.
47+ tearing *tearingHandler
48+ async, pendingAsync bool
4549 }
4650
4751 // subState is the subsurface tree: parent is set on subsurfaces, children
@@ -87,6 +91,7 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
8791 if s.xdg != nil {
8892 c.Geometry = s.xdg.geometry
8993 }
94+ c.Async = s.async
9095 return c
9196 }
9297
@@ -184,6 +189,8 @@ func (s *surface) Commit(*wayland.Surface) {
184189 if s.pendingScale > 0 {
185190 s.bufferScale = s.pendingScale
186191 }
192+ hinted := s.async != s.pendingAsync
193+ s.async = s.pendingAsync
187194 if s.viewport != nil {
188195 s.viewport.commit()
189196 }
@@ -237,7 +244,7 @@ func (s *surface) Commit(*wayland.Surface) {
237244 if s.xdg != nil && s.xdg.window != nil {
238245 s.xdg.window.afterCommit()
239246 }
240- if fresh || moved || geometry || s.sub.parent != nil {
247+ if fresh || moved || geometry || hinted || s.sub.parent != nil {
241248 s.redraw()
242249 }
243250 }
@@ -42,6 +42,10 @@ type surface struct {
42 content ports.SurfaceContent42 content ports.SurfaceContent
43 has bool43 has bool
44 sub subState44 sub subState
45+ // tearing is the surface's wp_tearing_control_v1; async is the
46+ // committed hint, pendingAsync the requested one.
47+ tearing *tearingHandler
48+ async, pendingAsync bool
45 }49 }
46 50
47 // subState is the subsurface tree: parent is set on subsurfaces, children51 // subState is the subsurface tree: parent is set on subsurfaces, children
@@ -87,6 +91,7 @@ func (s *surface) tree(id ports.WindowID) ports.SurfaceContent {
87 if s.xdg != nil {91 if s.xdg != nil {
88 c.Geometry = s.xdg.geometry92 c.Geometry = s.xdg.geometry
89 }93 }
94+ c.Async = s.async
90 return c95 return c
91 }96 }
92 97
@@ -184,6 +189,8 @@ func (s *surface) Commit(*wayland.Surface) {
184 if s.pendingScale > 0 {189 if s.pendingScale > 0 {
185 s.bufferScale = s.pendingScale190 s.bufferScale = s.pendingScale
186 }191 }
192+ hinted := s.async != s.pendingAsync
193+ s.async = s.pendingAsync
187 if s.viewport != nil {194 if s.viewport != nil {
188 s.viewport.commit()195 s.viewport.commit()
189 }196 }
@@ -237,7 +244,7 @@ func (s *surface) Commit(*wayland.Surface) {
237 if s.xdg != nil && s.xdg.window != nil {244 if s.xdg != nil && s.xdg.window != nil {
238 s.xdg.window.afterCommit()245 s.xdg.window.afterCommit()
239 }246 }
240- if fresh || moved || geometry || s.sub.parent != nil {247+ if fresh || moved || geometry || hinted || s.sub.parent != nil {
241 s.redraw()248 s.redraw()
242 }249 }
243 }250 }
A internal/adapters/wayland/tearing.go +55 -0
@@ -0,0 +1,55 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/tearingcontrol"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+// Tearing control: a surface asks for async presentation. Outputs honour it
10+// only when they scan the surface out directly (ADR 006); the hint is
11+// double-buffered state applied on commit.
12+
13+func registerTearing(d *server.Display, s *Server) error {
14+ return tearingcontrol.NewWpTearingControlManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
15+ _, _ = tearingcontrol.NewWpTearingControlManagerV1(c, int32(v), id, tearingManager{s})
16+ })
17+}
18+
19+type tearingManager struct{ server *Server }
20+
21+func (tearingManager) Destroy(*tearingcontrol.WpTearingControlManagerV1) {}
22+
23+func (m tearingManager) GetTearingControl(r *tearingcontrol.WpTearingControlManagerV1, id uint32, surf *wayland.Surface) {
24+ var state *surface
25+ if surf != nil {
26+ state = m.server.surfaces[surf.Resource]
27+ }
28+ if state != nil && state.tearing != nil {
29+ r.PostError(uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists), "surface already has a tearing control")
30+ return
31+ }
32+ h := &tearingHandler{surface: state}
33+ res, err := tearingcontrol.NewWpTearingControlV1(r.Client(), r.Version(), id, h)
34+ if err != nil || state == nil {
35+ return
36+ }
37+ state.tearing = h
38+ res.OnDestroy = func() {
39+ // Back to vsync on the next commit.
40+ if state.tearing == h {
41+ state.tearing = nil
42+ state.pendingAsync = false
43+ }
44+ }
45+}
46+
47+type tearingHandler struct{ surface *surface }
48+
49+func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) {
50+ if h.surface != nil && h.surface.tearing == h {
51+ h.surface.pendingAsync = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)
52+ }
53+}
54+
55+func (*tearingHandler) Destroy(*tearingcontrol.WpTearingControlV1) {}
@@ -0,0 +1,55 @@
1+package wayland
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/tearingcontrol"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+// Tearing control: a surface asks for async presentation. Outputs honour it
10+// only when they scan the surface out directly (ADR 006); the hint is
11+// double-buffered state applied on commit.
12+
13+func registerTearing(d *server.Display, s *Server) error {
14+ return tearingcontrol.NewWpTearingControlManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
15+ _, _ = tearingcontrol.NewWpTearingControlManagerV1(c, int32(v), id, tearingManager{s})
16+ })
17+}
18+
19+type tearingManager struct{ server *Server }
20+
21+func (tearingManager) Destroy(*tearingcontrol.WpTearingControlManagerV1) {}
22+
23+func (m tearingManager) GetTearingControl(r *tearingcontrol.WpTearingControlManagerV1, id uint32, surf *wayland.Surface) {
24+ var state *surface
25+ if surf != nil {
26+ state = m.server.surfaces[surf.Resource]
27+ }
28+ if state != nil && state.tearing != nil {
29+ r.PostError(uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists), "surface already has a tearing control")
30+ return
31+ }
32+ h := &tearingHandler{surface: state}
33+ res, err := tearingcontrol.NewWpTearingControlV1(r.Client(), r.Version(), id, h)
34+ if err != nil || state == nil {
35+ return
36+ }
37+ state.tearing = h
38+ res.OnDestroy = func() {
39+ // Back to vsync on the next commit.
40+ if state.tearing == h {
41+ state.tearing = nil
42+ state.pendingAsync = false
43+ }
44+ }
45+}
46+
47+type tearingHandler struct{ surface *surface }
48+
49+func (h *tearingHandler) SetPresentationHint(_ *tearingcontrol.WpTearingControlV1, hint uint32) {
50+ if h.surface != nil && h.surface.tearing == h {
51+ h.surface.pendingAsync = hint == uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)
52+ }
53+}
54+
55+func (*tearingHandler) Destroy(*tearingcontrol.WpTearingControlV1) {}
A internal/adapters/wayland/tearing_test.go +58 -0
@@ -0,0 +1,58 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/tearingcontrol"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+)
11+
12+// asyncContent waits for a content of window id with the given hint.
13+func asyncContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID, async bool) {
14+ t.Helper()
15+ deadline := time.After(2 * time.Second)
16+ for {
17+ select {
18+ case c := <-contents:
19+ if c.ID == id && c.Async == async {
20+ return
21+ }
22+ case <-deadline:
23+ t.Fatalf("no content with async=%v", async)
24+ }
25+ }
26+}
27+
28+func TestTearingHintAppliesOnCommit(t *testing.T) {
29+ s, events, _, contents, dir := contentServer(t)
30+ c := protocolClient(t, s, dir)
31+ w, surf, _ := surfaceMapper(t, c, events)()
32+ manager := bindProtocol(t, c, "wp_tearing_control_manager_v1")
33+ control := c.AllocateID()
34+ registerProtocol(t, c, control)
35+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, control, surf)
36+ requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync))
37+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
38+ if err := c.Roundtrip(); err != nil {
39+ t.Fatal(err)
40+ }
41+ asyncContent(t, contents, w.ID, true)
42+
43+ // Destroying the control goes back to vsync on the next commit.
44+ requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestDestroy)
45+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
46+ if err := c.Roundtrip(); err != nil {
47+ t.Fatal(err)
48+ }
49+ asyncContent(t, contents, w.ID, false)
50+
51+ // A surface has at most one control.
52+ first, second := c.AllocateID(), c.AllocateID()
53+ registerProtocol(t, c, first)
54+ registerProtocol(t, c, second)
55+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, surf)
56+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, surf)
57+ expectProtocolError(t, c, manager, uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists))
58+}
@@ -0,0 +1,58 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+ "github.com/bnema/purego-libwayland/protocol/tearingcontrol"
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+)
11+
12+// asyncContent waits for a content of window id with the given hint.
13+func asyncContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID, async bool) {
14+ t.Helper()
15+ deadline := time.After(2 * time.Second)
16+ for {
17+ select {
18+ case c := <-contents:
19+ if c.ID == id && c.Async == async {
20+ return
21+ }
22+ case <-deadline:
23+ t.Fatalf("no content with async=%v", async)
24+ }
25+ }
26+}
27+
28+func TestTearingHintAppliesOnCommit(t *testing.T) {
29+ s, events, _, contents, dir := contentServer(t)
30+ c := protocolClient(t, s, dir)
31+ w, surf, _ := surfaceMapper(t, c, events)()
32+ manager := bindProtocol(t, c, "wp_tearing_control_manager_v1")
33+ control := c.AllocateID()
34+ registerProtocol(t, c, control)
35+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, control, surf)
36+ requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync))
37+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
38+ if err := c.Roundtrip(); err != nil {
39+ t.Fatal(err)
40+ }
41+ asyncContent(t, contents, w.ID, true)
42+
43+ // Destroying the control goes back to vsync on the next commit.
44+ requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestDestroy)
45+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
46+ if err := c.Roundtrip(); err != nil {
47+ t.Fatal(err)
48+ }
49+ asyncContent(t, contents, w.ID, false)
50+
51+ // A surface has at most one control.
52+ first, second := c.AllocateID(), c.AllocateID()
53+ registerProtocol(t, c, first)
54+ registerProtocol(t, c, second)
55+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, surf)
56+ requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, surf)
57+ expectProtocolError(t, c, manager, uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists))
58+}
M internal/app/app.go +2 -2
@@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
6464 var hw *drmBackend
6565 if opts.Backend == "drm" {
6666 var err error
67- if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil {
67+ if hw, err = openDRM(ctx, opts.Config); err != nil {
6868 return err
6969 }
7070 defer hw.close()
@@ -208,7 +208,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
208208 }
209209 if hw != nil {
210210 done <- safe("output", func() error {
211- want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) }
211+ want := func() drm.Want { return wantFromConfig(opts.Config) }
212212 return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
213213 })
214214 return
@@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
64 var hw *drmBackend64 var hw *drmBackend
65 if opts.Backend == "drm" {65 if opts.Backend == "drm" {
66 var err error66 var err error
67- if hw, err = openDRM(ctx, opts.Config.Outputs, opts.Config.Render.DirectScanout); err != nil {67+ if hw, err = openDRM(ctx, opts.Config); err != nil {
68 return err68 return err
69 }69 }
70 defer hw.close()70 defer hw.close()
@@ -208,7 +208,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
208 }208 }
209 if hw != nil {209 if hw != nil {
210 done <- safe("output", func() error {210 done <- safe("output", func() error {
211- want := func() drm.Want { return wantFromConfig(opts.Config.Outputs, opts.Config.Render.DirectScanout) }211+ want := func() drm.Want { return wantFromConfig(opts.Config) }
212 return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))212 return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
213 })213 })
214 return214 return
M internal/app/drm.go +7 -6
@@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) {
4242
4343 // openDRM opens the seat and every card with a connected display. Outputs
4444 // are found by the first Scan of each card.
45-func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) {
45+func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) {
4646 log := logging.For(ctx, "drm")
4747 s, err := seat.Open(ctx, logging.For(ctx, "seat"))
4848 if err != nil {
4949 log.Error().Err(err).Msg("open seat")
5050 return nil, fmt.Errorf("open seat: %w", err)
5151 }
52- want := wantFromConfig(outputs, scanout)
52+ want := wantFromConfig(cfg)
5353 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")
5454 sort.Strings(paths)
5555 b := &drmBackend{seat: s}
@@ -78,10 +78,11 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*
7878 return b, nil
7979 }
8080
81-// wantFromConfig turns output.<name> entries into connector choices.
82-func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want {
83- want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout}
84- for _, o := range outputs {
81+// wantFromConfig turns output.<name> and render.* entries into connector choices.
82+func wantFromConfig(cfg ports.Config) drm.Want {
83+ r := cfg.Render
84+ want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !r.DirectScanout, NoTearing: !r.Tearing, NoVRR: !r.VRR}
85+ for _, o := range cfg.Outputs {
8586 if o.Off {
8687 want.Disabled[o.Name] = true
8788 continue
@@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) {
42 42
43 // openDRM opens the seat and every card with a connected display. Outputs43 // openDRM opens the seat and every card with a connected display. Outputs
44 // are found by the first Scan of each card.44 // are found by the first Scan of each card.
45-func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*drmBackend, error) {45+func openDRM(ctx context.Context, cfg ports.Config) (*drmBackend, error) {
46 log := logging.For(ctx, "drm")46 log := logging.For(ctx, "drm")
47 s, err := seat.Open(ctx, logging.For(ctx, "seat"))47 s, err := seat.Open(ctx, logging.For(ctx, "seat"))
48 if err != nil {48 if err != nil {
49 log.Error().Err(err).Msg("open seat")49 log.Error().Err(err).Msg("open seat")
50 return nil, fmt.Errorf("open seat: %w", err)50 return nil, fmt.Errorf("open seat: %w", err)
51 }51 }
52- want := wantFromConfig(outputs, scanout)52+ want := wantFromConfig(cfg)
53 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")53 paths, _ := filepath.Glob("/dev/dri/card[0-9]*")
54 sort.Strings(paths)54 sort.Strings(paths)
55 b := &drmBackend{seat: s}55 b := &drmBackend{seat: s}
@@ -78,10 +78,11 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (*
78 return b, nil78 return b, nil
79 }79 }
80 80
81-// wantFromConfig turns output.<name> entries into connector choices.81+// wantFromConfig turns output.<name> and render.* entries into connector choices.
82-func wantFromConfig(outputs []ports.OutputConfig, scanout bool) drm.Want {82+func wantFromConfig(cfg ports.Config) drm.Want {
83- want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !scanout}83+ r := cfg.Render
84- for _, o := range outputs {84+ want := drm.Want{Disabled: map[string]bool{}, Modes: map[string][3]float64{}, NoScanout: !r.DirectScanout, NoTearing: !r.Tearing, NoVRR: !r.VRR}
85+ for _, o := range cfg.Outputs {
85 if o.Off {86 if o.Off {
86 want.Disabled[o.Name] = true87 want.Disabled[o.Name] = true
87 continue88 continue
M internal/app/drm_test.go +4 -2
@@ -7,8 +7,10 @@ import (
77 )
88
99 func TestWantFromConfig(t *testing.T) {
10- want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true)
11- if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 {
10+ cfg := ports.Config{Outputs: []ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}}
11+ cfg.Render.DirectScanout, cfg.Render.VRR = true, true
12+ want := wantFromConfig(cfg)
13+ if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 || want.NoScanout || !want.NoTearing || want.NoVRR {
1214 t.Fatalf("%+v", want)
1315 }
1416 }
@@ -7,8 +7,10 @@ import (
7 )7 )
8 8
9 func TestWantFromConfig(t *testing.T) {9 func TestWantFromConfig(t *testing.T) {
10- want := wantFromConfig([]ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}, true)10+ cfg := ports.Config{Outputs: []ports.OutputConfig{{Name: "eDP-1", Scale: 2, ScaleOnly: true}, {Name: "HDMI-A-1", Mode: "1920x1080@60"}, {Name: "DP-3", Off: true}}}
11- if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 {11+ cfg.Render.DirectScanout, cfg.Render.VRR = true, true
12+ want := wantFromConfig(cfg)
13+ if m := want.Modes["HDMI-A-1"]; m != [3]float64{1920, 1080, 60} || !want.Disabled["DP-3"] || len(want.Modes) != 1 || want.NoScanout || !want.NoTearing || want.NoVRR {
12 t.Fatalf("%+v", want)14 t.Fatalf("%+v", want)
13 }15 }
14 }16 }
M internal/ports/config.go +3 -0
@@ -55,6 +55,9 @@ type Config struct {
5555 Binds map[string]string
5656 Render struct {
5757 DirectScanout bool
58+ // Tearing honours wp_tearing_control_v1 in direct scanout; VRR
59+ // turns variable refresh on while a buffer is scanned out.
60+ Tearing, VRR bool
5861 }
5962 Log struct {
6063 Level string
@@ -55,6 +55,9 @@ type Config struct {
55 Binds map[string]string55 Binds map[string]string
56 Render struct {56 Render struct {
57 DirectScanout bool57 DirectScanout bool
58+ // Tearing honours wp_tearing_control_v1 in direct scanout; VRR
59+ // turns variable refresh on while a buffer is scanned out.
60+ Tearing, VRR bool
58 }61 }
59 Log struct {62 Log struct {
60 Level string63 Level string
M internal/ports/messages.go +3 -0
@@ -543,6 +543,9 @@ type SurfaceContent struct {
543543 // Geometry is in logical pixels from the surface origin; empty means
544544 // the whole surface.
545545 Geometry Rect
546+ // Async asks for tearing presentation (wp_tearing_control_v1); outputs
547+ // honour it only in direct scanout.
548+ Async bool
546549 }
547550
548551 // Subsurface is a child surface at X, Y logical pixels from the root
@@ -543,6 +543,9 @@ type SurfaceContent struct {
543 // Geometry is in logical pixels from the surface origin; empty means543 // Geometry is in logical pixels from the surface origin; empty means
544 // the whole surface.544 // the whole surface.
545 Geometry Rect545 Geometry Rect
546+ // Async asks for tearing presentation (wp_tearing_control_v1); outputs
547+ // honour it only in direct scanout.
548+ Async bool
546 }549 }
547 550
548 // Subsurface is a child surface at X, Y logical pixels from the root551 // Subsurface is a child surface at X, Y logical pixels from the root

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 25

Tags 10

↑↓ moveenter openesc close