Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df94556101 |
No files matched your search
@@ -45,7 +45,9 @@ behaves as `New`'s Terminal does, the writer goroutine included; `New` is
|
|||||||
The application owns a flat `[]Cell` buffer (row-major, `cells[y*width+x]`) and
|
The application owns a flat `[]Cell` buffer (row-major, `cells[y*width+x]`) and
|
||||||
passes it to `Flush`. The output buffer diffs against the previously flushed frame:
|
passes it to `Flush`. The output buffer diffs against the previously flushed frame:
|
||||||
|
|
||||||
- Rows are scanned with early termination (trailing unchanged cells skipped).
|
- A row's first and last changed cells are found by memcmp, eight cells a compare:
|
||||||
|
an unchanged row costs those compares alone, and only the cells between are
|
||||||
|
scanned one by one.
|
||||||
- Cursor moves are emitted only when the write position is non-contiguous.
|
- Cursor moves are emitted only when the write position is non-contiguous.
|
||||||
- SGR state (fg, bg, attributes) is coalesced across cells; redundant sequences
|
- SGR state (fg, bg, attributes) is coalesced across cells; redundant sequences
|
||||||
are suppressed.
|
are suppressed.
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ func cellEqual(a, b Cell) bool {
|
|||||||
// fails to compile should a field change leave padding behind
|
// fails to compile should a field change leave padding behind
|
||||||
var _ [0]struct{} = [unsafe.Sizeof(Cell{}) - unsafe.Sizeof(rune(0)) - 2*unsafe.Sizeof(color.RGB{}) - unsafe.Sizeof(Attr(0))]struct{}{}
|
var _ [0]struct{} = [unsafe.Sizeof(Cell{}) - unsafe.Sizeof(rune(0)) - 2*unsafe.Sizeof(color.RGB{}) - unsafe.Sizeof(Attr(0))]struct{}{}
|
||||||
|
|
||||||
// rowEqual reports whether two rows of cells hold the same cells, as one memcmp
|
// cellsEqual reports whether two runs of cells hold the same cells, as one memcmp
|
||||||
func rowEqual(a, b []Cell) bool {
|
func cellsEqual(a, b []Cell) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,34 @@ func rowEqual(a, b []Cell) bool {
|
|||||||
return bytes.Equal(unsafe.Slice((*byte)(unsafe.Pointer(&a[0])), n), unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), n))
|
return bytes.Equal(unsafe.Slice((*byte)(unsafe.Pointer(&a[0])), n), unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diffChunk is the cells one memcmp settles while a row's changes are sought: few
|
||||||
|
// enough that a changed chunk costs little to search again cell by cell
|
||||||
|
const diffChunk = 8
|
||||||
|
|
||||||
|
// firstChange is the first cell where a and b differ, len(a) when none does
|
||||||
|
func firstChange(a, b []Cell) int {
|
||||||
|
i := 0
|
||||||
|
for i+diffChunk <= len(a) && cellsEqual(a[i:i+diffChunk], b[i:i+diffChunk]) {
|
||||||
|
i += diffChunk
|
||||||
|
}
|
||||||
|
for i < len(a) && cellEqual(a[i], b[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastChange is one past the last cell from on where a and b differ, from when none does
|
||||||
|
func lastChange(a, b []Cell, from int) int {
|
||||||
|
j := len(a)
|
||||||
|
for j-diffChunk >= from && cellsEqual(a[j-diffChunk:j], b[j-diffChunk:j]) {
|
||||||
|
j -= diffChunk
|
||||||
|
}
|
||||||
|
for j > from && cellEqual(a[j-1], b[j-1]) {
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
// flush renders the back buffer into buf, diffing against front buffer
|
// flush renders the back buffer into buf, diffing against front buffer
|
||||||
func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
||||||
if width != o.width || height != o.height {
|
if width != o.width || height != o.height {
|
||||||
@@ -104,20 +132,15 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
|||||||
|
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
rowStart := y * width
|
rowStart := y * width
|
||||||
// Most rows of a frame are unchanged; one compare settles each of those
|
// Memcmp finds the row's first and last changed cells, so an unchanged row is
|
||||||
if rowEqual(cells[rowStart:rowStart+width], o.front[rowStart:rowStart+width]) {
|
// one pass of compares and a changed one is scanned cell by cell between them
|
||||||
|
row, front := cells[rowStart:rowStart+width], o.front[rowStart:rowStart+width]
|
||||||
|
x := firstChange(row, front)
|
||||||
|
if x == width {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
rowEnd := lastChange(row, front, x+1)
|
||||||
|
|
||||||
rowEnd := width
|
|
||||||
for rowEnd > 0 && cellEqual(cells[rowStart+rowEnd-1], o.front[rowStart+rowEnd-1]) {
|
|
||||||
rowEnd--
|
|
||||||
}
|
|
||||||
if rowEnd == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
x := 0
|
|
||||||
for x < rowEnd {
|
for x < rowEnd {
|
||||||
idx := rowStart + x
|
idx := rowStart + x
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package terminal
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"math/rand/v2"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -133,6 +134,25 @@ func TestUnchangedFrameWritesNothing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every cell a frame changes is written, wherever in its row it lies and however
|
||||||
|
// wide the row: after a flush the screen the terminal holds is the frame
|
||||||
|
func TestEveryChangedCellIsWritten(t *testing.T) {
|
||||||
|
rng := rand.New(rand.NewPCG(1, 2))
|
||||||
|
for _, width := range []int{1, 7, 8, 9, 17, 120} {
|
||||||
|
o := newOutputBuffer(ColorModeTrueColor)
|
||||||
|
frame := make([]Cell, width*4)
|
||||||
|
for range 200 {
|
||||||
|
for range rng.IntN(len(frame)/2 + 1) {
|
||||||
|
frame[rng.IntN(len(frame))] = Cell{Rune: 'a' + rng.Int32N(4), Fg: color.RGB{G: uint8(rng.IntN(2))}}
|
||||||
|
}
|
||||||
|
o.flush(frame, width, 4)
|
||||||
|
if !slices.Equal(o.front, frame) {
|
||||||
|
t.Fatalf("width %d: a changed cell went unwritten", width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A paste arrives as one event, whatever reads split it into: an escape
|
// A paste arrives as one event, whatever reads split it into: an escape
|
||||||
// inside it is text even when a poll times out on it, and a paste the
|
// inside it is text even when a poll times out on it, and a paste the
|
||||||
// terminal never ends ends when input pauses, whether reads time out or
|
// terminal never ends ends when input pauses, whether reads time out or
|
||||||
|
|||||||
Reference in new issue
Block a user