okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Kieran Klukasfix(renderer): keep the touch list covering the screen, once6d7982b

upstream
7.2 KiB298 linesraw
1package uv
2
3import "hash/maphash"
4
5// hash returns the hash value of a [Line].
6func hash(h *maphash.Hash, l Line) uint64 {
7	h.Reset()
8	for _, c := range l {
9		// maphash writes can not fail
10		_, _ = h.WriteString(c.Content)
11	}
12
13	return h.Sum64()
14}
15
16// hashmap represents a single [Line] hash.
17type hashmap struct {
18	value              uint64
19	oldcount, newcount int
20	oldindex, newindex int
21}
22
23// The value used to indicate lines created by insertions and scrolls.
24const newIndex = -1
25
26// updateHashmap updates the hashmap with the new hash value.
27func (s *TerminalRenderer) updateHashmap(newbuf *RenderBuffer) {
28	height := newbuf.Height()
29
30	if len(s.oldhash) >= height && len(s.newhash) >= height {
31		// rehash changed lines
32		for i := range height {
33			if newbuf.Touched[i] != nil {
34				// TODO: Investigate why this is needed. If we remove this
35				// line, scroll optimization does not work correctly. This
36				// should happen else where.
37				s.oldhash[i] = hash(&s.hasher, s.curbuf.Line(i))
38				s.newhash[i] = hash(&s.hasher, newbuf.Line(i))
39			}
40		}
41	} else {
42		// rehash all
43		if len(s.oldhash) != height {
44			s.oldhash = make([]uint64, height)
45		}
46		if len(s.newhash) != height {
47			s.newhash = make([]uint64, height)
48		}
49		for i := range height {
50			s.oldhash[i] = hash(&s.hasher, s.curbuf.Line(i))
51			s.newhash[i] = hash(&s.hasher, newbuf.Line(i))
52		}
53	}
54
55	s.hashtab = make([]hashmap, (height+1)*2)
56	for i := range height {
57		hashval := s.oldhash[i]
58
59		// Find matching hash or empty slot
60		var idx int
61		for idx = 0; idx < len(s.hashtab) && s.hashtab[idx].value != 0; idx++ {
62			if s.hashtab[idx].value == hashval {
63				break
64			}
65		}
66
67		s.hashtab[idx].value = hashval // in case this is a new hash
68		s.hashtab[idx].oldcount++
69		s.hashtab[idx].oldindex = i
70	}
71	for i := range height {
72		hashval := s.newhash[i]
73
74		// Find matching hash or empty slot
75		var idx int
76		for idx = 0; idx < len(s.hashtab) && s.hashtab[idx].value != 0; idx++ {
77			if s.hashtab[idx].value == hashval {
78				break
79			}
80		}
81
82		s.hashtab[idx].value = hashval // in case this is a new hash
83		s.hashtab[idx].newcount++
84		s.hashtab[idx].newindex = i
85		s.oldnum[i] = newIndex // init old indices slice
86	}
87
88	// Mark line pair corresponding to unique hash pairs.
89	for i := 0; i < len(s.hashtab) && s.hashtab[i].value != 0; i++ {
90		hsp := &s.hashtab[i]
91		if hsp.oldcount == 1 && hsp.newcount == 1 && hsp.oldindex != hsp.newindex {
92			s.oldnum[hsp.newindex] = hsp.oldindex
93		}
94	}
95
96	s.growHunks(newbuf)
97
98	// Eliminate bad or impossible shifts. This includes removing those hunks
99	// which could not grow because of conflicts, as well those which are to be
100	// moved too far, they are likely to destroy more than carry.
101	for i := 0; i < height; {
102		var start, shift, size int
103		for i < height && s.oldnum[i] == newIndex {
104			i++
105		}
106		if i >= height {
107			break
108		}
109		start = i
110		shift = s.oldnum[i] - i
111		i++
112		for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
113			i++
114		}
115		size = i - start
116		if size < 3 || size+min(size/8, 2) < abs(shift) {
117			for start < i {
118				s.oldnum[start] = newIndex
119				start++
120			}
121		}
122	}
123
124	// After clearing invalid hunks, try grow the rest.
125	s.growHunks(newbuf)
126}
127
128// scrollOldhash scrolls the oldhash slice by 'n' lines between 'top' and 'bot'.
129func (s *TerminalRenderer) scrollOldhash(n, top, bot int) {
130	if len(s.oldhash) == 0 {
131		return
132	}
133
134	size := bot - top + 1 - abs(n)
135	if n > 0 {
136		// Move existing hashes up
137		copy(s.oldhash[top:], s.oldhash[top+n:top+n+size])
138		// Recalculate hashes for newly shifted-in lines
139		for i := bot; i > bot-n; i-- {
140			s.oldhash[i] = hash(&s.hasher, s.curbuf.Line(i))
141		}
142	} else {
143		// Move existing hashes down
144		copy(s.oldhash[top-n:], s.oldhash[top:top+size])
145		// Recalculate hashes for newly shifted-in lines
146		for i := top; i < top-n; i++ {
147			s.oldhash[i] = hash(&s.hasher, s.curbuf.Line(i))
148		}
149	}
150}
151
152func (s *TerminalRenderer) growHunks(newbuf *RenderBuffer) {
153	var (
154		backLimit    int // limits for cells to fill
155		backRefLimit int // limit for references
156		i            int
157		nextHunk     int
158	)
159
160	height := newbuf.Height()
161	for i < height && s.oldnum[i] == newIndex {
162		i++
163	}
164	for ; i < height; i = nextHunk {
165		var (
166			forwardLimit    int
167			forwardRefLimit int
168			end             int
169			start           = i
170			shift           = s.oldnum[i] - i
171		)
172
173		// get forward limit
174		i = start + 1
175		for i < height &&
176			s.oldnum[i] != newIndex &&
177			s.oldnum[i]-i == shift {
178			i++
179		}
180
181		end = i
182		for i < height && s.oldnum[i] == newIndex {
183			i++
184		}
185
186		nextHunk = i
187		forwardLimit = i
188		if i >= height || s.oldnum[i] >= i {
189			forwardRefLimit = i
190		} else {
191			forwardRefLimit = s.oldnum[i]
192		}
193
194		i = start - 1
195
196		// grow back
197		if shift < 0 {
198			backLimit = backRefLimit + (-shift)
199		}
200		for i >= backLimit {
201			if s.newhash[i] == s.oldhash[i+shift] ||
202				s.costEffective(newbuf, i+shift, i, shift < 0) {
203				s.oldnum[i] = i + shift
204			} else {
205				break
206			}
207			i--
208		}
209
210		i = end
211		// grow forward
212		if shift > 0 {
213			forwardLimit = forwardRefLimit - shift
214		}
215		for i < forwardLimit {
216			if s.newhash[i] == s.oldhash[i+shift] ||
217				s.costEffective(newbuf, i+shift, i, shift > 0) {
218				s.oldnum[i] = i + shift
219			} else {
220				break
221			}
222			i++
223		}
224
225		backLimit = i
226		backRefLimit = backLimit
227		if shift > 0 {
228			backRefLimit += shift
229		}
230	}
231}
232
233// costEffective returns true if the cost of moving line 'from' to line 'to' seems to be
234// cost effective. 'blank' indicates whether the line 'to' would become blank.
235func (s *TerminalRenderer) costEffective(newbuf *RenderBuffer, from, to int, blank bool) bool {
236	if from == to {
237		return false
238	}
239
240	newFrom := s.oldnum[from]
241	if newFrom == newIndex {
242		newFrom = from
243	}
244
245	// On the left side of >= is the cost before moving. On the right side --
246	// cost after moving.
247
248	// Calculate costs before moving.
249	var costBeforeMove int
250	if blank {
251		// Cost of updating blank line at destination.
252		costBeforeMove = s.updateCostBlank(newbuf, newbuf.Line(to))
253	} else {
254		// Cost of updating exiting line at destination.
255		costBeforeMove = s.updateCost(newbuf, s.curbuf.Line(to), newbuf.Line(to))
256	}
257
258	// Add cost of updating source line
259	costBeforeMove += s.updateCost(newbuf, s.curbuf.Line(newFrom), newbuf.Line(from))
260
261	// Calculate costs after moving.
262	var costAfterMove int
263	if newFrom == from {
264		// Source becomes blank after move
265		costAfterMove = s.updateCostBlank(newbuf, newbuf.Line(from))
266	} else {
267		// Source gets updated from another line
268		costAfterMove = s.updateCost(newbuf, s.curbuf.Line(newFrom), newbuf.Line(from))
269	}
270
271	// Add cost of moving source line to destination
272	costAfterMove += s.updateCost(newbuf, s.curbuf.Line(from), newbuf.Line(to))
273
274	// Return true if moving is cost effective (costs less or equal)
275	return costBeforeMove >= costAfterMove
276}
277
278func (s *TerminalRenderer) updateCost(_ *RenderBuffer, from, to Line) (cost int) {
279	var fidx, tidx int
280	for i := s.curbuf.Width(); i > 0; i, fidx, tidx = i-1, fidx+1, tidx+1 {
281		if !cellEqual(from.At(fidx), to.At(tidx)) {
282			cost++
283		}
284	}
285	return
286}
287
288func (s *TerminalRenderer) updateCostBlank(_ *RenderBuffer, to Line) (cost int) {
289	// This assumes bce capability.
290	blank := s.clearBlank()
291	var tidx int
292	for i := s.curbuf.Width(); i > 0; i, tidx = i-1, tidx+1 {
293		if !cellEqual(blank, to.At(tidx)) {
294			cost++
295		}
296	}
297	return
298}