detached 979e3cd

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
8888 }
8989
9090 // 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+}
9297
9398 // atomicReq collects (object, property, value) triples in insertion order.
9499 type atomicReq struct {
@@ -217,7 +222,16 @@ func (k kmsDevice) planes() ([]planeRes, error) {
217222 if ioctl(k.fd, ioctlGetPlane, unsafe.Pointer(&p)) != nil {
218223 continue
219224 }
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)
221235 }
222236 return out, nil
223237 }
@@ -256,6 +270,8 @@ type plane struct {
256270 props map[string]uint32
257271 crtc uint32 // CRTC_ID when read
258272 formats []ports.DMABufFormat
273+ // zposValue is the plane's zpos when read (its "zpos" property).
274+ zposValue uint64
259275 }
260276
261277 func (p *plane) prop(name string) uint32 {
@@ -280,7 +296,7 @@ func readPlanes(k kms, pipe int) ([]*plane, error) {
280296 if err != nil {
281297 continue
282298 }
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]}
284300 for name, v := range props {
285301 p.props[name] = uint32(v[0])
286302 }
@@ -289,6 +305,11 @@ func readPlanes(k kms, pipe int) ([]*plane, error) {
289305 p.formats = parseInFormats(data)
290306 }
291307 }
308+ if p.formats == nil {
309+ for _, f := range r.formats {
310+ p.formats = append(p.formats, ports.DMABufFormat{Format: f})
311+ }
312+ }
292313 out = append(out, p)
293314 }
294315 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 continue223 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, nil236 return out, nil
223 }237 }
@@ -256,6 +270,8 @@ type plane struct {
256 props map[string]uint32270 props map[string]uint32
257 crtc uint32 // CRTC_ID when read271 crtc uint32 // CRTC_ID when read
258 formats []ports.DMABufFormat272 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 continue297 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, nil315 return out, nil
M internal/adapters/drm/output.go +59 -7
@@ -40,6 +40,12 @@ type Output struct {
4040 primary *plane
4141 cursor *Cursor
4242 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
4349 // fbs are the two renderer images frames alternate between, back the
4450 // one the next frame draws into (ADR 014: zero copy).
4551 fbs [2]uint32
@@ -183,6 +189,7 @@ func (o *Output) readProps(pipe int, taken map[uint32]bool, cursorSide int) erro
183189 o.stray = append(o.stray, p)
184190 }
185191 }
192+ o.pickOverlay()
186193 return nil
187194 }
188195
@@ -192,6 +199,9 @@ func (o *Output) owned() []*plane {
192199 if o.cursor != nil {
193200 out = append(out, o.cursor.plane)
194201 }
202+ if o.overlay != nil {
203+ out = append(out, o.overlay)
204+ }
195205 return out
196206 }
197207
@@ -265,7 +275,7 @@ func (o *Output) modeset() error {
265275 _ = o.k.destroyBlob(o.modeBlob)
266276 }
267277 o.modeBlob = blob
268- o.vrrOn = false
278+ o.vrrOn, o.overlayOn = false, 0
269279 if o.cursor != nil {
270280 o.cursor.applied = cursorState{}
271281 o.cursor.screen, o.cursor.flying = 0, false
@@ -348,6 +358,7 @@ func (o *Output) modesetReq(blob uint32) *atomicReq {
348358 if o.cursor != nil {
349359 o.cursor.props(req, o.crtc, cursorState{})
350360 }
361+ o.overlayProps(req, overlayWin{})
351362 for _, p := range o.stray {
352363 // Only planes a previous master left on this CRTC.
353364 props, err := o.k.objProps(p.id, objPlane)
@@ -404,11 +415,16 @@ func (o *Output) wantVRR(direct bool) bool {
404415 // also move the cursor or change VRR flips at vblank. fence is the
405416 // frame's GPU fence (nil: none); the caller keeps and closes it.
406417 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 {
407423 cur := cursorState{}
408424 if o.cursor != nil {
409425 cur = o.cursor.desired()
410426 }
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) {
412428 async = false
413429 }
414430 req := &atomicReq{}
@@ -422,6 +438,9 @@ func (o *Output) commitFrame(fb uint32, fence *os.File, async bool, vrr bool, f
422438 if o.cursor != nil {
423439 o.cursor.props(req, o.crtc, cur)
424440 }
441+ if ov.buf != 0 || o.overlayOn != 0 {
442+ o.overlayProps(req, ov)
443+ }
425444 }
426445 if fence != nil {
427446 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
429448 // The kernel takes its own reference on the fence.
430449 if err := o.k.commit(req, flags, o.userData(userFrame)); err != nil {
431450 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)
433452 }
434453 if !async && vrr && !o.vrrOn && errors.Is(err, unix.EINVAL) {
435454 // 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
452471 // An async commit leaves the cursor plane as applied.
453472 o.cursor.committed(cur)
454473 }
474+ if !async {
475+ o.overlayOn = ov.buf
476+ }
455477 f.frame, f.async = true, async
456478 o.setAsync(async)
457479 o.begin(f)
@@ -483,6 +505,10 @@ func (o *Output) commitState(vrr bool) error {
483505 return nil
484506 }
485507 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+ }
486512 if cur.on && errors.Is(err, unix.EINVAL) && o.cursorRefused() {
487513 return o.commitState(vrr)
488514 }
@@ -772,7 +798,12 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
772798 }
773799 if !dirty || !haveScene {
774800 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) {
776807 return fmt.Errorf("commit: %w", err)
777808 }
778809 }
@@ -786,21 +817,42 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
786817 direct, err = o.commitScanout(fb, c, f)
787818 }
788819 if !direct {
820+ ov, composed := o.overlayFrame(scene, surfaces)
789821 r.UseTarget(o.back)
790- done, rerr := r.Render(scene, surfaces)
822+ done, rerr := r.Render(composed, surfaces)
791823 if rerr != nil {
792824 return fmt.Errorf("render frame: %w", rerr)
793825 }
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)
795840 if done != nil {
796841 done.Close()
797842 }
843+ if err != nil && o.overlayConflict(err, ov.buf) {
844+ err = errOverlayDropped
845+ }
798846 if err == nil {
799- o.queued = 0
847+ o.queued = ov.buf
800848 o.back = 1 - o.back
801849 }
802850 }
803851 if err != nil {
852+ if errors.Is(err, errOverlayDropped) {
853+ dirty = true
854+ continue
855+ }
804856 if !o.commitFailed(err, &enabled) {
805857 return fmt.Errorf("page flip: %w", err)
806858 }
@@ -40,6 +40,12 @@ type Output struct {
40 primary *plane40 primary *plane
41 cursor *Cursor41 cursor *Cursor
42 stray []*plane42 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 the49 // 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]uint3251 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 nil193 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 out205 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 = blob277 o.modeBlob = blob
268- o.vrrOn = false278+ 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, false281 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 the415 // 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 = false428 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 driver454 // 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, async477 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 nil505 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 = 0847+ o.queued = ov.buf
800 o.back = 1 - o.back848 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) {
201201 if useMod {
202202 cmd.flags = fbModifiers
203203 }
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}
206207 for i, pl := range b.Planes {
207208 if i >= len(cmd.handles) {
208209 break
@@ -213,7 +214,10 @@ func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
213214 if h, err = k.primeImport(pl.File); err != nil {
214215 return 0, err
215216 }
216- defer k.gemClose(h)
217+ if !closing[h] {
218+ closing[h] = true
219+ defer k.gemClose(h)
220+ }
217221 }
218222 cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
219223 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 = fbModifiers202 cmd.flags = fbModifiers
203 }203 }
204- // Every plane of a buffer lives in the same dmabuf or its own: each204+ // 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 break209 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, err215 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.Offset222 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) {
124124 }
125125 }
126126 }
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
283283 return dr
284284 }
285285
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) {
288289 d, c := r.dd, &r.compose
289290 clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
290291 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+ }
291295 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}
292296 d.CmdBeginRendering(cmd, &info)
293297 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 dr283 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.compose289 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) {
156156 t.Skipf("Vulkan unavailable: %v", err)
157157 }
158158 defer r.Close()
159+ if !r.syncFD {
160+ t.Skip("frames are finished on return without sync files")
161+ }
159162 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}, Borderless: true}}}
160163 var readers []uint64
161164 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 []uint64163 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 @@
11 package vulkan
22
33 import (
4- "errors"
54 "fmt"
5+ "image"
66 "math"
77 "os"
88 "runtime"
@@ -168,8 +168,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
168168 return nil, err
169169 }
170170 }
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)
172174 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+ }
173184 d := r.dd
174185 cmd := slot.cmd
175186 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
179190 if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
180191 return nil, err
181192 }
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.
185195 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+ }
186200 if tg.exported {
187201 // Acquire from the display (foreign queue family).
188202 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
198212 }
199213 }
200214 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)
202216 r.ownership(cmd, dmas, false)
203217 b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
204218 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
@@ -243,6 +257,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
243257 runtime.KeepAlive(waits)
244258 slot.frame, slot.busy, r.submitted = frame, true, frame
245259 tg.layout = b.NewLayout
260+ tg.hold(s, contents)
246261 r.last, r.readBack = tg, false
247262 r.dropUnused()
248263 r.dropShm()
@@ -251,13 +266,16 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
251266 }
252267 var fd int32
253268 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+ }
258276 d.DestroySemaphore(r.device, slot.done, nil)
259277 slot.done = 0
260- return nil, errors.Join(err, r.newDone(slot))
278+ return nil, r.newDone(slot)
261279 }
262280 if fd < 0 {
263281 // Already signalled: the driver may return -1.
@@ -1,8 +1,8 @@
1 package vulkan1 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, err168 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.dd184 d := r.dd
174 cmd := slot.cmd185 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, err191 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 never194+ // (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.family202 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.ImageLayoutColorAttachmentOptimal217 b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
204 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored218 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, frame258 slot.frame, slot.busy, r.submitted = frame, true, frame
245 tg.layout = b.NewLayout259 tg.layout = b.NewLayout
260+ tg.hold(s, contents)
246 r.last, r.readBack = tg, false261 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 int32267 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 is270+ // 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 = 0277 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 {
5757 // the bytes copied into them, for tests.
5858 shm map[shmKey]*shmSurface
5959 copied int
60+ // redrawn counts the target pixels drawn, for tests.
61+ redrawn int
6062 // cursors are the exported cursor images (CursorBuffers).
6163 cursors [2]*cursorImage
6264 // compose draws every frame (compose.go); maxRange bounds one
@@ -299,7 +301,7 @@ func windowColor(id ports.WindowID) [3]uint8 {
299301
300302 // draws walks the scene into quads in paint order, copying new wl_shm
301303 // 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 {
303305 // Scene rects are logical; everything below works in physical pixels.
304306 scale := s.Scale
305307 if scale <= 0 {
@@ -339,8 +341,13 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
339341 if err != nil {
340342 return
341343 }
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)
344351 if err != nil {
345352 return
346353 }
@@ -373,6 +380,9 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
373380 if dst.Empty() {
374381 return
375382 }
383+ if key.index == 0 {
384+ dmg.content(key.win, content, full, dst)
385+ }
376386 addContent(dst, full, content, key, seq)
377387 }
378388 // 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
406416 continue
407417 }
408418 content := contents[layer.ID]
419+ dmg.window(layer.ID, content, physRect(layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H))
409420 place(layer.ID, &content, layer.Rect.X, layer.Rect.Y, layer.Rect.W, layer.Rect.H)
410421 }
411422 }
@@ -426,6 +437,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
426437 cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b
427438 body := physRect(cx, cy, cw, ch)
428439 content := contents[w.ID]
440+ dmg.window(w.ID, content, physRect(x, y, w.Rect.W, w.Rect.H))
429441 if content.Empty() {
430442 add(body, windowColor(w.ID))
431443 } else {
@@ -455,6 +467,7 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
455467 for _, w := range s.Windows {
456468 if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
457469 content := contents[w.ID]
470+ dmg.window(w.ID, content, physRect(w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H))
458471 place(w.ID, &content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
459472 }
460473 }
@@ -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]*shmSurface58 shm map[shmKey]*shmSurface
59 copied int59 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]*cursorImage63 cursors [2]*cursorImage
62 // compose draws every frame (compose.go); maxRange bounds one64 // 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_shm302 // 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.Scale306 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 return342 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 return352 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 return381 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 continue416 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*b437 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 {
154154 index int
155155 }
156156
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.
158159 type shmState struct {
159- pool uint64
160- offset, stride int
161- w, h int
162- seq uint64
160+ w, h int
161+ seq uint64
163162 }
164163
165164 // shmCopy is one of a surface's two GPU buffers and the set binding it.
@@ -178,8 +177,11 @@ type shmSurface struct {
178177 }
179178
180179 // 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) {
183185 s := r.shm[key]
184186 if s == nil {
185187 s = &shmSurface{}
@@ -223,14 +225,27 @@ func (r *Renderer) shmCopyFor(key shmKey, st shmState, pixels []byte) (*shmCopy,
223225 }
224226 dst := unsafe.Slice((*byte)(c.gpu.mapped), size)
225227 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+ }
226234 // The client may shrink its pool under the mapping: its window then
227235 // shows garbage for this frame, never a crash, and is copied again.
228236 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+ }
231247 }
232248 })
233- r.copied += size
234249 c.holds, c.gpu.last = st, r.frame
235250 return c, nil
236251 }
@@ -154,12 +154,11 @@ type shmKey struct {
154 index int154 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 uint64160+ w, h int
160- offset, stride int161+ 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, copying179 // 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 * 4227 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 then234 // 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.frame249 c.holds, c.gpu.last = st, r.frame
235 return c, nil250 return c, nil
236 }251 }
M internal/adapters/vulkan/targets.go +5 -0
@@ -28,6 +28,11 @@ type target struct {
2828 // exported images are shared with the display: ownership goes to the
2929 // foreign queue family after each frame, and KMS reads them.
3030 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
3136 }
3237
3338 // 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 the28 // 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 bool30 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) {
193193 }
194194 for fb, surf := range g.feedbacks {
195195 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 {
197197 g.send(fb, surf)
198198 }
199199 }
@@ -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) {
264264 if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
265265 t.Fatalf("fullscreen: %+v", r)
266266 }
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+ }
267272 commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1"}
268273 if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
269274 t.Fatalf("left fullscreen: %+v", r)
270275 }
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+ }
271289 }
@@ -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 {
216216 barrier bool
217217 wait bool
218218 at time.Time
219+ damage []ports.Rect
220+ bufDamage []ports.Rect
219221 }
220222
221223 type childMove struct {
@@ -226,8 +228,9 @@ type childMove struct {
226228 // takePending moves the pending state into an update: one-shot state is
227229 // cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
228230 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}
230232 s.attached, s.pending, s.callbacks = false, nil, nil
233+ s.pendingDamage, s.pendingBufDamage = nil, nil
231234 s.pendingBarrier, s.pendingWait, s.pendingTime = false, false, time.Time{}
232235 if v := s.viewport; v != nil {
233236 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) {
256259 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
257260 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
258261 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at
262+ s.pendingDamage, s.pendingBufDamage = u.damage, u.bufDamage
259263 if u.vp != nil && s.viewport == u.vp {
260264 u.vp.pendingW, u.vp.pendingH, u.vp.pendingSet = u.vpW, u.vpH, u.vpSet
261265 }
@@ -216,6 +216,8 @@ type update struct {
216 barrier bool216 barrier bool
217 wait bool217 wait bool
218 at time.Time218 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 is228 // 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, nil232 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.pendingSet236 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.callbacks259 s.attached, s.pending, s.callbacks = u.attached, u.buffer, u.callbacks
257 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind260 s.pendingScale, s.pendingAsync, s.pendingKind = u.scale, u.async, u.kind
258 s.pendingBarrier, s.pendingWait, s.pendingTime = u.barrier, u.wait, u.at261 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.vpSet264 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() {
167167 l.acked = false
168168 l.serials = nil
169169 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})
171171 }
172172
173173 // close tells the client its output is gone; the surface stays unmapped
@@ -167,7 +167,7 @@ func (l *layerSurface) unmap() {
167 l.acked = false167 l.acked = false
168 l.serials = nil168 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 unmapped173 // 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 (
66 "fmt"
77 "github.com/bnema/purego-libwayland/protocol/relativepointer"
88 "os"
9+ "slices"
910 "sync"
1011 "time"
1112
@@ -101,12 +102,14 @@ type Server struct {
101102 // Events, which happens only at shutdown (core owns the receiving end). A
102103 // stalled core is a bug surfaced by ctx cancellation, not a reason to block
103104 // 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
110113 contentNotify chan struct{}
111114 contentReady chan struct{}
112115 outputs []*output
@@ -162,7 +165,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
162165 d.Close()
163166 return nil, err
164167 }
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}
166169 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
167170 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
168171 s.tokens = map[string]activationToken{}
@@ -242,6 +245,10 @@ func (s *Server) Run(ctx context.Context) error {
242245 return err
243246 }
244247
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+
245252 // forwardOutputFormats applies the outputs' scanout formats on the
246253 // display goroutine.
247254 func (s *Server) forwardOutputFormats(ctx context.Context) {
@@ -303,13 +310,24 @@ func (s *Server) forward(ctx context.Context) {
303310 }
304311 }
305312
306-func (s *Server) emitContent(c ports.SurfaceContent) {
313+func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
307314 if s.channels.Contents == nil {
308315 return
309316 }
310317 s.contentMu.Lock()
311318 s.contentSeq[c.ID]++
312319 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)
313331 s.contents[c.ID] = c
314332 close(s.contentNotify)
315333 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). A102 // 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 block103 // 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.Mutex105+ eventMu sync.Mutex
105- events []ports.ClientEvent106+ events []ports.ClientEvent
106- eventReady chan struct{}107+ eventReady chan struct{}
107- contentMu sync.Mutex108+ contentMu sync.Mutex
108- contents map[ports.WindowID]ports.SurfaceContent109+ contents map[ports.WindowID]ports.SurfaceContent
109- contentSeq map[ports.WindowID]uint64110+ 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 []*output115 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, err166 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 err245 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 the252 // 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 return315 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] = c331 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 @@
11 package wayland
22
33 import (
4+ "image"
45 "time"
56
67 "github.com/bnema/nefertty/internal/ports"
@@ -60,8 +61,22 @@ type surface struct {
6061 // contentType is the wp_content_type_v1; contentKind the committed type.
6162 contentType *contentTypeHandler
6263 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
6369 }
6470
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+
6580 // subState is the subsurface tree: parent is set on subsurfaces, children
6681 // are ordered bottom to top, below marks children under their parent.
6782 type subState struct {
@@ -127,11 +142,16 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
127142 }
128143 }
129144
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.
131147 func (s *surface) redraw() {
132148 r := s.root()
133149 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)
135155 }
136156 }
137157
@@ -248,6 +268,7 @@ func (s *surface) applyCommit() {
248268 if state, ok := s.server.buffers[s.current.Resource]; ok {
249269 if c, ok := state.content(0); ok {
250270 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)
251272 if s.sub.parent == nil && (c.Width != s.lastW || c.Height != s.lastH) {
252273 s.lastW, s.lastH = c.Width, c.Height
253274 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() {
260281 // replaces them.
261282 }
262283 }
284+ if !fresh {
285+ s.commitDamage(false, false, 0, 0)
286+ }
263287 if s.current == nil {
264288 s.content, s.has = ports.SurfaceContent{}, false
265289 }
@@ -280,14 +304,64 @@ func (s *surface) applyCommit() {
280304 s.xdg.window.afterCommit()
281305 }
282306 if fresh || moved || geometry || hinted || s.sub.parent != nil {
307+ if moved || geometry {
308+ s.committed = damage{full: true}
309+ }
283310 s.redraw()
284311 }
285312 }
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) {}
291365 func (s *surface) SetBufferScale(r *wayland.Surface, v int32) {
292366 if v < 1 {
293367 r.PostError(uint32(wayland.SurfaceErrorInvalidScale), "buffer scale must be positive")
@@ -1,6 +1,7 @@
1 package wayland1 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 *contentTypeHandler62 contentType *contentTypeHandler
62 contentKind, pendingKind uint3263 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, children80 // 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.Height273 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{}, false288 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() {
182182 w.xdg.acked = false
183183 w.xdg.serials = nil
184184 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})
186186 }
187187 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
188188 if x.window != nil {
@@ -182,7 +182,7 @@ func (w *window) unmap() {
182 w.xdg.acked = false182 w.xdg.acked = false
183 w.xdg.serials = nil183 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 {
622622 // Async asks for tearing presentation (wp_tearing_control_v1); outputs
623623 // honour it only in direct scanout.
624624 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
625661 }
626662
627663 // 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); outputs622 // 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 bool624 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 root663 // Subsurface is a child surface at X, Y logical pixels from the root

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close