fix(drm): keep a display off across modesets, make failed power objects inert and floor idle timeouts
- commit
- 7bfbe4fdf4cdb44128b281343fb6c48c5570093d
- author
- brice · · 2026-09-26 18:20 UTC
- parent
- e2a318d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +78 -16
M internal/adapters/drm/output.go +20 -13
| @@ -95,9 +95,10 @@ type Output struct { | |||
| 95 | 95 | vrrProp uint32 | |
| 96 | 96 | vrrOn bool | |
| 97 | 97 | composedSince time.Time | |
| 98 | - | // off: the CRTC is inactive because a client turned the display off | |
| 99 | - | // (Scene.Off); cleared by every modeset. | |
| 100 | - | off bool | |
| 98 | + | // wantOff is the latest Scene.Off: a client turned the display off. | |
| 99 | + | // off: the CRTC is inactive for it. Every modeset (resume, recovery) | |
| 100 | + | // follows wantOff, so a display turned off never lights up. | |
| 101 | + | wantOff, off bool | |
| 101 | 102 | // kind is how the images were made. | |
| 102 | 103 | kind imageKind | |
| 103 | 104 | // formats receives the direct scanout formats after each modeset; | |
| @@ -413,7 +414,7 @@ func (o *Output) modeset() error { | |||
| 413 | 414 | if err != nil { | |
| 414 | 415 | return err | |
| 415 | 416 | } | |
| 416 | - | if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil { | |
| 417 | + | if err := o.k.commit(o.modesetReq(blob, !o.wantOff), atomicAllowModes, 0); err != nil { | |
| 417 | 418 | _ = o.k.destroyBlob(blob) | |
| 418 | 419 | return fmt.Errorf("modeset: %w", err) | |
| 419 | 420 | } | |
| @@ -426,13 +427,16 @@ func (o *Output) modeset() error { | |||
| 426 | 427 | _ = o.k.destroyBlob(o.modeBlob) | |
| 427 | 428 | } | |
| 428 | 429 | o.modeBlob = blob | |
| 429 | - | o.vrrOn, o.overlayOn, o.off = false, 0, false | |
| 430 | + | o.vrrOn, o.overlayOn, o.off = false, 0, o.wantOff | |
| 430 | 431 | if o.cursor != nil { | |
| 431 | 432 | o.cursor.applied = cursorState{} | |
| 432 | 433 | o.cursor.screen, o.cursor.flying = 0, false | |
| 433 | 434 | } | |
| 434 | - | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | |
| 435 | - | o.testCursor() | |
| 435 | + | o.log.Info().Str("connector", o.conn.name).Bool("off", o.off).Msg("modeset") | |
| 436 | + | if !o.off { | |
| 437 | + | // An inactive CRTC tells nothing about the cursor plane. | |
| 438 | + | o.testCursor() | |
| 439 | + | } | |
| 436 | 440 | o.sendFormats() | |
| 437 | 441 | return nil | |
| 438 | 442 | } | |
| @@ -479,7 +483,8 @@ func (o *Output) testModeset() error { | |||
| 479 | 483 | return err | |
| 480 | 484 | } | |
| 481 | 485 | defer func() { _ = o.k.destroyBlob(blob) }() | |
| 482 | - | if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil { | |
| 486 | + | // Tested active: an inactive CRTC would accept any image. | |
| 487 | + | if err := o.k.commit(o.modesetReq(blob, true), atomicTestOnly|atomicAllowModes, 0); err != nil { | |
| 483 | 488 | return fmt.Errorf("modeset test: %w", err) | |
| 484 | 489 | } | |
| 485 | 490 | return nil | |
| @@ -513,11 +518,12 @@ func (o *Output) cursorOff(err error) { | |||
| 513 | 518 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | |
| 514 | 519 | } | |
| 515 | 520 | ||
| 516 | - | // modesetReq is the modeset of the front image with mode blob. | |
| 517 | - | func (o *Output) modesetReq(blob uint32) *atomicReq { | |
| 521 | + | // modesetReq is the modeset of the front image with mode blob; active | |
| 522 | + | // false keeps the display off (output power). | |
| 523 | + | func (o *Output) modesetReq(blob uint32, active bool) *atomicReq { | |
| 518 | 524 | req := &atomicReq{} | |
| 519 | 525 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | |
| 520 | - | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | |
| 526 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active)) | |
| 521 | 527 | req.set(o.crtc, o.vrrProp, 0) | |
| 522 | 528 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | |
| 523 | 529 | o.primaryProps(req, o.fbs[1-o.back]) | |
| @@ -965,6 +971,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 965 | 971 | stateDirty = true | |
| 966 | 972 | } | |
| 967 | 973 | scene, haveScene, dirty = s, true, true | |
| 974 | + | o.wantOff = s.Off | |
| 968 | 975 | case c := <-cursor: | |
| 969 | 976 | want = c | |
| 970 | 977 | // Before the first scene the scale is unknown: loaded then. | |
| @@ -998,9 +1005,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 998 | 1005 | } | |
| 999 | 1006 | // Output power: off waits for no commit in flight; on is a modeset | |
| 1000 | 1007 | // and a full frame. | |
| 1001 | - | if haveScene && scene.Off != o.off { | |
| 1008 | + | if o.wantOff != o.off { | |
| 1002 | 1009 | var err error | |
| 1003 | - | if scene.Off { | |
| 1010 | + | if o.wantOff { | |
| 1004 | 1011 | err = o.powerOff() | |
| 1005 | 1012 | } else if err = o.modeset(); err == nil { | |
| 1006 | 1013 | dirty = true | |
| @@ -95,9 +95,10 @@ type Output struct { | |||||
| 95 | vrrProp uint32 | 95 | vrrProp uint32 | ||
| 96 | vrrOn bool | 96 | vrrOn bool | ||
| 97 | composedSince time.Time | 97 | composedSince time.Time | ||
| 98 | - | // off: the CRTC is inactive because a client turned the display off | 98 | + | // wantOff is the latest Scene.Off: a client turned the display off. |
| 99 | - | // (Scene.Off); cleared by every modeset. | 99 | + | // off: the CRTC is inactive for it. Every modeset (resume, recovery) |
| 100 | - | off bool | 100 | + | // follows wantOff, so a display turned off never lights up. |
| 101 | + | wantOff, off bool | |||
| 101 | // kind is how the images were made. | 102 | // kind is how the images were made. | ||
| 102 | kind imageKind | 103 | kind imageKind | ||
| 103 | // formats receives the direct scanout formats after each modeset; | 104 | // formats receives the direct scanout formats after each modeset; | ||
| @@ -413,7 +414,7 @@ func (o *Output) modeset() error { | |||||
| 413 | if err != nil { | 414 | if err != nil { | ||
| 414 | return err | 415 | return err | ||
| 415 | } | 416 | } | ||
| 416 | - | if err := o.k.commit(o.modesetReq(blob), atomicAllowModes, 0); err != nil { | 417 | + | if err := o.k.commit(o.modesetReq(blob, !o.wantOff), atomicAllowModes, 0); err != nil { |
| 417 | _ = o.k.destroyBlob(blob) | 418 | _ = o.k.destroyBlob(blob) | ||
| 418 | return fmt.Errorf("modeset: %w", err) | 419 | return fmt.Errorf("modeset: %w", err) | ||
| 419 | } | 420 | } | ||
| @@ -426,13 +427,16 @@ func (o *Output) modeset() error { | |||||
| 426 | _ = o.k.destroyBlob(o.modeBlob) | 427 | _ = o.k.destroyBlob(o.modeBlob) | ||
| 427 | } | 428 | } | ||
| 428 | o.modeBlob = blob | 429 | o.modeBlob = blob | ||
| 429 | - | o.vrrOn, o.overlayOn, o.off = false, 0, false | 430 | + | o.vrrOn, o.overlayOn, o.off = false, 0, o.wantOff |
| 430 | if o.cursor != nil { | 431 | if o.cursor != nil { | ||
| 431 | o.cursor.applied = cursorState{} | 432 | o.cursor.applied = cursorState{} | ||
| 432 | o.cursor.screen, o.cursor.flying = 0, false | 433 | o.cursor.screen, o.cursor.flying = 0, false | ||
| 433 | } | 434 | } | ||
| 434 | - | o.log.Info().Str("connector", o.conn.name).Msg("modeset") | 435 | + | o.log.Info().Str("connector", o.conn.name).Bool("off", o.off).Msg("modeset") |
| 435 | - | o.testCursor() | 436 | + | if !o.off { |
| 437 | + | // An inactive CRTC tells nothing about the cursor plane. | |||
| 438 | + | o.testCursor() | |||
| 439 | + | } | |||
| 436 | o.sendFormats() | 440 | o.sendFormats() | ||
| 437 | return nil | 441 | return nil | ||
| 438 | } | 442 | } | ||
| @@ -479,7 +483,8 @@ func (o *Output) testModeset() error { | |||||
| 479 | return err | 483 | return err | ||
| 480 | } | 484 | } | ||
| 481 | defer func() { _ = o.k.destroyBlob(blob) }() | 485 | defer func() { _ = o.k.destroyBlob(blob) }() | ||
| 482 | - | if err := o.k.commit(o.modesetReq(blob), atomicTestOnly|atomicAllowModes, 0); err != nil { | 486 | + | // Tested active: an inactive CRTC would accept any image. |
| 487 | + | if err := o.k.commit(o.modesetReq(blob, true), atomicTestOnly|atomicAllowModes, 0); err != nil { | |||
| 483 | return fmt.Errorf("modeset test: %w", err) | 488 | return fmt.Errorf("modeset test: %w", err) | ||
| 484 | } | 489 | } | ||
| 485 | return nil | 490 | return nil | ||
| @@ -513,11 +518,12 @@ func (o *Output) cursorOff(err error) { | |||||
| 513 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | 518 | o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("cursor plane refused; hardware cursor off") | ||
| 514 | } | 519 | } | ||
| 515 | 520 | ||||
| 516 | - | // modesetReq is the modeset of the front image with mode blob. | 521 | + | // modesetReq is the modeset of the front image with mode blob; active |
| 517 | - | func (o *Output) modesetReq(blob uint32) *atomicReq { | 522 | + | // false keeps the display off (output power). |
| 523 | + | func (o *Output) modesetReq(blob uint32, active bool) *atomicReq { | |||
| 518 | req := &atomicReq{} | 524 | req := &atomicReq{} | ||
| 519 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | 525 | req.set(o.crtc, o.crtcProps["MODE_ID"], uint64(blob)) | ||
| 520 | - | req.set(o.crtc, o.crtcProps["ACTIVE"], 1) | 526 | + | req.set(o.crtc, o.crtcProps["ACTIVE"], boolValue(active)) |
| 521 | req.set(o.crtc, o.vrrProp, 0) | 527 | req.set(o.crtc, o.vrrProp, 0) | ||
| 522 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | 528 | req.set(o.conn.id, o.connCrtc, uint64(o.crtc)) | ||
| 523 | o.primaryProps(req, o.fbs[1-o.back]) | 529 | o.primaryProps(req, o.fbs[1-o.back]) | ||
| @@ -965,6 +971,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 965 | stateDirty = true | 971 | stateDirty = true | ||
| 966 | } | 972 | } | ||
| 967 | scene, haveScene, dirty = s, true, true | 973 | scene, haveScene, dirty = s, true, true | ||
| 974 | + | o.wantOff = s.Off | |||
| 968 | case c := <-cursor: | 975 | case c := <-cursor: | ||
| 969 | want = c | 976 | want = c | ||
| 970 | // Before the first scene the scale is unknown: loaded then. | 977 | // Before the first scene the scale is unknown: loaded then. | ||
| @@ -998,9 +1005,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 998 | } | 1005 | } | ||
| 999 | // Output power: off waits for no commit in flight; on is a modeset | 1006 | // Output power: off waits for no commit in flight; on is a modeset | ||
| 1000 | // and a full frame. | 1007 | // and a full frame. | ||
| 1001 | - | if haveScene && scene.Off != o.off { | 1008 | + | if o.wantOff != o.off { |
| 1002 | var err error | 1009 | var err error | ||
| 1003 | - | if scene.Off { | 1010 | + | if o.wantOff { |
| 1004 | err = o.powerOff() | 1011 | err = o.powerOff() | ||
| 1005 | } else if err = o.modeset(); err == nil { | 1012 | } else if err = o.modeset(); err == nil { | ||
| 1006 | dirty = true | 1013 | dirty = true | ||
M internal/adapters/drm/power_test.go +14 -1
| @@ -43,10 +43,11 @@ func TestRunOutputPower(t *testing.T) { | |||
| 43 | 43 | } | |
| 44 | 44 | active := func(c commitRec) (uint64, bool) { return c.req.value(tCrtc, pActive) } | |
| 45 | 45 | scenes := make(chan ports.Scene, 1) | |
| 46 | + | vt := make(chan bool) | |
| 46 | 47 | ctx, cancel := context.WithCancel(context.Background()) | |
| 47 | 48 | done := make(chan error, 1) | |
| 48 | 49 | go func() { | |
| 49 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8)) | |
| 50 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8)) | |
| 50 | 51 | }() | |
| 51 | 52 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | |
| 52 | 53 | scenes <- on | |
| @@ -87,6 +88,18 @@ func TestRunOutputPower(t *testing.T) { | |||
| 87 | 88 | t.Fatal("committed while off") | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 91 | + | // A VT switch back modesets while off: the CRTC stays inactive. | |
| 92 | + | vt <- false | |
| 93 | + | vt <- true | |
| 94 | + | waitFor(t, func() bool { return len(snapshot()) > n }) | |
| 95 | + | time.Sleep(20 * time.Millisecond) | |
| 96 | + | for _, c := range snapshot()[n:] { | |
| 97 | + | if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 { | |
| 98 | + | t.Fatal("VT resume turned the display on") | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | n = len(snapshot()) | |
| 102 | + | ||
| 90 | 103 | scenes <- on | |
| 91 | 104 | <-renders | |
| 92 | 105 | waitFor(t, func() bool { | |
| @@ -43,10 +43,11 @@ func TestRunOutputPower(t *testing.T) { | |||||
| 43 | } | 43 | } | ||
| 44 | active := func(c commitRec) (uint64, bool) { return c.req.value(tCrtc, pActive) } | 44 | active := func(c commitRec) (uint64, bool) { return c.req.value(tCrtc, pActive) } | ||
| 45 | scenes := make(chan ports.Scene, 1) | 45 | scenes := make(chan ports.Scene, 1) | ||
| 46 | + | vt := make(chan bool) | |||
| 46 | ctx, cancel := context.WithCancel(context.Background()) | 47 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 47 | done := make(chan error, 1) | 48 | done := make(chan error, 1) | ||
| 48 | go func() { | 49 | go func() { | ||
| 49 | - | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8)) | 50 | + | done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, vt, scenes, make(chan ports.SurfaceContent), nil, make(chan ports.OutputPresented, 8)) |
| 50 | }() | 51 | }() | ||
| 51 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | 52 | on := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1} | ||
| 52 | scenes <- on | 53 | scenes <- on | ||
| @@ -87,6 +88,18 @@ func TestRunOutputPower(t *testing.T) { | |||||
| 87 | t.Fatal("committed while off") | 88 | t.Fatal("committed while off") | ||
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
| 91 | + | // A VT switch back modesets while off: the CRTC stays inactive. | |||
| 92 | + | vt <- false | |||
| 93 | + | vt <- true | |||
| 94 | + | waitFor(t, func() bool { return len(snapshot()) > n }) | |||
| 95 | + | time.Sleep(20 * time.Millisecond) | |||
| 96 | + | for _, c := range snapshot()[n:] { | |||
| 97 | + | if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 { | |||
| 98 | + | t.Fatal("VT resume turned the display on") | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | n = len(snapshot()) | |||
| 102 | + | ||||
| 90 | scenes <- on | 103 | scenes <- on | ||
| 91 | <-renders | 104 | <-renders | ||
| 92 | waitFor(t, func() bool { | 105 | waitFor(t, func() bool { | ||
M internal/adapters/wayland/idle.go +6 -1
| @@ -91,7 +91,9 @@ func (s *Server) armIdle(n *idleNotification) { | |||
| 91 | 91 | return | |
| 92 | 92 | } | |
| 93 | 93 | gen := n.gen | |
| 94 | - | n.timer = time.AfterFunc(n.timeout, func() { | |
| 94 | + | // Core reports activity at most once per ActivityInterval: a shorter | |
| 95 | + | // timeout would fire while the user is still typing. | |
| 96 | + | n.timer = time.AfterFunc(max(n.timeout, ports.ActivityInterval), func() { | |
| 95 | 97 | s.display.Do(func() { | |
| 96 | 98 | // A timer stopped or replaced while it waited for Do is stale. | |
| 97 | 99 | if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) { | |
| @@ -200,8 +202,11 @@ func (s *Server) setOutputsOff(off map[string]bool) { | |||
| 200 | 202 | kept := s.powers[:0] | |
| 201 | 203 | for _, p := range s.powers { | |
| 202 | 204 | if !names[p.output] { | |
| 205 | + | // Failed objects are inert, even if an output of that name | |
| 206 | + | // comes back. | |
| 203 | 207 | p.res.SendFailed() | |
| 204 | 208 | p.res.OnDestroy = nil | |
| 209 | + | p.output = "" | |
| 205 | 210 | continue | |
| 206 | 211 | } | |
| 207 | 212 | if off[p.output] != s.outputsOff[p.output] { | |
| @@ -91,7 +91,9 @@ func (s *Server) armIdle(n *idleNotification) { | |||||
| 91 | return | 91 | return | ||
| 92 | } | 92 | } | ||
| 93 | gen := n.gen | 93 | gen := n.gen | ||
| 94 | - | n.timer = time.AfterFunc(n.timeout, func() { | 94 | + | // Core reports activity at most once per ActivityInterval: a shorter |
| 95 | + | // timeout would fire while the user is still typing. | |||
| 96 | + | n.timer = time.AfterFunc(max(n.timeout, ports.ActivityInterval), func() { | |||
| 95 | s.display.Do(func() { | 97 | s.display.Do(func() { | ||
| 96 | // A timer stopped or replaced while it waited for Do is stale. | 98 | // A timer stopped or replaced while it waited for Do is stale. | ||
| 97 | if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) { | 99 | if n.gen != gen || !n.res.Resource.Alive() || n.idle || s.idleHeld(n) { | ||
| @@ -200,8 +202,11 @@ func (s *Server) setOutputsOff(off map[string]bool) { | |||||
| 200 | kept := s.powers[:0] | 202 | kept := s.powers[:0] | ||
| 201 | for _, p := range s.powers { | 203 | for _, p := range s.powers { | ||
| 202 | if !names[p.output] { | 204 | if !names[p.output] { | ||
| 205 | + | // Failed objects are inert, even if an output of that name | |||
| 206 | + | // comes back. | |||
| 203 | p.res.SendFailed() | 207 | p.res.SendFailed() | ||
| 204 | p.res.OnDestroy = nil | 208 | p.res.OnDestroy = nil | ||
| 209 | + | p.output = "" | |||
| 205 | continue | 210 | continue | ||
| 206 | } | 211 | } | ||
| 207 | if off[p.output] != s.outputsOff[p.output] { | 212 | if off[p.output] != s.outputsOff[p.output] { | ||
M internal/adapters/wayland/idle_test.go +37 -0
| @@ -154,6 +154,43 @@ func TestOutputPower(t *testing.T) { | |||
| 154 | 154 | if e := p.next(t, c, 2*time.Second); e[0] != failed { | |
| 155 | 155 | t.Fatalf("after unplug %v", e) | |
| 156 | 156 | } | |
| 157 | + | // Back with the same name: the failed object controls nothing. | |
| 158 | + | commands <- ports.SetOutputs{Outputs: testOutputs} | |
| 159 | + | requestProtocol(t, c, id, wlroutputpower.ZwlrOutputPowerV1RequestSetMode, off) | |
| 160 | + | if err := c.Roundtrip(); err != nil { | |
| 161 | + | t.Fatal(err) | |
| 162 | + | } | |
| 163 | + | select { | |
| 164 | + | case ev := <-events: | |
| 165 | + | if v, ok := ev.(ports.OutputPower); ok { | |
| 166 | + | t.Fatalf("failed object sent %+v", v) | |
| 167 | + | } | |
| 168 | + | case <-time.After(50 * time.Millisecond): | |
| 169 | + | } | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | // A timeout shorter than the activity interval still waits for it, so | |
| 173 | + | // continuous input keeps the notification from idling. | |
| 174 | + | func TestIdleShortTimeout(t *testing.T) { | |
| 175 | + | s, _, commands, _, dir := contentServer(t) | |
| 176 | + | c := protocolClient(t, s, dir) | |
| 177 | + | seat := bindProtocol(t, c, "wl_seat") | |
| 178 | + | registerProtocol(t, c, seat) | |
| 179 | + | notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2) | |
| 180 | + | id, n := newEventProxy(c) | |
| 181 | + | start := time.Now() | |
| 182 | + | requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat) | |
| 183 | + | n.next(t, c, 2*time.Second) | |
| 184 | + | if d := time.Since(start); d < ports.ActivityInterval { | |
| 185 | + | t.Fatalf("idled after %v, before the activity interval", d) | |
| 186 | + | } | |
| 187 | + | commands <- ports.UserActivity{} | |
| 188 | + | n.next(t, c, 2*time.Second) // resumed | |
| 189 | + | for range 4 { | |
| 190 | + | time.Sleep(ports.ActivityInterval / 2) | |
| 191 | + | commands <- ports.UserActivity{} | |
| 192 | + | } | |
| 193 | + | n.none(t, c, ports.ActivityInterval/2) | |
| 157 | 194 | } | |
| 158 | 195 | ||
| 159 | 196 | // An unknown mode is a protocol error. | |
| @@ -154,6 +154,43 @@ func TestOutputPower(t *testing.T) { | |||||
| 154 | if e := p.next(t, c, 2*time.Second); e[0] != failed { | 154 | if e := p.next(t, c, 2*time.Second); e[0] != failed { | ||
| 155 | t.Fatalf("after unplug %v", e) | 155 | t.Fatalf("after unplug %v", e) | ||
| 156 | } | 156 | } | ||
| 157 | + | // Back with the same name: the failed object controls nothing. | |||
| 158 | + | commands <- ports.SetOutputs{Outputs: testOutputs} | |||
| 159 | + | requestProtocol(t, c, id, wlroutputpower.ZwlrOutputPowerV1RequestSetMode, off) | |||
| 160 | + | if err := c.Roundtrip(); err != nil { | |||
| 161 | + | t.Fatal(err) | |||
| 162 | + | } | |||
| 163 | + | select { | |||
| 164 | + | case ev := <-events: | |||
| 165 | + | if v, ok := ev.(ports.OutputPower); ok { | |||
| 166 | + | t.Fatalf("failed object sent %+v", v) | |||
| 167 | + | } | |||
| 168 | + | case <-time.After(50 * time.Millisecond): | |||
| 169 | + | } | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | // A timeout shorter than the activity interval still waits for it, so | |||
| 173 | + | // continuous input keeps the notification from idling. | |||
| 174 | + | func TestIdleShortTimeout(t *testing.T) { | |||
| 175 | + | s, _, commands, _, dir := contentServer(t) | |||
| 176 | + | c := protocolClient(t, s, dir) | |||
| 177 | + | seat := bindProtocol(t, c, "wl_seat") | |||
| 178 | + | registerProtocol(t, c, seat) | |||
| 179 | + | notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2) | |||
| 180 | + | id, n := newEventProxy(c) | |||
| 181 | + | start := time.Now() | |||
| 182 | + | requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat) | |||
| 183 | + | n.next(t, c, 2*time.Second) | |||
| 184 | + | if d := time.Since(start); d < ports.ActivityInterval { | |||
| 185 | + | t.Fatalf("idled after %v, before the activity interval", d) | |||
| 186 | + | } | |||
| 187 | + | commands <- ports.UserActivity{} | |||
| 188 | + | n.next(t, c, 2*time.Second) // resumed | |||
| 189 | + | for range 4 { | |||
| 190 | + | time.Sleep(ports.ActivityInterval / 2) | |||
| 191 | + | commands <- ports.UserActivity{} | |||
| 192 | + | } | |||
| 193 | + | n.none(t, c, ports.ActivityInterval/2) | |||
| 157 | } | 194 | } | ||
| 158 | 195 | ||||
| 159 | // An unknown mode is a protocol error. | 196 | // An unknown mode is a protocol error. | ||
M internal/ports/messages.go +1 -1
| @@ -415,7 +415,7 @@ type UserActivity struct{} | |||
| 415 | 415 | func (UserActivity) clientCommand() {} | |
| 416 | 416 | ||
| 417 | 417 | // ActivityInterval is the most often core reports UserActivity: idle | |
| 418 | - | // timers may start up to this much early. | |
| 418 | + | // timers may start up to this much early, so no idle timeout is shorter. | |
| 419 | 419 | const ActivityInterval = 100 * time.Millisecond | |
| 420 | 420 | ||
| 421 | 421 | // ConfigureWindow carries core → wayland geometry and state. Output is the | |
| @@ -415,7 +415,7 @@ type UserActivity struct{} | |||||
| 415 | func (UserActivity) clientCommand() {} | 415 | func (UserActivity) clientCommand() {} | ||
| 416 | 416 | ||||
| 417 | // ActivityInterval is the most often core reports UserActivity: idle | 417 | // ActivityInterval is the most often core reports UserActivity: idle | ||
| 418 | - | // timers may start up to this much early. | 418 | + | // timers may start up to this much early, so no idle timeout is shorter. |
| 419 | const ActivityInterval = 100 * time.Millisecond | 419 | const ActivityInterval = 100 * time.Millisecond | ||
| 420 | 420 | ||||
| 421 | // ConfigureWindow carries core → wayland geometry and state. Output is the | 421 | // ConfigureWindow carries core → wayland geometry and state. Output is the | ||