feat(render): redraw only damaged regions and show a lone opaque dmabuf window on an overlay plane
Wayland accumulates surface and buffer damage per commit and sends each window's last four damage entries with its content. The renderer copies only damaged wl_shm rows into its GPU buffers, and a target holding the same scene redraws only the union of damage since the contents it holds, keeping the rest of the image. A window drawn from one opaque dmabuf at integer coordinates with nothing above but the cursor goes on an overlay plane above the primary (zpos), validated by a TEST_ONLY commit that includes the cursor; refusals and cursor conflicts fall back to composition, cached per buffer. Also: planes without IN_FORMATS scan out their GETPLANE formats linear, shared GEM handles close once, output format changes resend only fullscreen feedback, a failed sync file export no longer fails the frame.
- commit
- 979e3cded8626192eae6e6c7daaad7c1d81bff27
- author
- brice · · 2026-09-26 11:59 UTC
- parent
- 8097a63
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
24 files · +1037 -58
M internal/adapters/drm/atomic.go +24 -3
| @@ -88,7 +88,12 @@ type kms interface { | |||
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | 90 | // planeRes is one plane of the card: possible is a bitmask of CRTC indices. | |
| 91 | - | type planeRes struct{ id, possible uint32 } | |
| 91 | + | type planeRes struct { | |
| 92 | + | id, possible uint32 | |
| 93 | + | // formats is the plane's format list (GETPLANE): used, all linear, | |
| 94 | + | // when the plane has no IN_FORMATS (drivers without modifiers). | |
| 95 | + | formats []uint32 | |
| 96 | + | } | |
| 92 | 97 | ||
| 93 | 98 | // atomicReq collects (object, property, value) triples in insertion order. | |
| 94 | 99 | type atomicReq struct { | |
| @@ -217,7 +222,16 @@ func (k kmsDevice) planes() ([]planeRes, error) { | |||
| 217 | 222 | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil { | |
| 218 | 223 | continue | |
| 219 | 224 | } | |
| 220 | - | out = append(out, planeRes{id: id, possible: p.possibleCrtcs}) | |
| 225 | + | res := planeRes{id: id, possible: p.possibleCrtcs} | |
| 226 | + | if p.countFormats > 0 { | |
| 227 | + | fs := make([]uint32, p.countFormats) | |
| 228 | + | p.formats = uint64(uintptr(unsafe.Pointer(&fs[0]))) | |
| 229 | + | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil { | |
| 230 | + | res.formats = fs[:min(int(p.countFormats), len(fs))] | |
| 231 | + | } | |
| 232 | + | runtime.KeepAlive(fs) | |
| 233 | + | } | |
| 234 | + | out = append(out, res) | |
| 221 | 235 | } | |
| 222 | 236 | return out, nil | |
| 223 | 237 | } | |
| @@ -256,6 +270,8 @@ type plane struct { | |||
| 256 | 270 | props map[string]uint32 | |
| 257 | 271 | crtc uint32 // CRTC_ID when read | |
| 258 | 272 | formats []ports.DMABufFormat | |
| 273 | + | // zposValue is the plane's zpos when read (its "zpos" property). | |
| 274 | + | zposValue uint64 | |
| 259 | 275 | } | |
| 260 | 276 | ||
| 261 | 277 | func (p *plane) prop(name string) uint32 { | |
| @@ -280,7 +296,7 @@ func readPlanes(k kms, pipe int) ([]*plane, error) { | |||
| 280 | 296 | if err != nil { | |
| 281 | 297 | continue | |
| 282 | 298 | } | |
| 283 | - | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1])} | |
| 299 | + | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]} | |
| 284 | 300 | for name, v := range props { | |
| 285 | 301 | p.props[name] = uint32(v[0]) | |
| 286 | 302 | } | |
| @@ -289,6 +305,11 @@ func readPlanes(k kms, pipe int) ([]*plane, error) { | |||
| 289 | 305 | p.formats = parseInFormats(data) | |
| 290 | 306 | } | |
| 291 | 307 | } | |
| 308 | + | if p.formats == nil { | |
| 309 | + | for _, f := range r.formats { | |
| 310 | + | p.formats = append(p.formats, ports.DMABufFormat{Format: f}) | |
| 311 | + | } | |
| 312 | + | } | |
| 292 | 313 | out = append(out, p) | |
| 293 | 314 | } | |
| 294 | 315 | return out, nil | |
| @@ -88,7 +88,12 @@ type kms interface { | |||||
| 88 | } | 88 | } | ||
| 89 | 89 | ||||
| 90 | // planeRes is one plane of the card: possible is a bitmask of CRTC indices. | 90 | // planeRes is one plane of the card: possible is a bitmask of CRTC indices. | ||
| 91 | - | type planeRes struct{ id, possible uint32 } | 91 | + | type planeRes struct { |
| 92 | + | id, possible uint32 | |||
| 93 | + | // formats is the plane's format list (GETPLANE): used, all linear, | |||
| 94 | + | // when the plane has no IN_FORMATS (drivers without modifiers). | |||
| 95 | + | formats []uint32 | |||
| 96 | + | } | |||
| 92 | 97 | ||||
| 93 | // atomicReq collects (object, property, value) triples in insertion order. | 98 | // atomicReq collects (object, property, value) triples in insertion order. | ||
| 94 | type atomicReq struct { | 99 | type atomicReq struct { | ||
| @@ -217,7 +222,16 @@ func (k kmsDevice) planes() ([]planeRes, error) { | |||||
| 217 | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil { | 222 | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil { | ||
| 218 | continue | 223 | continue | ||
| 219 | } | 224 | } | ||
| 220 | - | out = append(out, planeRes{id: id, possible: p.possibleCrtcs}) | 225 | + | res := planeRes{id: id, possible: p.possibleCrtcs} |
| 226 | + | if p.countFormats > 0 { | |||
| 227 | + | fs := make([]uint32, p.countFormats) | |||
| 228 | + | p.formats = uint64(uintptr(unsafe.Pointer(&fs[0]))) | |||
| 229 | + | if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) == nil { | |||
| 230 | + | res.formats = fs[:min(int(p.countFormats), len(fs))] | |||
| 231 | + | } | |||
| 232 | + | runtime.KeepAlive(fs) | |||
| 233 | + | } | |||
| 234 | + | out = append(out, res) | |||
| 221 | } | 235 | } | ||
| 222 | return out, nil | 236 | return out, nil | ||
| 223 | } | 237 | } | ||
| @@ -256,6 +270,8 @@ type plane struct { | |||||
| 256 | props map[string]uint32 | 270 | props map[string]uint32 | ||
| 257 | crtc uint32 // CRTC_ID when read | 271 | crtc uint32 // CRTC_ID when read | ||
| 258 | formats []ports.DMABufFormat | 272 | formats []ports.DMABufFormat | ||
| 273 | + | // zposValue is the plane's zpos when read (its "zpos" property). | |||
| 274 | + | zposValue uint64 | |||
| 259 | } | 275 | } | ||
| 260 | 276 | ||||
| 261 | func (p *plane) prop(name string) uint32 { | 277 | func (p *plane) prop(name string) uint32 { | ||
| @@ -280,7 +296,7 @@ func readPlanes(k kms, pipe int) ([]*plane, error) { | |||||
| 280 | if err != nil { | 296 | if err != nil { | ||
| 281 | continue | 297 | continue | ||
| 282 | } | 298 | } | ||
| 283 | - | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1])} | 299 | + | p := &plane{id: r.id, typ: uint32(props["type"][1]), props: map[string]uint32{}, crtc: uint32(props["CRTC_ID"][1]), zposValue: props["zpos"][1]} |
| 284 | for name, v := range props { | 300 | for name, v := range props { | ||
| 285 | p.props[name] = uint32(v[0]) | 301 | p.props[name] = uint32(v[0]) | ||
| 286 | } | 302 | } | ||
| @@ -289,6 +305,11 @@ func readPlanes(k kms, pipe int) ([]*plane, error) { | |||||
| 289 | p.formats = parseInFormats(data) | 305 | p.formats = parseInFormats(data) | ||
| 290 | } | 306 | } | ||
| 291 | } | 307 | } | ||
| 308 | + | if p.formats == nil { | |||
| 309 | + | for _, f := range r.formats { | |||
| 310 | + | p.formats = append(p.formats, ports.DMABufFormat{Format: f}) | |||
| 311 | + | } | |||
| 312 | + | } | |||
| 292 | out = append(out, p) | 313 | out = append(out, p) | ||
| 293 | } | 314 | } | ||
| 294 | return out, nil | 315 | return out, nil | ||
M internal/adapters/drm/output.go +59 -7
| @@ -40,6 +40,12 @@ type Output struct { | |||
| 40 | 40 | primary *plane | |
| 41 | 41 | cursor *Cursor | |
| 42 | 42 | stray []*plane | |
| 43 | + | // overlay shows one window's buffer above the composed frame | |
| 44 | + | // (overlay.go); overlayOn is the buffer on it in the pending or last | |
| 45 | + | // frame (0: off), overlayReason why it is off. | |
| 46 | + | overlay *plane | |
| 47 | + | overlayOn uint64 | |
| 48 | + | overlayReason string | |
| 43 | 49 | // fbs are the two renderer images frames alternate between, back the | |
| 44 | 50 | // one the next frame draws into (ADR 014: zero copy). | |
| 45 | 51 | fbs [2]uint32 | |
| @@ -183,6 +189,7 @@ func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) erro | |||
| 183 | 189 | o.stray = append(o.stray, p) | |
| 184 | 190 | } | |
| 185 | 191 | } | |
| 192 | + | o.pickOverlay() | |
| 186 | 193 | return nil | |
| 187 | 194 | } | |
| 188 | 195 | ||
| @@ -192,6 +199,9 @@ func (o *Output) owned() []*plane { | |||
| 192 | 199 | if o.cursor != nil { | |
| 193 | 200 | out = append(out, o.cursor.plane) | |
| 194 | 201 | } | |
| 202 | + | if o.overlay != nil { | |
| 203 | + | out = append(out, o.overlay) | |
| 204 | + | } | |
| 195 | 205 | return out | |
| 196 | 206 | } | |
| 197 | 207 | ||
| @@ -265,7 +275,7 @@ func (o *Output) modeset() error { | |||
| 265 | 275 | _ = o.k.destroyBlob(o.modeBlob) | |
| 266 | 276 | } | |
| 267 | 277 | o.modeBlob = blob | |
| 268 | - | o.vrrOn = false | |
| 278 | + | o.vrrOn, o.overlayOn = false, 0 | |
| 269 | 279 | if o.cursor != nil { | |
| 270 | 280 | o.cursor.applied = cursorState{} | |
| 271 | 281 | o.cursor.screen, o.cursor.flying = 0, false | |
| @@ -348,6 +358,7 @@ func (o *Output) modesetReq(blob uint32) *atomicReq { | |||
| 348 | 358 | if o.cursor != nil { | |
| 349 | 359 | o.cursor.props(req, o.crtc, cursorState{}) | |
| 350 | 360 | } | |
| 361 | + | o.overlayProps(req, overlayWin{}) | |
| 351 | 362 | for _, p := range o.stray { | |
| 352 | 363 | // Only planes a previous master left on this CRTC. | |
| 353 | 364 | props, err := o.k.objProps(p.id, objPlane) | |
| @@ -404,11 +415,16 @@ func (o *Output) wantVRR(direct bool) bool { | |||
| 404 | 415 | // also move the cursor or change VRR flips at vblank. fence is the | |
| 405 | 416 | // frame's GPU fence (nil: none); the caller keeps and closes it. | |
| 406 | 417 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | |
| 418 | + | return o.commitWith(fb, fence, async, vrr, f, overlayWin{}) | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // commitWith is commitFrame with ov on the overlay plane (zero: off). | |
| 422 | + | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | |
| 407 | 423 | cur := cursorState{} | |
| 408 | 424 | if o.cursor != nil { | |
| 409 | 425 | cur = o.cursor.desired() | |
| 410 | 426 | } | |
| 411 | - | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence) { | |
| 427 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) { | |
| 412 | 428 | async = false | |
| 413 | 429 | } | |
| 414 | 430 | req := &atomicReq{} | |
| @@ -422,6 +438,9 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||
| 422 | 438 | if o.cursor != nil { | |
| 423 | 439 | o.cursor.props(req, o.crtc, cur) | |
| 424 | 440 | } | |
| 441 | + | if ov.buf != 0 || o.overlayOn != 0 { | |
| 442 | + | o.overlayProps(req, ov) | |
| 443 | + | } | |
| 425 | 444 | } | |
| 426 | 445 | if fence != nil { | |
| 427 | 446 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | |
| @@ -429,7 +448,7 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||
| 429 | 448 | // The kernel takes its own reference on the fence. | |
| 430 | 449 | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { | |
| 431 | 450 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 432 | - | return o.commitFrame(fb, fence, false, vrr, f) | |
| 451 | + | return o.commitWith(fb, fence, false, vrr, f, ov) | |
| 433 | 452 | } | |
| 434 | 453 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | |
| 435 | 454 | // Retry without turning VRR on: if that passes, the driver | |
| @@ -452,6 +471,9 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||
| 452 | 471 | // An async commit leaves the cursor plane as applied. | |
| 453 | 472 | o.cursor.committed(cur) | |
| 454 | 473 | } | |
| 474 | + | if !async { | |
| 475 | + | o.overlayOn = ov.buf | |
| 476 | + | } | |
| 455 | 477 | f.frame, f.async = true, async | |
| 456 | 478 | o.setAsync(async) | |
| 457 | 479 | o.begin(f) | |
| @@ -483,6 +505,10 @@ func (o *Output) commitState(vrr bool) error { | |||
| 483 | 505 | return nil | |
| 484 | 506 | } | |
| 485 | 507 | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { | |
| 508 | + | if o.overlayConflict(err, o.overlayOn) { | |
| 509 | + | // The next frame is composed without the overlay. | |
| 510 | + | return errOverlayDropped | |
| 511 | + | } | |
| 486 | 512 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | |
| 487 | 513 | return o.commitState(vrr) | |
| 488 | 514 | } | |
| @@ -772,7 +798,12 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 772 | 798 | } | |
| 773 | 799 | if !dirty || !haveScene { | |
| 774 | 800 | if stateDirty { | |
| 775 | - | if err := o.commitState(o.wantVRR(o.shown != 0)); err != nil && !o.commitFailed(err, &enabled) { | |
| 801 | + | err := o.commitState(o.wantVRR(o.shown != 0)) | |
| 802 | + | if errors.Is(err, errOverlayDropped) { | |
| 803 | + | dirty = haveScene | |
| 804 | + | continue | |
| 805 | + | } | |
| 806 | + | if err != nil && !o.commitFailed(err, &enabled) { | |
| 776 | 807 | return fmt.Errorf("commit: %w", err) | |
| 777 | 808 | } | |
| 778 | 809 | } | |
| @@ -786,21 +817,42 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||
| 786 | 817 | direct, err = o.commitScanout(fb, c, f) | |
| 787 | 818 | } | |
| 788 | 819 | if !direct { | |
| 820 | + | ov, composed := o.overlayFrame(scene, surfaces) | |
| 789 | 821 | r.UseTarget(o.back) | |
| 790 | - | done, rerr := r.Render(scene, surfaces) | |
| 822 | + | done, rerr := r.Render(composed, surfaces) | |
| 791 | 823 | if rerr != nil { | |
| 792 | 824 | return fmt.Errorf("render frame: %w", rerr) | |
| 793 | 825 | } | |
| 794 | - | err = o.commitFrame(o.fbs[o.back], done, false, o.wantVRR(false), f) | |
| 826 | + | if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) { | |
| 827 | + | // Refused: compose the window too (cached per buffer). | |
| 828 | + | if done != nil { | |
| 829 | + | done.Close() | |
| 830 | + | } | |
| 831 | + | ov = overlayWin{} | |
| 832 | + | if done, rerr = r.Render(scene, surfaces); rerr != nil { | |
| 833 | + | return fmt.Errorf("render frame: %w", rerr) | |
| 834 | + | } | |
| 835 | + | } | |
| 836 | + | // The overlay buffer is on screen like a scanned-out one: it | |
| 837 | + | // is reported shown, so it is not released under the plane. | |
| 838 | + | f.queued, f.zeroCopy = ov.buf, ov.buf != 0 | |
| 839 | + | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | |
| 795 | 840 | if done != nil { | |
| 796 | 841 | done.Close() | |
| 797 | 842 | } | |
| 843 | + | if err != nil && o.overlayConflict(err, ov.buf) { | |
| 844 | + | err = errOverlayDropped | |
| 845 | + | } | |
| 798 | 846 | if err == nil { | |
| 799 | - | o.queued = 0 | |
| 847 | + | o.queued = ov.buf | |
| 800 | 848 | o.back = 1 - o.back | |
| 801 | 849 | } | |
| 802 | 850 | } | |
| 803 | 851 | if err != nil { | |
| 852 | + | if errors.Is(err, errOverlayDropped) { | |
| 853 | + | dirty = true | |
| 854 | + | continue | |
| 855 | + | } | |
| 804 | 856 | if !o.commitFailed(err, &enabled) { | |
| 805 | 857 | return fmt.Errorf("page flip: %w", err) | |
| 806 | 858 | } | |
| @@ -40,6 +40,12 @@ type Output struct { | |||||
| 40 | primary *plane | 40 | primary *plane | ||
| 41 | cursor *Cursor | 41 | cursor *Cursor | ||
| 42 | stray []*plane | 42 | stray []*plane | ||
| 43 | + | // overlay shows one window's buffer above the composed frame | |||
| 44 | + | // (overlay.go); overlayOn is the buffer on it in the pending or last | |||
| 45 | + | // frame (0: off), overlayReason why it is off. | |||
| 46 | + | overlay *plane | |||
| 47 | + | overlayOn uint64 | |||
| 48 | + | overlayReason string | |||
| 43 | // fbs are the two renderer images frames alternate between, back the | 49 | // fbs are the two renderer images frames alternate between, back the | ||
| 44 | // one the next frame draws into (ADR 014: zero copy). | 50 | // one the next frame draws into (ADR 014: zero copy). | ||
| 45 | fbs [2]uint32 | 51 | fbs [2]uint32 | ||
| @@ -183,6 +189,7 @@ func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) erro | |||||
| 183 | o.stray = append(o.stray, p) | 189 | o.stray = append(o.stray, p) | ||
| 184 | } | 190 | } | ||
| 185 | } | 191 | } | ||
| 192 | + | o.pickOverlay() | |||
| 186 | return nil | 193 | return nil | ||
| 187 | } | 194 | } | ||
| 188 | 195 | ||||
| @@ -192,6 +199,9 @@ func (o *Output) owned() []*plane { | |||||
| 192 | if o.cursor != nil { | 199 | if o.cursor != nil { | ||
| 193 | out = append(out, o.cursor.plane) | 200 | out = append(out, o.cursor.plane) | ||
| 194 | } | 201 | } | ||
| 202 | + | if o.overlay != nil { | |||
| 203 | + | out = append(out, o.overlay) | |||
| 204 | + | } | |||
| 195 | return out | 205 | return out | ||
| 196 | } | 206 | } | ||
| 197 | 207 | ||||
| @@ -265,7 +275,7 @@ func (o *Output) modeset() error { | |||||
| 265 | _ = o.k.destroyBlob(o.modeBlob) | 275 | _ = o.k.destroyBlob(o.modeBlob) | ||
| 266 | } | 276 | } | ||
| 267 | o.modeBlob = blob | 277 | o.modeBlob = blob | ||
| 268 | - | o.vrrOn = false | 278 | + | o.vrrOn, o.overlayOn = false, 0 |
| 269 | if o.cursor != nil { | 279 | if o.cursor != nil { | ||
| 270 | o.cursor.applied = cursorState{} | 280 | o.cursor.applied = cursorState{} | ||
| 271 | o.cursor.screen, o.cursor.flying = 0, false | 281 | o.cursor.screen, o.cursor.flying = 0, false | ||
| @@ -348,6 +358,7 @@ func (o *Output) modesetReq(blob uint32) *atomicReq { | |||||
| 348 | if o.cursor != nil { | 358 | if o.cursor != nil { | ||
| 349 | o.cursor.props(req, o.crtc, cursorState{}) | 359 | o.cursor.props(req, o.crtc, cursorState{}) | ||
| 350 | } | 360 | } | ||
| 361 | + | o.overlayProps(req, overlayWin{}) | |||
| 351 | for _, p := range o.stray { | 362 | for _, p := range o.stray { | ||
| 352 | // Only planes a previous master left on this CRTC. | 363 | // Only planes a previous master left on this CRTC. | ||
| 353 | props, err := o.k.objProps(p.id, objPlane) | 364 | props, err := o.k.objProps(p.id, objPlane) | ||
| @@ -404,11 +415,16 @@ func (o *Output) wantVRR(direct bool) bool { | |||||
| 404 | // also move the cursor or change VRR flips at vblank. fence is the | 415 | // also move the cursor or change VRR flips at vblank. fence is the | ||
| 405 | // frame's GPU fence (nil: none); the caller keeps and closes it. | 416 | // frame's GPU fence (nil: none); the caller keeps and closes it. | ||
| 406 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | 417 | func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame) error { | ||
| 418 | + | return o.commitWith(fb, fence, async, vrr, f, overlayWin{}) | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | // commitWith is commitFrame with ov on the overlay plane (zero: off). | |||
| 422 | + | func (o *Output) commitWith(fb uint32, fence *os.File, async bool, vrr bool, f pendingFrame, ov overlayWin) error { | |||
| 407 | cur := cursorState{} | 423 | cur := cursorState{} | ||
| 408 | if o.cursor != nil { | 424 | if o.cursor != nil { | ||
| 409 | cur = o.cursor.desired() | 425 | cur = o.cursor.desired() | ||
| 410 | } | 426 | } | ||
| 411 | - | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence) { | 427 | + | if async && (vrr != o.vrrOn || o.cursor != nil && cur != o.cursor.applied || fence != nil && !o.asyncFence || ov.buf != o.overlayOn) { |
| 412 | async = false | 428 | async = false | ||
| 413 | } | 429 | } | ||
| 414 | req := &atomicReq{} | 430 | req := &atomicReq{} | ||
| @@ -422,6 +438,9 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||||
| 422 | if o.cursor != nil { | 438 | if o.cursor != nil { | ||
| 423 | o.cursor.props(req, o.crtc, cur) | 439 | o.cursor.props(req, o.crtc, cur) | ||
| 424 | } | 440 | } | ||
| 441 | + | if ov.buf != 0 || o.overlayOn != 0 { | |||
| 442 | + | o.overlayProps(req, ov) | |||
| 443 | + | } | |||
| 425 | } | 444 | } | ||
| 426 | if fence != nil { | 445 | if fence != nil { | ||
| 427 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | 446 | req.set(o.primary.id, o.primary.prop("IN_FENCE_FD"), uint64(fence.Fd())) | ||
| @@ -429,7 +448,7 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||||
| 429 | // The kernel takes its own reference on the fence. | 448 | // The kernel takes its own reference on the fence. | ||
| 430 | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { | 449 | if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil { | ||
| 431 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 450 | if !async && cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 432 | - | return o.commitFrame(fb, fence, false, vrr, f) | 451 | + | return o.commitWith(fb, fence, false, vrr, f, ov) |
| 433 | } | 452 | } | ||
| 434 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | 453 | if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) { | ||
| 435 | // Retry without turning VRR on: if that passes, the driver | 454 | // Retry without turning VRR on: if that passes, the driver | ||
| @@ -452,6 +471,9 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f | |||||
| 452 | // An async commit leaves the cursor plane as applied. | 471 | // An async commit leaves the cursor plane as applied. | ||
| 453 | o.cursor.committed(cur) | 472 | o.cursor.committed(cur) | ||
| 454 | } | 473 | } | ||
| 474 | + | if !async { | |||
| 475 | + | o.overlayOn = ov.buf | |||
| 476 | + | } | |||
| 455 | f.frame, f.async = true, async | 477 | f.frame, f.async = true, async | ||
| 456 | o.setAsync(async) | 478 | o.setAsync(async) | ||
| 457 | o.begin(f) | 479 | o.begin(f) | ||
| @@ -483,6 +505,10 @@ func (o *Output) commitState(vrr bool) error { | |||||
| 483 | return nil | 505 | return nil | ||
| 484 | } | 506 | } | ||
| 485 | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { | 507 | if err := o.k.commit(req, atomicNonblock|flipEventFlag, o.userData(userState)); err != nil { | ||
| 508 | + | if o.overlayConflict(err, o.overlayOn) { | |||
| 509 | + | // The next frame is composed without the overlay. | |||
| 510 | + | return errOverlayDropped | |||
| 511 | + | } | |||
| 486 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | 512 | if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() { | ||
| 487 | return o.commitState(vrr) | 513 | return o.commitState(vrr) | ||
| 488 | } | 514 | } | ||
| @@ -772,7 +798,12 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 772 | } | 798 | } | ||
| 773 | if !dirty || !haveScene { | 799 | if !dirty || !haveScene { | ||
| 774 | if stateDirty { | 800 | if stateDirty { | ||
| 775 | - | if err := o.commitState(o.wantVRR(o.shown != 0)); err != nil && !o.commitFailed(err, &enabled) { | 801 | + | err := o.commitState(o.wantVRR(o.shown != 0)) |
| 802 | + | if errors.Is(err, errOverlayDropped) { | |||
| 803 | + | dirty = haveScene | |||
| 804 | + | continue | |||
| 805 | + | } | |||
| 806 | + | if err != nil && !o.commitFailed(err, &enabled) { | |||
| 776 | return fmt.Errorf("commit: %w", err) | 807 | return fmt.Errorf("commit: %w", err) | ||
| 777 | } | 808 | } | ||
| 778 | } | 809 | } | ||
| @@ -786,21 +817,42 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend | |||||
| 786 | direct, err = o.commitScanout(fb, c, f) | 817 | direct, err = o.commitScanout(fb, c, f) | ||
| 787 | } | 818 | } | ||
| 788 | if !direct { | 819 | if !direct { | ||
| 820 | + | ov, composed := o.overlayFrame(scene, surfaces) | |||
| 789 | r.UseTarget(o.back) | 821 | r.UseTarget(o.back) | ||
| 790 | - | done, rerr := r.Render(scene, surfaces) | 822 | + | done, rerr := r.Render(composed, surfaces) |
| 791 | if rerr != nil { | 823 | if rerr != nil { | ||
| 792 | return fmt.Errorf("render frame: %w", rerr) | 824 | return fmt.Errorf("render frame: %w", rerr) | ||
| 793 | } | 825 | } | ||
| 794 | - | err = o.commitFrame(o.fbs[o.back], done, false, o.wantVRR(false), f) | 826 | + | if ov.fb != 0 && !o.testOverlay(o.fbs[o.back], ov) { |
| 827 | + | // Refused: compose the window too (cached per buffer). | |||
| 828 | + | if done != nil { | |||
| 829 | + | done.Close() | |||
| 830 | + | } | |||
| 831 | + | ov = overlayWin{} | |||
| 832 | + | if done, rerr = r.Render(scene, surfaces); rerr != nil { | |||
| 833 | + | return fmt.Errorf("render frame: %w", rerr) | |||
| 834 | + | } | |||
| 835 | + | } | |||
| 836 | + | // The overlay buffer is on screen like a scanned-out one: it | |||
| 837 | + | // is reported shown, so it is not released under the plane. | |||
| 838 | + | f.queued, f.zeroCopy = ov.buf, ov.buf != 0 | |||
| 839 | + | err = o.commitWith(o.fbs[o.back], done, false, o.wantVRR(false), f, ov) | |||
| 795 | if done != nil { | 840 | if done != nil { | ||
| 796 | done.Close() | 841 | done.Close() | ||
| 797 | } | 842 | } | ||
| 843 | + | if err != nil && o.overlayConflict(err, ov.buf) { | |||
| 844 | + | err = errOverlayDropped | |||
| 845 | + | } | |||
| 798 | if err == nil { | 846 | if err == nil { | ||
| 799 | - | o.queued = 0 | 847 | + | o.queued = ov.buf |
| 800 | o.back = 1 - o.back | 848 | o.back = 1 - o.back | ||
| 801 | } | 849 | } | ||
| 802 | } | 850 | } | ||
| 803 | if err != nil { | 851 | if err != nil { | ||
| 852 | + | if errors.Is(err, errOverlayDropped) { | |||
| 853 | + | dirty = true | |||
| 854 | + | continue | |||
| 855 | + | } | |||
| 804 | if !o.commitFailed(err, &enabled) { | 856 | if !o.commitFailed(err, &enabled) { | ||
| 805 | return fmt.Errorf("page flip: %w", err) | 857 | return fmt.Errorf("page flip: %w", err) | ||
| 806 | } | 858 | } | ||
A internal/adapters/drm/overlay.go +199 -0
| @@ -0,0 +1,199 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Overlay plane: a window drawn from one opaque dmabuf, with nothing but | |
| 12 | + | // the cursor above it, goes on an overlay plane at its place on screen; | |
| 13 | + | // the rest of the scene is composed into the primary plane without it. | |
| 14 | + | // The client buffer is shown with no GPU pass, like direct scanout, but | |
| 15 | + | // the window need not cover the output. A refusal (TEST_ONLY) keeps the | |
| 16 | + | // window composed and is cached per buffer. | |
| 17 | + | ||
| 18 | + | // pickOverlay takes an overlay plane above the primary one (zpos), when | |
| 19 | + | // the output has one. | |
| 20 | + | func (o *Output) pickOverlay() { | |
| 21 | + | pz, ok := o.primary.zpos() | |
| 22 | + | if !ok { | |
| 23 | + | return | |
| 24 | + | } | |
| 25 | + | for _, p := range o.stray { | |
| 26 | + | if p.typ != planeOverlay { | |
| 27 | + | continue | |
| 28 | + | } | |
| 29 | + | if z, ok := p.zpos(); ok && z > pz { | |
| 30 | + | o.overlay = p | |
| 31 | + | return | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // zpos is the plane's zpos value when known. | |
| 37 | + | func (p *plane) zpos() (uint64, bool) { | |
| 38 | + | if p.props["zpos"] == 0 { | |
| 39 | + | return 0, false | |
| 40 | + | } | |
| 41 | + | return p.zposValue, true | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // overlayWin is the window on the overlay plane: its buffer and where it | |
| 45 | + | // shows, in physical pixels. | |
| 46 | + | type overlayWin struct { | |
| 47 | + | id ports.WindowID | |
| 48 | + | fb uint32 | |
| 49 | + | buf uint64 | |
| 50 | + | rect ports.Rect // CRTC rect | |
| 51 | + | w, h int // buffer size | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // overlayCandidate finds the one window the overlay can show: an opaque | |
| 55 | + | // dmabuf at integer physical coordinates, drawn 1:1, with no other window | |
| 56 | + | // or layer above it. reason is why none. | |
| 57 | + | func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (ports.SceneWindow, ports.SurfaceContent, string) { | |
| 58 | + | scale := s.Scale | |
| 59 | + | if scale <= 0 { | |
| 60 | + | scale = 1 | |
| 61 | + | } | |
| 62 | + | var pick *ports.SceneWindow | |
| 63 | + | for i := range s.Windows { | |
| 64 | + | w := &s.Windows[i] | |
| 65 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 66 | + | continue | |
| 67 | + | } | |
| 68 | + | c := surfaces[w.ID] | |
| 69 | + | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 && !w.Popup { | |
| 70 | + | pick = w | |
| 71 | + | continue | |
| 72 | + | } | |
| 73 | + | if pick != nil { | |
| 74 | + | // Something is drawn above the candidate. | |
| 75 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | if pick == nil { | |
| 79 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "no_candidate" | |
| 80 | + | } | |
| 81 | + | for _, l := range s.Layers { | |
| 82 | + | if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 && !(pick.Fullscreen && l.Layer == ports.LayerTop) { | |
| 83 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | c := surfaces[pick.ID] | |
| 87 | + | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | |
| 88 | + | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | |
| 89 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "scaled" | |
| 90 | + | } | |
| 91 | + | x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale | |
| 92 | + | if x != float64(int(x)) || y != float64(int(y)) { | |
| 93 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "fractional_position" | |
| 94 | + | } | |
| 95 | + | return *pick, c, "" | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // overlayFrame decides the overlay of a frame: the window, and the scene | |
| 99 | + | // the renderer composes (the window left out). A zero overlayWin means | |
| 100 | + | // none; the reason is logged on change. | |
| 101 | + | func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) { | |
| 102 | + | reason := "no_plane" | |
| 103 | + | var ov overlayWin | |
| 104 | + | if o.overlay != nil && o.scanout { | |
| 105 | + | var w ports.SceneWindow | |
| 106 | + | var c ports.SurfaceContent | |
| 107 | + | w, c, reason = overlayCandidate(s, surfaces) | |
| 108 | + | if reason == "" { | |
| 109 | + | var fb uint32 | |
| 110 | + | if fb, reason = o.scanoutFB(c.DMABuf, time.Now()); reason == "" { | |
| 111 | + | scale := s.Scale | |
| 112 | + | if scale <= 0 { | |
| 113 | + | scale = 1 | |
| 114 | + | } | |
| 115 | + | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | |
| 116 | + | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}} | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | if reason != o.overlayReason { | |
| 121 | + | o.log.Info().Str("component", "render").Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay") | |
| 122 | + | o.overlayReason = reason | |
| 123 | + | } | |
| 124 | + | if ov.fb == 0 { | |
| 125 | + | return overlayWin{}, s | |
| 126 | + | } | |
| 127 | + | // The composed frame leaves the window out: the overlay shows it. | |
| 128 | + | rest := s | |
| 129 | + | rest.Windows = make([]ports.SceneWindow, 0, len(s.Windows)) | |
| 130 | + | for _, w := range s.Windows { | |
| 131 | + | if w.ID != ov.id { | |
| 132 | + | rest.Windows = append(rest.Windows, w) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | return ov, rest | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // overlayProps puts ov on the overlay plane, or turns it off. | |
| 139 | + | func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |
| 140 | + | p := o.overlay | |
| 141 | + | if p == nil { | |
| 142 | + | return | |
| 143 | + | } | |
| 144 | + | if ov.fb == 0 { | |
| 145 | + | req.set(p.id, p.prop("FB_ID"), 0) | |
| 146 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | req.set(p.id, p.prop("FB_ID"), uint64(ov.fb)) | |
| 150 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |
| 151 | + | req.set(p.id, p.prop("SRC_X"), 0) | |
| 152 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |
| 153 | + | req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16) | |
| 154 | + | req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16) | |
| 155 | + | req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X))) | |
| 156 | + | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | |
| 157 | + | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | |
| 158 | + | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | |
| 162 | + | // the cursor at its current place included. A refusal is cached on the | |
| 163 | + | // buffer. | |
| 164 | + | func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |
| 165 | + | req := &atomicReq{} | |
| 166 | + | o.primaryProps(req, fb) | |
| 167 | + | o.overlayProps(req, ov) | |
| 168 | + | if o.cursor != nil { | |
| 169 | + | o.cursor.props(req, o.crtc, o.cursor.desired()) | |
| 170 | + | } | |
| 171 | + | err := o.k.commit(req, atomicTestOnly, 0) | |
| 172 | + | if err == nil { | |
| 173 | + | return true | |
| 174 | + | } | |
| 175 | + | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | |
| 176 | + | cfb.failed = "overlay_refused" | |
| 177 | + | } | |
| 178 | + | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | |
| 179 | + | return false | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // errOverlayDropped: a commit was refused with the overlay on; the frame is | |
| 183 | + | // composed again without it. | |
| 184 | + | var errOverlayDropped = errors.New("overlay dropped") | |
| 185 | + | ||
| 186 | + | // overlayConflict handles a commit refused with buffer buf on the overlay: | |
| 187 | + | // the buffer leaves the overlay for good (e.g. the cursor moved where the | |
| 188 | + | // plane forbids it) and the frame is composed. It reports whether it did. | |
| 189 | + | func (o *Output) overlayConflict(err error, buf uint64) bool { | |
| 190 | + | if buf == 0 || !errors.Is(err, unix.EINVAL) { | |
| 191 | + | return false | |
| 192 | + | } | |
| 193 | + | if cfb := o.clientFBs[buf]; cfb != nil { | |
| 194 | + | cfb.failed = "cursor_conflict" | |
| 195 | + | } | |
| 196 | + | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | |
| 197 | + | o.overlayReason = "cursor_conflict" | |
| 198 | + | return true | |
| 199 | + | } | |
| @@ -0,0 +1,199 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Overlay plane: a window drawn from one opaque dmabuf, with nothing but | |||
| 12 | + | // the cursor above it, goes on an overlay plane at its place on screen; | |||
| 13 | + | // the rest of the scene is composed into the primary plane without it. | |||
| 14 | + | // The client buffer is shown with no GPU pass, like direct scanout, but | |||
| 15 | + | // the window need not cover the output. A refusal (TEST_ONLY) keeps the | |||
| 16 | + | // window composed and is cached per buffer. | |||
| 17 | + | ||||
| 18 | + | // pickOverlay takes an overlay plane above the primary one (zpos), when | |||
| 19 | + | // the output has one. | |||
| 20 | + | func (o *Output) pickOverlay() { | |||
| 21 | + | pz, ok := o.primary.zpos() | |||
| 22 | + | if !ok { | |||
| 23 | + | return | |||
| 24 | + | } | |||
| 25 | + | for _, p := range o.stray { | |||
| 26 | + | if p.typ != planeOverlay { | |||
| 27 | + | continue | |||
| 28 | + | } | |||
| 29 | + | if z, ok := p.zpos(); ok && z > pz { | |||
| 30 | + | o.overlay = p | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // zpos is the plane's zpos value when known. | |||
| 37 | + | func (p *plane) zpos() (uint64, bool) { | |||
| 38 | + | if p.props["zpos"] == 0 { | |||
| 39 | + | return 0, false | |||
| 40 | + | } | |||
| 41 | + | return p.zposValue, true | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | // overlayWin is the window on the overlay plane: its buffer and where it | |||
| 45 | + | // shows, in physical pixels. | |||
| 46 | + | type overlayWin struct { | |||
| 47 | + | id ports.WindowID | |||
| 48 | + | fb uint32 | |||
| 49 | + | buf uint64 | |||
| 50 | + | rect ports.Rect // CRTC rect | |||
| 51 | + | w, h int // buffer size | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // overlayCandidate finds the one window the overlay can show: an opaque | |||
| 55 | + | // dmabuf at integer physical coordinates, drawn 1:1, with no other window | |||
| 56 | + | // or layer above it. reason is why none. | |||
| 57 | + | func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (ports.SceneWindow, ports.SurfaceContent, string) { | |||
| 58 | + | scale := s.Scale | |||
| 59 | + | if scale <= 0 { | |||
| 60 | + | scale = 1 | |||
| 61 | + | } | |||
| 62 | + | var pick *ports.SceneWindow | |||
| 63 | + | for i := range s.Windows { | |||
| 64 | + | w := &s.Windows[i] | |||
| 65 | + | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |||
| 66 | + | continue | |||
| 67 | + | } | |||
| 68 | + | c := surfaces[w.ID] | |||
| 69 | + | if c.DMABuf != nil && c.Opaque && len(c.Children) == 0 && !w.Popup { | |||
| 70 | + | pick = w | |||
| 71 | + | continue | |||
| 72 | + | } | |||
| 73 | + | if pick != nil { | |||
| 74 | + | // Something is drawn above the candidate. | |||
| 75 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "window_above" | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | if pick == nil { | |||
| 79 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "no_candidate" | |||
| 80 | + | } | |||
| 81 | + | for _, l := range s.Layers { | |||
| 82 | + | if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 && !(pick.Fullscreen && l.Layer == ports.LayerTop) { | |||
| 83 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "layer_above" | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | c := surfaces[pick.ID] | |||
| 87 | + | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | |||
| 88 | + | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | |||
| 89 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "scaled" | |||
| 90 | + | } | |||
| 91 | + | x, y := float64(pick.Rect.X)*scale, float64(pick.Rect.Y)*scale | |||
| 92 | + | if x != float64(int(x)) || y != float64(int(y)) { | |||
| 93 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, "fractional_position" | |||
| 94 | + | } | |||
| 95 | + | return *pick, c, "" | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // overlayFrame decides the overlay of a frame: the window, and the scene | |||
| 99 | + | // the renderer composes (the window left out). A zero overlayWin means | |||
| 100 | + | // none; the reason is logged on change. | |||
| 101 | + | func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) { | |||
| 102 | + | reason := "no_plane" | |||
| 103 | + | var ov overlayWin | |||
| 104 | + | if o.overlay != nil && o.scanout { | |||
| 105 | + | var w ports.SceneWindow | |||
| 106 | + | var c ports.SurfaceContent | |||
| 107 | + | w, c, reason = overlayCandidate(s, surfaces) | |||
| 108 | + | if reason == "" { | |||
| 109 | + | var fb uint32 | |||
| 110 | + | if fb, reason = o.scanoutFB(c.DMABuf, time.Now()); reason == "" { | |||
| 111 | + | scale := s.Scale | |||
| 112 | + | if scale <= 0 { | |||
| 113 | + | scale = 1 | |||
| 114 | + | } | |||
| 115 | + | ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height, | |||
| 116 | + | rect: ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height}} | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | if reason != o.overlayReason { | |||
| 121 | + | o.log.Info().Str("component", "render").Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay") | |||
| 122 | + | o.overlayReason = reason | |||
| 123 | + | } | |||
| 124 | + | if ov.fb == 0 { | |||
| 125 | + | return overlayWin{}, s | |||
| 126 | + | } | |||
| 127 | + | // The composed frame leaves the window out: the overlay shows it. | |||
| 128 | + | rest := s | |||
| 129 | + | rest.Windows = make([]ports.SceneWindow, 0, len(s.Windows)) | |||
| 130 | + | for _, w := range s.Windows { | |||
| 131 | + | if w.ID != ov.id { | |||
| 132 | + | rest.Windows = append(rest.Windows, w) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | return ov, rest | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // overlayProps puts ov on the overlay plane, or turns it off. | |||
| 139 | + | func (o *Output) overlayProps(req *atomicReq, ov overlayWin) { | |||
| 140 | + | p := o.overlay | |||
| 141 | + | if p == nil { | |||
| 142 | + | return | |||
| 143 | + | } | |||
| 144 | + | if ov.fb == 0 { | |||
| 145 | + | req.set(p.id, p.prop("FB_ID"), 0) | |||
| 146 | + | req.set(p.id, p.prop("CRTC_ID"), 0) | |||
| 147 | + | return | |||
| 148 | + | } | |||
| 149 | + | req.set(p.id, p.prop("FB_ID"), uint64(ov.fb)) | |||
| 150 | + | req.set(p.id, p.prop("CRTC_ID"), uint64(o.crtc)) | |||
| 151 | + | req.set(p.id, p.prop("SRC_X"), 0) | |||
| 152 | + | req.set(p.id, p.prop("SRC_Y"), 0) | |||
| 153 | + | req.set(p.id, p.prop("SRC_W"), uint64(ov.w)<<16) | |||
| 154 | + | req.set(p.id, p.prop("SRC_H"), uint64(ov.h)<<16) | |||
| 155 | + | req.set(p.id, p.prop("CRTC_X"), uint64(int64(ov.rect.X))) | |||
| 156 | + | req.set(p.id, p.prop("CRTC_Y"), uint64(int64(ov.rect.Y))) | |||
| 157 | + | req.set(p.id, p.prop("CRTC_W"), uint64(ov.rect.W)) | |||
| 158 | + | req.set(p.id, p.prop("CRTC_H"), uint64(ov.rect.H)) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // testOverlay asks KMS whether it takes the frame with ov on the overlay, | |||
| 162 | + | // the cursor at its current place included. A refusal is cached on the | |||
| 163 | + | // buffer. | |||
| 164 | + | func (o *Output) testOverlay(fb uint32, ov overlayWin) bool { | |||
| 165 | + | req := &atomicReq{} | |||
| 166 | + | o.primaryProps(req, fb) | |||
| 167 | + | o.overlayProps(req, ov) | |||
| 168 | + | if o.cursor != nil { | |||
| 169 | + | o.cursor.props(req, o.crtc, o.cursor.desired()) | |||
| 170 | + | } | |||
| 171 | + | err := o.k.commit(req, atomicTestOnly, 0) | |||
| 172 | + | if err == nil { | |||
| 173 | + | return true | |||
| 174 | + | } | |||
| 175 | + | if cfb := o.clientFBs[ov.buf]; cfb != nil && refused(err) { | |||
| 176 | + | cfb.failed = "overlay_refused" | |||
| 177 | + | } | |||
| 178 | + | o.log.Info().Str("component", "render").Err(err).Str("connector", o.conn.name).Msg("overlay refused") | |||
| 179 | + | return false | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // errOverlayDropped: a commit was refused with the overlay on; the frame is | |||
| 183 | + | // composed again without it. | |||
| 184 | + | var errOverlayDropped = errors.New("overlay dropped") | |||
| 185 | + | ||||
| 186 | + | // overlayConflict handles a commit refused with buffer buf on the overlay: | |||
| 187 | + | // the buffer leaves the overlay for good (e.g. the cursor moved where the | |||
| 188 | + | // plane forbids it) and the frame is composed. It reports whether it did. | |||
| 189 | + | func (o *Output) overlayConflict(err error, buf uint64) bool { | |||
| 190 | + | if buf == 0 || !errors.Is(err, unix.EINVAL) { | |||
| 191 | + | return false | |||
| 192 | + | } | |||
| 193 | + | if cfb := o.clientFBs[buf]; cfb != nil { | |||
| 194 | + | cfb.failed = "cursor_conflict" | |||
| 195 | + | } | |||
| 196 | + | o.log.Info().Str("component", "render").Bool("overlay", false).Str("reason", "cursor_conflict").Str("connector", o.conn.name).Msg("overlay") | |||
| 197 | + | o.overlayReason = "cursor_conflict" | |||
| 198 | + | return true | |||
| 199 | + | } | |||
A internal/adapters/drm/overlay_test.go +125 -0
| @@ -0,0 +1,125 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | "github.com/stretchr/testify/mock" | |
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | const pZpos = 30 | |
| 12 | + | ||
| 13 | + | // overlayOutput is a test output with an overlay plane above the primary. | |
| 14 | + | func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |
| 15 | + | o, k, commits := testOutput(t, errs...) | |
| 16 | + | props := map[string]uint32{"zpos": pZpos} | |
| 17 | + | for n, v := range planeProps { | |
| 18 | + | props[n] = v | |
| 19 | + | } | |
| 20 | + | o.primary.props = props | |
| 21 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | |
| 22 | + | o.pickOverlay() | |
| 23 | + | if o.overlay == nil { | |
| 24 | + | t.Fatal("no overlay picked") | |
| 25 | + | } | |
| 26 | + | return o, k, commits | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // A 1:1 opaque dmabuf window with nothing above goes on the overlay. | |
| 30 | + | func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) { | |
| 31 | + | s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{ | |
| 32 | + | {ID: 1, Rect: ports.Rect{W: 100, H: 100}}, | |
| 33 | + | {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}}, | |
| 34 | + | }} | |
| 35 | + | return s, map[ports.WindowID]ports.SurfaceContent{ | |
| 36 | + | 2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}}, | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func TestOverlayCandidate(t *testing.T) { | |
| 41 | + | s, c := overlayScene() | |
| 42 | + | if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 { | |
| 43 | + | t.Fatalf("candidate %v %q", w.ID, reason) | |
| 44 | + | } | |
| 45 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}}) | |
| 46 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |
| 47 | + | t.Fatalf("reason %q", reason) | |
| 48 | + | } | |
| 49 | + | s, _ = overlayScene() | |
| 50 | + | s.Scale = 1.5 | |
| 51 | + | if _, _, reason := overlayCandidate(s, c); reason != "scaled" { | |
| 52 | + | t.Fatalf("reason %q", reason) | |
| 53 | + | } | |
| 54 | + | s, _ = overlayScene() | |
| 55 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}} | |
| 56 | + | if _, _, reason := overlayCandidate(s, c); reason != "layer_above" { | |
| 57 | + | t.Fatalf("reason %q", reason) | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // The overlay is chosen and the composed scene leaves its window out; | |
| 62 | + | // the frame commit carries the plane at the window's place. | |
| 63 | + | func TestOverlayChosen(t *testing.T) { | |
| 64 | + | o, k, commits := overlayOutput(t) | |
| 65 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 66 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 67 | + | s, c := overlayScene() | |
| 68 | + | ov, rest := o.overlayFrame(s, c) | |
| 69 | + | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | |
| 70 | + | t.Fatalf("overlay %+v rest %+v", ov, rest.Windows) | |
| 71 | + | } | |
| 72 | + | if !o.testOverlay(70, ov) { | |
| 73 | + | t.Fatal("test refused") | |
| 74 | + | } | |
| 75 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |
| 76 | + | t.Fatal(err) | |
| 77 | + | } | |
| 78 | + | last := (*commits)[len(*commits)-1] | |
| 79 | + | if v, _ := last.req.value(tOverlay, pFB); v != 88 { | |
| 80 | + | t.Fatalf("overlay fb %d", v) | |
| 81 | + | } | |
| 82 | + | if v, _ := last.req.value(tOverlay, 14); v != 100 { | |
| 83 | + | t.Fatalf("overlay x %d", v) | |
| 84 | + | } | |
| 85 | + | if o.overlayOn != 9 { | |
| 86 | + | t.Fatalf("overlayOn %d", o.overlayOn) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | // A TEST_ONLY refusal composes the window and is cached on the buffer. | |
| 91 | + | func TestOverlayTestRefusedFallsBack(t *testing.T) { | |
| 92 | + | o, k, _ := overlayOutput(t, unix.EINVAL) | |
| 93 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 94 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 95 | + | s, c := overlayScene() | |
| 96 | + | ov, _ := o.overlayFrame(s, c) | |
| 97 | + | if o.testOverlay(70, ov) { | |
| 98 | + | t.Fatal("refusal taken") | |
| 99 | + | } | |
| 100 | + | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | |
| 101 | + | t.Fatal("refused buffer tried again") | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // A cursor commit refused with the overlay on drops the overlay: the | |
| 106 | + | // next frame is composed. | |
| 107 | + | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |
| 108 | + | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | |
| 109 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |
| 110 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |
| 111 | + | s, c := overlayScene() | |
| 112 | + | ov, _ := o.overlayFrame(s, c) | |
| 113 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | |
| 114 | + | t.Fatal(err) | |
| 115 | + | } | |
| 116 | + | o.pending = false | |
| 117 | + | o.cursor.image = true | |
| 118 | + | o.cursor.Move(150, 50) | |
| 119 | + | if err := o.commitState(false); err != errOverlayDropped { | |
| 120 | + | t.Fatalf("err %v", err) | |
| 121 | + | } | |
| 122 | + | if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" { | |
| 123 | + | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | |
| 124 | + | } | |
| 125 | + | } | |
| @@ -0,0 +1,125 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | "github.com/stretchr/testify/mock" | |||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | const pZpos = 30 | |||
| 12 | + | ||||
| 13 | + | // overlayOutput is a test output with an overlay plane above the primary. | |||
| 14 | + | func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) { | |||
| 15 | + | o, k, commits := testOutput(t, errs...) | |||
| 16 | + | props := map[string]uint32{"zpos": pZpos} | |||
| 17 | + | for n, v := range planeProps { | |||
| 18 | + | props[n] = v | |||
| 19 | + | } | |||
| 20 | + | o.primary.props = props | |||
| 21 | + | o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}} | |||
| 22 | + | o.pickOverlay() | |||
| 23 | + | if o.overlay == nil { | |||
| 24 | + | t.Fatal("no overlay picked") | |||
| 25 | + | } | |||
| 26 | + | return o, k, commits | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // A 1:1 opaque dmabuf window with nothing above goes on the overlay. | |||
| 30 | + | func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) { | |||
| 31 | + | s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{ | |||
| 32 | + | {ID: 1, Rect: ports.Rect{W: 100, H: 100}}, | |||
| 33 | + | {ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}}, | |||
| 34 | + | }} | |||
| 35 | + | return s, map[ports.WindowID]ports.SurfaceContent{ | |||
| 36 | + | 2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}}, | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func TestOverlayCandidate(t *testing.T) { | |||
| 41 | + | s, c := overlayScene() | |||
| 42 | + | if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 { | |||
| 43 | + | t.Fatalf("candidate %v %q", w.ID, reason) | |||
| 44 | + | } | |||
| 45 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}}) | |||
| 46 | + | if _, _, reason := overlayCandidate(s, c); reason != "window_above" { | |||
| 47 | + | t.Fatalf("reason %q", reason) | |||
| 48 | + | } | |||
| 49 | + | s, _ = overlayScene() | |||
| 50 | + | s.Scale = 1.5 | |||
| 51 | + | if _, _, reason := overlayCandidate(s, c); reason != "scaled" { | |||
| 52 | + | t.Fatalf("reason %q", reason) | |||
| 53 | + | } | |||
| 54 | + | s, _ = overlayScene() | |||
| 55 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}} | |||
| 56 | + | if _, _, reason := overlayCandidate(s, c); reason != "layer_above" { | |||
| 57 | + | t.Fatalf("reason %q", reason) | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // The overlay is chosen and the composed scene leaves its window out; | |||
| 62 | + | // the frame commit carries the plane at the window's place. | |||
| 63 | + | func TestOverlayChosen(t *testing.T) { | |||
| 64 | + | o, k, commits := overlayOutput(t) | |||
| 65 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 66 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 67 | + | s, c := overlayScene() | |||
| 68 | + | ov, rest := o.overlayFrame(s, c) | |||
| 69 | + | if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 { | |||
| 70 | + | t.Fatalf("overlay %+v rest %+v", ov, rest.Windows) | |||
| 71 | + | } | |||
| 72 | + | if !o.testOverlay(70, ov) { | |||
| 73 | + | t.Fatal("test refused") | |||
| 74 | + | } | |||
| 75 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil { | |||
| 76 | + | t.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | last := (*commits)[len(*commits)-1] | |||
| 79 | + | if v, _ := last.req.value(tOverlay, pFB); v != 88 { | |||
| 80 | + | t.Fatalf("overlay fb %d", v) | |||
| 81 | + | } | |||
| 82 | + | if v, _ := last.req.value(tOverlay, 14); v != 100 { | |||
| 83 | + | t.Fatalf("overlay x %d", v) | |||
| 84 | + | } | |||
| 85 | + | if o.overlayOn != 9 { | |||
| 86 | + | t.Fatalf("overlayOn %d", o.overlayOn) | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | ||||
| 90 | + | // A TEST_ONLY refusal composes the window and is cached on the buffer. | |||
| 91 | + | func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||
| 92 | + | o, k, _ := overlayOutput(t, unix.EINVAL) | |||
| 93 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 94 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 95 | + | s, c := overlayScene() | |||
| 96 | + | ov, _ := o.overlayFrame(s, c) | |||
| 97 | + | if o.testOverlay(70, ov) { | |||
| 98 | + | t.Fatal("refusal taken") | |||
| 99 | + | } | |||
| 100 | + | if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 { | |||
| 101 | + | t.Fatal("refused buffer tried again") | |||
| 102 | + | } | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // A cursor commit refused with the overlay on drops the overlay: the | |||
| 106 | + | // next frame is composed. | |||
| 107 | + | func TestOverlayCursorConflictDropsOverlay(t *testing.T) { | |||
| 108 | + | o, k, _ := overlayOutput(t, nil, unix.EINVAL) | |||
| 109 | + | k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once() | |||
| 110 | + | o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}} | |||
| 111 | + | s, c := overlayScene() | |||
| 112 | + | ov, _ := o.overlayFrame(s, c) | |||
| 113 | + | if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil { | |||
| 114 | + | t.Fatal(err) | |||
| 115 | + | } | |||
| 116 | + | o.pending = false | |||
| 117 | + | o.cursor.image = true | |||
| 118 | + | o.cursor.Move(150, 50) | |||
| 119 | + | if err := o.commitState(false); err != errOverlayDropped { | |||
| 120 | + | t.Fatalf("err %v", err) | |||
| 121 | + | } | |||
| 122 | + | if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" { | |||
| 123 | + | t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason) | |||
| 124 | + | } | |||
| 125 | + | } | |||
M internal/adapters/drm/scanout.go +7 -3
| @@ -201,8 +201,9 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |||
| 201 | 201 | if useMod { | |
| 202 | 202 | cmd.flags = fbModifiers | |
| 203 | 203 | } | |
| 204 | - | // Every plane of a buffer lives in the same dmabuf or its own: each | |
| 205 | - | // gets its GEM handle (one import per distinct file). | |
| 204 | + | // Planes in the same dmabuf share its GEM handle: each distinct | |
| 205 | + | // handle is closed once. | |
| 206 | + | closing := map[uint32]bool{p.handle: true} | |
| 206 | 207 | for i, pl := range b.Planes { | |
| 207 | 208 | if i >= len(cmd.handles) { | |
| 208 | 209 | break | |
| @@ -213,7 +214,10 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |||
| 213 | 214 | if h, err = k.primeImport(pl.File); err != nil { | |
| 214 | 215 | return 0, err | |
| 215 | 216 | } | |
| 216 | - | defer k.gemClose(h) | |
| 217 | + | if !closing[h] { | |
| 218 | + | closing[h] = true | |
| 219 | + | defer k.gemClose(h) | |
| 220 | + | } | |
| 217 | 221 | } | |
| 218 | 222 | cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset | |
| 219 | 223 | if useMod { | |
| @@ -201,8 +201,9 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |||||
| 201 | if useMod { | 201 | if useMod { | ||
| 202 | cmd.flags = fbModifiers | 202 | cmd.flags = fbModifiers | ||
| 203 | } | 203 | } | ||
| 204 | - | // Every plane of a buffer lives in the same dmabuf or its own: each | 204 | + | // Planes in the same dmabuf share its GEM handle: each distinct |
| 205 | - | // gets its GEM handle (one import per distinct file). | 205 | + | // handle is closed once. |
| 206 | + | closing := map[uint32]bool{p.handle: true} | |||
| 206 | for i, pl := range b.Planes { | 207 | for i, pl := range b.Planes { | ||
| 207 | if i >= len(cmd.handles) { | 208 | if i >= len(cmd.handles) { | ||
| 208 | break | 209 | break | ||
| @@ -213,7 +214,10 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) { | |||||
| 213 | if h, err = k.primeImport(pl.File); err != nil { | 214 | if h, err = k.primeImport(pl.File); err != nil { | ||
| 214 | return 0, err | 215 | return 0, err | ||
| 215 | } | 216 | } | ||
| 216 | - | defer k.gemClose(h) | 217 | + | if !closing[h] { |
| 218 | + | closing[h] = true | |||
| 219 | + | defer k.gemClose(h) | |||
| 220 | + | } | |||
| 217 | } | 221 | } | ||
| 218 | cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset | 222 | cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset | ||
| 219 | if useMod { | 223 | if useMod { | ||
M internal/adapters/drm/scanout_test.go +19 -0
| @@ -124,3 +124,22 @@ func TestModesetSendsScanoutFormats(t *testing.T) { | |||
| 124 | 124 | } | |
| 125 | 125 | } | |
| 126 | 126 | } | |
| 127 | + | ||
| 128 | + | // A plane without IN_FORMATS (driver without modifiers) scans out its | |
| 129 | + | // GETPLANE formats, linear. | |
| 130 | + | func TestPlaneWithoutInFormatsIsLinear(t *testing.T) { | |
| 131 | + | k := newMockkms(t) | |
| 132 | + | k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil) | |
| 133 | + | k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil) | |
| 134 | + | ps, err := readPlanes(k, 0) | |
| 135 | + | if err != nil || len(ps) != 1 { | |
| 136 | + | t.Fatalf("%v %v", ps, err) | |
| 137 | + | } | |
| 138 | + | o := &Output{primary: ps[0]} | |
| 139 | + | if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB { | |
| 140 | + | t.Fatalf("linear argb: %#x %v", f, ok) | |
| 141 | + | } | |
| 142 | + | if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok { | |
| 143 | + | t.Fatal("tiled accepted without IN_FORMATS") | |
| 144 | + | } | |
| 145 | + | } | |
| @@ -124,3 +124,22 @@ func TestModesetSendsScanoutFormats(t *testing.T) { | |||||
| 124 | } | 124 | } | ||
| 125 | } | 125 | } | ||
| 126 | } | 126 | } | ||
| 127 | + | ||||
| 128 | + | // A plane without IN_FORMATS (driver without modifiers) scans out its | |||
| 129 | + | // GETPLANE formats, linear. | |||
| 130 | + | func TestPlaneWithoutInFormatsIsLinear(t *testing.T) { | |||
| 131 | + | k := newMockkms(t) | |||
| 132 | + | k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil) | |||
| 133 | + | k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil) | |||
| 134 | + | ps, err := readPlanes(k, 0) | |||
| 135 | + | if err != nil || len(ps) != 1 { | |||
| 136 | + | t.Fatalf("%v %v", ps, err) | |||
| 137 | + | } | |||
| 138 | + | o := &Output{primary: ps[0]} | |||
| 139 | + | if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB { | |||
| 140 | + | t.Fatalf("linear argb: %#x %v", f, ok) | |||
| 141 | + | } | |||
| 142 | + | if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok { | |||
| 143 | + | t.Fatal("tiled accepted without IN_FORMATS") | |||
| 144 | + | } | |||
| 145 | + | } | |||
M internal/adapters/vulkan/compose.go +6 -2
| @@ -283,11 +283,15 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32 | |||
| 283 | 283 | return dr | |
| 284 | 284 | } | |
| 285 | 285 | ||
| 286 | - | // recordDraws draws ds in one rendering pass that clears view to bg. | |
| 287 | - | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw) { | |
| 286 | + | // recordDraws draws ds in one rendering pass that clears view to bg, or | |
| 287 | + | // keeps its contents (keep). | |
| 288 | + | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) { | |
| 288 | 289 | d, c := r.dd, &r.compose | |
| 289 | 290 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | |
| 290 | 291 | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear} | |
| 292 | + | if keep { | |
| 293 | + | attachment.LoadOp = vk.AttachmentLoadOpLoad | |
| 294 | + | } | |
| 291 | 295 | info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment} | |
| 292 | 296 | d.CmdBeginRendering(cmd, &info) | |
| 293 | 297 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline) | |
| @@ -283,11 +283,15 @@ func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, mode uint32 | |||||
| 283 | return dr | 283 | return dr | ||
| 284 | } | 284 | } | ||
| 285 | 285 | ||||
| 286 | - | // recordDraws draws ds in one rendering pass that clears view to bg. | 286 | + | // recordDraws draws ds in one rendering pass that clears view to bg, or |
| 287 | - | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw) { | 287 | + | // keeps its contents (keep). |
| 288 | + | func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) { | |||
| 288 | d, c := r.dd, &r.compose | 289 | d, c := r.dd, &r.compose | ||
| 289 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | 290 | clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)} | ||
| 290 | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear} | 291 | attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear} | ||
| 292 | + | if keep { | |||
| 293 | + | attachment.LoadOp = vk.AttachmentLoadOpLoad | |||
| 294 | + | } | |||
| 291 | info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment} | 295 | info := vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &attachment} | ||
| 292 | d.CmdBeginRendering(cmd, &info) | 296 | d.CmdBeginRendering(cmd, &info) | ||
| 293 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline) | 297 | d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, c.pipeline) | ||
M internal/adapters/vulkan/compose_test.go +3 -0
| @@ -156,6 +156,9 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||
| 156 | 156 | t.Skipf("Vulkan unavailable: %v", err) | |
| 157 | 157 | } | |
| 158 | 158 | defer r.Close() | |
| 159 | + | if !r.syncFD { | |
| 160 | + | t.Skip("frames are finished on return without sync files") | |
| 161 | + | } | |
| 159 | 162 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}} | |
| 160 | 163 | var readers []uint64 | |
| 161 | 164 | for seq := uint64(1); seq <= 3; seq++ { | |
| @@ -156,6 +156,9 @@ func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) { | |||||
| 156 | t.Skipf("Vulkan unavailable: %v", err) | 156 | t.Skipf("Vulkan unavailable: %v", err) | ||
| 157 | } | 157 | } | ||
| 158 | defer r.Close() | 158 | defer r.Close() | ||
| 159 | + | if !r.syncFD { | |||
| 160 | + | t.Skip("frames are finished on return without sync files") | |||
| 161 | + | } | |||
| 159 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}} | 162 | scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}} | ||
| 160 | var readers []uint64 | 163 | var readers []uint64 | ||
| 161 | for seq := uint64(1); seq <= 3; seq++ { | 164 | for seq := uint64(1); seq <= 3; seq++ { | ||
A internal/adapters/vulkan/damage.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Damage: an output target keeps what it holds (scene Seq, each window's | |
| 10 | + | // content Seq). When the scene is the same, only the region changed since | |
| 11 | + | // then is redrawn: every quad is clipped to it on the CPU and the pass | |
| 12 | + | // keeps the rest of the image. A changed scene, a target never drawn or | |
| 13 | + | // a history too short redraws everything. | |
| 14 | + | ||
| 15 | + | // damageRegion collects the region of a target to redraw this frame. | |
| 16 | + | type damageRegion struct { | |
| 17 | + | held *target // nil: redraw everything | |
| 18 | + | area image.Rectangle | |
| 19 | + | seen map[ports.WindowID]bool | |
| 20 | + | bound image.Rectangle | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | |
| 24 | + | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds} | |
| 25 | + | // Seq 0 is a scene core did not number (setup, tests): redraw all. | |
| 26 | + | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | |
| 27 | + | d.held = tg | |
| 28 | + | } | |
| 29 | + | return d | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // all reports whether the whole target is redrawn. | |
| 33 | + | func (d *damageRegion) all() bool { return d.held == nil } | |
| 34 | + | ||
| 35 | + | func (d *damageRegion) add(r image.Rectangle) { | |
| 36 | + | if d.held != nil { | |
| 37 | + | d.area = d.area.Union(r.Intersect(d.bound)) | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // window records a window drawn in rect (physical): if its content changed | |
| 42 | + | // and no finer damage comes (content), the whole rect is redrawn. | |
| 43 | + | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | |
| 44 | + | if d.held == nil || d.seen[id] { | |
| 45 | + | return | |
| 46 | + | } | |
| 47 | + | d.seen[id] = true | |
| 48 | + | held, ok := d.held.seqs[id] | |
| 49 | + | if ok && held == c.Seq { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | if !ok || c.Empty() || len(c.Children) > 0 { | |
| 53 | + | d.add(rect) | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | rects, known := c.DamageSince(held) | |
| 57 | + | if !known { | |
| 58 | + | d.add(rect) | |
| 59 | + | return | |
| 60 | + | } | |
| 61 | + | // Mapped by content, called next: nothing more here. | |
| 62 | + | _ = rects | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // content maps a root surface's buffer damage onto full (where its w×h | |
| 66 | + | // buffer is drawn), clipped to dst. Scaled buffers filter linearly: one | |
| 67 | + | // more pixel around each rect. | |
| 68 | + | func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, dst image.Rectangle) { | |
| 69 | + | if d.held == nil { | |
| 70 | + | return | |
| 71 | + | } | |
| 72 | + | held, ok := d.held.seqs[id] | |
| 73 | + | if !ok || held == c.Seq || len(c.Children) > 0 { | |
| 74 | + | return | |
| 75 | + | } | |
| 76 | + | rects, known := c.DamageSince(held) | |
| 77 | + | if !known || c.Width <= 0 || c.Height <= 0 { | |
| 78 | + | return | |
| 79 | + | } | |
| 80 | + | pad := 0 | |
| 81 | + | if full.Dx() != c.Width || full.Dy() != c.Height { | |
| 82 | + | pad = 1 | |
| 83 | + | } | |
| 84 | + | for _, r := range rects { | |
| 85 | + | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | |
| 86 | + | y0 := full.Min.Y + r.Y*full.Dy()/c.Height - pad | |
| 87 | + | x1 := full.Min.X + ((r.X+r.W)*full.Dx()+c.Width-1)/c.Width + pad | |
| 88 | + | y1 := full.Min.Y + ((r.Y+r.H)*full.Dy()+c.Height-1)/c.Height + pad | |
| 89 | + | d.add(image.Rect(x0, y0, x1, y1).Intersect(dst)) | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // clip limits the draws to the region; draws outside it are dropped. | |
| 94 | + | func (d *damageRegion) clip(ds []draw) []draw { | |
| 95 | + | if d.held == nil { | |
| 96 | + | return ds | |
| 97 | + | } | |
| 98 | + | out := ds[:0] | |
| 99 | + | for _, dr := range ds { | |
| 100 | + | r := image.Rect(int(dr.pc.rect[0]), int(dr.pc.rect[1]), int(dr.pc.rect[2]), int(dr.pc.rect[3])).Intersect(d.area) | |
| 101 | + | if r.Empty() { | |
| 102 | + | continue | |
| 103 | + | } | |
| 104 | + | dr.pc.rect = [4]int32{int32(r.Min.X), int32(r.Min.Y), int32(r.Max.X), int32(r.Max.Y)} | |
| 105 | + | out = append(out, dr) | |
| 106 | + | } | |
| 107 | + | return out | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // hold records what the target holds after this frame. | |
| 111 | + | func (tg *target) hold(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) { | |
| 112 | + | tg.valid, tg.sceneSeq = true, s.Seq | |
| 113 | + | tg.seqs = make(map[ports.WindowID]uint64, len(contents)) | |
| 114 | + | for id, c := range contents { | |
| 115 | + | tg.seqs[id] = c.Seq | |
| 116 | + | } | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Damage: an output target keeps what it holds (scene Seq, each window's | |||
| 10 | + | // content Seq). When the scene is the same, only the region changed since | |||
| 11 | + | // then is redrawn: every quad is clipped to it on the CPU and the pass | |||
| 12 | + | // keeps the rest of the image. A changed scene, a target never drawn or | |||
| 13 | + | // a history too short redraws everything. | |||
| 14 | + | ||||
| 15 | + | // damageRegion collects the region of a target to redraw this frame. | |||
| 16 | + | type damageRegion struct { | |||
| 17 | + | held *target // nil: redraw everything | |||
| 18 | + | area image.Rectangle | |||
| 19 | + | seen map[ports.WindowID]bool | |||
| 20 | + | bound image.Rectangle | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion { | |||
| 24 | + | d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds} | |||
| 25 | + | // Seq 0 is a scene core did not number (setup, tests): redraw all. | |||
| 26 | + | if tg.valid && s.Seq != 0 && tg.sceneSeq == s.Seq { | |||
| 27 | + | d.held = tg | |||
| 28 | + | } | |||
| 29 | + | return d | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // all reports whether the whole target is redrawn. | |||
| 33 | + | func (d *damageRegion) all() bool { return d.held == nil } | |||
| 34 | + | ||||
| 35 | + | func (d *damageRegion) add(r image.Rectangle) { | |||
| 36 | + | if d.held != nil { | |||
| 37 | + | d.area = d.area.Union(r.Intersect(d.bound)) | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // window records a window drawn in rect (physical): if its content changed | |||
| 42 | + | // and no finer damage comes (content), the whole rect is redrawn. | |||
| 43 | + | func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) { | |||
| 44 | + | if d.held == nil || d.seen[id] { | |||
| 45 | + | return | |||
| 46 | + | } | |||
| 47 | + | d.seen[id] = true | |||
| 48 | + | held, ok := d.held.seqs[id] | |||
| 49 | + | if ok && held == c.Seq { | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | if !ok || c.Empty() || len(c.Children) > 0 { | |||
| 53 | + | d.add(rect) | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | rects, known := c.DamageSince(held) | |||
| 57 | + | if !known { | |||
| 58 | + | d.add(rect) | |||
| 59 | + | return | |||
| 60 | + | } | |||
| 61 | + | // Mapped by content, called next: nothing more here. | |||
| 62 | + | _ = rects | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // content maps a root surface's buffer damage onto full (where its w×h | |||
| 66 | + | // buffer is drawn), clipped to dst. Scaled buffers filter linearly: one | |||
| 67 | + | // more pixel around each rect. | |||
| 68 | + | func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, dst image.Rectangle) { | |||
| 69 | + | if d.held == nil { | |||
| 70 | + | return | |||
| 71 | + | } | |||
| 72 | + | held, ok := d.held.seqs[id] | |||
| 73 | + | if !ok || held == c.Seq || len(c.Children) > 0 { | |||
| 74 | + | return | |||
| 75 | + | } | |||
| 76 | + | rects, known := c.DamageSince(held) | |||
| 77 | + | if !known || c.Width <= 0 || c.Height <= 0 { | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | pad := 0 | |||
| 81 | + | if full.Dx() != c.Width || full.Dy() != c.Height { | |||
| 82 | + | pad = 1 | |||
| 83 | + | } | |||
| 84 | + | for _, r := range rects { | |||
| 85 | + | x0 := full.Min.X + r.X*full.Dx()/c.Width - pad | |||
| 86 | + | y0 := full.Min.Y + r.Y*full.Dy()/c.Height - pad | |||
| 87 | + | x1 := full.Min.X + ((r.X+r.W)*full.Dx()+c.Width-1)/c.Width + pad | |||
| 88 | + | y1 := full.Min.Y + ((r.Y+r.H)*full.Dy()+c.Height-1)/c.Height + pad | |||
| 89 | + | d.add(image.Rect(x0, y0, x1, y1).Intersect(dst)) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // clip limits the draws to the region; draws outside it are dropped. | |||
| 94 | + | func (d *damageRegion) clip(ds []draw) []draw { | |||
| 95 | + | if d.held == nil { | |||
| 96 | + | return ds | |||
| 97 | + | } | |||
| 98 | + | out := ds[:0] | |||
| 99 | + | for _, dr := range ds { | |||
| 100 | + | r := image.Rect(int(dr.pc.rect[0]), int(dr.pc.rect[1]), int(dr.pc.rect[2]), int(dr.pc.rect[3])).Intersect(d.area) | |||
| 101 | + | if r.Empty() { | |||
| 102 | + | continue | |||
| 103 | + | } | |||
| 104 | + | dr.pc.rect = [4]int32{int32(r.Min.X), int32(r.Min.Y), int32(r.Max.X), int32(r.Max.Y)} | |||
| 105 | + | out = append(out, dr) | |||
| 106 | + | } | |||
| 107 | + | return out | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // hold records what the target holds after this frame. | |||
| 111 | + | func (tg *target) hold(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) { | |||
| 112 | + | tg.valid, tg.sceneSeq = true, s.Seq | |||
| 113 | + | tg.seqs = make(map[ports.WindowID]uint64, len(contents)) | |||
| 114 | + | for id, c := range contents { | |||
| 115 | + | tg.seqs[id] = c.Seq | |||
| 116 | + | } | |||
| 117 | + | } | |||
A internal/adapters/vulkan/damage_test.go +98 -0
| @@ -0,0 +1,98 @@ | |||
| 1 | + | package vulkan | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "image/color" | |
| 6 | + | "os" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // damagedContent writes px into the content's pool (the client drawing | |
| 13 | + | // into the same buffer) and records the damage of the new Seq. | |
| 14 | + | func damagedContent(t *testing.T, f *os.File, base ports.SurfaceContent, seq uint64, px []byte, hist []ports.SeqDamage) ports.SurfaceContent { | |
| 15 | + | t.Helper() | |
| 16 | + | if _, err := f.WriteAt(px, 0); err != nil { | |
| 17 | + | t.Fatal(err) | |
| 18 | + | } | |
| 19 | + | c := base | |
| 20 | + | c.Seq, c.DamageHistory = seq, hist | |
| 21 | + | return c | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Partial redraws are pixel-identical to full ones: with both targets | |
| 25 | + | // ages (A then B, then one render) and with shm double buffers | |
| 26 | + | // (render, skip, render). | |
| 27 | + | func TestRendererDamageMatchesFullRedraw(t *testing.T) { | |
| 28 | + | const w, h = 32, 32 | |
| 29 | + | newR := func() *Renderer { | |
| 30 | + | r, err := New(64, 48) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 33 | + | } | |
| 34 | + | t.Cleanup(r.Close) | |
| 35 | + | return r | |
| 36 | + | } | |
| 37 | + | damaged, full := newR(), newR() | |
| 38 | + | bufsD, err := damaged.ExportTargets(2, nil) | |
| 39 | + | if err != nil { | |
| 40 | + | t.Skipf("no exportable targets: %v", err) | |
| 41 | + | } | |
| 42 | + | for _, b := range bufsD { | |
| 43 | + | b.Planes[0].File.Close() | |
| 44 | + | } | |
| 45 | + | px := func(c color.RGBA, x0, y0, x1, y1 int, into []byte) []byte { | |
| 46 | + | for y := y0; y < y1; y++ { | |
| 47 | + | for x := x0; x < x1; x++ { | |
| 48 | + | copy(into[(y*w+x)*4:], []byte{c.B, c.G, c.R, 255}) | |
| 49 | + | } | |
| 50 | + | } | |
| 51 | + | return into | |
| 52 | + | } | |
| 53 | + | pixels := px(color.RGBA{10, 20, 30, 255}, 0, 0, w, h, make([]byte, w*h*4)) | |
| 54 | + | base := shmContent(t, w, h, w*4, pixels) | |
| 55 | + | base.ID = 1 | |
| 56 | + | f := base.SHM.File | |
| 57 | + | scene := ports.Scene{Seq: 5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: w, H: h}, Borderless: true}}} | |
| 58 | + | hist := []ports.SeqDamage{{Seq: 1, Full: true}} | |
| 59 | + | c := damagedContent(t, f, *base, 1, pixels, hist) | |
| 60 | + | step := func(i int, c ports.SurfaceContent) { | |
| 61 | + | t.Helper() | |
| 62 | + | damaged.UseTarget(i % 2) | |
| 63 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |
| 64 | + | if err := render(damaged, scene, contents); err != nil { | |
| 65 | + | t.Fatal(err) | |
| 66 | + | } | |
| 67 | + | if err := render(full, ports.Scene{Background: scene.Background, Windows: scene.Windows}, contents); err != nil { | |
| 68 | + | t.Fatal(err) | |
| 69 | + | } | |
| 70 | + | if a, b := damaged.Pixels(), full.Pixels(); !bytes.Equal(a.Pix, b.Pix) { | |
| 71 | + | t.Fatalf("frame %d differs from a full redraw", i) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | step(0, c) | |
| 75 | + | step(1, c) // target 1 drawn whole once | |
| 76 | + | before := damaged.redrawn | |
| 77 | + | // A: a red square; B: a green one elsewhere. Target 0 holds Seq 1, | |
| 78 | + | // so its next frame redraws the union of both. | |
| 79 | + | for i, d := range []struct { | |
| 80 | + | col color.RGBA | |
| 81 | + | rect ports.Rect | |
| 82 | + | }{{color.RGBA{255, 0, 0, 255}, ports.Rect{X: 2, Y: 2, W: 4, H: 4}}, {color.RGBA{0, 255, 0, 255}, ports.Rect{X: 20, Y: 10, W: 3, H: 5}}} { | |
| 83 | + | seq := uint64(2 + i) | |
| 84 | + | px(d.col, d.rect.X, d.rect.Y, d.rect.X+d.rect.W, d.rect.Y+d.rect.H, pixels) | |
| 85 | + | hist = append(hist, ports.SeqDamage{Seq: seq, Rects: []ports.Rect{d.rect}}) | |
| 86 | + | c = damagedContent(t, f, *base, seq, pixels, hist) | |
| 87 | + | } | |
| 88 | + | step(2, c) | |
| 89 | + | if got := damaged.redrawn - before; got >= 64*48 { | |
| 90 | + | t.Fatalf("partial frame redrew %d pixels", got) | |
| 91 | + | } | |
| 92 | + | // Render, skip (no frame), render: target 1 is two contents behind. | |
| 93 | + | px(color.RGBA{0, 0, 255, 255}, 0, 30, 2, 32, pixels) | |
| 94 | + | hist = append(hist, ports.SeqDamage{Seq: 4, Rects: []ports.Rect{{Y: 30, W: 2, H: 2}}}) | |
| 95 | + | c = damagedContent(t, f, *base, 4, pixels, hist) | |
| 96 | + | step(3, c) | |
| 97 | + | step(4, c) | |
| 98 | + | } | |
| @@ -0,0 +1,98 @@ | |||||
| 1 | + | package vulkan | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "image/color" | |||
| 6 | + | "os" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // damagedContent writes px into the content's pool (the client drawing | |||
| 13 | + | // into the same buffer) and records the damage of the new Seq. | |||
| 14 | + | func damagedContent(t *testing.T, f *os.File, base ports.SurfaceContent, seq uint64, px []byte, hist []ports.SeqDamage) ports.SurfaceContent { | |||
| 15 | + | t.Helper() | |||
| 16 | + | if _, err := f.WriteAt(px, 0); err != nil { | |||
| 17 | + | t.Fatal(err) | |||
| 18 | + | } | |||
| 19 | + | c := base | |||
| 20 | + | c.Seq, c.DamageHistory = seq, hist | |||
| 21 | + | return c | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // Partial redraws are pixel-identical to full ones: with both targets | |||
| 25 | + | // ages (A then B, then one render) and with shm double buffers | |||
| 26 | + | // (render, skip, render). | |||
| 27 | + | func TestRendererDamageMatchesFullRedraw(t *testing.T) { | |||
| 28 | + | const w, h = 32, 32 | |||
| 29 | + | newR := func() *Renderer { | |||
| 30 | + | r, err := New(64, 48) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 33 | + | } | |||
| 34 | + | t.Cleanup(r.Close) | |||
| 35 | + | return r | |||
| 36 | + | } | |||
| 37 | + | damaged, full := newR(), newR() | |||
| 38 | + | bufsD, err := damaged.ExportTargets(2, nil) | |||
| 39 | + | if err != nil { | |||
| 40 | + | t.Skipf("no exportable targets: %v", err) | |||
| 41 | + | } | |||
| 42 | + | for _, b := range bufsD { | |||
| 43 | + | b.Planes[0].File.Close() | |||
| 44 | + | } | |||
| 45 | + | px := func(c color.RGBA, x0, y0, x1, y1 int, into []byte) []byte { | |||
| 46 | + | for y := y0; y < y1; y++ { | |||
| 47 | + | for x := x0; x < x1; x++ { | |||
| 48 | + | copy(into[(y*w+x)*4:], []byte{c.B, c.G, c.R, 255}) | |||
| 49 | + | } | |||
| 50 | + | } | |||
| 51 | + | return into | |||
| 52 | + | } | |||
| 53 | + | pixels := px(color.RGBA{10, 20, 30, 255}, 0, 0, w, h, make([]byte, w*h*4)) | |||
| 54 | + | base := shmContent(t, w, h, w*4, pixels) | |||
| 55 | + | base.ID = 1 | |||
| 56 | + | f := base.SHM.File | |||
| 57 | + | scene := ports.Scene{Seq: 5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: w, H: h}, Borderless: true}}} | |||
| 58 | + | hist := []ports.SeqDamage{{Seq: 1, Full: true}} | |||
| 59 | + | c := damagedContent(t, f, *base, 1, pixels, hist) | |||
| 60 | + | step := func(i int, c ports.SurfaceContent) { | |||
| 61 | + | t.Helper() | |||
| 62 | + | damaged.UseTarget(i % 2) | |||
| 63 | + | contents := map[ports.WindowID]ports.SurfaceContent{1: c} | |||
| 64 | + | if err := render(damaged, scene, contents); err != nil { | |||
| 65 | + | t.Fatal(err) | |||
| 66 | + | } | |||
| 67 | + | if err := render(full, ports.Scene{Background: scene.Background, Windows: scene.Windows}, contents); err != nil { | |||
| 68 | + | t.Fatal(err) | |||
| 69 | + | } | |||
| 70 | + | if a, b := damaged.Pixels(), full.Pixels(); !bytes.Equal(a.Pix, b.Pix) { | |||
| 71 | + | t.Fatalf("frame %d differs from a full redraw", i) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | step(0, c) | |||
| 75 | + | step(1, c) // target 1 drawn whole once | |||
| 76 | + | before := damaged.redrawn | |||
| 77 | + | // A: a red square; B: a green one elsewhere. Target 0 holds Seq 1, | |||
| 78 | + | // so its next frame redraws the union of both. | |||
| 79 | + | for i, d := range []struct { | |||
| 80 | + | col color.RGBA | |||
| 81 | + | rect ports.Rect | |||
| 82 | + | }{{color.RGBA{255, 0, 0, 255}, ports.Rect{X: 2, Y: 2, W: 4, H: 4}}, {color.RGBA{0, 255, 0, 255}, ports.Rect{X: 20, Y: 10, W: 3, H: 5}}} { | |||
| 83 | + | seq := uint64(2 + i) | |||
| 84 | + | px(d.col, d.rect.X, d.rect.Y, d.rect.X+d.rect.W, d.rect.Y+d.rect.H, pixels) | |||
| 85 | + | hist = append(hist, ports.SeqDamage{Seq: seq, Rects: []ports.Rect{d.rect}}) | |||
| 86 | + | c = damagedContent(t, f, *base, seq, pixels, hist) | |||
| 87 | + | } | |||
| 88 | + | step(2, c) | |||
| 89 | + | if got := damaged.redrawn - before; got >= 64*48 { | |||
| 90 | + | t.Fatalf("partial frame redrew %d pixels", got) | |||
| 91 | + | } | |||
| 92 | + | // Render, skip (no frame), render: target 1 is two contents behind. | |||
| 93 | + | px(color.RGBA{0, 0, 255, 255}, 0, 30, 2, 32, pixels) | |||
| 94 | + | hist = append(hist, ports.SeqDamage{Seq: 4, Rects: []ports.Rect{{Y: 30, W: 2, H: 2}}}) | |||
| 95 | + | c = damagedContent(t, f, *base, 4, pixels, hist) | |||
| 96 | + | step(3, c) | |||
| 97 | + | step(4, c) | |||
| 98 | + | } | |||
M internal/adapters/vulkan/frame.go +29 -11
| @@ -1,8 +1,8 @@ | |||
| 1 | 1 | package vulkan | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "errors" | |
| 5 | 4 | "fmt" | |
| 5 | + | "image" | |
| 6 | 6 | "math" | |
| 7 | 7 | "os" | |
| 8 | 8 | "runtime" | |
| @@ -168,8 +168,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 168 | 168 | return nil, err | |
| 169 | 169 | } | |
| 170 | 170 | } | |
| 171 | - | ds := r.draws(s, contents) | |
| 171 | + | tg := r.target() | |
| 172 | + | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | |
| 173 | + | ds := r.draws(s, contents, dmg) | |
| 172 | 174 | r.dropPools() | |
| 175 | + | partial := !dmg.all() | |
| 176 | + | if partial { | |
| 177 | + | // The pass keeps the image: repaint the background under the | |
| 178 | + | // region, then everything clipped to it. | |
| 179 | + | ds = dmg.clip(append([]draw{r.fillDraw(dmg.area, parseColor(s.Background))}, ds...)) | |
| 180 | + | r.redrawn += dmg.area.Dx() * dmg.area.Dy() | |
| 181 | + | } else { | |
| 182 | + | r.redrawn += r.width * r.height | |
| 183 | + | } | |
| 173 | 184 | d := r.dd | |
| 174 | 185 | cmd := slot.cmd | |
| 175 | 186 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil { | |
| @@ -179,10 +190,13 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 179 | 190 | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil { | |
| 180 | 191 | return nil, err | |
| 181 | 192 | } | |
| 182 | - | tg := r.target() | |
| 183 | - | // The whole target is cleared by the pass: its old contents never | |
| 184 | - | // matter (UNDEFINED). | |
| 193 | + | // A full redraw clears the target: its old contents never matter | |
| 194 | + | // (UNDEFINED). A partial one keeps them. | |
| 185 | 195 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange} | |
| 196 | + | if partial { | |
| 197 | + | b.OldLayout = tg.layout | |
| 198 | + | b.DstAccessMask |= vk.AccessColorAttachmentReadBit | |
| 199 | + | } | |
| 186 | 200 | if tg.exported { | |
| 187 | 201 | // Acquire from the display (foreign queue family). | |
| 188 | 202 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | |
| @@ -198,7 +212,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 198 | 212 | } | |
| 199 | 213 | } | |
| 200 | 214 | r.ownership(cmd, dmas, true) | |
| 201 | - | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds) | |
| 215 | + | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) | |
| 202 | 216 | r.ownership(cmd, dmas, false) | |
| 203 | 217 | b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal | |
| 204 | 218 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored | |
| @@ -243,6 +257,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 243 | 257 | runtime.KeepAlive(waits) | |
| 244 | 258 | slot.frame, slot.busy, r.submitted = frame, true, frame | |
| 245 | 259 | tg.layout = b.NewLayout | |
| 260 | + | tg.hold(s, contents) | |
| 246 | 261 | r.last, r.readBack = tg, false | |
| 247 | 262 | r.dropUnused() | |
| 248 | 263 | r.dropShm() | |
| @@ -251,13 +266,16 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 251 | 266 | } | |
| 252 | 267 | var fd int32 | |
| 253 | 268 | get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit} | |
| 254 | - | if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil { | |
| 255 | - | // The semaphore stays signalled: replace it once the frame is | |
| 256 | - | // done, so the slot's next submit signals a fresh one. | |
| 257 | - | err = errors.Join(err, r.waitFrame(frame)) | |
| 269 | + | if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success { | |
| 270 | + | // The frame is drawn: finish it on the CPU. The semaphore stays | |
| 271 | + | // signalled: replace it, so the slot's next submit signals a | |
| 272 | + | // fresh one. | |
| 273 | + | if err := r.waitFrame(frame); err != nil { | |
| 274 | + | return nil, err | |
| 275 | + | } | |
| 258 | 276 | d.DestroySemaphore(r.device, slot.done, nil) | |
| 259 | 277 | slot.done = 0 | |
| 260 | - | return nil, errors.Join(err, r.newDone(slot)) | |
| 278 | + | return nil, r.newDone(slot) | |
| 261 | 279 | } | |
| 262 | 280 | if fd < 0 { | |
| 263 | 281 | // Already signalled: the driver may return -1. | |
| @@ -1,8 +1,8 @@ | |||||
| 1 | package vulkan | 1 | package vulkan | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "errors" | |||
| 5 | "fmt" | 4 | "fmt" | ||
| 5 | + | "image" | |||
| 6 | "math" | 6 | "math" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | "runtime" | 8 | "runtime" | ||
| @@ -168,8 +168,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 168 | return nil, err | 168 | return nil, err | ||
| 169 | } | 169 | } | ||
| 170 | } | 170 | } | ||
| 171 | - | ds := r.draws(s, contents) | 171 | + | tg := r.target() |
| 172 | + | dmg := newDamage(tg, s, image.Rect(0, 0, r.width, r.height)) | |||
| 173 | + | ds := r.draws(s, contents, dmg) | |||
| 172 | r.dropPools() | 174 | r.dropPools() | ||
| 175 | + | partial := !dmg.all() | |||
| 176 | + | if partial { | |||
| 177 | + | // The pass keeps the image: repaint the background under the | |||
| 178 | + | // region, then everything clipped to it. | |||
| 179 | + | ds = dmg.clip(append([]draw{r.fillDraw(dmg.area, parseColor(s.Background))}, ds...)) | |||
| 180 | + | r.redrawn += dmg.area.Dx() * dmg.area.Dy() | |||
| 181 | + | } else { | |||
| 182 | + | r.redrawn += r.width * r.height | |||
| 183 | + | } | |||
| 173 | d := r.dd | 184 | d := r.dd | ||
| 174 | cmd := slot.cmd | 185 | cmd := slot.cmd | ||
| 175 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil { | 186 | if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil { | ||
| @@ -179,10 +190,13 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 179 | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil { | 190 | if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil { | ||
| 180 | return nil, err | 191 | return nil, err | ||
| 181 | } | 192 | } | ||
| 182 | - | tg := r.target() | 193 | + | // A full redraw clears the target: its old contents never matter |
| 183 | - | // The whole target is cleared by the pass: its old contents never | 194 | + | // (UNDEFINED). A partial one keeps them. |
| 184 | - | // matter (UNDEFINED). | |||
| 185 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange} | 195 | b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange} | ||
| 196 | + | if partial { | |||
| 197 | + | b.OldLayout = tg.layout | |||
| 198 | + | b.DstAccessMask |= vk.AccessColorAttachmentReadBit | |||
| 199 | + | } | |||
| 186 | if tg.exported { | 200 | if tg.exported { | ||
| 187 | // Acquire from the display (foreign queue family). | 201 | // Acquire from the display (foreign queue family). | ||
| 188 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | 202 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family | ||
| @@ -198,7 +212,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 198 | } | 212 | } | ||
| 199 | } | 213 | } | ||
| 200 | r.ownership(cmd, dmas, true) | 214 | r.ownership(cmd, dmas, true) | ||
| 201 | - | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds) | 215 | + | r.recordDraws(cmd, tg.view, parseColor(s.Background), ds, partial) |
| 202 | r.ownership(cmd, dmas, false) | 216 | r.ownership(cmd, dmas, false) | ||
| 203 | b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal | 217 | b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal | ||
| 204 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored | 218 | b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored | ||
| @@ -243,6 +257,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 243 | runtime.KeepAlive(waits) | 257 | runtime.KeepAlive(waits) | ||
| 244 | slot.frame, slot.busy, r.submitted = frame, true, frame | 258 | slot.frame, slot.busy, r.submitted = frame, true, frame | ||
| 245 | tg.layout = b.NewLayout | 259 | tg.layout = b.NewLayout | ||
| 260 | + | tg.hold(s, contents) | |||
| 246 | r.last, r.readBack = tg, false | 261 | r.last, r.readBack = tg, false | ||
| 247 | r.dropUnused() | 262 | r.dropUnused() | ||
| 248 | r.dropShm() | 263 | r.dropShm() | ||
| @@ -251,13 +266,16 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 251 | } | 266 | } | ||
| 252 | var fd int32 | 267 | var fd int32 | ||
| 253 | get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit} | 268 | get := vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit} | ||
| 254 | - | if err := checked("vkGetSemaphoreFdKHR", d.GetSemaphoreFdKHR(r.device, &get, &fd)); err != nil { | 269 | + | if d.GetSemaphoreFdKHR(r.device, &get, &fd) != vk.Success { |
| 255 | - | // The semaphore stays signalled: replace it once the frame is | 270 | + | // The frame is drawn: finish it on the CPU. The semaphore stays |
| 256 | - | // done, so the slot's next submit signals a fresh one. | 271 | + | // signalled: replace it, so the slot's next submit signals a |
| 257 | - | err = errors.Join(err, r.waitFrame(frame)) | 272 | + | // fresh one. |
| 273 | + | if err := r.waitFrame(frame); err != nil { | |||
| 274 | + | return nil, err | |||
| 275 | + | } | |||
| 258 | d.DestroySemaphore(r.device, slot.done, nil) | 276 | d.DestroySemaphore(r.device, slot.done, nil) | ||
| 259 | slot.done = 0 | 277 | slot.done = 0 | ||
| 260 | - | return nil, errors.Join(err, r.newDone(slot)) | 278 | + | return nil, r.newDone(slot) |
| 261 | } | 279 | } | ||
| 262 | if fd < 0 { | 280 | if fd < 0 { | ||
| 263 | // Already signalled: the driver may return -1. | 281 | // Already signalled: the driver may return -1. | ||
M internal/adapters/vulkan/renderer.go +16 -3
| @@ -57,6 +57,8 @@ type Renderer struct { | |||
| 57 | 57 | // the bytes copied into them, for tests. | |
| 58 | 58 | shm map[shmKey]*shmSurface | |
| 59 | 59 | copied int | |
| 60 | + | // redrawn counts the target pixels drawn, for tests. | |
| 61 | + | redrawn int | |
| 60 | 62 | // cursors are the exported cursor images (CursorBuffers). | |
| 61 | 63 | cursors [2]*cursorImage | |
| 62 | 64 | // compose draws every frame (compose.go); maxRange bounds one | |
| @@ -299,7 +301,7 @@ func windowColor(id ports.WindowID) [3]uint8 { | |||
| 299 | 301 | ||
| 300 | 302 | // draws walks the scene into quads in paint order, copying new wl_shm | |
| 301 | 303 | // content into GPU buffers on the way. | |
| 302 | - | func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) []draw { | |
| 304 | + | func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw { | |
| 303 | 305 | // Scene rects are logical; everything below works in physical pixels. | |
| 304 | 306 | scale := s.Scale | |
| 305 | 307 | if scale <= 0 { | |
| @@ -339,8 +341,13 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 339 | 341 | if err != nil { | |
| 340 | 342 | return | |
| 341 | 343 | } | |
| 342 | - | st := shmState{pool: b.Pool, offset: b.Offset, stride: b.Stride, w: content.Width, h: content.Height, seq: seq} | |
| 343 | - | c, err := r.shmCopyFor(key, st, pixels) | |
| 344 | + | st := shmState{w: content.Width, h: content.Height, seq: seq} | |
| 345 | + | // Damage history is the root surface's: children copy in full. | |
| 346 | + | var damage func(uint64) ([]ports.Rect, bool) | |
| 347 | + | if key.index == 0 { | |
| 348 | + | damage = content.DamageSince | |
| 349 | + | } | |
| 350 | + | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | |
| 344 | 351 | if err != nil { | |
| 345 | 352 | return | |
| 346 | 353 | } | |
| @@ -373,6 +380,9 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 373 | 380 | if dst.Empty() { | |
| 374 | 381 | return | |
| 375 | 382 | } | |
| 383 | + | if key.index == 0 { | |
| 384 | + | dmg.content(key.win, content, full, dst) | |
| 385 | + | } | |
| 376 | 386 | addContent(dst, full, content, key, seq) | |
| 377 | 387 | } | |
| 378 | 388 | // place draws a surface tree with its window geometry at (x, y) | |
| @@ -406,6 +416,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 406 | 416 | continue | |
| 407 | 417 | } | |
| 408 | 418 | content := contents[layer.ID] | |
| 419 | + | dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)) | |
| 409 | 420 | place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H) | |
| 410 | 421 | } | |
| 411 | 422 | } | |
| @@ -426,6 +437,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 426 | 437 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | |
| 427 | 438 | body := physRect(cx, cy, cw, ch) | |
| 428 | 439 | content := contents[w.ID] | |
| 440 | + | dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H)) | |
| 429 | 441 | if content.Empty() { | |
| 430 | 442 | add(body, windowColor(w.ID)) | |
| 431 | 443 | } else { | |
| @@ -455,6 +467,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 455 | 467 | for _, w := range s.Windows { | |
| 456 | 468 | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | |
| 457 | 469 | content := contents[w.ID] | |
| 470 | + | dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) | |
| 458 | 471 | place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |
| 459 | 472 | } | |
| 460 | 473 | } | |
| @@ -57,6 +57,8 @@ type Renderer struct { | |||||
| 57 | // the bytes copied into them, for tests. | 57 | // the bytes copied into them, for tests. | ||
| 58 | shm map[shmKey]*shmSurface | 58 | shm map[shmKey]*shmSurface | ||
| 59 | copied int | 59 | copied int | ||
| 60 | + | // redrawn counts the target pixels drawn, for tests. | |||
| 61 | + | redrawn int | |||
| 60 | // cursors are the exported cursor images (CursorBuffers). | 62 | // cursors are the exported cursor images (CursorBuffers). | ||
| 61 | cursors [2]*cursorImage | 63 | cursors [2]*cursorImage | ||
| 62 | // compose draws every frame (compose.go); maxRange bounds one | 64 | // compose draws every frame (compose.go); maxRange bounds one | ||
| @@ -299,7 +301,7 @@ func windowColor(id ports.WindowID) [3]uint8 { | |||||
| 299 | 301 | ||||
| 300 | // draws walks the scene into quads in paint order, copying new wl_shm | 302 | // draws walks the scene into quads in paint order, copying new wl_shm | ||
| 301 | // content into GPU buffers on the way. | 303 | // content into GPU buffers on the way. | ||
| 302 | - | func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) []draw { | 304 | + | func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, dmg *damageRegion) []draw { |
| 303 | // Scene rects are logical; everything below works in physical pixels. | 305 | // Scene rects are logical; everything below works in physical pixels. | ||
| 304 | scale := s.Scale | 306 | scale := s.Scale | ||
| 305 | if scale <= 0 { | 307 | if scale <= 0 { | ||
| @@ -339,8 +341,13 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 339 | if err != nil { | 341 | if err != nil { | ||
| 340 | return | 342 | return | ||
| 341 | } | 343 | } | ||
| 342 | - | st := shmState{pool: b.Pool, offset: b.Offset, stride: b.Stride, w: content.Width, h: content.Height, seq: seq} | 344 | + | st := shmState{w: content.Width, h: content.Height, seq: seq} |
| 343 | - | c, err := r.shmCopyFor(key, st, pixels) | 345 | + | // Damage history is the root surface's: children copy in full. |
| 346 | + | var damage func(uint64) ([]ports.Rect, bool) | |||
| 347 | + | if key.index == 0 { | |||
| 348 | + | damage = content.DamageSince | |||
| 349 | + | } | |||
| 350 | + | c, err := r.shmCopyFor(key, st, pixels, b.Offset, b.Stride, damage) | |||
| 344 | if err != nil { | 351 | if err != nil { | ||
| 345 | return | 352 | return | ||
| 346 | } | 353 | } | ||
| @@ -373,6 +380,9 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 373 | if dst.Empty() { | 380 | if dst.Empty() { | ||
| 374 | return | 381 | return | ||
| 375 | } | 382 | } | ||
| 383 | + | if key.index == 0 { | |||
| 384 | + | dmg.content(key.win, content, full, dst) | |||
| 385 | + | } | |||
| 376 | addContent(dst, full, content, key, seq) | 386 | addContent(dst, full, content, key, seq) | ||
| 377 | } | 387 | } | ||
| 378 | // place draws a surface tree with its window geometry at (x, y) | 388 | // place draws a surface tree with its window geometry at (x, y) | ||
| @@ -406,6 +416,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 406 | continue | 416 | continue | ||
| 407 | } | 417 | } | ||
| 408 | content := contents[layer.ID] | 418 | content := contents[layer.ID] | ||
| 419 | + | dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)) | |||
| 409 | place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H) | 420 | place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H) | ||
| 410 | } | 421 | } | ||
| 411 | } | 422 | } | ||
| @@ -426,6 +437,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 426 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | 437 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | ||
| 427 | body := physRect(cx, cy, cw, ch) | 438 | body := physRect(cx, cy, cw, ch) | ||
| 428 | content := contents[w.ID] | 439 | content := contents[w.ID] | ||
| 440 | + | dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H)) | |||
| 429 | if content.Empty() { | 441 | if content.Empty() { | ||
| 430 | add(body, windowColor(w.ID)) | 442 | add(body, windowColor(w.ID)) | ||
| 431 | } else { | 443 | } else { | ||
| @@ -455,6 +467,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 455 | for _, w := range s.Windows { | 467 | for _, w := range s.Windows { | ||
| 456 | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | 468 | if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | ||
| 457 | content := contents[w.ID] | 469 | content := contents[w.ID] | ||
| 470 | + | dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)) | |||
| 458 | place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | 471 | place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | ||
| 459 | } | 472 | } | ||
| 460 | } | 473 | } | ||
M internal/adapters/vulkan/shm.go +25 -10
| @@ -154,12 +154,11 @@ type shmKey struct { | |||
| 154 | 154 | index int | |
| 155 | 155 | } | |
| 156 | 156 | ||
| 157 | - | // shmState is what a GPU buffer holds: a client buffer at a content Seq. | |
| 157 | + | // shmState is what a GPU buffer holds: a w×h client buffer at a content | |
| 158 | + | // Seq. | |
| 158 | 159 | type shmState struct { | |
| 159 | - | pool uint64 | |
| 160 | - | offset, stride int | |
| 161 | - | w, h int | |
| 162 | - | seq uint64 | |
| 160 | + | w, h int | |
| 161 | + | seq uint64 | |
| 163 | 162 | } | |
| 164 | 163 | ||
| 165 | 164 | // shmCopy is one of a surface's two GPU buffers and the set binding it. | |
| @@ -178,8 +177,11 @@ type shmSurface struct { | |||
| 178 | 177 | } | |
| 179 | 178 | ||
| 180 | 179 | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | |
| 181 | - | // the client pixels into one when no buffer holds them yet. | |
| 182 | - | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, error) { | |
| 180 | + | // the client pixels (stride bytes per row from offset) into one when no | |
| 181 | + | // buffer holds them yet. A buffer holding an older content of the same | |
| 182 | + | // size gets only what changed since (damage, in buffer pixels; false: | |
| 183 | + | // everything). | |
| 184 | + | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |
| 183 | 185 | s := r.shm[key] | |
| 184 | 186 | if s == nil { | |
| 185 | 187 | s = &shmSurface{} | |
| @@ -223,14 +225,27 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, | |||
| 223 | 225 | } | |
| 224 | 226 | dst := unsafe.Slice((*byte)(c.gpu.mapped), size) | |
| 225 | 227 | row := st.w * 4 | |
| 228 | + | rects := []ports.Rect{{W: st.w, H: st.h}} | |
| 229 | + | if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil { | |
| 230 | + | if d, ok := damage(c.holds.seq); ok { | |
| 231 | + | rects = d | |
| 232 | + | } | |
| 233 | + | } | |
| 226 | 234 | // The client may shrink its pool under the mapping: its window then | |
| 227 | 235 | // shows garbage for this frame, never a crash, and is copied again. | |
| 228 | 236 | c.valid = copyGuarded(func() { | |
| 229 | - | for y := range st.h { | |
| 230 | - | copy(dst[y*row:(y+1)*row], pixels[st.offset+y*st.stride:]) | |
| 237 | + | for _, rc := range rects { | |
| 238 | + | x0, y0 := max(rc.X, 0), max(rc.Y, 0) | |
| 239 | + | x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h) | |
| 240 | + | for y := y0; y < y1; y++ { | |
| 241 | + | d := y*row + x0*4 | |
| 242 | + | s := offset + y*stride + x0*4 | |
| 243 | + | n := (x1 - x0) * 4 | |
| 244 | + | copy(dst[d:d+n], pixels[s:s+n]) | |
| 245 | + | r.copied += n | |
| 246 | + | } | |
| 231 | 247 | } | |
| 232 | 248 | }) | |
| 233 | - | r.copied += size | |
| 234 | 249 | c.holds, c.gpu.last = st, r.frame | |
| 235 | 250 | return c, nil | |
| 236 | 251 | } | |
| @@ -154,12 +154,11 @@ type shmKey struct { | |||||
| 154 | index int | 154 | index int | ||
| 155 | } | 155 | } | ||
| 156 | 156 | ||||
| 157 | - | // shmState is what a GPU buffer holds: a client buffer at a content Seq. | 157 | + | // shmState is what a GPU buffer holds: a w×h client buffer at a content |
| 158 | + | // Seq. | |||
| 158 | type shmState struct { | 159 | type shmState struct { | ||
| 159 | - | pool uint64 | 160 | + | w, h int |
| 160 | - | offset, stride int | 161 | + | seq uint64 |
| 161 | - | w, h int | |||
| 162 | - | seq uint64 | |||
| 163 | } | 162 | } | ||
| 164 | 163 | ||||
| 165 | // shmCopy is one of a surface's two GPU buffers and the set binding it. | 164 | // shmCopy is one of a surface's two GPU buffers and the set binding it. | ||
| @@ -178,8 +177,11 @@ type shmSurface struct { | |||||
| 178 | } | 177 | } | ||
| 179 | 178 | ||||
| 180 | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | 179 | // shmCopyFor returns the GPU buffer holding a surface's content st, copying | ||
| 181 | - | // the client pixels into one when no buffer holds them yet. | 180 | + | // the client pixels (stride bytes per row from offset) into one when no |
| 182 | - | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, error) { | 181 | + | // buffer holds them yet. A buffer holding an older content of the same |
| 182 | + | // size gets only what changed since (damage, in buffer pixels; false: | |||
| 183 | + | // everything). | |||
| 184 | + | func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte, offset, stride int, damage func(since uint64) ([]ports.Rect, bool)) (*shmCopy, error) { | |||
| 183 | s := r.shm[key] | 185 | s := r.shm[key] | ||
| 184 | if s == nil { | 186 | if s == nil { | ||
| 185 | s = &shmSurface{} | 187 | s = &shmSurface{} | ||
| @@ -223,14 +225,27 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy, | |||||
| 223 | } | 225 | } | ||
| 224 | dst := unsafe.Slice((*byte)(c.gpu.mapped), size) | 226 | dst := unsafe.Slice((*byte)(c.gpu.mapped), size) | ||
| 225 | row := st.w * 4 | 227 | row := st.w * 4 | ||
| 228 | + | rects := []ports.Rect{{W: st.w, H: st.h}} | |||
| 229 | + | if c.valid && c.holds.w == st.w && c.holds.h == st.h && damage != nil { | |||
| 230 | + | if d, ok := damage(c.holds.seq); ok { | |||
| 231 | + | rects = d | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 226 | // The client may shrink its pool under the mapping: its window then | 234 | // The client may shrink its pool under the mapping: its window then | ||
| 227 | // shows garbage for this frame, never a crash, and is copied again. | 235 | // shows garbage for this frame, never a crash, and is copied again. | ||
| 228 | c.valid = copyGuarded(func() { | 236 | c.valid = copyGuarded(func() { | ||
| 229 | - | for y := range st.h { | 237 | + | for _, rc := range rects { |
| 230 | - | copy(dst[y*row:(y+1)*row], pixels[st.offset+y*st.stride:]) | 238 | + | x0, y0 := max(rc.X, 0), max(rc.Y, 0) |
| 239 | + | x1, y1 := min(rc.X+rc.W, st.w), min(rc.Y+rc.H, st.h) | |||
| 240 | + | for y := y0; y < y1; y++ { | |||
| 241 | + | d := y*row + x0*4 | |||
| 242 | + | s := offset + y*stride + x0*4 | |||
| 243 | + | n := (x1 - x0) * 4 | |||
| 244 | + | copy(dst[d:d+n], pixels[s:s+n]) | |||
| 245 | + | r.copied += n | |||
| 246 | + | } | |||
| 231 | } | 247 | } | ||
| 232 | }) | 248 | }) | ||
| 233 | - | r.copied += size | |||
| 234 | c.holds, c.gpu.last = st, r.frame | 249 | c.holds, c.gpu.last = st, r.frame | ||
| 235 | return c, nil | 250 | return c, nil | ||
| 236 | } | 251 | } | ||
M internal/adapters/vulkan/targets.go +5 -0
| @@ -28,6 +28,11 @@ type target struct { | |||
| 28 | 28 | // exported images are shared with the display: ownership goes to the | |
| 29 | 29 | // foreign queue family after each frame, and KMS reads them. | |
| 30 | 30 | exported bool | |
| 31 | + | // What the image holds (damage.go): the scene Seq and each window's | |
| 32 | + | // content Seq; valid once a frame was drawn whole. | |
| 33 | + | valid bool | |
| 34 | + | sceneSeq uint64 | |
| 35 | + | seqs map[ports.WindowID]uint64 | |
| 31 | 36 | } | |
| 32 | 37 | ||
| 33 | 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | |
| @@ -28,6 +28,11 @@ type target struct { | |||||
| 28 | // exported images are shared with the display: ownership goes to the | 28 | // exported images are shared with the display: ownership goes to the | ||
| 29 | // foreign queue family after each frame, and KMS reads them. | 29 | // foreign queue family after each frame, and KMS reads them. | ||
| 30 | exported bool | 30 | exported bool | ||
| 31 | + | // What the image holds (damage.go): the scene Seq and each window's | |||
| 32 | + | // content Seq; valid once a frame was drawn whole. | |||
| 33 | + | valid bool | |||
| 34 | + | sceneSeq uint64 | |||
| 35 | + | seqs map[ports.WindowID]uint64 | |||
| 31 | } | 36 | } | ||
| 32 | 37 | ||||
| 33 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | 38 | // fourccXRGB is DRM_FORMAT_XRGB8888: B8G8R8A8 in memory, alpha ignored. | ||
A internal/adapters/wayland/damage_test.go +99 -0
| @@ -0,0 +1,99 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 9 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Damage reaches the renderer in buffer pixels, per content, with the | |
| 14 | + | // window's recent history; a new buffer without damage is a full change. | |
| 15 | + | func TestDamageHistory(t *testing.T) { | |
| 16 | + | s, events, _, contents, dir := contentServer(t) | |
| 17 | + | c := protocolClient(t, s, dir) | |
| 18 | + | comp := bindVersion(t, c, "wl_compositor", 4) | |
| 19 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 20 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 21 | + | registerProtocol(t, c, shm) | |
| 22 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |
| 23 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |
| 24 | + | registerProtocol(t, c, surf) | |
| 25 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 26 | + | serials := make(chan uint32, 8) | |
| 27 | + | xp := &configureProxy{serial: serials} | |
| 28 | + | xp.SetID(xdg) | |
| 29 | + | c.Context().Register(xp) | |
| 30 | + | registerProtocol(t, c, top) | |
| 31 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |
| 32 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 33 | + | if err := c.Roundtrip(); err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |
| 37 | + | fd, err := unix.MemfdCreate("damage", 0) | |
| 38 | + | if err != nil { | |
| 39 | + | t.Fatal(err) | |
| 40 | + | } | |
| 41 | + | defer unix.Close(fd) | |
| 42 | + | if err := unix.Ftruncate(fd, 2*16*16*4); err != nil { | |
| 43 | + | t.Fatal(err) | |
| 44 | + | } | |
| 45 | + | pool := c.AllocateID() | |
| 46 | + | registerProtocol(t, c, pool) | |
| 47 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(2*16*16*4)); err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 50 | + | bufs := [2]uint32{c.AllocateID(), c.AllocateID()} | |
| 51 | + | for i, b := range bufs { | |
| 52 | + | registerProtocol(t, c, b) | |
| 53 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, b, int32(i*16*16*4), int32(16), int32(16), int32(64), uint32(0)) | |
| 54 | + | } | |
| 55 | + | next := func() ports.SurfaceContent { | |
| 56 | + | t.Helper() | |
| 57 | + | if err := c.Roundtrip(); err != nil { | |
| 58 | + | t.Fatal(err) | |
| 59 | + | } | |
| 60 | + | deadline := time.After(2 * time.Second) | |
| 61 | + | for { | |
| 62 | + | select { | |
| 63 | + | case got := <-contents: | |
| 64 | + | if got.Width == 16 { | |
| 65 | + | return got | |
| 66 | + | } | |
| 67 | + | case <-deadline: | |
| 68 | + | t.Fatal("no content") | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | // A new size: full. | |
| 73 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 74 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(4), int32(4)) | |
| 75 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 76 | + | mapped(t, events, 2*time.Second) | |
| 77 | + | first := next() | |
| 78 | + | if h := first.DamageHistory; len(h) == 0 || h[len(h)-1].Seq != first.Seq || !h[len(h)-1].Full { | |
| 79 | + | t.Fatalf("resize: %+v", h) | |
| 80 | + | } | |
| 81 | + | // Buffer and surface damage, clipped to the buffer. | |
| 82 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 83 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(2), int32(3), int32(4), int32(5)) | |
| 84 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamage, int32(14), int32(14), int32(1<<30), int32(1<<30)) | |
| 85 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 86 | + | second := next() | |
| 87 | + | got, ok := second.DamageSince(first.Seq) | |
| 88 | + | want := []ports.Rect{{X: 2, Y: 3, W: 4, H: 5}, {X: 14, Y: 14, W: 2, H: 2}} | |
| 89 | + | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | |
| 90 | + | t.Fatalf("damage %v %v", got, ok) | |
| 91 | + | } | |
| 92 | + | // No damage with a new buffer: full. | |
| 93 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 94 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 95 | + | third := next() | |
| 96 | + | if _, ok := third.DamageSince(second.Seq); ok { | |
| 97 | + | t.Fatal("undamaged new buffer taken as unchanged") | |
| 98 | + | } | |
| 99 | + | } | |
| @@ -0,0 +1,99 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/wayland" | |||
| 9 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Damage reaches the renderer in buffer pixels, per content, with the | |||
| 14 | + | // window's recent history; a new buffer without damage is a full change. | |||
| 15 | + | func TestDamageHistory(t *testing.T) { | |||
| 16 | + | s, events, _, contents, dir := contentServer(t) | |||
| 17 | + | c := protocolClient(t, s, dir) | |||
| 18 | + | comp := bindVersion(t, c, "wl_compositor", 4) | |||
| 19 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 20 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 21 | + | registerProtocol(t, c, shm) | |||
| 22 | + | surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID() | |||
| 23 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 24 | + | registerProtocol(t, c, surf) | |||
| 25 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 26 | + | serials := make(chan uint32, 8) | |||
| 27 | + | xp := &configureProxy{serial: serials} | |||
| 28 | + | xp.SetID(xdg) | |||
| 29 | + | c.Context().Register(xp) | |||
| 30 | + | registerProtocol(t, c, top) | |||
| 31 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top) | |||
| 32 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 33 | + | if err := c.Roundtrip(); err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials) | |||
| 37 | + | fd, err := unix.MemfdCreate("damage", 0) | |||
| 38 | + | if err != nil { | |||
| 39 | + | t.Fatal(err) | |||
| 40 | + | } | |||
| 41 | + | defer unix.Close(fd) | |||
| 42 | + | if err := unix.Ftruncate(fd, 2*16*16*4); err != nil { | |||
| 43 | + | t.Fatal(err) | |||
| 44 | + | } | |||
| 45 | + | pool := c.AllocateID() | |||
| 46 | + | registerProtocol(t, c, pool) | |||
| 47 | + | if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(2*16*16*4)); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | bufs := [2]uint32{c.AllocateID(), c.AllocateID()} | |||
| 51 | + | for i, b := range bufs { | |||
| 52 | + | registerProtocol(t, c, b) | |||
| 53 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, b, int32(i*16*16*4), int32(16), int32(16), int32(64), uint32(0)) | |||
| 54 | + | } | |||
| 55 | + | next := func() ports.SurfaceContent { | |||
| 56 | + | t.Helper() | |||
| 57 | + | if err := c.Roundtrip(); err != nil { | |||
| 58 | + | t.Fatal(err) | |||
| 59 | + | } | |||
| 60 | + | deadline := time.After(2 * time.Second) | |||
| 61 | + | for { | |||
| 62 | + | select { | |||
| 63 | + | case got := <-contents: | |||
| 64 | + | if got.Width == 16 { | |||
| 65 | + | return got | |||
| 66 | + | } | |||
| 67 | + | case <-deadline: | |||
| 68 | + | t.Fatal("no content") | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | // A new size: full. | |||
| 73 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 74 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(0), int32(0), int32(4), int32(4)) | |||
| 75 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 76 | + | mapped(t, events, 2*time.Second) | |||
| 77 | + | first := next() | |||
| 78 | + | if h := first.DamageHistory; len(h) == 0 || h[len(h)-1].Seq != first.Seq || !h[len(h)-1].Full { | |||
| 79 | + | t.Fatalf("resize: %+v", h) | |||
| 80 | + | } | |||
| 81 | + | // Buffer and surface damage, clipped to the buffer. | |||
| 82 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 83 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(2), int32(3), int32(4), int32(5)) | |||
| 84 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamage, int32(14), int32(14), int32(1<<30), int32(1<<30)) | |||
| 85 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 86 | + | second := next() | |||
| 87 | + | got, ok := second.DamageSince(first.Seq) | |||
| 88 | + | want := []ports.Rect{{X: 2, Y: 3, W: 4, H: 5}, {X: 14, Y: 14, W: 2, H: 2}} | |||
| 89 | + | if !ok || len(got) != 2 || got[0] != want[0] || got[1] != want[1] { | |||
| 90 | + | t.Fatalf("damage %v %v", got, ok) | |||
| 91 | + | } | |||
| 92 | + | // No damage with a new buffer: full. | |||
| 93 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 94 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 95 | + | third := next() | |||
| 96 | + | if _, ok := third.DamageSince(second.Seq); ok { | |||
| 97 | + | t.Fatal("undamaged new buffer taken as unchanged") | |||
| 98 | + | } | |||
| 99 | + | } | |||
M internal/adapters/wayland/dmabuf.go +1 -1
| @@ -193,7 +193,7 @@ func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) { | |||
| 193 | 193 | } | |
| 194 | 194 | for fb, surf := range g.feedbacks { | |
| 195 | 195 | if surf != nil && !surf.destroyed { | |
| 196 | - | if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Output == f.Output { | |
| 196 | + | if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output { | |
| 197 | 197 | g.send(fb, surf) | |
| 198 | 198 | } | |
| 199 | 199 | } | |
| @@ -193,7 +193,7 @@ func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) { | |||||
| 193 | } | 193 | } | ||
| 194 | for fb, surf := range g.feedbacks { | 194 | for fb, surf := range g.feedbacks { | ||
| 195 | if surf != nil && !surf.destroyed { | 195 | if surf != nil && !surf.destroyed { | ||
| 196 | - | if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Output == f.Output { | 196 | + | if root := surf.root(); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output { |
| 197 | g.send(fb, surf) | 197 | g.send(fb, surf) | ||
| 198 | } | 198 | } | ||
| 199 | } | 199 | } | ||
M internal/adapters/wayland/dmabuf_test.go +18 -0
| @@ -264,8 +264,26 @@ func TestDMABufScanoutTranche(t *testing.T) { | |||
| 264 | 264 | if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 { | |
| 265 | 265 | t.Fatalf("fullscreen: %+v", r) | |
| 266 | 266 | } | |
| 267 | + | // Fullscreen on another output: its formats (none) apply at once. | |
| 268 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1"} | |
| 269 | + | if r := round("other output"); len(r) != 1 { | |
| 270 | + | t.Fatalf("moved output: %+v", r) | |
| 271 | + | } | |
| 267 | 272 | commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"} | |
| 268 | 273 | if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 { | |
| 269 | 274 | t.Fatalf("left fullscreen: %+v", r) | |
| 270 | 275 | } | |
| 276 | + | // A destroyed feedback gets nothing more (a protocol error would | |
| 277 | + | // kill the connection) and leaves the server's list. | |
| 278 | + | requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy) | |
| 279 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"} | |
| 280 | + | formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7} | |
| 281 | + | if err := c.Roundtrip(); err != nil { | |
| 282 | + | t.Fatal(err) | |
| 283 | + | } | |
| 284 | + | var n int | |
| 285 | + | s.display.Do(func() { n = len(s.dmabuf.feedbacks) }) | |
| 286 | + | if n != 0 { | |
| 287 | + | t.Fatalf("%d feedbacks kept", n) | |
| 288 | + | } | |
| 271 | 289 | } | |
| @@ -264,8 +264,26 @@ func TestDMABufScanoutTranche(t *testing.T) { | |||||
| 264 | if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 { | 264 | if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 { | ||
| 265 | t.Fatalf("fullscreen: %+v", r) | 265 | t.Fatalf("fullscreen: %+v", r) | ||
| 266 | } | 266 | } | ||
| 267 | + | // Fullscreen on another output: its formats (none) apply at once. | |||
| 268 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1"} | |||
| 269 | + | if r := round("other output"); len(r) != 1 { | |||
| 270 | + | t.Fatalf("moved output: %+v", r) | |||
| 271 | + | } | |||
| 267 | commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"} | 272 | commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"} | ||
| 268 | if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 { | 273 | if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 { | ||
| 269 | t.Fatalf("left fullscreen: %+v", r) | 274 | t.Fatalf("left fullscreen: %+v", r) | ||
| 270 | } | 275 | } | ||
| 276 | + | // A destroyed feedback gets nothing more (a protocol error would | |||
| 277 | + | // kill the connection) and leaves the server's list. | |||
| 278 | + | requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy) | |||
| 279 | + | commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"} | |||
| 280 | + | formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7} | |||
| 281 | + | if err := c.Roundtrip(); err != nil { | |||
| 282 | + | t.Fatal(err) | |||
| 283 | + | } | |||
| 284 | + | var n int | |||
| 285 | + | s.display.Do(func() { n = len(s.dmabuf.feedbacks) }) | |||
| 286 | + | if n != 0 { | |||
| 287 | + | t.Fatalf("%d feedbacks kept", n) | |||
| 288 | + | } | |||
| 271 | } | 289 | } | ||
M internal/adapters/wayland/fifo.go +5 -1
| @@ -216,6 +216,8 @@ type update struct { | |||
| 216 | 216 | barrier bool | |
| 217 | 217 | wait bool | |
| 218 | 218 | at time.Time | |
| 219 | + | damage []ports.Rect | |
| 220 | + | bufDamage []ports.Rect | |
| 219 | 221 | } | |
| 220 | 222 | ||
| 221 | 223 | type childMove struct { | |
| @@ -226,8 +228,9 @@ type childMove struct { | |||
| 226 | 228 | // takePending moves the pending state into an update: one-shot state is | |
| 227 | 229 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | |
| 228 | 230 | func (s *surface) takePending() update { | |
| 229 | - | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime} | |
| 231 | + | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage} | |
| 230 | 232 | s.attached, s.pending, s.callbacks = false, nil, nil | |
| 233 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| 231 | 234 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | |
| 232 | 235 | if v := s.viewport; v != nil { | |
| 233 | 236 | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | |
| @@ -256,6 +259,7 @@ func (s *surface) putPending(u update) { | |||
| 256 | 259 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | |
| 257 | 260 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | |
| 258 | 261 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | |
| 262 | + | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |
| 259 | 263 | if u.vp != nil && s.viewport == u.vp { | |
| 260 | 264 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | |
| 261 | 265 | } | |
| @@ -216,6 +216,8 @@ type update struct { | |||||
| 216 | barrier bool | 216 | barrier bool | ||
| 217 | wait bool | 217 | wait bool | ||
| 218 | at time.Time | 218 | at time.Time | ||
| 219 | + | damage []ports.Rect | |||
| 220 | + | bufDamage []ports.Rect | |||
| 219 | } | 221 | } | ||
| 220 | 222 | ||||
| 221 | type childMove struct { | 223 | type childMove struct { | ||
| @@ -226,8 +228,9 @@ type childMove struct { | |||||
| 226 | // takePending moves the pending state into an update: one-shot state is | 228 | // takePending moves the pending state into an update: one-shot state is | ||
| 227 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | 229 | // cleared, sticky state (scale, hints, viewport, geometry, layer) kept. | ||
| 228 | func (s *surface) takePending() update { | 230 | func (s *surface) takePending() update { | ||
| 229 | - | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime} | 231 | + | u := update{attached: s.attached, buffer: s.pending, scale: s.pendingScale, async: s.pendingAsync, kind: s.pendingKind, callbacks: s.callbacks, barrier: s.pendingBarrier, wait: s.pendingWait, at: s.pendingTime, damage: s.pendingDamage, bufDamage: s.pendingBufDamage} |
| 230 | s.attached, s.pending, s.callbacks = false, nil, nil | 232 | s.attached, s.pending, s.callbacks = false, nil, nil | ||
| 233 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |||
| 231 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | 234 | s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{} | ||
| 232 | if v := s.viewport; v != nil { | 235 | if v := s.viewport; v != nil { | ||
| 233 | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | 236 | u.vp, u.vpW, u.vpH, u.vpSet = v, v.pendingW, v.pendingH, v.pendingSet | ||
| @@ -256,6 +259,7 @@ func (s *surface) putPending(u update) { | |||||
| 256 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | 259 | s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks | ||
| 257 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | 260 | s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind | ||
| 258 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | 261 | s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at | ||
| 262 | + | s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage | |||
| 259 | if u.vp != nil && s.viewport == u.vp { | 263 | if u.vp != nil && s.viewport == u.vp { | ||
| 260 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | 264 | u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet | ||
| 261 | } | 265 | } | ||
M internal/adapters/wayland/layer.go +1 -1
| @@ -167,7 +167,7 @@ func (l *layerSurface) unmap() { | |||
| 167 | 167 | l.acked = false | |
| 168 | 168 | l.serials = nil | |
| 169 | 169 | l.shell.server.layerChanged() | |
| 170 | - | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}) | |
| 170 | + | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true}) | |
| 171 | 171 | } | |
| 172 | 172 | ||
| 173 | 173 | // close tells the client its output is gone; the surface stays unmapped | |
| @@ -167,7 +167,7 @@ func (l *layerSurface) unmap() { | |||||
| 167 | l.acked = false | 167 | l.acked = false | ||
| 168 | l.serials = nil | 168 | l.serials = nil | ||
| 169 | l.shell.server.layerChanged() | 169 | l.shell.server.layerChanged() | ||
| 170 | - | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}) | 170 | + | l.shell.server.emitContent(ports.SurfaceContent{ID: l.id}, damage{full: true}) |
| 171 | } | 171 | } | ||
| 172 | 172 | ||||
| 173 | // close tells the client its output is gone; the surface stays unmapped | 173 | // close tells the client its output is gone; the surface stays unmapped | ||
M internal/adapters/wayland/server.go +26 -8
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "fmt" | |
| 7 | 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | |
| 8 | 8 | "os" | |
| 9 | + | "slices" | |
| 9 | 10 | "sync" | |
| 10 | 11 | "time" | |
| 11 | 12 | ||
| @@ -101,12 +102,14 @@ type Server struct { | |||
| 101 | 102 | // Events, which happens only at shutdown (core owns the receiving end). A | |
| 102 | 103 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | |
| 103 | 104 | // the display goroutine. | |
| 104 | - | eventMu sync.Mutex | |
| 105 | - | events []ports.ClientEvent | |
| 106 | - | eventReady chan struct{} | |
| 107 | - | contentMu sync.Mutex | |
| 108 | - | contents map[ports.WindowID]ports.SurfaceContent | |
| 109 | - | contentSeq map[ports.WindowID]uint64 | |
| 105 | + | eventMu sync.Mutex | |
| 106 | + | events []ports.ClientEvent | |
| 107 | + | eventReady chan struct{} | |
| 108 | + | contentMu sync.Mutex | |
| 109 | + | contents map[ports.WindowID]ports.SurfaceContent | |
| 110 | + | contentSeq map[ports.WindowID]uint64 | |
| 111 | + | // damage is each window's recent damage (contentMu). | |
| 112 | + | damage map[ports.WindowID][]ports.SeqDamage | |
| 110 | 113 | contentNotify chan struct{} | |
| 111 | 114 | contentReady chan struct{} | |
| 112 | 115 | outputs []*output | |
| @@ -162,7 +165,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 162 | 165 | d.Close() | |
| 163 | 166 | return nil, err | |
| 164 | 167 | } | |
| 165 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 168 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | |
| 166 | 169 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 167 | 170 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | |
| 168 | 171 | s.tokens = map[string]activationToken{} | |
| @@ -242,6 +245,10 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 242 | 245 | return err | |
| 243 | 246 | } | |
| 244 | 247 | ||
| 248 | + | // damageHistory is how many contents a window's damage history covers: | |
| 249 | + | // enough for a renderer target a few frames old. | |
| 250 | + | const damageHistory = 4 | |
| 251 | + | ||
| 245 | 252 | // forwardOutputFormats applies the outputs' scanout formats on the | |
| 246 | 253 | // display goroutine. | |
| 247 | 254 | func (s *Server) forwardOutputFormats(ctx context.Context) { | |
| @@ -303,13 +310,24 @@ func (s *Server) forward(ctx context.Context) { | |||
| 303 | 310 | } | |
| 304 | 311 | } | |
| 305 | 312 | ||
| 306 | - | func (s *Server) emitContent(c ports.SurfaceContent) { | |
| 313 | + | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { | |
| 307 | 314 | if s.channels.Contents == nil { | |
| 308 | 315 | return | |
| 309 | 316 | } | |
| 310 | 317 | s.contentMu.Lock() | |
| 311 | 318 | s.contentSeq[c.ID]++ | |
| 312 | 319 | c.Seq = s.contentSeq[c.ID] | |
| 320 | + | // The window's damage history: its last damageHistory contents. | |
| 321 | + | h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | |
| 322 | + | if len(h) > damageHistory { | |
| 323 | + | h = h[len(h)-damageHistory:] | |
| 324 | + | } | |
| 325 | + | if c.Empty() { | |
| 326 | + | delete(s.damage, c.ID) | |
| 327 | + | } else { | |
| 328 | + | s.damage[c.ID] = h | |
| 329 | + | } | |
| 330 | + | c.DamageHistory = slices.Clone(h) | |
| 313 | 331 | s.contents[c.ID] = c | |
| 314 | 332 | close(s.contentNotify) | |
| 315 | 333 | s.contentNotify = make(chan struct{}) | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | 7 | "github.com/bnema/purego-libwayland/protocol/relativepointer" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | + | "slices" | |||
| 9 | "sync" | 10 | "sync" | ||
| 10 | "time" | 11 | "time" | ||
| 11 | 12 | ||||
| @@ -101,12 +102,14 @@ type Server struct { | |||||
| 101 | // Events, which happens only at shutdown (core owns the receiving end). A | 102 | // Events, which happens only at shutdown (core owns the receiving end). A | ||
| 102 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | 103 | // stalled core is a bug surfaced by ctx cancellation, not a reason to block | ||
| 103 | // the display goroutine. | 104 | // the display goroutine. | ||
| 104 | - | eventMu sync.Mutex | 105 | + | eventMu sync.Mutex |
| 105 | - | events []ports.ClientEvent | 106 | + | events []ports.ClientEvent |
| 106 | - | eventReady chan struct{} | 107 | + | eventReady chan struct{} |
| 107 | - | contentMu sync.Mutex | 108 | + | contentMu sync.Mutex |
| 108 | - | contents map[ports.WindowID]ports.SurfaceContent | 109 | + | contents map[ports.WindowID]ports.SurfaceContent |
| 109 | - | contentSeq map[ports.WindowID]uint64 | 110 | + | contentSeq map[ports.WindowID]uint64 |
| 111 | + | // damage is each window's recent damage (contentMu). | |||
| 112 | + | damage map[ports.WindowID][]ports.SeqDamage | |||
| 110 | contentNotify chan struct{} | 113 | contentNotify chan struct{} | ||
| 111 | contentReady chan struct{} | 114 | contentReady chan struct{} | ||
| 112 | outputs []*output | 115 | outputs []*output | ||
| @@ -162,7 +165,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 162 | d.Close() | 165 | d.Close() | ||
| 163 | return nil, err | 166 | return nil, err | ||
| 164 | } | 167 | } | ||
| 165 | - | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} | 168 | + | s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1), reports: map[string]ports.OutputPresented{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), damage: map[ports.WindowID][]ports.SeqDamage{}, contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay} |
| 166 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | 169 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | ||
| 167 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | 170 | s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{} | ||
| 168 | s.tokens = map[string]activationToken{} | 171 | s.tokens = map[string]activationToken{} | ||
| @@ -242,6 +245,10 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 242 | return err | 245 | return err | ||
| 243 | } | 246 | } | ||
| 244 | 247 | ||||
| 248 | + | // damageHistory is how many contents a window's damage history covers: | |||
| 249 | + | // enough for a renderer target a few frames old. | |||
| 250 | + | const damageHistory = 4 | |||
| 251 | + | ||||
| 245 | // forwardOutputFormats applies the outputs' scanout formats on the | 252 | // forwardOutputFormats applies the outputs' scanout formats on the | ||
| 246 | // display goroutine. | 253 | // display goroutine. | ||
| 247 | func (s *Server) forwardOutputFormats(ctx context.Context) { | 254 | func (s *Server) forwardOutputFormats(ctx context.Context) { | ||
| @@ -303,13 +310,24 @@ func (s *Server) forward(ctx context.Context) { | |||||
| 303 | } | 310 | } | ||
| 304 | } | 311 | } | ||
| 305 | 312 | ||||
| 306 | - | func (s *Server) emitContent(c ports.SurfaceContent) { | 313 | + | func (s *Server) emitContent(c ports.SurfaceContent, d damage) { |
| 307 | if s.channels.Contents == nil { | 314 | if s.channels.Contents == nil { | ||
| 308 | return | 315 | return | ||
| 309 | } | 316 | } | ||
| 310 | s.contentMu.Lock() | 317 | s.contentMu.Lock() | ||
| 311 | s.contentSeq[c.ID]++ | 318 | s.contentSeq[c.ID]++ | ||
| 312 | c.Seq = s.contentSeq[c.ID] | 319 | c.Seq = s.contentSeq[c.ID] | ||
| 320 | + | // The window's damage history: its last damageHistory contents. | |||
| 321 | + | h := append(s.damage[c.ID], ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects}) | |||
| 322 | + | if len(h) > damageHistory { | |||
| 323 | + | h = h[len(h)-damageHistory:] | |||
| 324 | + | } | |||
| 325 | + | if c.Empty() { | |||
| 326 | + | delete(s.damage, c.ID) | |||
| 327 | + | } else { | |||
| 328 | + | s.damage[c.ID] = h | |||
| 329 | + | } | |||
| 330 | + | c.DamageHistory = slices.Clone(h) | |||
| 313 | s.contents[c.ID] = c | 331 | s.contents[c.ID] = c | ||
| 314 | close(s.contentNotify) | 332 | close(s.contentNotify) | ||
| 315 | s.contentNotify = make(chan struct{}) | 333 | s.contentNotify = make(chan struct{}) | ||
M internal/adapters/wayland/surface.go +81 -7
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "image" | |
| 4 | 5 | "time" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/nefertty/internal/ports" | |
| @@ -60,8 +61,22 @@ type surface struct { | |||
| 60 | 61 | // contentType is the wp_content_type_v1; contentKind the committed type. | |
| 61 | 62 | contentType *contentTypeHandler | |
| 62 | 63 | contentKind, pendingKind uint32 | |
| 64 | + | // Damage requested since the last commit: in surface (logical) and | |
| 65 | + | // buffer pixels; committed is what the last commit changed, in buffer | |
| 66 | + | // pixels (full: everything), read by the window's damage history. | |
| 67 | + | pendingDamage, pendingBufDamage []ports.Rect | |
| 68 | + | committed damage | |
| 63 | 69 | } | |
| 64 | 70 | ||
| 71 | + | // damage is what one commit changed in a surface's buffer. | |
| 72 | + | type damage struct { | |
| 73 | + | full bool | |
| 74 | + | rects []ports.Rect | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // maxDamageRects bounds one commit's rects: past it the commit is full. | |
| 78 | + | const maxDamageRects = 32 | |
| 79 | + | ||
| 65 | 80 | // subState is the subsurface tree: parent is set on subsurfaces, children | |
| 66 | 81 | // are ordered bottom to top, below marks children under their parent. | |
| 67 | 82 | type subState struct { | |
| @@ -127,11 +142,16 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||
| 127 | 142 | } | |
| 128 | 143 | } | |
| 129 | 144 | ||
| 130 | - | // redraw sends the tree of a mapped root to the outputs. | |
| 145 | + | // redraw sends the tree of a mapped root to the outputs. Only a commit | |
| 146 | + | // of the root's own buffer with no other change keeps partial damage. | |
| 131 | 147 | func (s *surface) redraw() { | |
| 132 | 148 | r := s.root() | |
| 133 | 149 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | |
| 134 | - | s.server.emitContent(r.tree(id)) | |
| 150 | + | d := damage{full: true} | |
| 151 | + | if s == r && len(r.sub.children) == 0 { | |
| 152 | + | d = r.committed | |
| 153 | + | } | |
| 154 | + | s.server.emitContent(r.tree(id), d) | |
| 135 | 155 | } | |
| 136 | 156 | } | |
| 137 | 157 | ||
| @@ -248,6 +268,7 @@ func (s *surface) applyCommit() { | |||
| 248 | 268 | if state, ok := s.server.buffers[s.current.Resource]; ok { | |
| 249 | 269 | if c, ok := state.content(0); ok { | |
| 250 | 270 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | |
| 271 | + | s.commitDamage(true, !s.has || c.Width != s.content.Width || c.Height != s.content.Height, c.Width, c.Height) | |
| 251 | 272 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | |
| 252 | 273 | s.lastW, s.lastH = c.Width, c.Height | |
| 253 | 274 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | |
| @@ -260,6 +281,9 @@ func (s *surface) applyCommit() { | |||
| 260 | 281 | // replaces them. | |
| 261 | 282 | } | |
| 262 | 283 | } | |
| 284 | + | if !fresh { | |
| 285 | + | s.commitDamage(false, false, 0, 0) | |
| 286 | + | } | |
| 263 | 287 | if s.current == nil { | |
| 264 | 288 | s.content, s.has = ports.SurfaceContent{}, false | |
| 265 | 289 | } | |
| @@ -280,14 +304,64 @@ func (s *surface) applyCommit() { | |||
| 280 | 304 | s.xdg.window.afterCommit() | |
| 281 | 305 | } | |
| 282 | 306 | if fresh || moved || geometry || hinted || s.sub.parent != nil { | |
| 307 | + | if moved || geometry { | |
| 308 | + | s.committed = damage{full: true} | |
| 309 | + | } | |
| 283 | 310 | s.redraw() | |
| 284 | 311 | } | |
| 285 | 312 | } | |
| 286 | - | func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {} | |
| 287 | - | func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {} | |
| 288 | - | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | |
| 289 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |
| 290 | - | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |
| 313 | + | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { | |
| 314 | + | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { | |
| 315 | + | s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |
| 316 | + | } | |
| 317 | + | } | |
| 318 | + | func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) { | |
| 319 | + | if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects { | |
| 320 | + | s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |
| 321 | + | } | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | // commitDamage turns the requested damage into buffer pixels of a bw×bh | |
| 325 | + | // buffer. Surface damage scales by the buffer/logical ratio, rounded out. | |
| 326 | + | // A new size, too many rects or none at all with a new buffer (clients | |
| 327 | + | // that do not report damage) is a full change. | |
| 328 | + | func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |
| 329 | + | surf, buf := s.pendingDamage, s.pendingBufDamage | |
| 330 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |
| 331 | + | if !fresh { | |
| 332 | + | s.committed = damage{} | |
| 333 | + | return | |
| 334 | + | } | |
| 335 | + | if resized || len(surf)+len(buf) == 0 || len(surf)+len(buf) > maxDamageRects { | |
| 336 | + | s.committed = damage{full: true} | |
| 337 | + | return | |
| 338 | + | } | |
| 339 | + | lw, lh := s.logicalSize(bw, bh) | |
| 340 | + | d := damage{rects: make([]ports.Rect, 0, len(surf)+len(buf))} | |
| 341 | + | bounds := image.Rect(0, 0, bw, bh) | |
| 342 | + | add := func(r image.Rectangle) { | |
| 343 | + | if r = r.Intersect(bounds); !r.Empty() { | |
| 344 | + | d.rects = append(d.rects, ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()}) | |
| 345 | + | } | |
| 346 | + | } | |
| 347 | + | for _, r := range buf { | |
| 348 | + | add(image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H)) | |
| 349 | + | } | |
| 350 | + | for _, r := range surf { | |
| 351 | + | if lw <= 0 || lh <= 0 { | |
| 352 | + | s.committed = damage{full: true} | |
| 353 | + | return | |
| 354 | + | } | |
| 355 | + | // Clients may damage INT32_MAX-sized rects: clamp before scaling. | |
| 356 | + | x0, y0 := max(r.X, 0), max(r.Y, 0) | |
| 357 | + | x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh) | |
| 358 | + | add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh)) | |
| 359 | + | } | |
| 360 | + | s.committed = d | |
| 361 | + | } | |
| 362 | + | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | |
| 363 | + | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |
| 364 | + | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |
| 291 | 365 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | |
| 292 | 366 | if v < 1 { | |
| 293 | 367 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "image" | |||
| 4 | "time" | 5 | "time" | ||
| 5 | 6 | ||||
| 6 | "github.com/bnema/nefertty/internal/ports" | 7 | "github.com/bnema/nefertty/internal/ports" | ||
| @@ -60,8 +61,22 @@ type surface struct { | |||||
| 60 | // contentType is the wp_content_type_v1; contentKind the committed type. | 61 | // contentType is the wp_content_type_v1; contentKind the committed type. | ||
| 61 | contentType *contentTypeHandler | 62 | contentType *contentTypeHandler | ||
| 62 | contentKind, pendingKind uint32 | 63 | contentKind, pendingKind uint32 | ||
| 64 | + | // Damage requested since the last commit: in surface (logical) and | |||
| 65 | + | // buffer pixels; committed is what the last commit changed, in buffer | |||
| 66 | + | // pixels (full: everything), read by the window's damage history. | |||
| 67 | + | pendingDamage, pendingBufDamage []ports.Rect | |||
| 68 | + | committed damage | |||
| 63 | } | 69 | } | ||
| 64 | 70 | ||||
| 71 | + | // damage is what one commit changed in a surface's buffer. | |||
| 72 | + | type damage struct { | |||
| 73 | + | full bool | |||
| 74 | + | rects []ports.Rect | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // maxDamageRects bounds one commit's rects: past it the commit is full. | |||
| 78 | + | const maxDamageRects = 32 | |||
| 79 | + | ||||
| 65 | // subState is the subsurface tree: parent is set on subsurfaces, children | 80 | // subState is the subsurface tree: parent is set on subsurfaces, children | ||
| 66 | // are ordered bottom to top, below marks children under their parent. | 81 | // are ordered bottom to top, below marks children under their parent. | ||
| 67 | type subState struct { | 82 | type subState struct { | ||
| @@ -127,11 +142,16 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||||
| 127 | } | 142 | } | ||
| 128 | } | 143 | } | ||
| 129 | 144 | ||||
| 130 | - | // redraw sends the tree of a mapped root to the outputs. | 145 | + | // redraw sends the tree of a mapped root to the outputs. Only a commit |
| 146 | + | // of the root's own buffer with no other change keeps partial damage. | |||
| 131 | func (s *surface) redraw() { | 147 | func (s *surface) redraw() { | ||
| 132 | r := s.root() | 148 | r := s.root() | ||
| 133 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | 149 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | ||
| 134 | - | s.server.emitContent(r.tree(id)) | 150 | + | d := damage{full: true} |
| 151 | + | if s == r && len(r.sub.children) == 0 { | |||
| 152 | + | d = r.committed | |||
| 153 | + | } | |||
| 154 | + | s.server.emitContent(r.tree(id), d) | |||
| 135 | } | 155 | } | ||
| 136 | } | 156 | } | ||
| 137 | 157 | ||||
| @@ -248,6 +268,7 @@ func (s *surface) applyCommit() { | |||||
| 248 | if state, ok := s.server.buffers[s.current.Resource]; ok { | 268 | if state, ok := s.server.buffers[s.current.Resource]; ok { | ||
| 249 | if c, ok := state.content(0); ok { | 269 | if c, ok := state.content(0); ok { | ||
| 250 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | 270 | c.LogicalW, c.LogicalH = s.logicalSize(c.Width, c.Height) | ||
| 271 | + | s.commitDamage(true, !s.has || c.Width != s.content.Width || c.Height != s.content.Height, c.Width, c.Height) | |||
| 251 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | 272 | if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) { | ||
| 252 | s.lastW, s.lastH = c.Width, c.Height | 273 | s.lastW, s.lastH = c.Width, c.Height | ||
| 253 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | 274 | s.server.log.Info().Uint64("id", uint64(s.windowID())).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | ||
| @@ -260,6 +281,9 @@ func (s *surface) applyCommit() { | |||||
| 260 | // replaces them. | 281 | // replaces them. | ||
| 261 | } | 282 | } | ||
| 262 | } | 283 | } | ||
| 284 | + | if !fresh { | |||
| 285 | + | s.commitDamage(false, false, 0, 0) | |||
| 286 | + | } | |||
| 263 | if s.current == nil { | 287 | if s.current == nil { | ||
| 264 | s.content, s.has = ports.SurfaceContent{}, false | 288 | s.content, s.has = ports.SurfaceContent{}, false | ||
| 265 | } | 289 | } | ||
| @@ -280,14 +304,64 @@ func (s *surface) applyCommit() { | |||||
| 280 | s.xdg.window.afterCommit() | 304 | s.xdg.window.afterCommit() | ||
| 281 | } | 305 | } | ||
| 282 | if fresh || moved || geometry || hinted || s.sub.parent != nil { | 306 | if fresh || moved || geometry || hinted || s.sub.parent != nil { | ||
| 307 | + | if moved || geometry { | |||
| 308 | + | s.committed = damage{full: true} | |||
| 309 | + | } | |||
| 283 | s.redraw() | 310 | s.redraw() | ||
| 284 | } | 311 | } | ||
| 285 | } | 312 | } | ||
| 286 | - | func (*surface) Damage(*wayland.Surface, int32, int32, int32, int32) {} | 313 | + | func (s *surface) Damage(_ *wayland.Surface, x, y, w, h int32) { |
| 287 | - | func (*surface) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {} | 314 | + | if w > 0 && h > 0 && len(s.pendingDamage) <= maxDamageRects { |
| 288 | - | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | 315 | + | s.pendingDamage = append(s.pendingDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) |
| 289 | - | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | 316 | + | } |
| 290 | - | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | 317 | + | } |
| 318 | + | func (s *surface) DamageBuffer(_ *wayland.Surface, x, y, w, h int32) { | |||
| 319 | + | if w > 0 && h > 0 && len(s.pendingBufDamage) <= maxDamageRects { | |||
| 320 | + | s.pendingBufDamage = append(s.pendingBufDamage, ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}) | |||
| 321 | + | } | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | // commitDamage turns the requested damage into buffer pixels of a bw×bh | |||
| 325 | + | // buffer. Surface damage scales by the buffer/logical ratio, rounded out. | |||
| 326 | + | // A new size, too many rects or none at all with a new buffer (clients | |||
| 327 | + | // that do not report damage) is a full change. | |||
| 328 | + | func (s *surface) commitDamage(fresh, resized bool, bw, bh int) { | |||
| 329 | + | surf, buf := s.pendingDamage, s.pendingBufDamage | |||
| 330 | + | s.pendingDamage, s.pendingBufDamage = nil, nil | |||
| 331 | + | if !fresh { | |||
| 332 | + | s.committed = damage{} | |||
| 333 | + | return | |||
| 334 | + | } | |||
| 335 | + | if resized || len(surf)+len(buf) == 0 || len(surf)+len(buf) > maxDamageRects { | |||
| 336 | + | s.committed = damage{full: true} | |||
| 337 | + | return | |||
| 338 | + | } | |||
| 339 | + | lw, lh := s.logicalSize(bw, bh) | |||
| 340 | + | d := damage{rects: make([]ports.Rect, 0, len(surf)+len(buf))} | |||
| 341 | + | bounds := image.Rect(0, 0, bw, bh) | |||
| 342 | + | add := func(r image.Rectangle) { | |||
| 343 | + | if r = r.Intersect(bounds); !r.Empty() { | |||
| 344 | + | d.rects = append(d.rects, ports.Rect{X: r.Min.X, Y: r.Min.Y, W: r.Dx(), H: r.Dy()}) | |||
| 345 | + | } | |||
| 346 | + | } | |||
| 347 | + | for _, r := range buf { | |||
| 348 | + | add(image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H)) | |||
| 349 | + | } | |||
| 350 | + | for _, r := range surf { | |||
| 351 | + | if lw <= 0 || lh <= 0 { | |||
| 352 | + | s.committed = damage{full: true} | |||
| 353 | + | return | |||
| 354 | + | } | |||
| 355 | + | // Clients may damage INT32_MAX-sized rects: clamp before scaling. | |||
| 356 | + | x0, y0 := max(r.X, 0), max(r.Y, 0) | |||
| 357 | + | x1, y1 := min(r.X+min(r.W, lw), lw), min(r.Y+min(r.H, lh), lh) | |||
| 358 | + | add(image.Rect(x0*bw/lw, y0*bh/lh, (x1*bw+lw-1)/lw, (y1*bh+lh-1)/lh)) | |||
| 359 | + | } | |||
| 360 | + | s.committed = d | |||
| 361 | + | } | |||
| 362 | + | func (*surface) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {} | |||
| 363 | + | func (*surface) SetInputRegion(*wayland.Surface, *wayland.Region) {} | |||
| 364 | + | func (*surface) SetBufferTransform(*wayland.Surface, int32) {} | |||
| 291 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | 365 | func (s *surface) SetBufferScale(r *wayland.Surface, v int32) { | ||
| 292 | if v < 1 { | 366 | if v < 1 { | ||
| 293 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | 367 | r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive") | ||
M internal/adapters/wayland/xdg.go +1 -1
| @@ -182,7 +182,7 @@ func (w *window) unmap() { | |||
| 182 | 182 | w.xdg.acked = false | |
| 183 | 183 | w.xdg.serials = nil | |
| 184 | 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 185 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}) | |
| 185 | + | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 186 | 186 | } | |
| 187 | 187 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 188 | 188 | if x.window != nil { | |
| @@ -182,7 +182,7 @@ func (w *window) unmap() { | |||||
| 182 | w.xdg.acked = false | 182 | w.xdg.acked = false | ||
| 183 | w.xdg.serials = nil | 183 | w.xdg.serials = nil | ||
| 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 184 | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | ||
| 185 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}) | 185 | + | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) |
| 186 | } | 186 | } | ||
| 187 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 187 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 188 | if x.window != nil { | 188 | if x.window != nil { | ||
A internal/ports/damage_test.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | package ports | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestDamageSince(t *testing.T) { | |
| 9 | + | a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1} | |
| 10 | + | c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 5, Rects: []Rect{a}}, {Seq: 6, Full: true}, {Seq: 7, Rects: []Rect{b}}}} | |
| 11 | + | for _, tc := range []struct { | |
| 12 | + | name string | |
| 13 | + | since uint64 | |
| 14 | + | want []Rect | |
| 15 | + | ok bool | |
| 16 | + | }{ | |
| 17 | + | {"up to date", 7, nil, true}, | |
| 18 | + | {"one behind", 6, []Rect{b}, true}, | |
| 19 | + | {"across a full change", 5, nil, false}, | |
| 20 | + | {"history too short", 3, nil, false}, | |
| 21 | + | {"first content", 0, nil, false}, | |
| 22 | + | } { | |
| 23 | + | got, ok := c.DamageSince(tc.since) | |
| 24 | + | if ok != tc.ok || !slices.Equal(got, tc.want) { | |
| 25 | + | t.Errorf("%s: %v %v", tc.name, got, ok) | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | c.DamageHistory[1] = SeqDamage{Seq: 6, Rects: []Rect{a}} | |
| 29 | + | if got, ok := c.DamageSince(4); !ok || !slices.Equal(got, []Rect{a, a, b}) { | |
| 30 | + | t.Fatalf("union %v %v", got, ok) | |
| 31 | + | } | |
| 32 | + | // A history not ending at the content is stale: redraw all. | |
| 33 | + | c.Seq = 8 | |
| 34 | + | if _, ok := c.DamageSince(6); ok { | |
| 35 | + | t.Fatal("stale history used") | |
| 36 | + | } | |
| 37 | + | } | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestDamageSince(t *testing.T) { | |||
| 9 | + | a, b := Rect{X: 1, W: 2, H: 2}, Rect{Y: 5, W: 1, H: 1} | |||
| 10 | + | c := SurfaceContent{Seq: 7, DamageHistory: []SeqDamage{{Seq: 5, Rects: []Rect{a}}, {Seq: 6, Full: true}, {Seq: 7, Rects: []Rect{b}}}} | |||
| 11 | + | for _, tc := range []struct { | |||
| 12 | + | name string | |||
| 13 | + | since uint64 | |||
| 14 | + | want []Rect | |||
| 15 | + | ok bool | |||
| 16 | + | }{ | |||
| 17 | + | {"up to date", 7, nil, true}, | |||
| 18 | + | {"one behind", 6, []Rect{b}, true}, | |||
| 19 | + | {"across a full change", 5, nil, false}, | |||
| 20 | + | {"history too short", 3, nil, false}, | |||
| 21 | + | {"first content", 0, nil, false}, | |||
| 22 | + | } { | |||
| 23 | + | got, ok := c.DamageSince(tc.since) | |||
| 24 | + | if ok != tc.ok || !slices.Equal(got, tc.want) { | |||
| 25 | + | t.Errorf("%s: %v %v", tc.name, got, ok) | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | c.DamageHistory[1] = SeqDamage{Seq: 6, Rects: []Rect{a}} | |||
| 29 | + | if got, ok := c.DamageSince(4); !ok || !slices.Equal(got, []Rect{a, a, b}) { | |||
| 30 | + | t.Fatalf("union %v %v", got, ok) | |||
| 31 | + | } | |||
| 32 | + | // A history not ending at the content is stale: redraw all. | |||
| 33 | + | c.Seq = 8 | |||
| 34 | + | if _, ok := c.DamageSince(6); ok { | |||
| 35 | + | t.Fatal("stale history used") | |||
| 36 | + | } | |||
| 37 | + | } | |||
M internal/ports/messages.go +36 -0
| @@ -622,6 +622,42 @@ type SurfaceContent struct { | |||
| 622 | 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | |
| 623 | 623 | // honour it only in direct scanout. | |
| 624 | 624 | Async bool | |
| 625 | + | // DamageHistory is what changed in the root surface's buffer over the | |
| 626 | + | // window's last contents, oldest first, ending with this one (Seq). | |
| 627 | + | // A renderer holding an older content redraws the union of the | |
| 628 | + | // entries after it, or everything when the history does not reach it. | |
| 629 | + | DamageHistory []SeqDamage | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | // SeqDamage is what content Seq changed from the one before: Rects in | |
| 633 | + | // root buffer pixels, or everything when Full. | |
| 634 | + | type SeqDamage struct { | |
| 635 | + | Seq uint64 | |
| 636 | + | Full bool | |
| 637 | + | Rects []Rect | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // DamageSince is the union of changes after content seq up to this one, | |
| 641 | + | // and false when the history does not reach back to seq (redraw all). | |
| 642 | + | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | |
| 643 | + | if seq >= c.Seq { | |
| 644 | + | return nil, true | |
| 645 | + | } | |
| 646 | + | h := c.DamageHistory | |
| 647 | + | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | |
| 648 | + | return nil, false | |
| 649 | + | } | |
| 650 | + | var out []Rect | |
| 651 | + | for _, d := range h { | |
| 652 | + | if d.Seq <= seq { | |
| 653 | + | continue | |
| 654 | + | } | |
| 655 | + | if d.Full { | |
| 656 | + | return nil, false | |
| 657 | + | } | |
| 658 | + | out = append(out, d.Rects...) | |
| 659 | + | } | |
| 660 | + | return out, true | |
| 625 | 661 | } | |
| 626 | 662 | ||
| 627 | 663 | // Subsurface is a child surface at X, Y logical pixels from the root | |
| @@ -622,6 +622,42 @@ type SurfaceContent struct { | |||||
| 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | 622 | // Async asks for tearing presentation (wp_tearing_control_v1); outputs | ||
| 623 | // honour it only in direct scanout. | 623 | // honour it only in direct scanout. | ||
| 624 | Async bool | 624 | Async bool | ||
| 625 | + | // DamageHistory is what changed in the root surface's buffer over the | |||
| 626 | + | // window's last contents, oldest first, ending with this one (Seq). | |||
| 627 | + | // A renderer holding an older content redraws the union of the | |||
| 628 | + | // entries after it, or everything when the history does not reach it. | |||
| 629 | + | DamageHistory []SeqDamage | |||
| 630 | + | } | |||
| 631 | + | ||||
| 632 | + | // SeqDamage is what content Seq changed from the one before: Rects in | |||
| 633 | + | // root buffer pixels, or everything when Full. | |||
| 634 | + | type SeqDamage struct { | |||
| 635 | + | Seq uint64 | |||
| 636 | + | Full bool | |||
| 637 | + | Rects []Rect | |||
| 638 | + | } | |||
| 639 | + | ||||
| 640 | + | // DamageSince is the union of changes after content seq up to this one, | |||
| 641 | + | // and false when the history does not reach back to seq (redraw all). | |||
| 642 | + | func (c SurfaceContent) DamageSince(seq uint64) ([]Rect, bool) { | |||
| 643 | + | if seq >= c.Seq { | |||
| 644 | + | return nil, true | |||
| 645 | + | } | |||
| 646 | + | h := c.DamageHistory | |||
| 647 | + | if len(h) == 0 || h[0].Seq > seq+1 || h[len(h)-1].Seq != c.Seq { | |||
| 648 | + | return nil, false | |||
| 649 | + | } | |||
| 650 | + | var out []Rect | |||
| 651 | + | for _, d := range h { | |||
| 652 | + | if d.Seq <= seq { | |||
| 653 | + | continue | |||
| 654 | + | } | |||
| 655 | + | if d.Full { | |||
| 656 | + | return nil, false | |||
| 657 | + | } | |||
| 658 | + | out = append(out, d.Rects...) | |||
| 659 | + | } | |||
| 660 | + | return out, true | |||
| 625 | } | 661 | } | ||
| 626 | 662 | ||||
| 627 | // Subsurface is a child surface at X, Y logical pixels from the root | 663 | // Subsurface is a child surface at X, Y logical pixels from the root | ||