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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 106 | 106 | c.Binds[move] = fmt.Sprintf("move-to-workspace %d", n) | |
| 107 | 107 | } | |
| 108 | 108 | c.Render.DirectScanout = true | |
| 109 | + | c.Render.Tearing = true | |
| 110 | + | c.Render.VRR = true | |
| 109 | 111 | c.Log.Level = "info" | |
| 110 | 112 | c.Log.Debug = []string{} | |
| 111 | 113 | return c | |
| @@ -451,6 +453,18 @@ func set(c *ports.Config, key, v string) error { | |||
| 451 | 453 | return err | |
| 452 | 454 | } | |
| 453 | 455 | 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 | 468 | case "log.level": | |
| 455 | 469 | if v != "debug" && v != "info" && v != "warn" && v != "error" { | |
| 456 | 470 | 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 = true | 108 | 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 c | 113 | return c | ||
| @@ -451,6 +453,18 @@ func set(c *ports.Config, key, v string) error { | |||||
| 451 | return err | 453 | return err | ||
| 452 | } | 454 | } | ||
| 453 | c.Render.DirectScanout = b | 455 | 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 | |||
| 62 | 62 | output.DP-1.primary = on | |
| 63 | 63 | bind.cmd+code:30 = quit | |
| 64 | 64 | render.direct-scanout = off | |
| 65 | + | render.tearing = off | |
| 66 | + | render.vrr = off | |
| 65 | 67 | startup = wl-paste --watch cliphist store | |
| 66 | 68 | startup = wl-paste --primary --watch cliphist store | |
| 67 | 69 | `) | |
| @@ -71,7 +73,7 @@ startup = wl-paste --primary --watch cliphist store | |||
| 71 | 73 | if !reflect.DeepEqual(c.Startup, [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"wl-paste", "--primary", "--watch", "cliphist", "store"}}) { | |
| 72 | 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 | 77 | t.Fatalf("%+v", c) | |
| 76 | 78 | } | |
| 77 | 79 | 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 = on | 62 | output.DP-1.primary = on | ||
| 63 | bind.cmd+code:30 = quit | 63 | bind.cmd+code:30 = quit | ||
| 64 | render.direct-scanout = off | 64 | render.direct-scanout = off | ||
| 65 | + | render.tearing = off | |||
| 66 | + | render.vrr = off | |||
| 65 | startup = wl-paste --watch cliphist store | 67 | startup = wl-paste --watch cliphist store | ||
| 66 | startup = wl-paste --primary --watch cliphist store | 68 | 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 { | |||
| 29 | 29 | // their own goroutines); modifiers is DRM_CAP_ADDFB2_MODIFIERS. | |
| 30 | 30 | gemMu sync.Mutex | |
| 31 | 31 | modifiers bool | |
| 32 | + | // async is DRM_CAP_ASYNC_PAGE_FLIP. | |
| 33 | + | async bool | |
| 32 | 34 | } | |
| 33 | 35 | ||
| 34 | 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 | 46 | cp := getCap{capability: capAddFB2Modifiers} | |
| 45 | 47 | 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 | |
| 47 | 51 | } | |
| 48 | 52 | ||
| 49 | 53 | // 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.Mutex | 30 | gemMu sync.Mutex | ||
| 31 | modifiers bool | 31 | 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 == 1 | 47 | 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 | |||
| 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 ( | |||
| 25 | 25 | ioctlDestroyDumb = 0xC00464B4 | |
| 26 | 26 | ||
| 27 | 27 | pageFlipEvent = 1 | |
| 28 | + | pageFlipAsync = 2 | |
| 28 | 29 | eventFlipDone = 2 | |
| 29 | 30 | modeTypePrefered = 1 << 3 | |
| 30 | 31 | connected = 1 | |
| @@ -121,6 +122,10 @@ type Want struct { | |||
| 121 | 122 | Modes map[string][3]float64 | |
| 122 | 123 | // NoScanout disables direct scanout (render.direct-scanout = off). | |
| 123 | 124 | 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 | 131 | // usable reports whether a connector should be driven. | |
| @@ -316,8 +321,11 @@ func getCrtc(fd int, crtc uint32) (modeCrtc, error) { | |||
| 316 | 321 | 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 | 325 | p := pageFlip{crtcID: crtc, fbID: fb, flags: pageFlipEvent} | |
| 326 | + | if async { | |
| 327 | + | p.flags |= pageFlipAsync | |
| 328 | + | } | |
| 321 | 329 | return ioctl(fd, ioctlPageFlip, unsafe.Pointer(&p)) | |
| 322 | 330 | } | |
| 323 | 331 | ||
| @@ -25,6 +25,7 @@ const ( | |||||
| 25 | ioctlDestroyDumb = 0xC00464B4 | 25 | ioctlDestroyDumb = 0xC00464B4 | ||
| 26 | 26 | ||||
| 27 | pageFlipEvent = 1 | 27 | pageFlipEvent = 1 | ||
| 28 | + | pageFlipAsync = 2 | |||
| 28 | eventFlipDone = 2 | 29 | eventFlipDone = 2 | ||
| 29 | modeTypePrefered = 1 << 3 | 30 | modeTypePrefered = 1 << 3 | ||
| 30 | connected = 1 | 31 | connected = 1 | ||
| @@ -121,6 +122,10 @@ type Want struct { | |||||
| 121 | Modes map[string][3]float64 | 122 | 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 bool | 124 | 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, err | 321 | 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) { | |||
| 23 | 23 | {"fb_cmd2", unsafe.Sizeof(fbCmd2{}), ioctlAddFB2}, | |
| 24 | 24 | {"prime_handle", unsafe.Sizeof(primeHandle{}), ioctlPrimeFDToHandle}, | |
| 25 | 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 | 30 | if want := (tc.req >> 16) & 0x3fff; tc.got != want { | |
| 28 | 31 | 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 { | |||
| 44 | 44 | shown, queued uint64 | |
| 45 | 45 | reason string // why the last frame was composed ("" = scanout) | |
| 46 | 46 | 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 | 53 | // kind is how the images were made; validated once a modeset took them. | |
| 48 | 54 | kind imageKind | |
| 49 | 55 | validated bool | |
| @@ -61,10 +67,14 @@ func newOutput(card *Card, c connector, mode modeInfo, crtc uint32) (*Output, er | |||
| 61 | 67 | if o.saved, err = getCrtc(card.fd, crtc); err != nil { | |
| 62 | 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 | 74 | if o.cursor, err = newCursor(card.fd, crtc, slices.Index(card.crtcs, crtc)); err != nil { | |
| 65 | 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 | 78 | return o, nil | |
| 69 | 79 | } | |
| 70 | 80 | ||
| @@ -93,6 +103,9 @@ func (o *Output) modeset() error { | |||
| 93 | 103 | return fmt.Errorf("set crtc: %w", err) | |
| 94 | 104 | } | |
| 95 | 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 | 109 | return nil | |
| 97 | 110 | } | |
| 98 | 111 | ||
| @@ -102,7 +115,9 @@ func (o *Output) present(r ports.Renderer) error { | |||
| 102 | 115 | if b := o.bufs[o.back]; b != nil { | |
| 103 | 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 | 121 | return err | |
| 107 | 122 | } | |
| 108 | 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 | 149 | // Already on screen: nothing to flip. | |
| 135 | 150 | 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 | 165 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 139 | 166 | // KMS refuses this buffer on the plane: compose it instead. | |
| 140 | 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 | 177 | 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 | 206 | // Close restores the CRTC state found at startup, when it had a mode, and | |
| 154 | 207 | // frees buffers. | |
| 155 | 208 | func (o *Output) Close() { | |
| 156 | 209 | if o.cursor != nil { | |
| 157 | 210 | o.cursor.close() | |
| 158 | 211 | } | |
| 212 | + | o.setVRR(false) | |
| 159 | 213 | if o.saved.crtcID != 0 { | |
| 160 | 214 | ids := []uint32{o.conn.id} | |
| 161 | 215 | s := o.saved | |
| @@ -44,6 +44,12 @@ type Output struct { | |||||
| 44 | shown, queued uint64 | 44 | 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.OutputPresented | 46 | 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 imageKind | 54 | kind imageKind | ||
| 49 | validated bool | 55 | 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, nil | 78 | 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 nil | 109 | 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 err | 121 | 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, nil | 150 | 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, nil | 177 | 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, and | 206 | // 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.saved | 215 | 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 ( | |||
| 23 | 23 | ||
| 24 | 24 | fbModifiers = 1 << 1 | |
| 25 | 25 | capAddFB2Modifiers = 0x10 | |
| 26 | + | capAsyncPageFlip = 0x7 | |
| 26 | 27 | modInvalid = 0x00ffffffffffffff | |
| 27 | 28 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | |
| 28 | 29 | ) | |
| @@ -23,6 +23,7 @@ const ( | |||||
| 23 | 23 | ||||
| 24 | fbModifiers = 1 << 1 | 24 | fbModifiers = 1 << 1 | ||
| 25 | capAddFB2Modifiers = 0x10 | 25 | capAddFB2Modifiers = 0x10 | ||
| 26 | + | capAsyncPageFlip = 0x7 | |||
| 26 | modInvalid = 0x00ffffffffffffff | 27 | modInvalid = 0x00ffffffffffffff | ||
| 27 | scanoutIdleTTL = 5 * time.Second // a cached framebuffer survives unused | 28 | 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 { | |||
| 17 | 17 | func() error { return registerCursorShape(d, s) }, | |
| 18 | 18 | func() error { return registerDMABuf(d, s, o.DMABuf) }, | |
| 19 | 19 | func() error { return registerPointerConstraints(d, s) }, | |
| 20 | + | func() error { return registerTearing(d, s) }, | |
| 20 | 21 | func() error { | |
| 21 | 22 | return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) }) | |
| 22 | 23 | }, | |
| @@ -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 { | |||
| 42 | 42 | content ports.SurfaceContent | |
| 43 | 43 | has bool | |
| 44 | 44 | 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 | 51 | // 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 | 91 | if s.xdg != nil { | |
| 88 | 92 | c.Geometry = s.xdg.geometry | |
| 89 | 93 | } | |
| 94 | + | c.Async = s.async | |
| 90 | 95 | return c | |
| 91 | 96 | } | |
| 92 | 97 | ||
| @@ -184,6 +189,8 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 184 | 189 | if s.pendingScale > 0 { | |
| 185 | 190 | s.bufferScale = s.pendingScale | |
| 186 | 191 | } | |
| 192 | + | hinted := s.async != s.pendingAsync | |
| 193 | + | s.async = s.pendingAsync | |
| 187 | 194 | if s.viewport != nil { | |
| 188 | 195 | s.viewport.commit() | |
| 189 | 196 | } | |
| @@ -237,7 +244,7 @@ func (s *surface) Commit(*wayland.Surface) { | |||
| 237 | 244 | if s.xdg != nil && s.xdg.window != nil { | |
| 238 | 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 | 248 | s.redraw() | |
| 242 | 249 | } | |
| 243 | 250 | } | |
| @@ -42,6 +42,10 @@ type surface struct { | |||||
| 42 | content ports.SurfaceContent | 42 | content ports.SurfaceContent | ||
| 43 | has bool | 43 | has bool | ||
| 44 | sub subState | 44 | 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, children | 51 | // 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.geometry | 92 | c.Geometry = s.xdg.geometry | ||
| 89 | } | 93 | } | ||
| 94 | + | c.Async = s.async | |||
| 90 | return c | 95 | 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.pendingScale | 190 | 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) | |||
| 64 | 64 | var hw *drmBackend | |
| 65 | 65 | if opts.Backend == "drm" { | |
| 66 | 66 | 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 | 68 | return err | |
| 69 | 69 | } | |
| 70 | 70 | defer hw.close() | |
| @@ -208,7 +208,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 208 | 208 | } | |
| 209 | 209 | if hw != nil { | |
| 210 | 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 | 212 | return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm")) | |
| 213 | 213 | }) | |
| 214 | 214 | return | |
| @@ -64,7 +64,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 64 | var hw *drmBackend | 64 | var hw *drmBackend | ||
| 65 | if opts.Backend == "drm" { | 65 | if opts.Backend == "drm" { | ||
| 66 | var err error | 66 | 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 err | 68 | 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 | return | 214 | return | ||
M internal/app/drm.go +7 -6
| @@ -42,14 +42,14 @@ func safe(name string, fn func() error) (err error) { | |||
| 42 | 42 | ||
| 43 | 43 | // openDRM opens the seat and every card with a connected display. Outputs | |
| 44 | 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 | 46 | log := logging.For(ctx, "drm") | |
| 47 | 47 | s, err := seat.Open(ctx, logging.For(ctx, "seat")) | |
| 48 | 48 | if err != nil { | |
| 49 | 49 | log.Error().Err(err).Msg("open seat") | |
| 50 | 50 | return nil, fmt.Errorf("open seat: %w", err) | |
| 51 | 51 | } | |
| 52 | - | want := wantFromConfig(outputs, scanout) | |
| 52 | + | want := wantFromConfig(cfg) | |
| 53 | 53 | paths, _ := filepath.Glob("/dev/dri/card[0-9]*") | |
| 54 | 54 | sort.Strings(paths) | |
| 55 | 55 | b := &drmBackend{seat: s} | |
| @@ -78,10 +78,11 @@ func openDRM(ctx context.Context, outputs []ports.OutputConfig, scanout bool) (* | |||
| 78 | 78 | return b, nil | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 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 { | |
| 85 | 86 | if o.Off { | |
| 86 | 87 | want.Disabled[o.Name] = true | |
| 87 | 88 | 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. Outputs | 43 | // 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, nil | 78 | 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] = true | 87 | want.Disabled[o.Name] = true | ||
| 87 | continue | 88 | continue | ||
M internal/app/drm_test.go +4 -2
| @@ -7,8 +7,10 @@ import ( | |||
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 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) | |
| 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 { | |
| 12 | 14 | t.Fatalf("%+v", want) | |
| 13 | 15 | } | |
| 14 | 16 | } | |
| @@ -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 { | |||
| 55 | 55 | Binds map[string]string | |
| 56 | 56 | Render struct { | |
| 57 | 57 | 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 | 62 | Log struct { | |
| 60 | 63 | Level string | |
| @@ -55,6 +55,9 @@ type Config struct { | |||||
| 55 | Binds map[string]string | 55 | Binds map[string]string | ||
| 56 | Render struct { | 56 | Render struct { | ||
| 57 | DirectScanout bool | 57 | 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 string | 63 | Level string | ||
M internal/ports/messages.go +3 -0
| @@ -543,6 +543,9 @@ type SurfaceContent struct { | |||
| 543 | 543 | // Geometry is in logical pixels from the surface origin; empty means | |
| 544 | 544 | // the whole surface. | |
| 545 | 545 | 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 | 551 | // 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 means | 543 | // Geometry is in logical pixels from the surface origin; empty means | ||
| 544 | // the whole surface. | 544 | // the whole surface. | ||
| 545 | Geometry Rect | 545 | 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 root | 551 | // Subsurface is a child surface at X, Y logical pixels from the root | ||