v0.4.0 b6be3a8

damage.go RawHistoryFolder

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

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