fix(drm): keep VRR across lost master and short compositions, remember refused async flips
- commit
- cdcafdbe3cbc6e6df0779a68e1297c7cea7a7fae
- author
- brice · · 2026-09-25 18:01 UTC
- parent
- b633319
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +65 -10
M examples/config +4 -0
| @@ -63,6 +63,10 @@ layout.presets = 1/3, 1/2, 2/3, 1 | |||
| 63 | 63 | # output.DP-2.scale = 4/3 | |
| 64 | 64 | ||
| 65 | 65 | render.direct-scanout = on | |
| 66 | + | # Games asking for tearing flip without waiting for vblank, and variable | |
| 67 | + | # refresh runs; both only while a fullscreen window is scanned out directly. | |
| 68 | + | render.tearing = on | |
| 69 | + | render.vrr = on | |
| 66 | 70 | ||
| 67 | 71 | # log.level: debug, info, warn or error. log.debug: components or all. | |
| 68 | 72 | log.level = info | |
| @@ -63,6 +63,10 @@ layout.presets = 1/3, 1/2, 2/3, 1 | |||||
| 63 | # output.DP-2.scale = 4/3 | 63 | # output.DP-2.scale = 4/3 | ||
| 64 | 64 | ||||
| 65 | render.direct-scanout = on | 65 | render.direct-scanout = on | ||
| 66 | + | # Games asking for tearing flip without waiting for vblank, and variable | |||
| 67 | + | # refresh runs; both only while a fullscreen window is scanned out directly. | |||
| 68 | + | render.tearing = on | |||
| 69 | + | render.vrr = on | |||
| 66 | 70 | ||||
| 67 | # log.level: debug, info, warn or error. log.debug: components or all. | 71 | # log.level: debug, info, warn or error. log.debug: components or all. | ||
| 68 | log.level = info | 72 | log.level = info | ||
M internal/adapters/drm/output.go +33 -8
| @@ -50,6 +50,8 @@ type Output struct { | |||
| 50 | 50 | tearing bool | |
| 51 | 51 | vrrProp uint32 | |
| 52 | 52 | vrrOn, async bool | |
| 53 | + | // composedSince is when composition took over from scanout. | |
| 54 | + | composedSince time.Time | |
| 53 | 55 | // kind is how the images were made; validated once a modeset took them. | |
| 54 | 56 | kind imageKind | |
| 55 | 57 | validated bool | |
| @@ -115,7 +117,13 @@ func (o *Output) present(r ports.Renderer) error { | |||
| 115 | 117 | if b := o.bufs[o.back]; b != nil { | |
| 116 | 118 | r.CopyBGRX(b.mem, int(b.pitch)) | |
| 117 | 119 | } | |
| 118 | - | o.setVRR(false) | |
| 120 | + | // VRR goes off only after composing for vrrHold: each toggle is a | |
| 121 | + | // blocking commit and may flicker, so short compositions keep it. | |
| 122 | + | if o.composedSince.IsZero() { | |
| 123 | + | o.composedSince = time.Now() | |
| 124 | + | } else if time.Since(o.composedSince) > vrrHold { | |
| 125 | + | o.setVRR(false) | |
| 126 | + | } | |
| 119 | 127 | o.setAsync(false) | |
| 120 | 128 | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | |
| 121 | 129 | return err | |
| @@ -149,23 +157,28 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||
| 149 | 157 | // Already on screen: nothing to flip. | |
| 150 | 158 | return true, nil | |
| 151 | 159 | } | |
| 152 | - | o.setVRR(true) | |
| 153 | 160 | // Drivers refuse async flips that change the format or modifier: the | |
| 154 | 161 | // flip from the composed image into scanout waits for vblank. | |
| 155 | - | async := c.Async && o.tearing && o.shown != 0 | |
| 156 | - | o.setAsync(async) | |
| 162 | + | cfb := o.clientFBs[c.DMABuf.ID] | |
| 163 | + | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync | |
| 157 | 164 | err := flip(o.fd, o.crtc, fb, async) | |
| 158 | 165 | 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. | |
| 166 | + | // Refused (e.g. a layout change, or not a fast update): this | |
| 167 | + | // buffer flips at vblank from now on. | |
| 161 | 168 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | |
| 169 | + | cfb.noAsync, async = true, false | |
| 162 | 170 | err = flip(o.fd, o.crtc, fb, false) | |
| 163 | 171 | } | |
| 172 | + | if err == nil { | |
| 173 | + | o.setAsync(async) | |
| 174 | + | o.setVRR(true) | |
| 175 | + | o.composedSince = time.Time{} | |
| 176 | + | } | |
| 164 | 177 | if err != nil { | |
| 165 | 178 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | |
| 166 | 179 | // KMS refuses this buffer on the plane: compose it instead. | |
| 167 | 180 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | |
| 168 | - | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" | |
| 181 | + | cfb.failed = "flip_refused" | |
| 169 | 182 | o.reason = "flip_refused" | |
| 170 | 183 | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | |
| 171 | 184 | return false, nil | |
| @@ -177,6 +190,9 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||
| 177 | 190 | return true, nil | |
| 178 | 191 | } | |
| 179 | 192 | ||
| 193 | + | // vrrHold is how long composition runs before VRR turns off. | |
| 194 | + | const vrrHold = 500 * time.Millisecond | |
| 195 | + | ||
| 180 | 196 | // setVRR turns variable refresh on or off, when the output has it. | |
| 181 | 197 | func (o *Output) setVRR(on bool) { | |
| 182 | 198 | if o.vrrProp == 0 || on == o.vrrOn { | |
| @@ -186,13 +202,18 @@ func (o *Output) setVRR(on bool) { | |||
| 186 | 202 | if on { | |
| 187 | 203 | v = 1 | |
| 188 | 204 | } | |
| 205 | + | // SETPROPERTY is a blocking atomic commit (about one refresh). | |
| 206 | + | start := time.Now() | |
| 189 | 207 | if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil { | |
| 208 | + | if lostMaster(err) { | |
| 209 | + | return // switched away: retried on the next change | |
| 210 | + | } | |
| 190 | 211 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled") | |
| 191 | 212 | o.vrrProp = 0 | |
| 192 | 213 | return | |
| 193 | 214 | } | |
| 194 | 215 | o.vrrOn = on | |
| 195 | - | o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Msg("vrr") | |
| 216 | + | o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Dur("took", time.Since(start)).Msg("vrr") | |
| 196 | 217 | } | |
| 197 | 218 | ||
| 198 | 219 | // setAsync logs when the output starts or stops tearing. | |
| @@ -344,6 +365,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 344 | 365 | dirty = dirty || scene.Shows(c.ID) | |
| 345 | 366 | case <-stats.C: | |
| 346 | 367 | o.dropClientFBs(time.Now(), false) | |
| 368 | + | // A composed screen that stopped changing still drops VRR. | |
| 369 | + | if !o.composedSince.IsZero() && time.Since(o.composedSince) > vrrHold { | |
| 370 | + | o.setVRR(false) | |
| 371 | + | } | |
| 347 | 372 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | |
| 348 | 373 | if o.cursor != nil { | |
| 349 | 374 | cs := o.cursor.TakeStats() | |
| @@ -50,6 +50,8 @@ type Output struct { | |||||
| 50 | tearing bool | 50 | tearing bool | ||
| 51 | vrrProp uint32 | 51 | vrrProp uint32 | ||
| 52 | vrrOn, async bool | 52 | vrrOn, async bool | ||
| 53 | + | // composedSince is when composition took over from scanout. | |||
| 54 | + | composedSince time.Time | |||
| 53 | // kind is how the images were made; validated once a modeset took them. | 55 | // kind is how the images were made; validated once a modeset took them. | ||
| 54 | kind imageKind | 56 | kind imageKind | ||
| 55 | validated bool | 57 | validated bool | ||
| @@ -115,7 +117,13 @@ func (o *Output) present(r ports.Renderer) error { | |||||
| 115 | if b := o.bufs[o.back]; b != nil { | 117 | if b := o.bufs[o.back]; b != nil { | ||
| 116 | r.CopyBGRX(b.mem, int(b.pitch)) | 118 | r.CopyBGRX(b.mem, int(b.pitch)) | ||
| 117 | } | 119 | } | ||
| 118 | - | o.setVRR(false) | 120 | + | // VRR goes off only after composing for vrrHold: each toggle is a |
| 121 | + | // blocking commit and may flicker, so short compositions keep it. | |||
| 122 | + | if o.composedSince.IsZero() { | |||
| 123 | + | o.composedSince = time.Now() | |||
| 124 | + | } else if time.Since(o.composedSince) > vrrHold { | |||
| 125 | + | o.setVRR(false) | |||
| 126 | + | } | |||
| 119 | o.setAsync(false) | 127 | o.setAsync(false) | ||
| 120 | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | 128 | if err := flip(o.fd, o.crtc, o.fbs[o.back], false); err != nil { | ||
| 121 | return err | 129 | return err | ||
| @@ -149,23 +157,28 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||||
| 149 | // Already on screen: nothing to flip. | 157 | // Already on screen: nothing to flip. | ||
| 150 | return true, nil | 158 | return true, nil | ||
| 151 | } | 159 | } | ||
| 152 | - | o.setVRR(true) | |||
| 153 | // Drivers refuse async flips that change the format or modifier: the | 160 | // Drivers refuse async flips that change the format or modifier: the | ||
| 154 | // flip from the composed image into scanout waits for vblank. | 161 | // flip from the composed image into scanout waits for vblank. | ||
| 155 | - | async := c.Async && o.tearing && o.shown != 0 | 162 | + | cfb := o.clientFBs[c.DMABuf.ID] |
| 156 | - | o.setAsync(async) | 163 | + | async := c.Async && o.tearing && o.shown != 0 && !cfb.noAsync |
| 157 | err := flip(o.fd, o.crtc, fb, async) | 164 | err := flip(o.fd, o.crtc, fb, async) | ||
| 158 | if err != nil && async && errors.Is(err, unix.EINVAL) { | 165 | if err != nil && async && errors.Is(err, unix.EINVAL) { | ||
| 159 | - | // Refused anyway (e.g. the client changed buffer layout): flip | 166 | + | // Refused (e.g. a layout change, or not a fast update): this |
| 160 | - | // at vblank this time. | 167 | + | // buffer flips at vblank from now on. |
| 161 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | 168 | o.log.Debug().Err(err).Str("connector", o.conn.name).Msg("async flip refused") | ||
| 169 | + | cfb.noAsync, async = true, false | |||
| 162 | err = flip(o.fd, o.crtc, fb, false) | 170 | err = flip(o.fd, o.crtc, fb, false) | ||
| 163 | } | 171 | } | ||
| 172 | + | if err == nil { | |||
| 173 | + | o.setAsync(async) | |||
| 174 | + | o.setVRR(true) | |||
| 175 | + | o.composedSince = time.Time{} | |||
| 176 | + | } | |||
| 164 | if err != nil { | 177 | if err != nil { | ||
| 165 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | 178 | if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ERANGE) { | ||
| 166 | // KMS refuses this buffer on the plane: compose it instead. | 179 | // KMS refuses this buffer on the plane: compose it instead. | ||
| 167 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | 180 | o.log.Info().Err(err).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).Msg("scanout flip refused") | ||
| 168 | - | o.clientFBs[c.DMABuf.ID].failed = "flip_refused" | 181 | + | cfb.failed = "flip_refused" |
| 169 | o.reason = "flip_refused" | 182 | o.reason = "flip_refused" | ||
| 170 | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | 183 | o.log.Info().Bool("direct_scanout", false).Str("reason", o.reason).Str("connector", o.conn.name).Msg("scanout") | ||
| 171 | return false, nil | 184 | return false, nil | ||
| @@ -177,6 +190,9 @@ func (o *Output) tryScanout(scene ports.Scene, surfaces map[ports.WindowID]ports | |||||
| 177 | return true, nil | 190 | return true, nil | ||
| 178 | } | 191 | } | ||
| 179 | 192 | ||||
| 193 | + | // vrrHold is how long composition runs before VRR turns off. | |||
| 194 | + | const vrrHold = 500 * time.Millisecond | |||
| 195 | + | ||||
| 180 | // setVRR turns variable refresh on or off, when the output has it. | 196 | // setVRR turns variable refresh on or off, when the output has it. | ||
| 181 | func (o *Output) setVRR(on bool) { | 197 | func (o *Output) setVRR(on bool) { | ||
| 182 | if o.vrrProp == 0 || on == o.vrrOn { | 198 | if o.vrrProp == 0 || on == o.vrrOn { | ||
| @@ -186,13 +202,18 @@ func (o *Output) setVRR(on bool) { | |||||
| 186 | if on { | 202 | if on { | ||
| 187 | v = 1 | 203 | v = 1 | ||
| 188 | } | 204 | } | ||
| 205 | + | // SETPROPERTY is a blocking atomic commit (about one refresh). | |||
| 206 | + | start := time.Now() | |||
| 189 | if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil { | 207 | if err := setProp(o.fd, o.crtc, objCrtc, o.vrrProp, v); err != nil { | ||
| 208 | + | if lostMaster(err) { | |||
| 209 | + | return // switched away: retried on the next change | |||
| 210 | + | } | |||
| 190 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled") | 211 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("vrr; disabled") | ||
| 191 | o.vrrProp = 0 | 212 | o.vrrProp = 0 | ||
| 192 | return | 213 | return | ||
| 193 | } | 214 | } | ||
| 194 | o.vrrOn = on | 215 | o.vrrOn = on | ||
| 195 | - | o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Msg("vrr") | 216 | + | o.log.Info().Bool("vrr", on).Str("connector", o.conn.name).Dur("took", time.Since(start)).Msg("vrr") |
| 196 | } | 217 | } | ||
| 197 | 218 | ||||
| 198 | // setAsync logs when the output starts or stops tearing. | 219 | // setAsync logs when the output starts or stops tearing. | ||
| @@ -344,6 +365,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 344 | dirty = dirty || scene.Shows(c.ID) | 365 | dirty = dirty || scene.Shows(c.ID) | ||
| 345 | case <-stats.C: | 366 | case <-stats.C: | ||
| 346 | o.dropClientFBs(time.Now(), false) | 367 | o.dropClientFBs(time.Now(), false) | ||
| 368 | + | // A composed screen that stopped changing still drops VRR. | |||
| 369 | + | if !o.composedSince.IsZero() && time.Since(o.composedSince) > vrrHold { | |||
| 370 | + | o.setVRR(false) | |||
| 371 | + | } | |||
| 347 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | 372 | ev := o.log.Info().Int("frames", frame).Int("flips", o.flips) | ||
| 348 | if o.cursor != nil { | 373 | if o.cursor != nil { | ||
| 349 | cs := o.cursor.TakeStats() | 374 | cs := o.cursor.TakeStats() | ||
M internal/adapters/drm/scanout.go +2 -0
| @@ -90,6 +90,8 @@ type clientFB struct { | |||
| 90 | 90 | last time.Time // when last wanted on screen | |
| 91 | 91 | // failed is the reason KMS refused the buffer; it is not retried. | |
| 92 | 92 | failed string | |
| 93 | + | // noAsync: KMS refused an async flip to it; it flips at vblank. | |
| 94 | + | noAsync bool | |
| 93 | 95 | } | |
| 94 | 96 | ||
| 95 | 97 | // scanoutFB returns the framebuffer of a client buffer, importing it on | |
| @@ -90,6 +90,8 @@ type clientFB struct { | |||||
| 90 | last time.Time // when last wanted on screen | 90 | last time.Time // when last wanted on screen | ||
| 91 | // failed is the reason KMS refused the buffer; it is not retried. | 91 | // failed is the reason KMS refused the buffer; it is not retried. | ||
| 92 | failed string | 92 | failed string | ||
| 93 | + | // noAsync: KMS refused an async flip to it; it flips at vblank. | |||
| 94 | + | noAsync bool | |||
| 93 | } | 95 | } | ||
| 94 | 96 | ||||
| 95 | // scanoutFB returns the framebuffer of a client buffer, importing it on | 97 | // scanoutFB returns the framebuffer of a client buffer, importing it on | ||
M internal/adapters/wayland/surface.go +1 -0
| @@ -140,6 +140,7 @@ func (s *surface) detach() { | |||
| 140 | 140 | ||
| 141 | 141 | func (s *surface) Destroy(*wayland.Surface) { | |
| 142 | 142 | s.destroyed = true | |
| 143 | + | s.tearing = nil // the control becomes inert | |
| 143 | 144 | if s.server.cursorSurface == s { | |
| 144 | 145 | // The pointer keeps no cursor until the client sets another. | |
| 145 | 146 | s.server.cursorSurface = nil | |
| @@ -140,6 +140,7 @@ func (s *surface) detach() { | |||||
| 140 | 140 | ||||
| 141 | func (s *surface) Destroy(*wayland.Surface) { | 141 | func (s *surface) Destroy(*wayland.Surface) { | ||
| 142 | s.destroyed = true | 142 | s.destroyed = true | ||
| 143 | + | s.tearing = nil // the control becomes inert | |||
| 143 | if s.server.cursorSurface == s { | 144 | if s.server.cursorSurface == s { | ||
| 144 | // The pointer keeps no cursor until the client sets another. | 145 | // The pointer keeps no cursor until the client sets another. | ||
| 145 | s.server.cursorSurface = nil | 146 | s.server.cursorSurface = nil | ||
M internal/adapters/wayland/tearing_test.go +25 -2
| @@ -34,6 +34,17 @@ func TestTearingHintAppliesOnCommit(t *testing.T) { | |||
| 34 | 34 | registerProtocol(t, c, control) | |
| 35 | 35 | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, control, surf) | |
| 36 | 36 | requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)) | |
| 37 | + | if err := c.Roundtrip(); err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | // Double-buffered: nothing changes before the commit. | |
| 41 | + | select { | |
| 42 | + | case got := <-contents: | |
| 43 | + | if got.Async { | |
| 44 | + | t.Fatal("hint applied before commit") | |
| 45 | + | } | |
| 46 | + | case <-time.After(50 * time.Millisecond): | |
| 47 | + | } | |
| 37 | 48 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 38 | 49 | if err := c.Roundtrip(); err != nil { | |
| 39 | 50 | t.Fatal(err) | |
| @@ -48,11 +59,23 @@ func TestTearingHintAppliesOnCommit(t *testing.T) { | |||
| 48 | 59 | } | |
| 49 | 60 | asyncContent(t, contents, w.ID, false) | |
| 50 | 61 | ||
| 62 | + | // A control outliving its surface is inert. | |
| 63 | + | inert := c.AllocateID() | |
| 64 | + | registerProtocol(t, c, inert) | |
| 65 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, inert, surf) | |
| 66 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |
| 67 | + | requestProtocol(t, c, inert, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)) | |
| 68 | + | if err := c.Roundtrip(); err != nil { | |
| 69 | + | t.Fatal(err) | |
| 70 | + | } | |
| 71 | + | unmapped(t, events, w.ID) | |
| 72 | + | ||
| 51 | 73 | // A surface has at most one control. | |
| 74 | + | _, other, _ := surfaceMapper(t, c, events)() | |
| 52 | 75 | first, second := c.AllocateID(), c.AllocateID() | |
| 53 | 76 | registerProtocol(t, c, first) | |
| 54 | 77 | registerProtocol(t, c, second) | |
| 55 | - | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, surf) | |
| 56 | - | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, surf) | |
| 78 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, other) | |
| 79 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, other) | |
| 57 | 80 | expectProtocolError(t, c, manager, uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists)) | |
| 58 | 81 | } | |
| @@ -34,6 +34,17 @@ func TestTearingHintAppliesOnCommit(t *testing.T) { | |||||
| 34 | registerProtocol(t, c, control) | 34 | registerProtocol(t, c, control) | ||
| 35 | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, control, surf) | 35 | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, control, surf) | ||
| 36 | requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)) | 36 | requestProtocol(t, c, control, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)) | ||
| 37 | + | if err := c.Roundtrip(); err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | // Double-buffered: nothing changes before the commit. | |||
| 41 | + | select { | |||
| 42 | + | case got := <-contents: | |||
| 43 | + | if got.Async { | |||
| 44 | + | t.Fatal("hint applied before commit") | |||
| 45 | + | } | |||
| 46 | + | case <-time.After(50 * time.Millisecond): | |||
| 47 | + | } | |||
| 37 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | 48 | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | ||
| 38 | if err := c.Roundtrip(); err != nil { | 49 | if err := c.Roundtrip(); err != nil { | ||
| 39 | t.Fatal(err) | 50 | t.Fatal(err) | ||
| @@ -48,11 +59,23 @@ func TestTearingHintAppliesOnCommit(t *testing.T) { | |||||
| 48 | } | 59 | } | ||
| 49 | asyncContent(t, contents, w.ID, false) | 60 | asyncContent(t, contents, w.ID, false) | ||
| 50 | 61 | ||||
| 62 | + | // A control outliving its surface is inert. | |||
| 63 | + | inert := c.AllocateID() | |||
| 64 | + | registerProtocol(t, c, inert) | |||
| 65 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, inert, surf) | |||
| 66 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy) | |||
| 67 | + | requestProtocol(t, c, inert, tearingcontrol.WpTearingControlV1RequestSetPresentationHint, uint32(tearingcontrol.WpTearingControlV1PresentationHintAsync)) | |||
| 68 | + | if err := c.Roundtrip(); err != nil { | |||
| 69 | + | t.Fatal(err) | |||
| 70 | + | } | |||
| 71 | + | unmapped(t, events, w.ID) | |||
| 72 | + | ||||
| 51 | // A surface has at most one control. | 73 | // A surface has at most one control. | ||
| 74 | + | _, other, _ := surfaceMapper(t, c, events)() | |||
| 52 | first, second := c.AllocateID(), c.AllocateID() | 75 | first, second := c.AllocateID(), c.AllocateID() | ||
| 53 | registerProtocol(t, c, first) | 76 | registerProtocol(t, c, first) | ||
| 54 | registerProtocol(t, c, second) | 77 | registerProtocol(t, c, second) | ||
| 55 | - | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, surf) | 78 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, first, other) |
| 56 | - | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, surf) | 79 | + | requestProtocol(t, c, manager, tearingcontrol.WpTearingControlManagerV1RequestGetTearingControl, second, other) |
| 57 | expectProtocolError(t, c, manager, uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists)) | 80 | expectProtocolError(t, c, manager, uint32(tearingcontrol.WpTearingControlManagerV1ErrorTearingControlExists)) | ||
| 58 | } | 81 | } | ||