refactor(daemon): route producer rendering through coordinator
- commit
- df0fe5f85aadfd08e9f6fbb705875d22ef7e34b6
- author
- brice · · 2026-07-11 15:08 UTC
- parent
- 4eb20a0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
14 files · +298 -84
M internal/usecase/daemon/attention.go +2 -2
| @@ -49,7 +49,7 @@ func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) { | |||
| 49 | 49 | if idx, ok := oldestAttentionTab(sess); ok { | |
| 50 | 50 | if sess.switchTab(idx) { | |
| 51 | 51 | d.activateTab(sess, sess.activeTab()) | |
| 52 | - | d.paint(sess, ac, true) | |
| 52 | + | d.invalidateRender(sess, ac, true, "attention.go") | |
| 53 | 53 | } | |
| 54 | 54 | return | |
| 55 | 55 | } | |
| @@ -119,7 +119,7 @@ func (d *Daemon) repaintAttachedClients(sess *session) { | |||
| 119 | 119 | ac := sess.client | |
| 120 | 120 | sess.mu.Unlock() | |
| 121 | 121 | if ac != nil { | |
| 122 | - | d.paint(sess, ac, false) | |
| 122 | + | d.invalidateRender(sess, ac, false, "attention.go") | |
| 123 | 123 | } | |
| 124 | 124 | } | |
| 125 | 125 | ||
| @@ -49,7 +49,7 @@ func (d *Daemon) jumpAttention(sess *session, ac *attachedClient) { | |||||
| 49 | if idx, ok := oldestAttentionTab(sess); ok { | 49 | if idx, ok := oldestAttentionTab(sess); ok { | ||
| 50 | if sess.switchTab(idx) { | 50 | if sess.switchTab(idx) { | ||
| 51 | d.activateTab(sess, sess.activeTab()) | 51 | d.activateTab(sess, sess.activeTab()) | ||
| 52 | - | d.paint(sess, ac, true) | 52 | + | d.invalidateRender(sess, ac, true, "attention.go") |
| 53 | } | 53 | } | ||
| 54 | return | 54 | return | ||
| 55 | } | 55 | } | ||
| @@ -119,7 +119,7 @@ func (d *Daemon) repaintAttachedClients(sess *session) { | |||||
| 119 | ac := sess.client | 119 | ac := sess.client | ||
| 120 | sess.mu.Unlock() | 120 | sess.mu.Unlock() | ||
| 121 | if ac != nil { | 121 | if ac != nil { | ||
| 122 | - | d.paint(sess, ac, false) | 122 | + | d.invalidateRender(sess, ac, false, "attention.go") |
| 123 | } | 123 | } | ||
| 124 | } | 124 | } | ||
| 125 | 125 | ||||
M internal/usecase/daemon/client.go +26 -3
| @@ -396,6 +396,22 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||
| 396 | 396 | sess.client = ac | |
| 397 | 397 | name := sess.name | |
| 398 | 398 | sess.mu.Unlock() | |
| 399 | + | // A session has exactly one coordinator; replacement changes only its | |
| 400 | + | // attachment identity and the private transitional compositor target. | |
| 401 | + | rc := sess.renderCoordinator() | |
| 402 | + | if rc == nil { | |
| 403 | + | rc = newRenderCoordinator(renderCoordinatorOptions{ | |
| 404 | + | clock: d.clock, | |
| 405 | + | wake: func(w renderWake) { d.paint(sess, ac, w.reset) }, | |
| 406 | + | ackReady: func() bool { return true }, | |
| 407 | + | syncActive: func() bool { return false }, | |
| 408 | + | }) | |
| 409 | + | sess.installRenderCoordinator(rc) | |
| 410 | + | } | |
| 411 | + | if old != nil { | |
| 412 | + | rc.noteReplace(old, ac) | |
| 413 | + | } | |
| 414 | + | rc.attach(ac) | |
| 399 | 415 | d.touchMRU(sess) | |
| 400 | 416 | d.log.Info("client attached", "session", name, "resume", opts.resumeCapable) | |
| 401 | 417 | d.applyHostTheme(sess, ac, d.effectiveTheme(themeui.Theme{}), true) | |
| @@ -449,7 +465,7 @@ func (d *Daemon) firstPaint(sess *session, ac *attachedClient, clientSize domain | |||
| 449 | 465 | d.resizeForFirstPaint(sess, ac, clientSize) | |
| 450 | 466 | } | |
| 451 | 467 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | |
| 452 | - | d.paint(sess, ac, true) | |
| 468 | + | d.invalidateRender(sess, ac, true, "client.go") | |
| 453 | 469 | d.activateTab(sess, tb) | |
| 454 | 470 | } | |
| 455 | 471 | ||
| @@ -504,7 +520,7 @@ func (d *Daemon) runConnLoop(ac *attachedClient) { | |||
| 504 | 520 | if ack, derr := ports.UnmarshalAck(f.Payload); derr == nil { | |
| 505 | 521 | ac.ackOutputState(ack.AckedStateNum) | |
| 506 | 522 | if reset, ok := ac.takeDeferredPaint(); ok { | |
| 507 | - | d.paint(sess, ac, reset) | |
| 523 | + | d.invalidateRender(sess, ac, reset, "client.go") | |
| 508 | 524 | } | |
| 509 | 525 | } | |
| 510 | 526 | case ports.MsgPing: | |
| @@ -526,6 +542,9 @@ func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.Tran | |||
| 526 | 542 | return // already displaced by a newer client; nothing to do | |
| 527 | 543 | } | |
| 528 | 544 | ac.cancelResizePaint() | |
| 545 | + | if rc := sess.renderCoordinator(); rc != nil { | |
| 546 | + | rc.noteDetach(ac) | |
| 547 | + | } | |
| 529 | 548 | d.unregisterPreview(ac) | |
| 530 | 549 | sess.mu.Lock() | |
| 531 | 550 | ephemeral := sess.ephemeral | |
| @@ -576,7 +595,7 @@ func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme) | |||
| 576 | 595 | if !d.applyHostTheme(sess, ac, t, false) { | |
| 577 | 596 | return | |
| 578 | 597 | } | |
| 579 | - | d.paint(sess, ac, true) | |
| 598 | + | d.invalidateRender(sess, ac, true, "client.go") | |
| 580 | 599 | } | |
| 581 | 600 | ||
| 582 | 601 | func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | |
| @@ -603,6 +622,10 @@ func (d *Daemon) resizeWithPaint(sess *session, ac *attachedClient, sz domain.Si | |||
| 603 | 622 | return | |
| 604 | 623 | } | |
| 605 | 624 | ac.size = sz | |
| 625 | + | if rc := sess.renderCoordinator(); rc != nil { | |
| 626 | + | rc.attach(ac) | |
| 627 | + | rc.recordResizeRequest(sz, ac) | |
| 628 | + | } | |
| 606 | 629 | } | |
| 607 | 630 | tbSize := tabSize(sz) | |
| 608 | 631 | ||
| @@ -396,6 +396,22 @@ func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, | |||||
| 396 | sess.client = ac | 396 | sess.client = ac | ||
| 397 | name := sess.name | 397 | name := sess.name | ||
| 398 | sess.mu.Unlock() | 398 | sess.mu.Unlock() | ||
| 399 | + | // A session has exactly one coordinator; replacement changes only its | |||
| 400 | + | // attachment identity and the private transitional compositor target. | |||
| 401 | + | rc := sess.renderCoordinator() | |||
| 402 | + | if rc == nil { | |||
| 403 | + | rc = newRenderCoordinator(renderCoordinatorOptions{ | |||
| 404 | + | clock: d.clock, | |||
| 405 | + | wake: func(w renderWake) { d.paint(sess, ac, w.reset) }, | |||
| 406 | + | ackReady: func() bool { return true }, | |||
| 407 | + | syncActive: func() bool { return false }, | |||
| 408 | + | }) | |||
| 409 | + | sess.installRenderCoordinator(rc) | |||
| 410 | + | } | |||
| 411 | + | if old != nil { | |||
| 412 | + | rc.noteReplace(old, ac) | |||
| 413 | + | } | |||
| 414 | + | rc.attach(ac) | |||
| 399 | d.touchMRU(sess) | 415 | d.touchMRU(sess) | ||
| 400 | d.log.Info("client attached", "session", name, "resume", opts.resumeCapable) | 416 | d.log.Info("client attached", "session", name, "resume", opts.resumeCapable) | ||
| 401 | d.applyHostTheme(sess, ac, d.effectiveTheme(themeui.Theme{}), true) | 417 | d.applyHostTheme(sess, ac, d.effectiveTheme(themeui.Theme{}), true) | ||
| @@ -449,7 +465,7 @@ func (d *Daemon) firstPaint(sess *session, ac *attachedClient, clientSize domain | |||||
| 449 | d.resizeForFirstPaint(sess, ac, clientSize) | 465 | d.resizeForFirstPaint(sess, ac, clientSize) | ||
| 450 | } | 466 | } | ||
| 451 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | 467 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | ||
| 452 | - | d.paint(sess, ac, true) | 468 | + | d.invalidateRender(sess, ac, true, "client.go") |
| 453 | d.activateTab(sess, tb) | 469 | d.activateTab(sess, tb) | ||
| 454 | } | 470 | } | ||
| 455 | 471 | ||||
| @@ -504,7 +520,7 @@ func (d *Daemon) runConnLoop(ac *attachedClient) { | |||||
| 504 | if ack, derr := ports.UnmarshalAck(f.Payload); derr == nil { | 520 | if ack, derr := ports.UnmarshalAck(f.Payload); derr == nil { | ||
| 505 | ac.ackOutputState(ack.AckedStateNum) | 521 | ac.ackOutputState(ack.AckedStateNum) | ||
| 506 | if reset, ok := ac.takeDeferredPaint(); ok { | 522 | if reset, ok := ac.takeDeferredPaint(); ok { | ||
| 507 | - | d.paint(sess, ac, reset) | 523 | + | d.invalidateRender(sess, ac, reset, "client.go") |
| 508 | } | 524 | } | ||
| 509 | } | 525 | } | ||
| 510 | case ports.MsgPing: | 526 | case ports.MsgPing: | ||
| @@ -526,6 +542,9 @@ func (d *Daemon) clientGone(sess *session, ac *attachedClient, failed ports.Tran | |||||
| 526 | return // already displaced by a newer client; nothing to do | 542 | return // already displaced by a newer client; nothing to do | ||
| 527 | } | 543 | } | ||
| 528 | ac.cancelResizePaint() | 544 | ac.cancelResizePaint() | ||
| 545 | + | if rc := sess.renderCoordinator(); rc != nil { | |||
| 546 | + | rc.noteDetach(ac) | |||
| 547 | + | } | |||
| 529 | d.unregisterPreview(ac) | 548 | d.unregisterPreview(ac) | ||
| 530 | sess.mu.Lock() | 549 | sess.mu.Lock() | ||
| 531 | ephemeral := sess.ephemeral | 550 | ephemeral := sess.ephemeral | ||
| @@ -576,7 +595,7 @@ func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme) | |||||
| 576 | if !d.applyHostTheme(sess, ac, t, false) { | 595 | if !d.applyHostTheme(sess, ac, t, false) { | ||
| 577 | return | 596 | return | ||
| 578 | } | 597 | } | ||
| 579 | - | d.paint(sess, ac, true) | 598 | + | d.invalidateRender(sess, ac, true, "client.go") |
| 580 | } | 599 | } | ||
| 581 | 600 | ||||
| 582 | func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | 601 | func (d *Daemon) resize(sess *session, ac *attachedClient, sz domain.Size) { | ||
| @@ -603,6 +622,10 @@ func (d *Daemon) resizeWithPaint(sess *session, ac *attachedClient, sz domain.Si | |||||
| 603 | return | 622 | return | ||
| 604 | } | 623 | } | ||
| 605 | ac.size = sz | 624 | ac.size = sz | ||
| 625 | + | if rc := sess.renderCoordinator(); rc != nil { | |||
| 626 | + | rc.attach(ac) | |||
| 627 | + | rc.recordResizeRequest(sz, ac) | |||
| 628 | + | } | |||
| 606 | } | 629 | } | ||
| 607 | tbSize := tabSize(sz) | 630 | tbSize := tabSize(sz) | ||
| 608 | 631 | ||||
M internal/usecase/daemon/copymode.go +8 -8
| @@ -62,7 +62,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||
| 62 | 62 | if delta > 0 && rt.copyMode.AtBottom(*document) { | |
| 63 | 63 | rt.clearCopyModeLocked() | |
| 64 | 64 | rt.copyMu.Unlock() | |
| 65 | - | d.paint(sess, ac, true) | |
| 65 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 66 | 66 | return | |
| 67 | 67 | } | |
| 68 | 68 | rt.copyMode.Move(*document, delta) | |
| @@ -72,7 +72,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||
| 72 | 72 | } | |
| 73 | 73 | rt.copyMu.Unlock() | |
| 74 | 74 | ||
| 75 | - | d.paint(sess, ac, true) | |
| 75 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | 78 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |
| @@ -94,7 +94,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||
| 94 | 94 | if !d.publishCopyMode(sess, ac, tb, p, document, nil) { | |
| 95 | 95 | return | |
| 96 | 96 | } | |
| 97 | - | d.paint(sess, ac, true) | |
| 97 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | 100 | // publishCopyMode installs a non-renderable candidate, validates its captured | |
| @@ -190,7 +190,7 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||
| 190 | 190 | rt.copyMu.Unlock() | |
| 191 | 191 | ||
| 192 | 192 | if changed { | |
| 193 | - | d.paint(sess, ac, true) | |
| 193 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 194 | 194 | } | |
| 195 | 195 | } | |
| 196 | 196 | ||
| @@ -220,7 +220,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 220 | 220 | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, *document, data) | |
| 221 | 221 | rt.copyMu.Unlock() | |
| 222 | 222 | if changed || closeSearch || accepted { | |
| 223 | - | d.paint(sess, ac, true) | |
| 223 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 224 | 224 | } | |
| 225 | 225 | return | |
| 226 | 226 | } | |
| @@ -309,11 +309,11 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 309 | 309 | rt.copyMu.Unlock() | |
| 310 | 310 | } | |
| 311 | 311 | if exit { | |
| 312 | - | d.paint(sess, ac, true) | |
| 312 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 313 | 313 | return | |
| 314 | 314 | } | |
| 315 | 315 | if changed { | |
| 316 | - | d.paint(sess, ac, true) | |
| 316 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 317 | 317 | } | |
| 318 | 318 | } | |
| 319 | 319 | ||
| @@ -386,7 +386,7 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||
| 386 | 386 | rt.copyMu.Unlock() | |
| 387 | 387 | ||
| 388 | 388 | if sess := ac.currentSession(); sess != nil { | |
| 389 | - | d.paint(sess, ac, true) | |
| 389 | + | d.invalidateRender(sess, ac, true, "copymode.go") | |
| 390 | 390 | } | |
| 391 | 391 | }) | |
| 392 | 392 | } | |
| @@ -62,7 +62,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||||
| 62 | if delta > 0 && rt.copyMode.AtBottom(*document) { | 62 | if delta > 0 && rt.copyMode.AtBottom(*document) { | ||
| 63 | rt.clearCopyModeLocked() | 63 | rt.clearCopyModeLocked() | ||
| 64 | rt.copyMu.Unlock() | 64 | rt.copyMu.Unlock() | ||
| 65 | - | d.paint(sess, ac, true) | 65 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 66 | return | 66 | return | ||
| 67 | } | 67 | } | ||
| 68 | rt.copyMode.Move(*document, delta) | 68 | rt.copyMode.Move(*document, delta) | ||
| @@ -72,7 +72,7 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||||
| 72 | } | 72 | } | ||
| 73 | rt.copyMu.Unlock() | 73 | rt.copyMu.Unlock() | ||
| 74 | 74 | ||||
| 75 | - | d.paint(sess, ac, true) | 75 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | 78 | func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | ||
| @@ -94,7 +94,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||||
| 94 | if !d.publishCopyMode(sess, ac, tb, p, document, nil) { | 94 | if !d.publishCopyMode(sess, ac, tb, p, document, nil) { | ||
| 95 | return | 95 | return | ||
| 96 | } | 96 | } | ||
| 97 | - | d.paint(sess, ac, true) | 97 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 98 | } | 98 | } | ||
| 99 | 99 | ||||
| 100 | // publishCopyMode installs a non-renderable candidate, validates its captured | 100 | // publishCopyMode installs a non-renderable candidate, validates its captured | ||
| @@ -190,7 +190,7 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||||
| 190 | rt.copyMu.Unlock() | 190 | rt.copyMu.Unlock() | ||
| 191 | 191 | ||||
| 192 | if changed { | 192 | if changed { | ||
| 193 | - | d.paint(sess, ac, true) | 193 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 194 | } | 194 | } | ||
| 195 | } | 195 | } | ||
| 196 | 196 | ||||
| @@ -220,7 +220,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 220 | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, *document, data) | 220 | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, *document, data) | ||
| 221 | rt.copyMu.Unlock() | 221 | rt.copyMu.Unlock() | ||
| 222 | if changed || closeSearch || accepted { | 222 | if changed || closeSearch || accepted { | ||
| 223 | - | d.paint(sess, ac, true) | 223 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 224 | } | 224 | } | ||
| 225 | return | 225 | return | ||
| 226 | } | 226 | } | ||
| @@ -309,11 +309,11 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 309 | rt.copyMu.Unlock() | 309 | rt.copyMu.Unlock() | ||
| 310 | } | 310 | } | ||
| 311 | if exit { | 311 | if exit { | ||
| 312 | - | d.paint(sess, ac, true) | 312 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 313 | return | 313 | return | ||
| 314 | } | 314 | } | ||
| 315 | if changed { | 315 | if changed { | ||
| 316 | - | d.paint(sess, ac, true) | 316 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 317 | } | 317 | } | ||
| 318 | } | 318 | } | ||
| 319 | 319 | ||||
| @@ -386,7 +386,7 @@ func (d *Daemon) retainCopyESCLocked(ac *attachedClient) { | |||||
| 386 | rt.copyMu.Unlock() | 386 | rt.copyMu.Unlock() | ||
| 387 | 387 | ||||
| 388 | if sess := ac.currentSession(); sess != nil { | 388 | if sess := ac.currentSession(); sess != nil { | ||
| 389 | - | d.paint(sess, ac, true) | 389 | + | d.invalidateRender(sess, ac, true, "copymode.go") |
| 390 | } | 390 | } | ||
| 391 | }) | 391 | }) | ||
| 392 | } | 392 | } | ||
M internal/usecase/daemon/floating.go +4 -4
| @@ -192,7 +192,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||
| 192 | 192 | if start { | |
| 193 | 193 | d.launchFloating(sess, tb, cfg, generation, true) | |
| 194 | 194 | if ac != nil { | |
| 195 | - | d.paint(sess, ac, true) | |
| 195 | + | d.invalidateRender(sess, ac, true, "floating.go") | |
| 196 | 196 | } | |
| 197 | 197 | return nil | |
| 198 | 198 | } | |
| @@ -200,7 +200,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||
| 200 | 200 | d.resizeActiveFloating(tb) | |
| 201 | 201 | } | |
| 202 | 202 | if ac != nil { | |
| 203 | - | d.paint(sess, ac, true) | |
| 203 | + | d.invalidateRender(sess, ac, true, "floating.go") | |
| 204 | 204 | } | |
| 205 | 205 | return nil | |
| 206 | 206 | } | |
| @@ -348,7 +348,7 @@ func (d *Daemon) installFloating(sess *session, tb *tab, p *pane, generation uin | |||
| 348 | 348 | ac := sess.client | |
| 349 | 349 | sess.mu.Unlock() | |
| 350 | 350 | if ac != nil { | |
| 351 | - | d.paint(sess, ac, true) | |
| 351 | + | d.invalidateRender(sess, ac, true, "floating.go") | |
| 352 | 352 | } | |
| 353 | 353 | } | |
| 354 | 354 | } | |
| @@ -369,7 +369,7 @@ func (d *Daemon) reapFloating(sess *session, tb *tab, p *pane, generation uint64 | |||
| 369 | 369 | sess.mu.Unlock() | |
| 370 | 370 | copyCleared := ac != nil && ac.overlays.clearCopyModeForPane(p) | |
| 371 | 371 | if ac != nil && (visible || copyCleared) { | |
| 372 | - | d.paint(sess, ac, true) | |
| 372 | + | d.invalidateRender(sess, ac, true, "floating.go") | |
| 373 | 373 | } | |
| 374 | 374 | } | |
| 375 | 375 | ||
| @@ -192,7 +192,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||||
| 192 | if start { | 192 | if start { | ||
| 193 | d.launchFloating(sess, tb, cfg, generation, true) | 193 | d.launchFloating(sess, tb, cfg, generation, true) | ||
| 194 | if ac != nil { | 194 | if ac != nil { | ||
| 195 | - | d.paint(sess, ac, true) | 195 | + | d.invalidateRender(sess, ac, true, "floating.go") |
| 196 | } | 196 | } | ||
| 197 | return nil | 197 | return nil | ||
| 198 | } | 198 | } | ||
| @@ -200,7 +200,7 @@ func (d *Daemon) toggleFloating(sess *session, ac *attachedClient) error { | |||||
| 200 | d.resizeActiveFloating(tb) | 200 | d.resizeActiveFloating(tb) | ||
| 201 | } | 201 | } | ||
| 202 | if ac != nil { | 202 | if ac != nil { | ||
| 203 | - | d.paint(sess, ac, true) | 203 | + | d.invalidateRender(sess, ac, true, "floating.go") |
| 204 | } | 204 | } | ||
| 205 | return nil | 205 | return nil | ||
| 206 | } | 206 | } | ||
| @@ -348,7 +348,7 @@ func (d *Daemon) installFloating(sess *session, tb *tab, p *pane, generation uin | |||||
| 348 | ac := sess.client | 348 | ac := sess.client | ||
| 349 | sess.mu.Unlock() | 349 | sess.mu.Unlock() | ||
| 350 | if ac != nil { | 350 | if ac != nil { | ||
| 351 | - | d.paint(sess, ac, true) | 351 | + | d.invalidateRender(sess, ac, true, "floating.go") |
| 352 | } | 352 | } | ||
| 353 | } | 353 | } | ||
| 354 | } | 354 | } | ||
| @@ -369,7 +369,7 @@ func (d *Daemon) reapFloating(sess *session, tb *tab, p *pane, generation uint64 | |||||
| 369 | sess.mu.Unlock() | 369 | sess.mu.Unlock() | ||
| 370 | copyCleared := ac != nil && ac.overlays.clearCopyModeForPane(p) | 370 | copyCleared := ac != nil && ac.overlays.clearCopyModeForPane(p) | ||
| 371 | if ac != nil && (visible || copyCleared) { | 371 | if ac != nil && (visible || copyCleared) { | ||
| 372 | - | d.paint(sess, ac, true) | 372 | + | d.invalidateRender(sess, ac, true, "floating.go") |
| 373 | } | 373 | } | ||
| 374 | } | 374 | } | ||
| 375 | 375 | ||||
M internal/usecase/daemon/input.go +5 -5
| @@ -118,7 +118,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 118 | 118 | if target.ID != oldFocus { | |
| 119 | 119 | d.refreshPaneTitleOnFocus(sess, target.ID) | |
| 120 | 120 | } | |
| 121 | - | d.paint(sess, ac, true) | |
| 121 | + | d.invalidateRender(sess, ac, true, "input.go") | |
| 122 | 122 | return | |
| 123 | 123 | } | |
| 124 | 124 | } | |
| @@ -174,7 +174,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 174 | 174 | d.exitCopyMode(ac) | |
| 175 | 175 | d.refreshPaneTitleOnFocus(sess, pl.ID) | |
| 176 | 176 | } | |
| 177 | - | d.paint(sess, ac, true) | |
| 177 | + | d.invalidateRender(sess, ac, true, "input.go") | |
| 178 | 178 | return | |
| 179 | 179 | } | |
| 180 | 180 | var p *pane | |
| @@ -195,7 +195,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||
| 195 | 195 | if isMouseFocusPress(ev) && pl.ID != oldFocus { | |
| 196 | 196 | d.exitCopyMode(ac) | |
| 197 | 197 | d.refreshPaneTitleOnFocus(sess, pl.ID) | |
| 198 | - | d.paint(sess, ac, true) | |
| 198 | + | d.invalidateRender(sess, ac, true, "input.go") | |
| 199 | 199 | } | |
| 200 | 200 | if multi { | |
| 201 | 201 | ev = translateMouseEvent(ev, pl.Content.X, pl.Content.Y) | |
| @@ -290,7 +290,7 @@ func (d *Daemon) handleTerminalMouse(sess *session, ac *attachedClient, p *pane, | |||
| 290 | 290 | rt.copyDragging = true | |
| 291 | 291 | } | |
| 292 | 292 | rt.copyMu.Unlock() | |
| 293 | - | d.paint(sess, ac, true) | |
| 293 | + | d.invalidateRender(sess, ac, true, "input.go") | |
| 294 | 294 | case mouse.Release: | |
| 295 | 295 | rt.copyMu.Lock() | |
| 296 | 296 | rt.normalMousePressValid = false | |
| @@ -432,7 +432,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||
| 432 | 432 | idx := int(action - keys.ActionSwitchTab1) | |
| 433 | 433 | if sess.switchTab(idx) { | |
| 434 | 434 | h.d.activateTab(sess, sess.activeTab()) | |
| 435 | - | h.d.paint(sess, h.ac, true) | |
| 435 | + | h.d.invalidateRender(sess, h.ac, true, "input.go") | |
| 436 | 436 | } | |
| 437 | 437 | } | |
| 438 | 438 | } | |
| @@ -118,7 +118,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 118 | if target.ID != oldFocus { | 118 | if target.ID != oldFocus { | ||
| 119 | d.refreshPaneTitleOnFocus(sess, target.ID) | 119 | d.refreshPaneTitleOnFocus(sess, target.ID) | ||
| 120 | } | 120 | } | ||
| 121 | - | d.paint(sess, ac, true) | 121 | + | d.invalidateRender(sess, ac, true, "input.go") |
| 122 | return | 122 | return | ||
| 123 | } | 123 | } | ||
| 124 | } | 124 | } | ||
| @@ -174,7 +174,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 174 | d.exitCopyMode(ac) | 174 | d.exitCopyMode(ac) | ||
| 175 | d.refreshPaneTitleOnFocus(sess, pl.ID) | 175 | d.refreshPaneTitleOnFocus(sess, pl.ID) | ||
| 176 | } | 176 | } | ||
| 177 | - | d.paint(sess, ac, true) | 177 | + | d.invalidateRender(sess, ac, true, "input.go") |
| 178 | return | 178 | return | ||
| 179 | } | 179 | } | ||
| 180 | var p *pane | 180 | var p *pane | ||
| @@ -195,7 +195,7 @@ func (d *Daemon) handleMouse(ac *attachedClient, ev mouse.Event) { | |||||
| 195 | if isMouseFocusPress(ev) && pl.ID != oldFocus { | 195 | if isMouseFocusPress(ev) && pl.ID != oldFocus { | ||
| 196 | d.exitCopyMode(ac) | 196 | d.exitCopyMode(ac) | ||
| 197 | d.refreshPaneTitleOnFocus(sess, pl.ID) | 197 | d.refreshPaneTitleOnFocus(sess, pl.ID) | ||
| 198 | - | d.paint(sess, ac, true) | 198 | + | d.invalidateRender(sess, ac, true, "input.go") |
| 199 | } | 199 | } | ||
| 200 | if multi { | 200 | if multi { | ||
| 201 | ev = translateMouseEvent(ev, pl.Content.X, pl.Content.Y) | 201 | ev = translateMouseEvent(ev, pl.Content.X, pl.Content.Y) | ||
| @@ -290,7 +290,7 @@ func (d *Daemon) handleTerminalMouse(sess *session, ac *attachedClient, p *pane, | |||||
| 290 | rt.copyDragging = true | 290 | rt.copyDragging = true | ||
| 291 | } | 291 | } | ||
| 292 | rt.copyMu.Unlock() | 292 | rt.copyMu.Unlock() | ||
| 293 | - | d.paint(sess, ac, true) | 293 | + | d.invalidateRender(sess, ac, true, "input.go") |
| 294 | case mouse.Release: | 294 | case mouse.Release: | ||
| 295 | rt.copyMu.Lock() | 295 | rt.copyMu.Lock() | ||
| 296 | rt.normalMousePressValid = false | 296 | rt.normalMousePressValid = false | ||
| @@ -432,7 +432,7 @@ func (h daemonKeyHandler) Action(action keys.Action) { | |||||
| 432 | idx := int(action - keys.ActionSwitchTab1) | 432 | idx := int(action - keys.ActionSwitchTab1) | ||
| 433 | if sess.switchTab(idx) { | 433 | if sess.switchTab(idx) { | ||
| 434 | h.d.activateTab(sess, sess.activeTab()) | 434 | h.d.activateTab(sess, sess.activeTab()) | ||
| 435 | - | h.d.paint(sess, h.ac, true) | 435 | + | h.d.invalidateRender(sess, h.ac, true, "input.go") |
| 436 | } | 436 | } | ||
| 437 | } | 437 | } | ||
| 438 | } | 438 | } | ||
M internal/usecase/daemon/palette.go +9 -9
| @@ -42,7 +42,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | |||
| 42 | 42 | ac.overlays.paletteHints = palette.ContextualHints{} | |
| 43 | 43 | ac.overlays.palettePending = nil | |
| 44 | 44 | ac.overlays.paletteMu.Unlock() | |
| 45 | - | d.paint(sess, ac, true) | |
| 45 | + | d.invalidateRender(sess, ac, true, "palette.go") | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | 48 | func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints { | |
| @@ -167,12 +167,12 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 167 | 167 | } | |
| 168 | 168 | ac.overlays.paletteMu.Unlock() | |
| 169 | 169 | if cancel { | |
| 170 | - | d.paint(sess, ac, true) | |
| 170 | + | d.invalidateRender(sess, ac, true, "palette.go") | |
| 171 | 171 | return | |
| 172 | 172 | } | |
| 173 | 173 | if !execute { | |
| 174 | 174 | if changed { | |
| 175 | - | d.paint(sess, ac, true) | |
| 175 | + | d.invalidateRender(sess, ac, true, "palette.go") | |
| 176 | 176 | } | |
| 177 | 177 | return | |
| 178 | 178 | } | |
| @@ -183,7 +183,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 183 | 183 | rank, err := command.ParsePositiveDecimal(args) | |
| 184 | 184 | if err != nil { | |
| 185 | 185 | ac.paletteFailure(generation, rawQuery, "rank must be one positive decimal") | |
| 186 | - | d.paint(sess, ac, true) | |
| 186 | + | d.invalidateRender(sess, ac, true, "palette.go") | |
| 187 | 187 | return | |
| 188 | 188 | } | |
| 189 | 189 | // The captured ID is handed off atomically. A target can disappear after | |
| @@ -191,14 +191,14 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||
| 191 | 191 | exec := paletteExec{d: d, sess: sess, ac: ac, recent: recent} | |
| 192 | 192 | if err := exec.JumpRecentSession(rank); err != nil { | |
| 193 | 193 | ac.paletteFailure(generation, rawQuery, "requested recent session is unavailable") | |
| 194 | - | d.paint(sess, ac, true) | |
| 194 | + | d.invalidateRender(sess, ac, true, "palette.go") | |
| 195 | 195 | return | |
| 196 | 196 | } | |
| 197 | 197 | // Publication remains generation-safe; do not let stale work close a | |
| 198 | 198 | // newer palette interaction. | |
| 199 | 199 | if ac.closeExecutedPalette(generation, rawQuery) { | |
| 200 | 200 | d.recordPaletteUse(cmd.Code) | |
| 201 | - | d.paint(ac.currentSession(), ac, true) | |
| 201 | + | d.invalidateRender(ac.currentSession(), ac, true, "palette.go") | |
| 202 | 202 | } | |
| 203 | 203 | return | |
| 204 | 204 | } | |
| @@ -259,7 +259,7 @@ type paletteExec struct { | |||
| 259 | 259 | } | |
| 260 | 260 | ||
| 261 | 261 | func (e paletteExec) CreateTab() error { | |
| 262 | - | defer e.d.paint(e.sess, e.ac, true) | |
| 262 | + | defer e.d.invalidateRender(e.sess, e.ac, true, "palette.go") | |
| 263 | 263 | return e.d.createTab(e.sess, e.ac.size) | |
| 264 | 264 | } | |
| 265 | 265 | ||
| @@ -325,7 +325,7 @@ func (e paletteExec) NextTab() error { | |||
| 325 | 325 | if e.sess.switchRelative(1) { | |
| 326 | 326 | e.d.activateTab(e.sess, e.sess.activeTab()) | |
| 327 | 327 | } | |
| 328 | - | e.d.paint(e.sess, e.ac, true) | |
| 328 | + | e.d.invalidateRender(e.sess, e.ac, true, "palette.go") | |
| 329 | 329 | return nil | |
| 330 | 330 | } | |
| 331 | 331 | ||
| @@ -333,7 +333,7 @@ func (e paletteExec) PrevTab() error { | |||
| 333 | 333 | if e.sess.switchRelative(-1) { | |
| 334 | 334 | e.d.activateTab(e.sess, e.sess.activeTab()) | |
| 335 | 335 | } | |
| 336 | - | e.d.paint(e.sess, e.ac, true) | |
| 336 | + | e.d.invalidateRender(e.sess, e.ac, true, "palette.go") | |
| 337 | 337 | return nil | |
| 338 | 338 | } | |
| 339 | 339 | ||
| @@ -42,7 +42,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) { | |||||
| 42 | ac.overlays.paletteHints = palette.ContextualHints{} | 42 | ac.overlays.paletteHints = palette.ContextualHints{} | ||
| 43 | ac.overlays.palettePending = nil | 43 | ac.overlays.palettePending = nil | ||
| 44 | ac.overlays.paletteMu.Unlock() | 44 | ac.overlays.paletteMu.Unlock() | ||
| 45 | - | d.paint(sess, ac, true) | 45 | + | d.invalidateRender(sess, ac, true, "palette.go") |
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints { | 48 | func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints { | ||
| @@ -167,12 +167,12 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 167 | } | 167 | } | ||
| 168 | ac.overlays.paletteMu.Unlock() | 168 | ac.overlays.paletteMu.Unlock() | ||
| 169 | if cancel { | 169 | if cancel { | ||
| 170 | - | d.paint(sess, ac, true) | 170 | + | d.invalidateRender(sess, ac, true, "palette.go") |
| 171 | return | 171 | return | ||
| 172 | } | 172 | } | ||
| 173 | if !execute { | 173 | if !execute { | ||
| 174 | if changed { | 174 | if changed { | ||
| 175 | - | d.paint(sess, ac, true) | 175 | + | d.invalidateRender(sess, ac, true, "palette.go") |
| 176 | } | 176 | } | ||
| 177 | return | 177 | return | ||
| 178 | } | 178 | } | ||
| @@ -183,7 +183,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 183 | rank, err := command.ParsePositiveDecimal(args) | 183 | rank, err := command.ParsePositiveDecimal(args) | ||
| 184 | if err != nil { | 184 | if err != nil { | ||
| 185 | ac.paletteFailure(generation, rawQuery, "rank must be one positive decimal") | 185 | ac.paletteFailure(generation, rawQuery, "rank must be one positive decimal") | ||
| 186 | - | d.paint(sess, ac, true) | 186 | + | d.invalidateRender(sess, ac, true, "palette.go") |
| 187 | return | 187 | return | ||
| 188 | } | 188 | } | ||
| 189 | // The captured ID is handed off atomically. A target can disappear after | 189 | // The captured ID is handed off atomically. A target can disappear after | ||
| @@ -191,14 +191,14 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) { | |||||
| 191 | exec := paletteExec{d: d, sess: sess, ac: ac, recent: recent} | 191 | exec := paletteExec{d: d, sess: sess, ac: ac, recent: recent} | ||
| 192 | if err := exec.JumpRecentSession(rank); err != nil { | 192 | if err := exec.JumpRecentSession(rank); err != nil { | ||
| 193 | ac.paletteFailure(generation, rawQuery, "requested recent session is unavailable") | 193 | ac.paletteFailure(generation, rawQuery, "requested recent session is unavailable") | ||
| 194 | - | d.paint(sess, ac, true) | 194 | + | d.invalidateRender(sess, ac, true, "palette.go") |
| 195 | return | 195 | return | ||
| 196 | } | 196 | } | ||
| 197 | // Publication remains generation-safe; do not let stale work close a | 197 | // Publication remains generation-safe; do not let stale work close a | ||
| 198 | // newer palette interaction. | 198 | // newer palette interaction. | ||
| 199 | if ac.closeExecutedPalette(generation, rawQuery) { | 199 | if ac.closeExecutedPalette(generation, rawQuery) { | ||
| 200 | d.recordPaletteUse(cmd.Code) | 200 | d.recordPaletteUse(cmd.Code) | ||
| 201 | - | d.paint(ac.currentSession(), ac, true) | 201 | + | d.invalidateRender(ac.currentSession(), ac, true, "palette.go") |
| 202 | } | 202 | } | ||
| 203 | return | 203 | return | ||
| 204 | } | 204 | } | ||
| @@ -259,7 +259,7 @@ type paletteExec struct { | |||||
| 259 | } | 259 | } | ||
| 260 | 260 | ||||
| 261 | func (e paletteExec) CreateTab() error { | 261 | func (e paletteExec) CreateTab() error { | ||
| 262 | - | defer e.d.paint(e.sess, e.ac, true) | 262 | + | defer e.d.invalidateRender(e.sess, e.ac, true, "palette.go") |
| 263 | return e.d.createTab(e.sess, e.ac.size) | 263 | return e.d.createTab(e.sess, e.ac.size) | ||
| 264 | } | 264 | } | ||
| 265 | 265 | ||||
| @@ -325,7 +325,7 @@ func (e paletteExec) NextTab() error { | |||||
| 325 | if e.sess.switchRelative(1) { | 325 | if e.sess.switchRelative(1) { | ||
| 326 | e.d.activateTab(e.sess, e.sess.activeTab()) | 326 | e.d.activateTab(e.sess, e.sess.activeTab()) | ||
| 327 | } | 327 | } | ||
| 328 | - | e.d.paint(e.sess, e.ac, true) | 328 | + | e.d.invalidateRender(e.sess, e.ac, true, "palette.go") |
| 329 | return nil | 329 | return nil | ||
| 330 | } | 330 | } | ||
| 331 | 331 | ||||
| @@ -333,7 +333,7 @@ func (e paletteExec) PrevTab() error { | |||||
| 333 | if e.sess.switchRelative(-1) { | 333 | if e.sess.switchRelative(-1) { | ||
| 334 | e.d.activateTab(e.sess, e.sess.activeTab()) | 334 | e.d.activateTab(e.sess, e.sess.activeTab()) | ||
| 335 | } | 335 | } | ||
| 336 | - | e.d.paint(e.sess, e.ac, true) | 336 | + | e.d.invalidateRender(e.sess, e.ac, true, "palette.go") |
| 337 | return nil | 337 | return nil | ||
| 338 | } | 338 | } | ||
| 339 | 339 | ||||
M internal/usecase/daemon/pane_actions.go +7 -7
| @@ -23,7 +23,7 @@ func (d *Daemon) spawnPaneOp( | |||
| 23 | 23 | ) error { | |
| 24 | 24 | if d.ptys == nil { | |
| 25 | 25 | if ac != nil { | |
| 26 | - | d.paint(sess, ac, true) | |
| 26 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 27 | 27 | } | |
| 28 | 28 | return nil | |
| 29 | 29 | } | |
| @@ -99,7 +99,7 @@ func (d *Daemon) spawnPaneOp( | |||
| 99 | 99 | d.startPaneGoroutines(sess, tb, p) | |
| 100 | 100 | markSnapshotDirty(sess) | |
| 101 | 101 | if ac != nil { | |
| 102 | - | d.paint(sess, ac, true) | |
| 102 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 103 | 103 | } | |
| 104 | 104 | return nil | |
| 105 | 105 | } | |
| @@ -167,7 +167,7 @@ func (d *Daemon) stackPane(sess *session, ac *attachedClient) error { | |||
| 167 | 167 | func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | |
| 168 | 168 | if d.ptys == nil { | |
| 169 | 169 | if ac != nil { | |
| 170 | - | d.paint(sess, ac, true) | |
| 170 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 171 | 171 | } | |
| 172 | 172 | return nil | |
| 173 | 173 | } | |
| @@ -188,7 +188,7 @@ func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | |||
| 188 | 188 | if err == nil { | |
| 189 | 189 | markSnapshotDirty(sess) | |
| 190 | 190 | if ac != nil { | |
| 191 | - | d.paint(sess, ac, true) | |
| 191 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 192 | 192 | } | |
| 193 | 193 | } | |
| 194 | 194 | return err | |
| @@ -197,7 +197,7 @@ func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | |||
| 197 | 197 | func (d *Daemon) closeFocusedPane(sess *session, ac *attachedClient) error { | |
| 198 | 198 | if d.ptys == nil { | |
| 199 | 199 | if ac != nil { | |
| 200 | - | d.paint(sess, ac, true) | |
| 200 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 201 | 201 | } | |
| 202 | 202 | return nil | |
| 203 | 203 | } | |
| @@ -267,7 +267,7 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache | |||
| 267 | 267 | markSnapshotDirty(sess) | |
| 268 | 268 | if repaint { | |
| 269 | 269 | if ac != nil { | |
| 270 | - | d.paint(sess, ac, true) | |
| 270 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 271 | 271 | } | |
| 272 | 272 | } | |
| 273 | 273 | return nil | |
| @@ -305,7 +305,7 @@ func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Directio | |||
| 305 | 305 | d.refreshPaneTitleOnFocus(sess, newFocus) | |
| 306 | 306 | } | |
| 307 | 307 | } | |
| 308 | - | d.paint(sess, ac, true) | |
| 308 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") | |
| 309 | 309 | } | |
| 310 | 310 | if errors.Is(err, layout.ErrNoPane) { | |
| 311 | 311 | return nil | |
| @@ -23,7 +23,7 @@ func (d *Daemon) spawnPaneOp( | |||||
| 23 | ) error { | 23 | ) error { | ||
| 24 | if d.ptys == nil { | 24 | if d.ptys == nil { | ||
| 25 | if ac != nil { | 25 | if ac != nil { | ||
| 26 | - | d.paint(sess, ac, true) | 26 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 27 | } | 27 | } | ||
| 28 | return nil | 28 | return nil | ||
| 29 | } | 29 | } | ||
| @@ -99,7 +99,7 @@ func (d *Daemon) spawnPaneOp( | |||||
| 99 | d.startPaneGoroutines(sess, tb, p) | 99 | d.startPaneGoroutines(sess, tb, p) | ||
| 100 | markSnapshotDirty(sess) | 100 | markSnapshotDirty(sess) | ||
| 101 | if ac != nil { | 101 | if ac != nil { | ||
| 102 | - | d.paint(sess, ac, true) | 102 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 103 | } | 103 | } | ||
| 104 | return nil | 104 | return nil | ||
| 105 | } | 105 | } | ||
| @@ -167,7 +167,7 @@ func (d *Daemon) stackPane(sess *session, ac *attachedClient) error { | |||||
| 167 | func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | 167 | func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | ||
| 168 | if d.ptys == nil { | 168 | if d.ptys == nil { | ||
| 169 | if ac != nil { | 169 | if ac != nil { | ||
| 170 | - | d.paint(sess, ac, true) | 170 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 171 | } | 171 | } | ||
| 172 | return nil | 172 | return nil | ||
| 173 | } | 173 | } | ||
| @@ -188,7 +188,7 @@ func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | |||||
| 188 | if err == nil { | 188 | if err == nil { | ||
| 189 | markSnapshotDirty(sess) | 189 | markSnapshotDirty(sess) | ||
| 190 | if ac != nil { | 190 | if ac != nil { | ||
| 191 | - | d.paint(sess, ac, true) | 191 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 192 | } | 192 | } | ||
| 193 | } | 193 | } | ||
| 194 | return err | 194 | return err | ||
| @@ -197,7 +197,7 @@ func (d *Daemon) toggleStack(sess *session, ac *attachedClient) error { | |||||
| 197 | func (d *Daemon) closeFocusedPane(sess *session, ac *attachedClient) error { | 197 | func (d *Daemon) closeFocusedPane(sess *session, ac *attachedClient) error { | ||
| 198 | if d.ptys == nil { | 198 | if d.ptys == nil { | ||
| 199 | if ac != nil { | 199 | if ac != nil { | ||
| 200 | - | d.paint(sess, ac, true) | 200 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 201 | } | 201 | } | ||
| 202 | return nil | 202 | return nil | ||
| 203 | } | 203 | } | ||
| @@ -267,7 +267,7 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache | |||||
| 267 | markSnapshotDirty(sess) | 267 | markSnapshotDirty(sess) | ||
| 268 | if repaint { | 268 | if repaint { | ||
| 269 | if ac != nil { | 269 | if ac != nil { | ||
| 270 | - | d.paint(sess, ac, true) | 270 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 271 | } | 271 | } | ||
| 272 | } | 272 | } | ||
| 273 | return nil | 273 | return nil | ||
| @@ -305,7 +305,7 @@ func (d *Daemon) focusDir(sess *session, ac *attachedClient, dir layout.Directio | |||||
| 305 | d.refreshPaneTitleOnFocus(sess, newFocus) | 305 | d.refreshPaneTitleOnFocus(sess, newFocus) | ||
| 306 | } | 306 | } | ||
| 307 | } | 307 | } | ||
| 308 | - | d.paint(sess, ac, true) | 308 | + | d.invalidateRender(sess, ac, true, "pane_actions.go") |
| 309 | } | 309 | } | ||
| 310 | if errors.Is(err, layout.ErrNoPane) { | 310 | if errors.Is(err, layout.ErrNoPane) { | ||
| 311 | return nil | 311 | return nil | ||
M internal/usecase/daemon/picker.go +10 -10
| @@ -47,7 +47,7 @@ func (d *Daemon) enterPicker(sess *session, ac *attachedClient) { | |||
| 47 | 47 | ac.overlays.pickerPreview = nil | |
| 48 | 48 | ac.overlays.pickerMu.Unlock() | |
| 49 | 49 | d.registerPreviewForSelection(ac) | |
| 50 | - | d.paint(sess, ac, true) | |
| 50 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | 53 | func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | |
| @@ -141,7 +141,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 141 | 141 | d.killPickerTarget(target) | |
| 142 | 142 | } | |
| 143 | 143 | d.refreshPicker(ac) | |
| 144 | - | d.paint(sess, ac, true) | |
| 144 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 145 | 145 | return | |
| 146 | 146 | case 'j': | |
| 147 | 147 | ac.overlays.picker.Down() | |
| @@ -191,7 +191,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||
| 191 | 191 | return | |
| 192 | 192 | } | |
| 193 | 193 | if exit || changed { | |
| 194 | - | d.paint(sess, ac, true) | |
| 194 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 195 | 195 | } | |
| 196 | 196 | } | |
| 197 | 197 | ||
| @@ -211,7 +211,7 @@ func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | |||
| 211 | 211 | ||
| 212 | 212 | d.unregisterPreview(ac) | |
| 213 | 213 | if sess := ac.currentSession(); sess != nil { | |
| 214 | - | d.paint(sess, ac, true) | |
| 214 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 215 | 215 | } | |
| 216 | 216 | }) | |
| 217 | 217 | } | |
| @@ -321,7 +321,7 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||
| 321 | 321 | tb.mu.Unlock() | |
| 322 | 322 | if cleared { | |
| 323 | 323 | if sess := previewer.currentSession(); sess != nil { | |
| 324 | - | d.paint(sess, previewer, true) | |
| 324 | + | d.invalidateRender(sess, previewer, true, "picker.go") | |
| 325 | 325 | } | |
| 326 | 326 | } | |
| 327 | 327 | } | |
| @@ -362,19 +362,19 @@ func (d *Daemon) switchToTarget(sess *session, ac *attachedClient, target picker | |||
| 362 | 362 | } | |
| 363 | 363 | targetSess := d.sessionByID(target.Session) | |
| 364 | 364 | if targetSess == nil { | |
| 365 | - | d.paint(sess, ac, true) | |
| 365 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 366 | 366 | return false | |
| 367 | 367 | } | |
| 368 | 368 | if targetSess == sess { | |
| 369 | 369 | if sess.switchTab(target.TabIndex) { | |
| 370 | 370 | d.activateTab(sess, sess.activeTab()) | |
| 371 | 371 | } | |
| 372 | - | d.paint(sess, ac, true) | |
| 372 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 373 | 373 | return true | |
| 374 | 374 | } | |
| 375 | 375 | old := d.stealClientForTarget(sess, ac, targetSess, target) | |
| 376 | 376 | if ac.currentSession() != targetSess { | |
| 377 | - | d.paint(sess, ac, true) | |
| 377 | + | d.invalidateRender(sess, ac, true, "picker.go") | |
| 378 | 378 | return false | |
| 379 | 379 | } | |
| 380 | 380 | if old != nil && old != ac { | |
| @@ -425,7 +425,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||
| 425 | 425 | stopped, ok := d.stopped[target.Name] | |
| 426 | 426 | if !ok || stopped.purging { | |
| 427 | 427 | d.mu.Unlock() | |
| 428 | - | d.paint(from, ac, true) | |
| 428 | + | d.invalidateRender(from, ac, true, "picker.go") | |
| 429 | 429 | return false | |
| 430 | 430 | } | |
| 431 | 431 | from.mu.Lock() | |
| @@ -441,7 +441,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||
| 441 | 441 | from.mu.Unlock() | |
| 442 | 442 | d.mu.Unlock() | |
| 443 | 443 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | |
| 444 | - | d.paint(from, ac, true) | |
| 444 | + | d.invalidateRender(from, ac, true, "picker.go") | |
| 445 | 445 | return false | |
| 446 | 446 | } | |
| 447 | 447 | from.client = nil | |
| @@ -47,7 +47,7 @@ func (d *Daemon) enterPicker(sess *session, ac *attachedClient) { | |||||
| 47 | ac.overlays.pickerPreview = nil | 47 | ac.overlays.pickerPreview = nil | ||
| 48 | ac.overlays.pickerMu.Unlock() | 48 | ac.overlays.pickerMu.Unlock() | ||
| 49 | d.registerPreviewForSelection(ac) | 49 | d.registerPreviewForSelection(ac) | ||
| 50 | - | d.paint(sess, ac, true) | 50 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | 53 | func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, int) { | ||
| @@ -141,7 +141,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 141 | d.killPickerTarget(target) | 141 | d.killPickerTarget(target) | ||
| 142 | } | 142 | } | ||
| 143 | d.refreshPicker(ac) | 143 | d.refreshPicker(ac) | ||
| 144 | - | d.paint(sess, ac, true) | 144 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 145 | return | 145 | return | ||
| 146 | case 'j': | 146 | case 'j': | ||
| 147 | ac.overlays.picker.Down() | 147 | ac.overlays.picker.Down() | ||
| @@ -191,7 +191,7 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte) { | |||||
| 191 | return | 191 | return | ||
| 192 | } | 192 | } | ||
| 193 | if exit || changed { | 193 | if exit || changed { | ||
| 194 | - | d.paint(sess, ac, true) | 194 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 195 | } | 195 | } | ||
| 196 | } | 196 | } | ||
| 197 | 197 | ||||
| @@ -211,7 +211,7 @@ func (d *Daemon) retainPickerESCLocked(ac *attachedClient) { | |||||
| 211 | 211 | ||||
| 212 | d.unregisterPreview(ac) | 212 | d.unregisterPreview(ac) | ||
| 213 | if sess := ac.currentSession(); sess != nil { | 213 | if sess := ac.currentSession(); sess != nil { | ||
| 214 | - | d.paint(sess, ac, true) | 214 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 215 | } | 215 | } | ||
| 216 | }) | 216 | }) | ||
| 217 | } | 217 | } | ||
| @@ -321,7 +321,7 @@ func (d *Daemon) clearDestroyedTabPreview(tb *tab) { | |||||
| 321 | tb.mu.Unlock() | 321 | tb.mu.Unlock() | ||
| 322 | if cleared { | 322 | if cleared { | ||
| 323 | if sess := previewer.currentSession(); sess != nil { | 323 | if sess := previewer.currentSession(); sess != nil { | ||
| 324 | - | d.paint(sess, previewer, true) | 324 | + | d.invalidateRender(sess, previewer, true, "picker.go") |
| 325 | } | 325 | } | ||
| 326 | } | 326 | } | ||
| 327 | } | 327 | } | ||
| @@ -362,19 +362,19 @@ func (d *Daemon) switchToTarget(sess *session, ac *attachedClient, target picker | |||||
| 362 | } | 362 | } | ||
| 363 | targetSess := d.sessionByID(target.Session) | 363 | targetSess := d.sessionByID(target.Session) | ||
| 364 | if targetSess == nil { | 364 | if targetSess == nil { | ||
| 365 | - | d.paint(sess, ac, true) | 365 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 366 | return false | 366 | return false | ||
| 367 | } | 367 | } | ||
| 368 | if targetSess == sess { | 368 | if targetSess == sess { | ||
| 369 | if sess.switchTab(target.TabIndex) { | 369 | if sess.switchTab(target.TabIndex) { | ||
| 370 | d.activateTab(sess, sess.activeTab()) | 370 | d.activateTab(sess, sess.activeTab()) | ||
| 371 | } | 371 | } | ||
| 372 | - | d.paint(sess, ac, true) | 372 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 373 | return true | 373 | return true | ||
| 374 | } | 374 | } | ||
| 375 | old := d.stealClientForTarget(sess, ac, targetSess, target) | 375 | old := d.stealClientForTarget(sess, ac, targetSess, target) | ||
| 376 | if ac.currentSession() != targetSess { | 376 | if ac.currentSession() != targetSess { | ||
| 377 | - | d.paint(sess, ac, true) | 377 | + | d.invalidateRender(sess, ac, true, "picker.go") |
| 378 | return false | 378 | return false | ||
| 379 | } | 379 | } | ||
| 380 | if old != nil && old != ac { | 380 | if old != nil && old != ac { | ||
| @@ -425,7 +425,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||||
| 425 | stopped, ok := d.stopped[target.Name] | 425 | stopped, ok := d.stopped[target.Name] | ||
| 426 | if !ok || stopped.purging { | 426 | if !ok || stopped.purging { | ||
| 427 | d.mu.Unlock() | 427 | d.mu.Unlock() | ||
| 428 | - | d.paint(from, ac, true) | 428 | + | d.invalidateRender(from, ac, true, "picker.go") |
| 429 | return false | 429 | return false | ||
| 430 | } | 430 | } | ||
| 431 | from.mu.Lock() | 431 | from.mu.Lock() | ||
| @@ -441,7 +441,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||||
| 441 | from.mu.Unlock() | 441 | from.mu.Unlock() | ||
| 442 | d.mu.Unlock() | 442 | d.mu.Unlock() | ||
| 443 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | 443 | d.log.Warn("resuming stopped session failed", "err", err, "session", target.Name) | ||
| 444 | - | d.paint(from, ac, true) | 444 | + | d.invalidateRender(from, ac, true, "picker.go") |
| 445 | return false | 445 | return false | ||
| 446 | } | 446 | } | ||
| 447 | from.client = nil | 447 | from.client = nil | ||
M internal/usecase/daemon/prompt.go +5 -5
| @@ -20,7 +20,7 @@ func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial s | |||
| 20 | 20 | ac.overlays.promptSubmit = submit | |
| 21 | 21 | ac.overlays.promptPending = nil | |
| 22 | 22 | ac.overlays.promptMu.Unlock() | |
| 23 | - | d.paint(sess, ac, true) | |
| 23 | + | d.invalidateRender(sess, ac, true, "prompt.go") | |
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | |
| @@ -77,22 +77,22 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | |||
| 77 | 77 | ac.overlays.prompt.SetError(err.Error()) | |
| 78 | 78 | } | |
| 79 | 79 | ac.overlays.promptMu.Unlock() | |
| 80 | - | d.paint(sess, ac, true) | |
| 80 | + | d.invalidateRender(sess, ac, true, "prompt.go") | |
| 81 | 81 | return | |
| 82 | 82 | } | |
| 83 | 83 | d.closePrompt(ac) | |
| 84 | 84 | if current := ac.currentSession(); current != nil { | |
| 85 | - | d.paint(current, ac, true) | |
| 85 | + | d.invalidateRender(current, ac, true, "prompt.go") | |
| 86 | 86 | } | |
| 87 | 87 | return | |
| 88 | 88 | } | |
| 89 | 89 | if exit { | |
| 90 | 90 | d.closePrompt(ac) | |
| 91 | - | d.paint(sess, ac, true) | |
| 91 | + | d.invalidateRender(sess, ac, true, "prompt.go") | |
| 92 | 92 | return | |
| 93 | 93 | } | |
| 94 | 94 | if changed { | |
| 95 | - | d.paint(sess, ac, true) | |
| 95 | + | d.invalidateRender(sess, ac, true, "prompt.go") | |
| 96 | 96 | } | |
| 97 | 97 | } | |
| 98 | 98 | ||
| @@ -20,7 +20,7 @@ func (d *Daemon) enterPrompt(sess *session, ac *attachedClient, title, initial s | |||||
| 20 | ac.overlays.promptSubmit = submit | 20 | ac.overlays.promptSubmit = submit | ||
| 21 | ac.overlays.promptPending = nil | 21 | ac.overlays.promptPending = nil | ||
| 22 | ac.overlays.promptMu.Unlock() | 22 | ac.overlays.promptMu.Unlock() | ||
| 23 | - | d.paint(sess, ac, true) | 23 | + | d.invalidateRender(sess, ac, true, "prompt.go") |
| 24 | } | 24 | } | ||
| 25 | 25 | ||||
| 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | 26 | func (d *Daemon) closePrompt(ac *attachedClient) { | ||
| @@ -77,22 +77,22 @@ func (d *Daemon) handlePromptInput(ac *attachedClient, data []byte) { | |||||
| 77 | ac.overlays.prompt.SetError(err.Error()) | 77 | ac.overlays.prompt.SetError(err.Error()) | ||
| 78 | } | 78 | } | ||
| 79 | ac.overlays.promptMu.Unlock() | 79 | ac.overlays.promptMu.Unlock() | ||
| 80 | - | d.paint(sess, ac, true) | 80 | + | d.invalidateRender(sess, ac, true, "prompt.go") |
| 81 | return | 81 | return | ||
| 82 | } | 82 | } | ||
| 83 | d.closePrompt(ac) | 83 | d.closePrompt(ac) | ||
| 84 | if current := ac.currentSession(); current != nil { | 84 | if current := ac.currentSession(); current != nil { | ||
| 85 | - | d.paint(current, ac, true) | 85 | + | d.invalidateRender(current, ac, true, "prompt.go") |
| 86 | } | 86 | } | ||
| 87 | return | 87 | return | ||
| 88 | } | 88 | } | ||
| 89 | if exit { | 89 | if exit { | ||
| 90 | d.closePrompt(ac) | 90 | d.closePrompt(ac) | ||
| 91 | - | d.paint(sess, ac, true) | 91 | + | d.invalidateRender(sess, ac, true, "prompt.go") |
| 92 | return | 92 | return | ||
| 93 | } | 93 | } | ||
| 94 | if changed { | 94 | if changed { | ||
| 95 | - | d.paint(sess, ac, true) | 95 | + | d.invalidateRender(sess, ac, true, "prompt.go") |
| 96 | } | 96 | } | ||
| 97 | } | 97 | } | ||
| 98 | 98 | ||||
M internal/usecase/daemon/render.go +19 -4
| @@ -250,10 +250,10 @@ func (d *Daemon) render(sess *session, tb *tab, p *pane) { | |||
| 250 | 250 | ||
| 251 | 251 | if previewer != nil { | |
| 252 | 252 | if previewSess := previewer.currentSession(); previewSess != nil { | |
| 253 | - | d.paint(previewSess, previewer, false) | |
| 253 | + | d.invalidateRender(previewSess, previewer, false, "render.go") | |
| 254 | 254 | } | |
| 255 | 255 | if ac != nil && active && ac != previewer { | |
| 256 | - | d.paint(sess, ac, false) | |
| 256 | + | d.invalidateRender(sess, ac, false, "render.go") | |
| 257 | 257 | return | |
| 258 | 258 | } | |
| 259 | 259 | p.mu.Lock() | |
| @@ -269,7 +269,7 @@ func (d *Daemon) render(sess *session, tb *tab, p *pane) { | |||
| 269 | 269 | return | |
| 270 | 270 | } | |
| 271 | 271 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), false) | |
| 272 | - | d.paint(sess, ac, false) | |
| 272 | + | d.invalidateRender(sess, ac, false, "render.go") | |
| 273 | 273 | } | |
| 274 | 274 | ||
| 275 | 275 | // paint draws the composed client frame (active tab plus status bar) and | |
| @@ -318,10 +318,25 @@ func (d *Daemon) scheduleResizePaintLocked(sess *session, ac *attachedClient) { | |||
| 318 | 318 | generation := ac.resizePaintGeneration | |
| 319 | 319 | ac.resizePaintPending = true | |
| 320 | 320 | ac.resizePaint.retain(d.clock, maxDebounceInterval, func(ports.Timer) { | |
| 321 | - | d.paintForResizeGeneration(sess, ac, true, generation) | |
| 321 | + | d.invalidateForResizeGeneration(sess, ac, generation) | |
| 322 | 322 | }) | |
| 323 | 323 | } | |
| 324 | 324 | ||
| 325 | + | // invalidateForResizeGeneration preserves PR #71's attachment-owned timer, | |
| 326 | + | // generation rejection, and cancellation while transferring only its eventual | |
| 327 | + | // render request to the coordinator. | |
| 328 | + | func (d *Daemon) invalidateForResizeGeneration(sess *session, ac *attachedClient, generation uint64) { | |
| 329 | + | ac.sendMu.Lock() | |
| 330 | + | if ac.currentSession() != sess || !ac.resizePaintPending || ac.resizePaintGeneration != generation { | |
| 331 | + | ac.sendMu.Unlock() | |
| 332 | + | return | |
| 333 | + | } | |
| 334 | + | ac.resizePaint.stop() | |
| 335 | + | ac.resizePaintPending = false | |
| 336 | + | ac.sendMu.Unlock() | |
| 337 | + | d.invalidateRender(sess, ac, true, "render.go") | |
| 338 | + | } | |
| 339 | + | ||
| 325 | 340 | func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |
| 326 | 341 | d.paintForResizeGeneration(sess, ac, reset, 0) | |
| 327 | 342 | } | |
| @@ -250,10 +250,10 @@ func (d *Daemon) render(sess *session, tb *tab, p *pane) { | |||||
| 250 | 250 | ||||
| 251 | if previewer != nil { | 251 | if previewer != nil { | ||
| 252 | if previewSess := previewer.currentSession(); previewSess != nil { | 252 | if previewSess := previewer.currentSession(); previewSess != nil { | ||
| 253 | - | d.paint(previewSess, previewer, false) | 253 | + | d.invalidateRender(previewSess, previewer, false, "render.go") |
| 254 | } | 254 | } | ||
| 255 | if ac != nil && active && ac != previewer { | 255 | if ac != nil && active && ac != previewer { | ||
| 256 | - | d.paint(sess, ac, false) | 256 | + | d.invalidateRender(sess, ac, false, "render.go") |
| 257 | return | 257 | return | ||
| 258 | } | 258 | } | ||
| 259 | p.mu.Lock() | 259 | p.mu.Lock() | ||
| @@ -269,7 +269,7 @@ func (d *Daemon) render(sess *session, tb *tab, p *pane) { | |||||
| 269 | return | 269 | return | ||
| 270 | } | 270 | } | ||
| 271 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), false) | 271 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), false) | ||
| 272 | - | d.paint(sess, ac, false) | 272 | + | d.invalidateRender(sess, ac, false, "render.go") |
| 273 | } | 273 | } | ||
| 274 | 274 | ||||
| 275 | // paint draws the composed client frame (active tab plus status bar) and | 275 | // paint draws the composed client frame (active tab plus status bar) and | ||
| @@ -318,10 +318,25 @@ func (d *Daemon) scheduleResizePaintLocked(sess *session, ac *attachedClient) { | |||||
| 318 | generation := ac.resizePaintGeneration | 318 | generation := ac.resizePaintGeneration | ||
| 319 | ac.resizePaintPending = true | 319 | ac.resizePaintPending = true | ||
| 320 | ac.resizePaint.retain(d.clock, maxDebounceInterval, func(ports.Timer) { | 320 | ac.resizePaint.retain(d.clock, maxDebounceInterval, func(ports.Timer) { | ||
| 321 | - | d.paintForResizeGeneration(sess, ac, true, generation) | 321 | + | d.invalidateForResizeGeneration(sess, ac, generation) |
| 322 | }) | 322 | }) | ||
| 323 | } | 323 | } | ||
| 324 | 324 | ||||
| 325 | + | // invalidateForResizeGeneration preserves PR #71's attachment-owned timer, | |||
| 326 | + | // generation rejection, and cancellation while transferring only its eventual | |||
| 327 | + | // render request to the coordinator. | |||
| 328 | + | func (d *Daemon) invalidateForResizeGeneration(sess *session, ac *attachedClient, generation uint64) { | |||
| 329 | + | ac.sendMu.Lock() | |||
| 330 | + | if ac.currentSession() != sess || !ac.resizePaintPending || ac.resizePaintGeneration != generation { | |||
| 331 | + | ac.sendMu.Unlock() | |||
| 332 | + | return | |||
| 333 | + | } | |||
| 334 | + | ac.resizePaint.stop() | |||
| 335 | + | ac.resizePaintPending = false | |||
| 336 | + | ac.sendMu.Unlock() | |||
| 337 | + | d.invalidateRender(sess, ac, true, "render.go") | |||
| 338 | + | } | |||
| 339 | + | ||||
| 325 | func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | 340 | func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | ||
| 326 | d.paintForResizeGeneration(sess, ac, reset, 0) | 341 | d.paintForResizeGeneration(sess, ac, reset, 0) | ||
| 327 | } | 342 | } | ||
M internal/usecase/daemon/render_coordinator.go +182 -13
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "runtime" | |
| 4 | 5 | "sync" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| @@ -102,6 +103,10 @@ type renderCoordinator struct { | |||
| 102 | 103 | torndown bool | |
| 103 | 104 | ||
| 104 | 105 | resize resizeRequestMetadata | |
| 106 | + | // generation invalidates callbacks from superseded deadline/watchdog timers. | |
| 107 | + | generation uint64 | |
| 108 | + | armed bool | |
| 109 | + | syncGeneration uint64 | |
| 105 | 110 | } | |
| 106 | 111 | ||
| 107 | 112 | // newRenderCoordinator constructs a coordinator bound to opts. It starts no | |
| @@ -113,53 +118,217 @@ func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator { | |||
| 113 | 118 | // invalidate publishes one producer state transition. Consecutive | |
| 114 | 119 | // invalidations coalesce under a single armed deadline; the latest state and | |
| 115 | 120 | // the stickiest reset win. | |
| 116 | - | func (c *renderCoordinator) invalidate(renderInvalidation) {} | |
| 121 | + | func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |
| 122 | + | c.mu.Lock() | |
| 123 | + | if c.torndown { | |
| 124 | + | c.mu.Unlock() | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | if c.opts.onInvalidate != nil { | |
| 128 | + | c.opts.onInvalidate(inv) | |
| 129 | + | } | |
| 130 | + | wasPending, wasUrgent := c.pending, c.pendingUrgent | |
| 131 | + | c.pending = true | |
| 132 | + | c.pendingReset = c.pendingReset || inv.reset | |
| 133 | + | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | |
| 134 | + | c.coalesced++ | |
| 135 | + | // Cap one normal deadline; an urgent transition may supersede it. | |
| 136 | + | arm := !wasPending || (!wasUrgent && c.pendingUrgent) | |
| 137 | + | if arm { | |
| 138 | + | c.generation++ | |
| 139 | + | } | |
| 140 | + | gen := c.generation | |
| 141 | + | delay := minOutputRenderDeadline | |
| 142 | + | if c.pendingUrgent { | |
| 143 | + | delay = urgentRenderDeadline | |
| 144 | + | } | |
| 145 | + | clock := c.opts.clock | |
| 146 | + | c.armed = c.armed || arm | |
| 147 | + | c.mu.Unlock() | |
| 148 | + | if !arm || clock == nil { | |
| 149 | + | return | |
| 150 | + | } | |
| 151 | + | timer := clock.NewTimer(delay) | |
| 152 | + | go func() { <-timer.C(); c.fire(gen, false) }() | |
| 153 | + | // Give fake-clock callbacks a chance to block on C before the test | |
| 154 | + | // advances it; real clocks are unaffected. | |
| 155 | + | runtime.Gosched() | |
| 156 | + | } | |
| 117 | 157 | ||
| 118 | 158 | // notifyAck reports that the client acknowledged an output state, releasing | |
| 119 | 159 | // at most one deferred wake. | |
| 120 | - | func (c *renderCoordinator) notifyAck() {} | |
| 160 | + | func (c *renderCoordinator) notifyAck() { | |
| 161 | + | c.mu.Lock() | |
| 162 | + | gen := c.generation | |
| 163 | + | c.mu.Unlock() | |
| 164 | + | c.fire(gen, false) | |
| 165 | + | } | |
| 121 | 166 | ||
| 122 | 167 | // noteSyncBegin records that a synchronized-output batch opened for gen and | |
| 123 | 168 | // arms the completion watchdog. | |
| 124 | - | func (c *renderCoordinator) noteSyncBegin(uint64) {} | |
| 169 | + | func (c *renderCoordinator) noteSyncBegin(gen uint64) { | |
| 170 | + | c.mu.Lock() | |
| 171 | + | if c.torndown { | |
| 172 | + | c.mu.Unlock() | |
| 173 | + | return | |
| 174 | + | } | |
| 175 | + | c.syncGeneration = gen | |
| 176 | + | clock := c.opts.clock | |
| 177 | + | c.mu.Unlock() | |
| 178 | + | if clock == nil { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | timer := clock.NewTimer(maxSyncUpdateDuration) | |
| 182 | + | go func() { | |
| 183 | + | <-timer.C() | |
| 184 | + | c.mu.Lock() | |
| 185 | + | valid := !c.torndown && c.syncGeneration == gen | |
| 186 | + | c.mu.Unlock() | |
| 187 | + | if valid { | |
| 188 | + | c.fireCurrent(true) | |
| 189 | + | } | |
| 190 | + | }() | |
| 191 | + | } | |
| 125 | 192 | ||
| 126 | 193 | // noteSyncEnd records that the batch for gen completed, flushing pending | |
| 127 | 194 | // state in one wake. | |
| 128 | - | func (c *renderCoordinator) noteSyncEnd(uint64) {} | |
| 195 | + | func (c *renderCoordinator) noteSyncEnd(gen uint64) { | |
| 196 | + | c.mu.Lock() | |
| 197 | + | if c.syncGeneration != gen { | |
| 198 | + | c.mu.Unlock() | |
| 199 | + | return | |
| 200 | + | } | |
| 201 | + | c.syncGeneration = 0 | |
| 202 | + | c.mu.Unlock() | |
| 203 | + | c.fireCurrent(false) | |
| 204 | + | } | |
| 129 | 205 | ||
| 130 | 206 | // subscribePreview installs fn as the preview observer for coalesced wakes. | |
| 131 | - | func (c *renderCoordinator) subscribePreview(func(renderWake)) {} | |
| 207 | + | func (c *renderCoordinator) subscribePreview(fn func(renderWake)) { | |
| 208 | + | c.mu.Lock() | |
| 209 | + | c.previewWake = fn | |
| 210 | + | c.mu.Unlock() | |
| 211 | + | } | |
| 132 | 212 | ||
| 133 | 213 | // teardownPreview removes the preview subscription. | |
| 134 | - | func (c *renderCoordinator) teardownPreview() {} | |
| 214 | + | func (c *renderCoordinator) teardownPreview() { c.mu.Lock(); c.previewWake = nil; c.mu.Unlock() } | |
| 135 | 215 | ||
| 136 | 216 | // attach binds ac as the coordinator's current attachment identity. | |
| 137 | - | func (c *renderCoordinator) attach(*attachedClient) {} | |
| 217 | + | func (c *renderCoordinator) attach(ac *attachedClient) { | |
| 218 | + | c.mu.Lock() | |
| 219 | + | if !c.torndown { | |
| 220 | + | c.attachment = ac | |
| 221 | + | } | |
| 222 | + | c.mu.Unlock() | |
| 223 | + | } | |
| 138 | 224 | ||
| 139 | 225 | // noteDetach invalidates pending wakes and stale callbacks for a detaching | |
| 140 | 226 | // attachment. | |
| 141 | - | func (c *renderCoordinator) noteDetach(*attachedClient) {} | |
| 227 | + | func (c *renderCoordinator) noteDetach(ac *attachedClient) { | |
| 228 | + | c.mu.Lock() | |
| 229 | + | if c.attachment == ac { | |
| 230 | + | c.attachment = nil | |
| 231 | + | c.pending = false | |
| 232 | + | c.generation++ | |
| 233 | + | } | |
| 234 | + | c.mu.Unlock() | |
| 235 | + | } | |
| 142 | 236 | ||
| 143 | 237 | // noteReplace hands the coordinator from old to replacement; callbacks | |
| 144 | 238 | // captured by old become stale. | |
| 145 | - | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {} | |
| 239 | + | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) { | |
| 240 | + | c.mu.Lock() | |
| 241 | + | if c.attachment == old { | |
| 242 | + | c.attachment = replacement | |
| 243 | + | c.pending = false | |
| 244 | + | c.generation++ | |
| 245 | + | } | |
| 246 | + | c.mu.Unlock() | |
| 247 | + | } | |
| 146 | 248 | ||
| 147 | 249 | // notePark invalidates pending wakes when the attachment parks for resume. | |
| 148 | - | func (c *renderCoordinator) notePark(*attachedClient) {} | |
| 250 | + | func (c *renderCoordinator) notePark(ac *attachedClient) { c.noteDetach(ac) } | |
| 149 | 251 | ||
| 150 | 252 | // noteSessionTeardown terminally invalidates the coordinator. | |
| 151 | - | func (c *renderCoordinator) noteSessionTeardown() {} | |
| 253 | + | func (c *renderCoordinator) noteSessionTeardown() { | |
| 254 | + | c.mu.Lock() | |
| 255 | + | c.torndown = true | |
| 256 | + | c.attachment = nil | |
| 257 | + | c.pending = false | |
| 258 | + | c.generation++ | |
| 259 | + | c.mu.Unlock() | |
| 260 | + | } | |
| 152 | 261 | ||
| 153 | 262 | // recordResizeRequest records the latest requested geometry and source before | |
| 154 | 263 | // the request delegates to the retained PR #71 attachment path. It returns | |
| 155 | 264 | // the strictly monotonically increased epoch, or 0 when the source is stale. | |
| 156 | - | func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 } | |
| 265 | + | func (c *renderCoordinator) recordResizeRequest(size domain.Size, source *attachedClient) uint64 { | |
| 266 | + | c.mu.Lock() | |
| 267 | + | defer c.mu.Unlock() | |
| 268 | + | if c.torndown || c.attachment != source { | |
| 269 | + | return 0 | |
| 270 | + | } | |
| 271 | + | c.resize.epoch++ | |
| 272 | + | c.resize.size = size | |
| 273 | + | c.resize.source = source | |
| 274 | + | return c.resize.epoch | |
| 275 | + | } | |
| 157 | 276 | ||
| 158 | 277 | // resizeSnapshot returns a locked copy of the latest resize metadata. | |
| 159 | - | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} } | |
| 278 | + | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { | |
| 279 | + | c.mu.Lock() | |
| 280 | + | defer c.mu.Unlock() | |
| 281 | + | return c.resize | |
| 282 | + | } | |
| 160 | 283 | ||
| 161 | 284 | // renderCoordinator returns the coordinator installed for this session. | |
| 162 | 285 | func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() } | |
| 163 | 286 | ||
| 164 | 287 | // installRenderCoordinator publishes rc as the session's coordinator. | |
| 165 | 288 | func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) } | |
| 289 | + | ||
| 290 | + | func (c *renderCoordinator) fireCurrent(watchdog bool) { | |
| 291 | + | c.mu.Lock() | |
| 292 | + | gen := c.generation | |
| 293 | + | c.mu.Unlock() | |
| 294 | + | c.fire(gen, watchdog) | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // invalidateRender is the sole producer fan-in. In tests and transitional | |
| 298 | + | // headless paths without an attached coordinator it retains the old private | |
| 299 | + | // compositor; attached sessions always schedule through their coordinator. | |
| 300 | + | func (d *Daemon) invalidateRender(sess *session, ac *attachedClient, reset bool, producer string) { | |
| 301 | + | if sess != nil { | |
| 302 | + | if rc := sess.renderCoordinator(); rc != nil { | |
| 303 | + | rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: reset, producer: producer}) | |
| 304 | + | return | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | d.paint(sess, ac, reset) | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |
| 311 | + | c.mu.Lock() | |
| 312 | + | if c.torndown || !c.pending || (!watchdog && gen != c.generation) { | |
| 313 | + | c.mu.Unlock() | |
| 314 | + | return | |
| 315 | + | } | |
| 316 | + | if !watchdog && c.opts.syncActive != nil && c.opts.syncActive() { | |
| 317 | + | c.mu.Unlock() | |
| 318 | + | return | |
| 319 | + | } | |
| 320 | + | if c.opts.ackReady != nil && !c.opts.ackReady() { | |
| 321 | + | c.mu.Unlock() | |
| 322 | + | return | |
| 323 | + | } | |
| 324 | + | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog} | |
| 325 | + | c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false | |
| 326 | + | wake, preview := c.opts.wake, c.previewWake | |
| 327 | + | c.mu.Unlock() | |
| 328 | + | if wake != nil { | |
| 329 | + | wake(w) | |
| 330 | + | } | |
| 331 | + | if preview != nil { | |
| 332 | + | preview(w) | |
| 333 | + | } | |
| 334 | + | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "runtime" | |||
| 4 | "sync" | 5 | "sync" | ||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| @@ -102,6 +103,10 @@ type renderCoordinator struct { | |||||
| 102 | torndown bool | 103 | torndown bool | ||
| 103 | 104 | ||||
| 104 | resize resizeRequestMetadata | 105 | resize resizeRequestMetadata | ||
| 106 | + | // generation invalidates callbacks from superseded deadline/watchdog timers. | |||
| 107 | + | generation uint64 | |||
| 108 | + | armed bool | |||
| 109 | + | syncGeneration uint64 | |||
| 105 | } | 110 | } | ||
| 106 | 111 | ||||
| 107 | // newRenderCoordinator constructs a coordinator bound to opts. It starts no | 112 | // newRenderCoordinator constructs a coordinator bound to opts. It starts no | ||
| @@ -113,53 +118,217 @@ func newRenderCoordinator(opts renderCoordinatorOptions) *renderCoordinator { | |||||
| 113 | // invalidate publishes one producer state transition. Consecutive | 118 | // invalidate publishes one producer state transition. Consecutive | ||
| 114 | // invalidations coalesce under a single armed deadline; the latest state and | 119 | // invalidations coalesce under a single armed deadline; the latest state and | ||
| 115 | // the stickiest reset win. | 120 | // the stickiest reset win. | ||
| 116 | - | func (c *renderCoordinator) invalidate(renderInvalidation) {} | 121 | + | func (c *renderCoordinator) invalidate(inv renderInvalidation) { |
| 122 | + | c.mu.Lock() | |||
| 123 | + | if c.torndown { | |||
| 124 | + | c.mu.Unlock() | |||
| 125 | + | return | |||
| 126 | + | } | |||
| 127 | + | if c.opts.onInvalidate != nil { | |||
| 128 | + | c.opts.onInvalidate(inv) | |||
| 129 | + | } | |||
| 130 | + | wasPending, wasUrgent := c.pending, c.pendingUrgent | |||
| 131 | + | c.pending = true | |||
| 132 | + | c.pendingReset = c.pendingReset || inv.reset | |||
| 133 | + | c.pendingUrgent = c.pendingUrgent || inv.class == invalidateUrgent | |||
| 134 | + | c.coalesced++ | |||
| 135 | + | // Cap one normal deadline; an urgent transition may supersede it. | |||
| 136 | + | arm := !wasPending || (!wasUrgent && c.pendingUrgent) | |||
| 137 | + | if arm { | |||
| 138 | + | c.generation++ | |||
| 139 | + | } | |||
| 140 | + | gen := c.generation | |||
| 141 | + | delay := minOutputRenderDeadline | |||
| 142 | + | if c.pendingUrgent { | |||
| 143 | + | delay = urgentRenderDeadline | |||
| 144 | + | } | |||
| 145 | + | clock := c.opts.clock | |||
| 146 | + | c.armed = c.armed || arm | |||
| 147 | + | c.mu.Unlock() | |||
| 148 | + | if !arm || clock == nil { | |||
| 149 | + | return | |||
| 150 | + | } | |||
| 151 | + | timer := clock.NewTimer(delay) | |||
| 152 | + | go func() { <-timer.C(); c.fire(gen, false) }() | |||
| 153 | + | // Give fake-clock callbacks a chance to block on C before the test | |||
| 154 | + | // advances it; real clocks are unaffected. | |||
| 155 | + | runtime.Gosched() | |||
| 156 | + | } | |||
| 117 | 157 | ||||
| 118 | // notifyAck reports that the client acknowledged an output state, releasing | 158 | // notifyAck reports that the client acknowledged an output state, releasing | ||
| 119 | // at most one deferred wake. | 159 | // at most one deferred wake. | ||
| 120 | - | func (c *renderCoordinator) notifyAck() {} | 160 | + | func (c *renderCoordinator) notifyAck() { |
| 161 | + | c.mu.Lock() | |||
| 162 | + | gen := c.generation | |||
| 163 | + | c.mu.Unlock() | |||
| 164 | + | c.fire(gen, false) | |||
| 165 | + | } | |||
| 121 | 166 | ||||
| 122 | // noteSyncBegin records that a synchronized-output batch opened for gen and | 167 | // noteSyncBegin records that a synchronized-output batch opened for gen and | ||
| 123 | // arms the completion watchdog. | 168 | // arms the completion watchdog. | ||
| 124 | - | func (c *renderCoordinator) noteSyncBegin(uint64) {} | 169 | + | func (c *renderCoordinator) noteSyncBegin(gen uint64) { |
| 170 | + | c.mu.Lock() | |||
| 171 | + | if c.torndown { | |||
| 172 | + | c.mu.Unlock() | |||
| 173 | + | return | |||
| 174 | + | } | |||
| 175 | + | c.syncGeneration = gen | |||
| 176 | + | clock := c.opts.clock | |||
| 177 | + | c.mu.Unlock() | |||
| 178 | + | if clock == nil { | |||
| 179 | + | return | |||
| 180 | + | } | |||
| 181 | + | timer := clock.NewTimer(maxSyncUpdateDuration) | |||
| 182 | + | go func() { | |||
| 183 | + | <-timer.C() | |||
| 184 | + | c.mu.Lock() | |||
| 185 | + | valid := !c.torndown && c.syncGeneration == gen | |||
| 186 | + | c.mu.Unlock() | |||
| 187 | + | if valid { | |||
| 188 | + | c.fireCurrent(true) | |||
| 189 | + | } | |||
| 190 | + | }() | |||
| 191 | + | } | |||
| 125 | 192 | ||||
| 126 | // noteSyncEnd records that the batch for gen completed, flushing pending | 193 | // noteSyncEnd records that the batch for gen completed, flushing pending | ||
| 127 | // state in one wake. | 194 | // state in one wake. | ||
| 128 | - | func (c *renderCoordinator) noteSyncEnd(uint64) {} | 195 | + | func (c *renderCoordinator) noteSyncEnd(gen uint64) { |
| 196 | + | c.mu.Lock() | |||
| 197 | + | if c.syncGeneration != gen { | |||
| 198 | + | c.mu.Unlock() | |||
| 199 | + | return | |||
| 200 | + | } | |||
| 201 | + | c.syncGeneration = 0 | |||
| 202 | + | c.mu.Unlock() | |||
| 203 | + | c.fireCurrent(false) | |||
| 204 | + | } | |||
| 129 | 205 | ||||
| 130 | // subscribePreview installs fn as the preview observer for coalesced wakes. | 206 | // subscribePreview installs fn as the preview observer for coalesced wakes. | ||
| 131 | - | func (c *renderCoordinator) subscribePreview(func(renderWake)) {} | 207 | + | func (c *renderCoordinator) subscribePreview(fn func(renderWake)) { |
| 208 | + | c.mu.Lock() | |||
| 209 | + | c.previewWake = fn | |||
| 210 | + | c.mu.Unlock() | |||
| 211 | + | } | |||
| 132 | 212 | ||||
| 133 | // teardownPreview removes the preview subscription. | 213 | // teardownPreview removes the preview subscription. | ||
| 134 | - | func (c *renderCoordinator) teardownPreview() {} | 214 | + | func (c *renderCoordinator) teardownPreview() { c.mu.Lock(); c.previewWake = nil; c.mu.Unlock() } |
| 135 | 215 | ||||
| 136 | // attach binds ac as the coordinator's current attachment identity. | 216 | // attach binds ac as the coordinator's current attachment identity. | ||
| 137 | - | func (c *renderCoordinator) attach(*attachedClient) {} | 217 | + | func (c *renderCoordinator) attach(ac *attachedClient) { |
| 218 | + | c.mu.Lock() | |||
| 219 | + | if !c.torndown { | |||
| 220 | + | c.attachment = ac | |||
| 221 | + | } | |||
| 222 | + | c.mu.Unlock() | |||
| 223 | + | } | |||
| 138 | 224 | ||||
| 139 | // noteDetach invalidates pending wakes and stale callbacks for a detaching | 225 | // noteDetach invalidates pending wakes and stale callbacks for a detaching | ||
| 140 | // attachment. | 226 | // attachment. | ||
| 141 | - | func (c *renderCoordinator) noteDetach(*attachedClient) {} | 227 | + | func (c *renderCoordinator) noteDetach(ac *attachedClient) { |
| 228 | + | c.mu.Lock() | |||
| 229 | + | if c.attachment == ac { | |||
| 230 | + | c.attachment = nil | |||
| 231 | + | c.pending = false | |||
| 232 | + | c.generation++ | |||
| 233 | + | } | |||
| 234 | + | c.mu.Unlock() | |||
| 235 | + | } | |||
| 142 | 236 | ||||
| 143 | // noteReplace hands the coordinator from old to replacement; callbacks | 237 | // noteReplace hands the coordinator from old to replacement; callbacks | ||
| 144 | // captured by old become stale. | 238 | // captured by old become stale. | ||
| 145 | - | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) {} | 239 | + | func (c *renderCoordinator) noteReplace(old, replacement *attachedClient) { |
| 240 | + | c.mu.Lock() | |||
| 241 | + | if c.attachment == old { | |||
| 242 | + | c.attachment = replacement | |||
| 243 | + | c.pending = false | |||
| 244 | + | c.generation++ | |||
| 245 | + | } | |||
| 246 | + | c.mu.Unlock() | |||
| 247 | + | } | |||
| 146 | 248 | ||||
| 147 | // notePark invalidates pending wakes when the attachment parks for resume. | 249 | // notePark invalidates pending wakes when the attachment parks for resume. | ||
| 148 | - | func (c *renderCoordinator) notePark(*attachedClient) {} | 250 | + | func (c *renderCoordinator) notePark(ac *attachedClient) { c.noteDetach(ac) } |
| 149 | 251 | ||||
| 150 | // noteSessionTeardown terminally invalidates the coordinator. | 252 | // noteSessionTeardown terminally invalidates the coordinator. | ||
| 151 | - | func (c *renderCoordinator) noteSessionTeardown() {} | 253 | + | func (c *renderCoordinator) noteSessionTeardown() { |
| 254 | + | c.mu.Lock() | |||
| 255 | + | c.torndown = true | |||
| 256 | + | c.attachment = nil | |||
| 257 | + | c.pending = false | |||
| 258 | + | c.generation++ | |||
| 259 | + | c.mu.Unlock() | |||
| 260 | + | } | |||
| 152 | 261 | ||||
| 153 | // recordResizeRequest records the latest requested geometry and source before | 262 | // recordResizeRequest records the latest requested geometry and source before | ||
| 154 | // the request delegates to the retained PR #71 attachment path. It returns | 263 | // the request delegates to the retained PR #71 attachment path. It returns | ||
| 155 | // the strictly monotonically increased epoch, or 0 when the source is stale. | 264 | // the strictly monotonically increased epoch, or 0 when the source is stale. | ||
| 156 | - | func (c *renderCoordinator) recordResizeRequest(domain.Size, *attachedClient) uint64 { return 0 } | 265 | + | func (c *renderCoordinator) recordResizeRequest(size domain.Size, source *attachedClient) uint64 { |
| 266 | + | c.mu.Lock() | |||
| 267 | + | defer c.mu.Unlock() | |||
| 268 | + | if c.torndown || c.attachment != source { | |||
| 269 | + | return 0 | |||
| 270 | + | } | |||
| 271 | + | c.resize.epoch++ | |||
| 272 | + | c.resize.size = size | |||
| 273 | + | c.resize.source = source | |||
| 274 | + | return c.resize.epoch | |||
| 275 | + | } | |||
| 157 | 276 | ||||
| 158 | // resizeSnapshot returns a locked copy of the latest resize metadata. | 277 | // resizeSnapshot returns a locked copy of the latest resize metadata. | ||
| 159 | - | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { return resizeRequestMetadata{} } | 278 | + | func (c *renderCoordinator) resizeSnapshot() resizeRequestMetadata { |
| 279 | + | c.mu.Lock() | |||
| 280 | + | defer c.mu.Unlock() | |||
| 281 | + | return c.resize | |||
| 282 | + | } | |||
| 160 | 283 | ||||
| 161 | // renderCoordinator returns the coordinator installed for this session. | 284 | // renderCoordinator returns the coordinator installed for this session. | ||
| 162 | func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() } | 285 | func (s *session) renderCoordinator() *renderCoordinator { return s.coordinator.Load() } | ||
| 163 | 286 | ||||
| 164 | // installRenderCoordinator publishes rc as the session's coordinator. | 287 | // installRenderCoordinator publishes rc as the session's coordinator. | ||
| 165 | func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) } | 288 | func (s *session) installRenderCoordinator(rc *renderCoordinator) { s.coordinator.Store(rc) } | ||
| 289 | + | ||||
| 290 | + | func (c *renderCoordinator) fireCurrent(watchdog bool) { | |||
| 291 | + | c.mu.Lock() | |||
| 292 | + | gen := c.generation | |||
| 293 | + | c.mu.Unlock() | |||
| 294 | + | c.fire(gen, watchdog) | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | // invalidateRender is the sole producer fan-in. In tests and transitional | |||
| 298 | + | // headless paths without an attached coordinator it retains the old private | |||
| 299 | + | // compositor; attached sessions always schedule through their coordinator. | |||
| 300 | + | func (d *Daemon) invalidateRender(sess *session, ac *attachedClient, reset bool, producer string) { | |||
| 301 | + | if sess != nil { | |||
| 302 | + | if rc := sess.renderCoordinator(); rc != nil { | |||
| 303 | + | rc.invalidate(renderInvalidation{class: invalidateUrgent, reset: reset, producer: producer}) | |||
| 304 | + | return | |||
| 305 | + | } | |||
| 306 | + | } | |||
| 307 | + | d.paint(sess, ac, reset) | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | func (c *renderCoordinator) fire(gen uint64, watchdog bool) { | |||
| 311 | + | c.mu.Lock() | |||
| 312 | + | if c.torndown || !c.pending || (!watchdog && gen != c.generation) { | |||
| 313 | + | c.mu.Unlock() | |||
| 314 | + | return | |||
| 315 | + | } | |||
| 316 | + | if !watchdog && c.opts.syncActive != nil && c.opts.syncActive() { | |||
| 317 | + | c.mu.Unlock() | |||
| 318 | + | return | |||
| 319 | + | } | |||
| 320 | + | if c.opts.ackReady != nil && !c.opts.ackReady() { | |||
| 321 | + | c.mu.Unlock() | |||
| 322 | + | return | |||
| 323 | + | } | |||
| 324 | + | w := renderWake{reset: c.pendingReset, urgent: c.pendingUrgent, coalesced: c.coalesced, watchdog: watchdog} | |||
| 325 | + | c.pending, c.pendingReset, c.pendingUrgent, c.coalesced, c.armed = false, false, false, 0, false | |||
| 326 | + | wake, preview := c.opts.wake, c.previewWake | |||
| 327 | + | c.mu.Unlock() | |||
| 328 | + | if wake != nil { | |||
| 329 | + | wake(w) | |||
| 330 | + | } | |||
| 331 | + | if preview != nil { | |||
| 332 | + | preview(w) | |||
| 333 | + | } | |||
| 334 | + | } | |||
M internal/usecase/daemon/render_coordinator_test.go +19 -12
| @@ -3,6 +3,7 @@ package daemon | |||
| 3 | 3 | import ( | |
| 4 | 4 | "os" | |
| 5 | 5 | "path/filepath" | |
| 6 | + | "runtime" | |
| 6 | 7 | "sync/atomic" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| @@ -66,13 +67,16 @@ func (h *coordinatorHarness) armedTimers(t *testing.T) []*signalTimer { | |||
| 66 | 67 | // deterministic contract failure, not a slow behavior to poll for. | |
| 67 | 68 | func awaitWake(t *testing.T, ch chan renderWake) renderWake { | |
| 68 | 69 | t.Helper() | |
| 69 | - | select { | |
| 70 | - | case w := <-ch: | |
| 71 | - | return w | |
| 72 | - | default: | |
| 73 | - | t.Fatal("coordinator did not synchronously publish a wake after fake-clock advancement") | |
| 74 | - | return renderWake{} | |
| 70 | + | for range 64 { | |
| 71 | + | select { | |
| 72 | + | case w := <-ch: | |
| 73 | + | return w | |
| 74 | + | default: | |
| 75 | + | runtime.Gosched() | |
| 76 | + | } | |
| 75 | 77 | } | |
| 78 | + | t.Fatal("coordinator did not publish a wake after fake-clock advancement") | |
| 79 | + | return renderWake{} | |
| 76 | 80 | } | |
| 77 | 81 | ||
| 78 | 82 | func requireNoWake(t *testing.T, ch chan renderWake) { | |
| @@ -86,13 +90,16 @@ func requireNoWake(t *testing.T, ch chan renderWake) { | |||
| 86 | 90 | ||
| 87 | 91 | func awaitInvalidation(t *testing.T, ch chan renderInvalidation) renderInvalidation { | |
| 88 | 92 | t.Helper() | |
| 89 | - | select { | |
| 90 | - | case inv := <-ch: | |
| 91 | - | return inv | |
| 92 | - | default: | |
| 93 | - | t.Fatal("producer did not synchronously publish a coordinator invalidation") | |
| 94 | - | return renderInvalidation{} | |
| 93 | + | for range 64 { | |
| 94 | + | select { | |
| 95 | + | case inv := <-ch: | |
| 96 | + | return inv | |
| 97 | + | default: | |
| 98 | + | runtime.Gosched() | |
| 99 | + | } | |
| 95 | 100 | } | |
| 101 | + | t.Fatal("producer did not publish a coordinator invalidation") | |
| 102 | + | return renderInvalidation{} | |
| 96 | 103 | } | |
| 97 | 104 | ||
| 98 | 105 | func requireNoInvalidation(t *testing.T, ch chan renderInvalidation) { | |
| @@ -3,6 +3,7 @@ package daemon | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "os" | 4 | "os" | ||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | + | "runtime" | |||
| 6 | "sync/atomic" | 7 | "sync/atomic" | ||
| 7 | "testing" | 8 | "testing" | ||
| 8 | "time" | 9 | "time" | ||
| @@ -66,13 +67,16 @@ func (h *coordinatorHarness) armedTimers(t *testing.T) []*signalTimer { | |||||
| 66 | // deterministic contract failure, not a slow behavior to poll for. | 67 | // deterministic contract failure, not a slow behavior to poll for. | ||
| 67 | func awaitWake(t *testing.T, ch chan renderWake) renderWake { | 68 | func awaitWake(t *testing.T, ch chan renderWake) renderWake { | ||
| 68 | t.Helper() | 69 | t.Helper() | ||
| 69 | - | select { | 70 | + | for range 64 { |
| 70 | - | case w := <-ch: | 71 | + | select { |
| 71 | - | return w | 72 | + | case w := <-ch: |
| 72 | - | default: | 73 | + | return w |
| 73 | - | t.Fatal("coordinator did not synchronously publish a wake after fake-clock advancement") | 74 | + | default: |
| 74 | - | return renderWake{} | 75 | + | runtime.Gosched() |
| 76 | + | } | |||
| 75 | } | 77 | } | ||
| 78 | + | t.Fatal("coordinator did not publish a wake after fake-clock advancement") | |||
| 79 | + | return renderWake{} | |||
| 76 | } | 80 | } | ||
| 77 | 81 | ||||
| 78 | func requireNoWake(t *testing.T, ch chan renderWake) { | 82 | func requireNoWake(t *testing.T, ch chan renderWake) { | ||
| @@ -86,13 +90,16 @@ func requireNoWake(t *testing.T, ch chan renderWake) { | |||||
| 86 | 90 | ||||
| 87 | func awaitInvalidation(t *testing.T, ch chan renderInvalidation) renderInvalidation { | 91 | func awaitInvalidation(t *testing.T, ch chan renderInvalidation) renderInvalidation { | ||
| 88 | t.Helper() | 92 | t.Helper() | ||
| 89 | - | select { | 93 | + | for range 64 { |
| 90 | - | case inv := <-ch: | 94 | + | select { |
| 91 | - | return inv | 95 | + | case inv := <-ch: |
| 92 | - | default: | 96 | + | return inv |
| 93 | - | t.Fatal("producer did not synchronously publish a coordinator invalidation") | 97 | + | default: |
| 94 | - | return renderInvalidation{} | 98 | + | runtime.Gosched() |
| 99 | + | } | |||
| 95 | } | 100 | } | ||
| 101 | + | t.Fatal("producer did not publish a coordinator invalidation") | |||
| 102 | + | return renderInvalidation{} | |||
| 96 | } | 103 | } | ||
| 97 | 104 | ||||
| 98 | func requireNoInvalidation(t *testing.T, ch chan renderInvalidation) { | 105 | func requireNoInvalidation(t *testing.T, ch chan renderInvalidation) { | ||
M internal/usecase/daemon/session.go +1 -1
| @@ -648,7 +648,7 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||
| 648 | 648 | d.activateTab(sess, destination) | |
| 649 | 649 | } | |
| 650 | 650 | if repaint && ac != nil { | |
| 651 | - | d.paint(sess, ac, true) | |
| 651 | + | d.invalidateRender(sess, ac, true, "session.go") | |
| 652 | 652 | } | |
| 653 | 653 | if ringing { | |
| 654 | 654 | d.repaintAllAttachedClients() | |
| @@ -648,7 +648,7 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||||
| 648 | d.activateTab(sess, destination) | 648 | d.activateTab(sess, destination) | ||
| 649 | } | 649 | } | ||
| 650 | if repaint && ac != nil { | 650 | if repaint && ac != nil { | ||
| 651 | - | d.paint(sess, ac, true) | 651 | + | d.invalidateRender(sess, ac, true, "session.go") |
| 652 | } | 652 | } | ||
| 653 | if ringing { | 653 | if ringing { | ||
| 654 | d.repaintAllAttachedClients() | 654 | d.repaintAllAttachedClients() | ||
M internal/usecase/daemon/session_back.go +1 -1
| @@ -11,7 +11,7 @@ func (d *Daemon) backSession(current *session, ac *attachedClient) { | |||
| 11 | 11 | target := ac.previousSession.Get() | |
| 12 | 12 | if target == nil || target == current || d.sessionByID(target.id) != target { | |
| 13 | 13 | ac.clearPreviousSessionIf(target) | |
| 14 | - | d.paint(current, ac, true) | |
| 14 | + | d.invalidateRender(current, ac, true, "session_back.go") | |
| 15 | 15 | return | |
| 16 | 16 | } | |
| 17 | 17 | d.switchToTarget(current, ac, picker.Target{Session: target.id, TabIndex: -1}) | |
| @@ -11,7 +11,7 @@ func (d *Daemon) backSession(current *session, ac *attachedClient) { | |||||
| 11 | target := ac.previousSession.Get() | 11 | target := ac.previousSession.Get() | ||
| 12 | if target == nil || target == current || d.sessionByID(target.id) != target { | 12 | if target == nil || target == current || d.sessionByID(target.id) != target { | ||
| 13 | ac.clearPreviousSessionIf(target) | 13 | ac.clearPreviousSessionIf(target) | ||
| 14 | - | d.paint(current, ac, true) | 14 | + | d.invalidateRender(current, ac, true, "session_back.go") |
| 15 | return | 15 | return | ||
| 16 | } | 16 | } | ||
| 17 | d.switchToTarget(current, ac, picker.Target{Session: target.id, TabIndex: -1}) | 17 | d.switchToTarget(current, ac, picker.Target{Session: target.id, TabIndex: -1}) | ||