1package vulkan
2
3import (
4 "image"
5 "math"
6 "slices"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// Damage: an output target keeps what it holds (scene Seq, each window's
12// content Seq). When the scene is the same, only the region changed since
13// then is redrawn: every quad is clipped to it on the CPU and the pass
14// keeps the rest of the image. A scene with a new Seq redraws only the
15// windows and separators that differ from the scene the target holds
16// (sceneDelta). A scene that differs in anything else, a target never
17// drawn or a history too short redraws everything.
18
19// damageRegion collects the region of a target to redraw this frame and
20// what the target will hold after it: the windows drawn, their content
21// Seq and rect.
22type damageRegion struct {
23 held *target // nil: redraw everything
24 area image.Rectangle
25 seen map[ports.WindowID]bool
26 bound image.Rectangle
27 drawn map[ports.WindowID]heldWindow
28 // rects is scratch for a content's damage rects (reused across frames).
29 rects []ports.Rect
30 // why counts the reasons of full redraws (the renderer's; nil: none).
31 why *fullRedraws
32}
33
34// heldWindow is a window a target holds: its content Seq and rect.
35type heldWindow struct {
36 seq uint64
37 rect image.Rectangle
38}
39
40func newDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion {
41 d := &damageRegion{seen: map[ports.WindowID]bool{}, bound: bounds, drawn: map[ports.WindowID]heldWindow{}}
42 d.hold(tg, s)
43 return d
44}
45
46// hold decides what the target keeps. Seq 0 is a scene core did not
47// number (setup, tests): redraw all. The same Seq keeps everything; a new
48// one keeps all but what sceneDelta finds changed.
49func (d *damageRegion) hold(tg *target, s ports.Scene) {
50 if !tg.valid || s.Seq == 0 {
51 d.why.count(fullTarget)
52 return
53 }
54 if tg.sceneSeq == s.Seq {
55 d.held = tg
56 return
57 }
58 scale := s.Scale
59 if scale <= 0 {
60 scale = 1
61 }
62 phys := func(r ports.Rect) image.Rectangle {
63 p := func(v int) int { return int(math.Round(float64(v) * scale)) }
64 return image.Rect(p(r.X), p(r.Y), p(r.X+r.W), p(r.Y+r.H)).Intersect(d.bound)
65 }
66 // The limit only bounds the CPU cost of clipping every quad.
67 limit := d.bound.Dx() * d.bound.Dy() * 9 / 10
68 if area, ok := sceneDelta(tg.scene, s, phys, limit); ok {
69 d.held, d.area = tg, area
70 } else {
71 d.why.count(fullScene)
72 }
73}
74
75// frameDamage reuses the bookkeeping maps. drawn is swapped with the
76// target's previous map after hold, so a target never observes mutations.
77func (r *Renderer) frameDamage(tg *target, s ports.Scene, bounds image.Rectangle) *damageRegion {
78 if r.damageSeen == nil {
79 r.damageSeen = make(map[ports.WindowID]bool)
80 }
81 clear(r.damageSeen)
82 if r.damageDrawn == nil {
83 r.damageDrawn = make(map[ports.WindowID]heldWindow)
84 }
85 clear(r.damageDrawn)
86 r.damage = damageRegion{seen: r.damageSeen, drawn: r.damageDrawn, bound: bounds, rects: r.damage.rects[:0], why: &r.fullRedraws}
87 r.damage.hold(tg, s)
88 return &r.damage
89}
90
91// all reports whether the whole target is redrawn.
92func (d *damageRegion) all() bool { return d.held == nil }
93
94func (d *damageRegion) add(r image.Rectangle) {
95 if d.held != nil {
96 d.area = d.area.Union(r.Intersect(d.bound))
97 }
98}
99
100// window records a window drawn in rect (physical): if its content changed
101// and no finer damage comes (content), the whole rect is redrawn.
102func (d *damageRegion) window(id ports.WindowID, c ports.SurfaceContent, rect image.Rectangle) {
103 if d.seen[id] {
104 return
105 }
106 d.seen[id] = true
107 d.drawn[id] = heldWindow{seq: c.Seq, rect: rect}
108 if d.held == nil {
109 return
110 }
111 hw, ok := d.held.windows[id]
112 held := hw.seq
113 if ok && held == c.Seq {
114 return
115 }
116 switch {
117 case !ok:
118 d.why.count(fullNewWindow)
119 case c.Empty():
120 d.why.count(fullEmpty)
121 case len(c.Children) > 0:
122 d.why.count(fullChildren)
123 default:
124 var known bool
125 d.rects, known = c.AppendDamageSince(d.rects[:0], held)
126 if known {
127 return // mapped by content, called next
128 }
129 d.why.count(fullHistory)
130 }
131 d.add(rect)
132}
133
134// fullReason is why a whole target or window rect is redrawn.
135type fullReason int
136
137const (
138 fullTarget fullReason = iota // target never drawn, or an unnumbered scene
139 fullScene // the scene changed past sceneDelta
140 fullNewWindow // the target never held the window
141 fullEmpty // the window has no content
142 fullChildren // the window has subsurfaces
143 fullHistory // the damage history does not reach the held content
144 fullReasons
145)
146
147// fullRedraws counts full redraws by reason since the last take.
148type fullRedraws [fullReasons]int
149
150func (f *fullRedraws) count(r fullReason) {
151 if f != nil {
152 f[r]++
153 }
154}
155
156// take returns the counts and resets them.
157func (f *fullRedraws) take() fullRedraws {
158 n := *f
159 *f = fullRedraws{}
160 return n
161}
162
163// content maps a root surface's buffer damage onto full (where its w×h
164// buffer is drawn), clipped to dst. Scaled buffers filter linearly: one
165// more pixel around each rect. A transformed buffer redraws all of dst.
166func (d *damageRegion) content(id ports.WindowID, c *ports.SurfaceContent, full, dst image.Rectangle) {
167 if d.held == nil {
168 return
169 }
170 hw, ok := d.held.windows[id]
171 held := hw.seq
172 if !ok || held == c.Seq || len(c.Children) > 0 {
173 return
174 }
175 var known bool
176 d.rects, known = c.AppendDamageSince(d.rects[:0], held)
177 if !known || c.Width <= 0 || c.Height <= 0 {
178 return
179 }
180 rects := d.rects
181 if c.Transform != 0 {
182 d.add(dst)
183 return
184 }
185 // Linear filtering spreads a texel over half a buffer pixel around
186 // it: scale/2 target pixels, plus one for rounding.
187 pad := 0
188 if full.Dx() != c.Width || full.Dy() != c.Height {
189 s := max(float64(full.Dx())/float64(c.Width), float64(full.Dy())/float64(c.Height))
190 pad = int(math.Ceil(s/2)) + 1
191 }
192 for _, r := range rects {
193 x0 := full.Min.X + r.X*full.Dx()/c.Width - pad
194 y0 := full.Min.Y + r.Y*full.Dy()/c.Height - pad
195 x1 := full.Min.X + ((r.X+r.W)*full.Dx()+c.Width-1)/c.Width + pad
196 y1 := full.Min.Y + ((r.Y+r.H)*full.Dy()+c.Height-1)/c.Height + pad
197 d.add(image.Rect(x0, y0, x1, y1).Intersect(dst))
198 }
199}
200
201// finish adds the rects of windows the target holds that this frame does
202// not draw (e.g. a window that moved to an overlay plane and back).
203func (d *damageRegion) finish() {
204 if d.held == nil {
205 return
206 }
207 for id, hw := range d.held.windows {
208 if _, ok := d.drawn[id]; !ok {
209 d.add(hw.rect)
210 }
211 }
212}
213
214// clip limits the draws to the region; draws outside it are dropped.
215func (d *damageRegion) clip(ds []draw) []draw {
216 if d.held == nil {
217 return ds
218 }
219 out := ds[:0]
220 for _, dr := range ds {
221 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)
222 if r.Empty() {
223 continue
224 }
225 dr.pc.rect = [4]int32{int32(r.Min.X), int32(r.Min.Y), int32(r.Max.X), int32(r.Max.Y)}
226 out = append(out, dr)
227 }
228 return out
229}
230
231// hold records what the target holds after this frame: only the windows
232// it drew, and the scene with its compared slices copied into the target's
233// buffers (zero allocations once they are large enough): the caller may
234// rewrite its slices in place later.
235func (tg *target) hold(s ports.Scene, d *damageRegion) {
236 tg.heldWindows = append(tg.heldWindows[:0], s.Windows...)
237 tg.heldSeparators = append(tg.heldSeparators[:0], s.Separators...)
238 tg.heldLayers = append(tg.heldLayers[:0], s.Layers...)
239 tg.heldHints = append(tg.heldHints[:0], s.DropHints...)
240 tg.heldIndicators = append(tg.heldIndicators[:0], s.CaptureIndicators...)
241 tg.heldLabels = append(tg.heldLabels[:0], s.HintLabels...)
242 tg.valid, tg.sceneSeq, tg.scene, tg.windows = true, s.Seq, s, d.drawn
243 tg.scene.Windows, tg.scene.Separators, tg.scene.Layers = tg.heldWindows, tg.heldSeparators, tg.heldLayers
244 tg.scene.DropHints, tg.scene.CaptureIndicators = tg.heldHints, tg.heldIndicators
245 tg.scene.HintLabels = tg.heldLabels
246 // Only whether they are set matters (any capture redraws everything):
247 // sentinels, so no pointer into the caller's capture state is kept.
248 tg.scene.Capture, tg.scene.CaptureScene = nil, nil
249 if s.Capture != nil {
250 tg.scene.Capture = &heldCapture
251 }
252 if s.CaptureScene != nil {
253 tg.scene.CaptureScene = &heldCaptureScene
254 }
255}
256
257var (
258 heldCapture ports.SceneCapture
259 heldCaptureScene ports.Scene
260)
261
262// sceneDelta is the region (physical, through phys) where cur differs from
263// old, two scenes of one output; ok is false when the whole output must be
264// redrawn: anything but the windows and separators differs, the windows
265// are not the same list, or the region covers more than limit pixels.
266// Each window that differs (any field) adds its old and new rects, each
267// separator present in only one scene adds its own. A new Scene field
268// belongs here too (see Scene.SameAs, TestSceneSameAsCoversEveryField); a
269// new SceneWindow field is checked by TestSceneWindowFieldCount.
270// Scene.Animating is not compared: it schedules the next frame, it draws
271// nothing.
272func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, limit int) (image.Rectangle, bool) {
273 if old.Security != cur.Security || old.Output != cur.Output ||
274 old.OutputWidth != cur.OutputWidth || old.OutputHeight != cur.OutputHeight ||
275 old.Scale != cur.Scale || old.Transform != cur.Transform || old.Off != cur.Off ||
276 old.HintStyle != cur.HintStyle || old.Background != cur.Background || old.Border != cur.Border ||
277 old.WorkspaceClip != cur.WorkspaceClip || old.TileInset != cur.TileInset || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind ||
278 old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil ||
279 !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) ||
280 !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) || !slices.Equal(old.HintLabels, cur.HintLabels) ||
281 len(old.Windows) != len(cur.Windows) {
282 return image.Rectangle{}, false
283 }
284 // windows() paints the tile lines and the Dim veil under the first
285 // window that opens the floats: when that moves, the paint order of the
286 // whole output changed, not only the rects of the windows that differ.
287 if floatsStart(old) != floatsStart(cur) {
288 return image.Rectangle{}, false
289 }
290 var area image.Rectangle
291 if (old.Flash == nil) != (cur.Flash == nil) || (old.Flash != nil && cur.Flash != nil && *old.Flash != *cur.Flash) {
292 if old.Flash != nil {
293 area = area.Union(phys(old.Flash.Rect))
294 }
295 if cur.Flash != nil {
296 area = area.Union(phys(cur.Flash.Rect))
297 }
298 }
299 for i := range old.Windows {
300 a, b := old.Windows[i], cur.Windows[i]
301 if a.ID != b.ID {
302 return image.Rectangle{}, false
303 }
304 if a != b {
305 area = area.Union(phys(a.Rect)).Union(phys(b.Rect))
306 }
307 }
308 area = area.Union(separatorDelta(old.Separators, cur.Separators, phys))
309 if area.Dx()*area.Dy() > limit {
310 return image.Rectangle{}, false
311 }
312 return area, true
313}
314
315// separatorDelta is the region where the separator lists differ: a line in
316// one list more often than in the other. Core builds the lists in layout
317// order (one pass over the placements, sides in a fixed order), so between
318// two frames of the same windows they are index-aligned: equal lists cost
319// one comparison, and with lists of the same length only the pairs that
320// differ by index are counted in the other list (a focus change lights a
321// few lines, not all). Lists of different lengths (a line scrolled on or
322// off) count every line.
323func separatorDelta(old, cur []ports.Separator, phys func(ports.Rect) image.Rectangle) image.Rectangle {
324 var area image.Rectangle
325 if slices.Equal(old, cur) {
326 return area
327 }
328 if len(old) == len(cur) {
329 for i := range old {
330 if old[i] == cur[i] {
331 continue
332 }
333 if countSeparator(old, old[i]) != countSeparator(cur, old[i]) {
334 area = area.Union(phys(old[i].Rect))
335 }
336 if countSeparator(old, cur[i]) != countSeparator(cur, cur[i]) {
337 area = area.Union(phys(cur[i].Rect))
338 }
339 }
340 return area
341 }
342 for _, sep := range old {
343 if countSeparator(old, sep) != countSeparator(cur, sep) {
344 area = area.Union(phys(sep.Rect))
345 }
346 }
347 for _, sep := range cur {
348 if countSeparator(old, sep) != countSeparator(cur, sep) {
349 area = area.Union(phys(sep.Rect))
350 }
351 }
352 return area
353}
354
355func countSeparator(list []ports.Separator, sep ports.Separator) int {
356 n := 0
357 for _, o := range list {
358 if o == sep {
359 n++
360 }
361 }
362 return n
363}