Performance optimization.

This commit is contained in:
Daniel Schmidt 2023-05-23 20:53:49 +02:00
parent c18e3badb7
commit 032bbda893
6 changed files with 104 additions and 25 deletions

54
crt.go
View File

@ -14,6 +14,7 @@ import (
"image/color"
"io"
"sync"
"unicode/utf8"
)
// colorCache is the ansi color cache.
@ -53,6 +54,7 @@ type Window struct {
onPostDraw func(screen *ebiten.Image)
// Other
seqBuffer []byte
showTps bool
fonts Fonts
bgColors *image.RGBA
@ -109,6 +111,7 @@ func NewGame(width int, height int, fonts Fonts, tty io.Reader, adapter InputAda
onPreDraw: func(screen *ebiten.Image) {},
onPostDraw: func(screen *ebiten.Image) {},
invalidateBuffer: true,
seqBuffer: make([]byte, 0, 2^12),
}
game.inputAdapter.HandleWindowSize(WindowSize{
@ -187,6 +190,18 @@ func (g *Window) SetBgPixels(x, y int, c color.Color) {
g.InvalidateBuffer()
}
// SetBg sets the background color of a cell and checks if it needs to be redrawn.
func (g *Window) SetBg(x, y int, c color.Color) {
ra, rg, rb, _ := g.grid[y][x].Bg.RGBA()
ca, cg, cb, _ := c.RGBA()
if ra == ca && rg == cg && rb == cb {
return
}
g.SetBgPixels(x, y, c)
g.grid[y][x].Bg = c
}
// GetCellsWidth returns the number of cells in the x direction.
func (g *Window) GetCellsWidth() int {
return g.cellsWidth
@ -266,22 +281,19 @@ func (g *Window) handleCSI(csi any) {
for i := g.cursorX; i < g.cellsWidth-g.cursorX; i++ {
g.grid[g.cursorY][g.cursorX+i].Char = ' '
g.grid[g.cursorY][g.cursorX+i].Fg = color.White
g.grid[g.cursorY][g.cursorX+i].Bg = g.defaultBg
g.SetBgPixels(g.cursorX+i, g.cursorY, g.defaultBg)
g.SetBg(g.cursorX+i, g.cursorY, g.defaultBg)
}
case 1: // erase from start of line to cursor
for i := 0; i < g.cursorX; i++ {
g.grid[g.cursorY][i].Char = ' '
g.grid[g.cursorY][i].Fg = color.White
g.grid[g.cursorY][i].Bg = g.defaultBg
g.SetBgPixels(i, g.cursorY, g.defaultBg)
g.SetBg(i, g.cursorY, g.defaultBg)
}
case 2: // erase entire line
for i := 0; i < g.cellsWidth; i++ {
g.grid[g.cursorY][i].Char = ' '
g.grid[g.cursorY][i].Fg = color.White
g.grid[g.cursorY][i].Bg = g.defaultBg
g.SetBgPixels(i, g.cursorY, g.defaultBg)
g.SetBg(i, g.cursorY, g.defaultBg)
}
}
case CursorShowSeq:
@ -334,11 +346,9 @@ func (g *Window) handleSGR(sgr any) {
}
func (g *Window) parseSequences(str string, printExtra bool) int {
runes := []rune(str)
lastFound := 0
for i := 0; i < len(runes); i++ {
if sgr, ok := extractSGR(string(runes[i:])); ok {
for i := 0; i < len(str); i++ {
if sgr, ok := extractSGR(str[i:]); ok {
i += len(sgr) - 1
if sgr, ok := parseSGR(sgr); ok {
@ -348,7 +358,7 @@ func (g *Window) parseSequences(str string, printExtra bool) int {
g.InvalidateBuffer()
}
}
} else if csi, ok := extractCSI(string(runes[i:])); ok {
} else if csi, ok := extractCSI(str[i:]); ok {
i += len(csi) - 1
if csi, ok := parseCSI(csi); ok {
@ -357,13 +367,23 @@ func (g *Window) parseSequences(str string, printExtra bool) int {
g.InvalidateBuffer()
}
} else if printExtra {
g.PrintChar(runes[i], g.curFg, g.curBg, g.curWeight)
if r, size := utf8.DecodeRuneInString(str[i:]); r != utf8.RuneError {
g.PrintChar(r, g.curFg, g.curBg, g.curWeight)
i += size - 1
}
}
}
return lastFound
}
func (g *Window) drainSequence() {
if len(g.seqBuffer) > 0 {
g.parseSequences(string(g.seqBuffer), true)
g.seqBuffer = g.seqBuffer[:0]
}
}
// RecalculateBackgrounds syncs the background colors to the background pixels.
func (g *Window) RecalculateBackgrounds() {
for i := 0; i < g.cellsWidth; i++ {
@ -439,8 +459,7 @@ func (g *Window) Update() error {
g.Lock()
{
line := string(buf[:n])
g.parseSequences(line, true)
g.seqBuffer = append(g.seqBuffer, buf[:n]...)
}
g.Unlock()
}
@ -511,10 +530,13 @@ func (g *Window) Draw(screen *ebiten.Image) {
g.Lock()
defer g.Unlock()
screen.Fill(g.defaultBg)
g.onPreDraw(screen)
// We process the sequence buffer here so that we don't get flickering
g.drainSequence()
screen.Fill(g.defaultBg)
// Get current buffer
bufferImage := g.lastBuffer

23
csi.go
View File

@ -4,8 +4,12 @@ import (
"github.com/muesli/termenv"
"strconv"
"strings"
"sync"
)
var csiMtx = &sync.Mutex{}
var csiCache = map[string]any{}
type CursorUpSeq struct {
Count int
}
@ -109,6 +113,25 @@ func parseCSI(s string) (any, bool) {
return nil, false
}
csiMtx.Lock()
if cached, ok := csiCache[s]; ok {
csiMtx.Unlock()
return cached, true
}
csiMtx.Unlock()
if val, ok := parseCSIStruct(s); ok {
csiMtx.Lock()
csiCache[s] = val
csiMtx.Unlock()
return val, true
}
return nil, false
}
func parseCSIStruct(s string) (any, bool) {
switch s {
case termenv.ShowCursorSeq:
return CursorShowSeq{}, true

View File

@ -11,6 +11,8 @@ import (
"github.com/hajimehoshi/ebiten/v2"
"image/color"
"math/rand"
"net/http"
_ "net/http/pprof"
"time"
)
@ -36,10 +38,16 @@ func (m *model) View() string {
}
func main() {
go func() {
fmt.Println(http.ListenAndServe("localhost:6060", nil))
}()
rand.Seed(0)
enableShader := flag.Bool("shader", false, "Enable shader")
flag.Parse()
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 8.0)
fonts, err := crt.LoadFaces("./fonts/IosevkaTermNerdFontMono-Regular.ttf", "./fonts/IosevkaTermNerdFontMono-Bold.ttf", "./fonts/IosevkaTermNerdFontMono-Italic.ttf", 72.0, 9.0)
if err != nil {
panic(err)
}
@ -60,13 +68,15 @@ func main() {
}()
var lastStart int64
win.SetOnPreDraw(func(screen *ebiten.Image) {
lastStart = time.Now().UnixMicro()
})
win.SetOnPostDraw(func(screen *ebiten.Image) {
elapsed := time.Now().UnixMicro() - lastStart
if (1000 / (float64(elapsed) * 0.001)) > 500 {
return
}
fmt.Printf("Frame took %d micro seconds FPS=%.2f\n", elapsed, 1000/(float64(elapsed)*0.001))
})
@ -79,6 +89,8 @@ func main() {
win.SetShader(lotte)
}
win.ShowTPS(true)
if err := win.Run("Simple"); err != nil {
panic(err)
}

11
go.mod
View File

@ -3,24 +3,26 @@ module github.com/BigJk/crt
go 1.20
require (
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.24.0
github.com/charmbracelet/lipgloss v0.7.1
github.com/hajimehoshi/ebiten/v2 v2.5.4
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/termenv v0.15.1
github.com/stretchr/testify v1.8.2
golang.org/x/image v0.7.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.15.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.3.0 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/jezek/xgb v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
@ -28,11 +30,10 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

6
go.sum
View File

@ -1,4 +1,5 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@ -23,6 +24,7 @@ github.com/ebitengine/purego v0.3.0 h1:BDv9pD98k6AuGNQf3IF41dDppGBOe0F4AofvhFtBX
github.com/ebitengine/purego v0.3.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/hajimehoshi/bitmapfont/v2 v2.2.3 h1:jmq/TMNj352V062Tr5e3hAoipkoxCbY1JWTzor0zNps=
github.com/hajimehoshi/ebiten/v2 v2.5.4 h1:NvUU6LvVc6oc+u+rD9KfHMjruRdpNwbpalVUINNXufU=
github.com/hajimehoshi/ebiten/v2 v2.5.4/go.mod h1:mnHSOVysTr/nUZrN1lBTRqhK4NG+T9NR3JsJP2rCppk=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
@ -103,8 +105,9 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -122,6 +125,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

17
sgr.go
View File

@ -4,8 +4,12 @@ import (
"fmt"
"github.com/muesli/termenv"
"strings"
"sync"
)
var sgrMtx = &sync.Mutex{}
var sgrCache = map[string][]any{}
// extractSGR extracts an SGR ansi sequence from the beginning of the string.
func extractSGR(s string) (string, bool) {
if len(s) < 2 {
@ -66,6 +70,15 @@ func parseSGR(s string) ([]any, bool) {
return nil, false
}
sgrMtx.Lock()
if cached, ok := sgrCache[s]; ok {
sgrMtx.Unlock()
return cached, true
}
sgrMtx.Unlock()
full := s
if !strings.HasSuffix(s, "m") {
return nil, false
}
@ -138,5 +151,9 @@ func parseSGR(s string) ([]any, bool) {
s = s[len(code)+1:]
}
sgrMtx.Lock()
sgrCache[full] = res
sgrMtx.Unlock()
return res, len(res) > 0
}