Browse Source

Optimize static rendering elements, optimize the stdout rendering, use terminal raw mode

main
meutraa 7 months ago
parent
commit
dde9ec926b
Signed by: meutraa GPG Key ID: 82664A5F8DAC3400
  1. 1
      .gitignore
  2. 62
      benchmarks.md
  3. 1
      go.mod
  4. 2
      go.sum
  5. 3
      internal/config/main.go
  6. 6
      internal/game/note.go
  7. 50
      internal/render/default.go
  8. 8
      internal/render/renderer.go
  9. 333
      main.go

1
.gitignore

@ -1,3 +1,4 @@
songs
eott
cpu.prof
scores.db

62
benchmarks.md

@ -91,4 +91,64 @@ Showing top 25 nodes out of 174
100ms 1.31% 55.35% 100ms 1.31% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
100ms 1.31% 56.66% 650ms 8.49% runtime.mallocgc
100ms 1.31% 57.96% 100ms 1.31% runtime.read
80ms 1.04% 59.01% 130ms 1.70% fmt.(*fmt).fmtInteger
80ms 1.04% 59.01% 130ms 1.70% fmt.(*fmt).fmtInteger
Showing nodes accounting for 2680ms, 68.37% of 3920ms total
Dropped 70 nodes (cum <= 19.60ms)
Showing top 25 nodes out of 172
flat flat% sum% cum cum%
400ms 10.20% 10.20% 400ms 10.20% runtime.futex
260ms 6.63% 16.84% 430ms 10.97% github.com/jfreymuth/vorbis.(*residue).Decode
250ms 6.38% 23.21% 970ms 24.74% runtime.findrunnable
140ms 3.57% 26.79% 140ms 3.57% <unknown>
130ms 3.32% 30.10% 140ms 3.57% github.com/jfreymuth/vorbis.imdct
130ms 3.32% 33.42% 140ms 3.57% syscall.Syscall
120ms 3.06% 36.48% 120ms 3.06% runtime.nanotime (inline)
120ms 3.06% 39.54% 120ms 3.06% runtime.unlock2
100ms 2.55% 42.09% 300ms 7.65% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
100ms 2.55% 44.64% 1010ms 25.77% github.com/jfreymuth/oggvorbis.(*Reader).Read
90ms 2.30% 46.94% 90ms 2.30% [libpthread-2.32.so]
90ms 2.30% 49.23% 170ms 4.34% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
90ms 2.30% 51.53% 120ms 3.06% runtime.cgocall
80ms 2.04% 53.57% 80ms 2.04% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
70ms 1.79% 55.36% 880ms 22.45% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
70ms 1.79% 57.14% 260ms 6.63% runtime.checkTimers
60ms 1.53% 58.67% 60ms 1.53% github.com/jfreymuth/vorbis.(*Decoder).inverseCoupling
50ms 1.28% 59.95% 1090ms 27.81% github.com/faiface/beep.(*Mixer).Stream
50ms 1.28% 61.22% 50ms 1.28% runtime.(*randomEnum).next (inline)
50ms 1.28% 62.50% 50ms 1.28% runtime.lock2
50ms 1.28% 63.78% 50ms 1.28% runtime.madvise
50ms 1.28% 65.05% 50ms 1.28% runtime.memclrNoHeapPointers
50ms 1.28% 66.33% 50ms 1.28% runtime.read
40ms 1.02% 67.35% 1250ms 31.89% github.com/faiface/beep/speaker.update
40ms 1.02% 68.37% 40ms 1.02% runtime.epollwait
Showing nodes accounting for 2660ms, 67.34% of 3950ms total
Dropped 76 nodes (cum <= 19.75ms)
Showing top 25 nodes out of 172
flat flat% sum% cum cum%
470ms 11.90% 11.90% 470ms 11.90% runtime.futex
260ms 6.58% 18.48% 260ms 6.58% [libspa-audioconvert.so]
210ms 5.32% 23.80% 210ms 5.32% [libpthread-2.32.so]
180ms 4.56% 28.35% 180ms 4.56% github.com/jfreymuth/vorbis.imdct
150ms 3.80% 32.15% 920ms 23.29% runtime.findrunnable
120ms 3.04% 35.19% 870ms 22.03% github.com/jfreymuth/oggvorbis.(*Reader).Read
110ms 2.78% 37.97% 420ms 10.63% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
100ms 2.53% 40.51% 670ms 16.96% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
90ms 2.28% 42.78% 110ms 2.78% syscall.Syscall
80ms 2.03% 44.81% 140ms 3.54% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
80ms 2.03% 46.84% 80ms 2.03% runtime.(*randomEnum).next (inline)
80ms 2.03% 48.86% 100ms 2.53% runtime.lock2
80ms 2.03% 50.89% 80ms 2.03% runtime.nanotime (inline)
70ms 1.77% 52.66% 210ms 5.32% github.com/jfreymuth/vorbis.(*residue).Decode
70ms 1.77% 54.43% 120ms 3.04% runtime.cgocall
70ms 1.77% 56.20% 100ms 2.53% runtime.scanobject
60ms 1.52% 57.72% 60ms 1.52% github.com/jfreymuth/vorbis.(*Decoder).inverseCoupling
60ms 1.52% 59.24% 60ms 1.52% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
50ms 1.27% 60.51% 50ms 1.27% [libc-2.32.so]
50ms 1.27% 61.77% 50ms 1.27% runtime.epollwait
50ms 1.27% 63.04% 50ms 1.27% runtime.madvise
50ms 1.27% 64.30% 50ms 1.27% runtime.runqgrab
40ms 1.01% 65.32% 40ms 1.01% github.com/jfreymuth/oggvorbis.crcUpdate
40ms 1.01% 66.33% 40ms 1.01% github.com/jfreymuth/vorbis.renderLine
40ms 1.01% 67.34% 40ms 1.01% runtime.memclrNoHeapPointers

1
go.mod

@ -10,6 +10,7 @@ require (
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/mattn/go-sqlite3 v1.14.7
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/exp v0.0.0-20210417010653-0739314eea07 // indirect
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect

2
go.sum

@ -45,6 +45,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=

3
internal/config/main.go

@ -14,6 +14,7 @@ import (
var (
Directory = kingpin.Arg("directory", "Song/chart directory").Required().ExistingDir()
CpuProfile = kingpin.Flag("profile", "Profile CPU").String()
DebugUpdateRate = kingpin.Flag("debug-update-rate", "Every n frames").Default("240").Uint16()
Input = kingpin.Flag("input", "Input device").Default("/dev/input/by-id/usb-OLKB_Planck-event-kbd").Short('i').ExistingFile()
Rate = kingpin.Flag("rate", "Playback % rate").Default("100").Short('r').Uint16()
Offset = kingpin.Flag("offset", "Global offset").Default("0ms").Short('o').Duration()
@ -25,7 +26,7 @@ var (
keys4 = kingpin.Flag("keys-single", "Keys for 4k").Default("12,40,17,50").Short('k').String()
keys6 = kingpin.Flag("keys-solo", "Keys for 6k").Default("23,18,24,20,31,46").String()
keys8 = kingpin.Flag("keys-double", "Keys for 8k").Default("23,18,24,49,35,20,31,46").String()
BarOffsetFromBottom = kingpin.Flag("bar-row", "Console row to render hit bar").Default("8").Uint16()
BarOffsetFromBottom = kingpin.Flag("bar-row", "Console row to render hit bar").Default("8").Uint16()
BarSym = kingpin.Flag("bar-decoration", "Decoration at the hitfield").Default("\033[2m\033[1D[ ]").String()
NoteSym = kingpin.Flag("note-symbol", "Restricted to 1 column").Default("⬤").String()
MineSym = kingpin.Flag("mine-symbol", "Restricted to 1 column").Default("⨯").String()

6
internal/game/note.go

@ -1,7 +1,6 @@
package game
import (
"math"
"time"
)
@ -15,9 +14,4 @@ type Note struct {
// This is state
Row uint16 // The current row this note is rendered on, for clearing
HitTime time.Duration // When the note was hit
Miss bool // Has the note scrolled past the bottom edge of the terminal
}
func (note *Note) Visible() bool {
return note.Row != math.MaxUint16
}

50
internal/render/default.go

@ -3,14 +3,19 @@ package render
import (
"fmt"
"image/color"
"os"
"strconv"
"strings"
"time"
"git.lost.host/meutraa/eott/internal/config"
"golang.org/x/crypto/ssh/terminal"
)
type DefaultRenderer struct {
buffer string
decorations []*decoration
buffer strings.Builder
restoreState *terminal.State
decorations []*decoration
}
type decoration struct {
@ -20,6 +25,12 @@ type decoration struct {
}
func (r *DefaultRenderer) Init() error {
state, err := terminal.MakeRaw(int(os.Stdout.Fd()))
if nil != err {
return err
}
r.restoreState = state
fmt.Printf("%s%s%s",
"\033[?1049h", // Enable alternate buffer
"\033[?25l", // Make the cursor invisible
@ -33,7 +44,7 @@ func (r *DefaultRenderer) Deinit() error {
"\033[?1049l", // Disable alternate buffer
"\033[?25h", // Make the cursor visible
)
return nil
return terminal.Restore(int(os.Stdout.Fd()), r.restoreState)
}
func (r *DefaultRenderer) AddDecoration(col, row uint16, content string, frames int) {
@ -47,7 +58,7 @@ func (r *DefaultRenderer) AddDecoration(col, row uint16, content string, frames
}
func (r *DefaultRenderer) tickDecorations() {
nd := []*decoration{}
nd := make([]*decoration, 0, len(r.decorations))
for _, d := range r.decorations {
if d.Frames == 0 {
r.Fill(d.Y, d.X, " ")
@ -59,7 +70,11 @@ func (r *DefaultRenderer) tickDecorations() {
r.decorations = nd
}
func (r *DefaultRenderer) RenderLoop(delay time.Duration, render func(startTime time.Time, duration time.Duration) bool) {
func (r *DefaultRenderer) RenderLoop(
delay time.Duration,
render func(startTime time.Time, duration time.Duration) bool,
endRender func(renderDuration time.Duration),
) {
cont := true
startTime := time.Now().Add(delay)
for cont {
@ -72,20 +87,37 @@ func (r *DefaultRenderer) RenderLoop(delay time.Duration, render func(startTime
r.tickDecorations()
r.flush()
endRender(time.Now().Sub(now))
remainingTime := deadline.Sub(time.Now())
time.Sleep(remainingTime)
}
}
func (r *DefaultRenderer) Fill(row, column uint16, message string) {
r.buffer += fmt.Sprintf("\033[%d;%dH%v", row, column, message)
r.buffer.WriteString("\033[")
r.buffer.WriteString(strconv.FormatInt(int64(row), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(column), 10))
r.buffer.WriteString("H")
r.buffer.WriteString(message)
}
func (r *DefaultRenderer) FillColor(row, column uint16, c color.RGBA, message string) {
r.buffer += fmt.Sprintf("\033[%d;%dH\033[38;2;%v;%v;%vm%v\033[0m", row, column, c.R, c.G, c.B, message)
r.buffer.WriteString("\033[")
r.buffer.WriteString(strconv.FormatInt(int64(row), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(column), 10))
r.buffer.WriteString("H\033[38;2;")
r.buffer.WriteString(strconv.FormatInt(int64(c.R), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(c.G), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(c.B), 10))
r.buffer.WriteString(message)
r.buffer.WriteString("\033[0m")
}
func (r *DefaultRenderer) flush() {
fmt.Print(r.buffer)
r.buffer = ""
os.Stdout.Write([]byte(r.buffer.String()))
r.buffer.Reset()
}

8
internal/render/renderer.go

@ -9,7 +9,13 @@ type Renderer interface {
Init() error
Deinit() error
AddDecoration(col, row uint16, content string, frames int)
RenderLoop(delay time.Duration, render func(startTime time.Time, duration time.Duration) bool)
RenderLoop(
delay time.Duration,
render func(startTime time.Time, duration time.Duration) bool,
// Ran after rendering all the ui, this will render again, so don't do much
// but print debug info
endRender func(renderDuration time.Duration),
)
Fill(row, column uint16, message string)
FillColor(row, column uint16, color color.RGBA, message string)
}

333
main.go

@ -10,9 +10,11 @@ import (
"log"
"math"
"os"
"os/signal"
"path"
"path/filepath"
"runtime/pprof"
"strconv"
"time"
"git.lost.host/meutraa/eott/internal/config"
@ -33,15 +35,6 @@ import (
func main() {
config.Init()
if *config.CpuProfile != "" {
f, err := os.Create(*config.CpuProfile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
if err := run(); nil != err {
log.Fatalln(err)
}
@ -74,6 +67,12 @@ func run() error {
var psr parser.Parser = &parser.DefaultParser{}
var scorer score.Scorer = &score.DefaultScorer{}
var totalFrameCounter, totalRenderDuration uint64 = 0, 0
quitChannel := make(chan os.Signal, 1)
signal.Notify(quitChannel, os.Interrupt)
// Get the dimensions of the terminal
_columns, _rows, err := term.GetSize(int(os.Stdout.Fd()))
if nil != err {
return fmt.Errorf("unable to get terminal size: %w", err)
@ -82,6 +81,7 @@ func run() error {
middleColumn, middleRow := columnCount>>1, totalRows>>1
hitRow := totalRows - *config.BarOffsetFromBottom
// Start reading keyboard input
in := make(chan *input.Event, 128)
input.ReadInput(*config.Input, in)
@ -168,7 +168,10 @@ func run() error {
if err != nil {
return err
}
defer streamer.Close()
buffer := beep.NewBuffer(format)
buffer.Append(streamer)
streamer.Close()
speaker.Init(beep.SampleRate(math.Round(0.01*float64(format.SampleRate)*float64(*config.Rate))), format.SampleRate.N(time.Second/60))
@ -183,6 +186,7 @@ func run() error {
if sideCol < 2 {
sideCol = 2
}
sideColData := 14 + sideCol
distanceError, sumOfDistance := time.Millisecond, time.Millisecond
counts := make([]int, len(config.Judgements))
@ -195,6 +199,14 @@ func run() error {
go func() {
time.Sleep(*config.Delay + *config.Offset)
speaker.Play(streamer)
song := buffer.Streamer(0, buffer.Len())
speaker.Play(song)
for {
if song.Position() == song.Len() {
quitChannel <- os.Interrupt
}
time.Sleep(time.Second)
}
}()
// Render the hit bar
@ -202,166 +214,197 @@ func run() error {
r.Fill(totalRows-*config.BarOffsetFromBottom, getColumn(chart.Difficulty.NKeys, middleColumn, i), th.RenderHitField(i))
}
lastNote := chart.Notes[len(chart.Notes)-1]
r.RenderLoop(*config.Delay, func(startTime time.Time, duration time.Duration) bool {
// This fails if the last note is not meant to be hit
if lastNote.Miss || lastNote.HitTime != 0 {
finished = true
return false
// Render the static stat ui
r.Fill(3, sideCol, " Render: ")
r.Fill(10, sideCol, " Error dt: ")
r.Fill(11, sideCol, " Stdev: ")
r.Fill(12, sideCol, " Mean: ")
r.Fill(13, sideCol, fmt.Sprintf(" Total: %6v", chart.NoteCount))
r.Fill(14, sideCol, fmt.Sprintf(" Mines: %6v", chart.MineCount))
for i, judgement := range config.Judgements {
r.Fill(uint16(18+i), sideCol, judgement.Name+": ")
}
// I do not care about all the above, it is instantish
if *config.CpuProfile != "" {
f, err := os.Create(*config.CpuProfile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
// get the key inputs that occured so far
for i := 0; i < len(in); i++ {
ev := <-in
if ev.Code == C.KEY_ESC {
r.RenderLoop(*config.Delay,
func(startTime time.Time, duration time.Duration) bool {
// This fails if the last note is not meant to be hit
if len(quitChannel) != 0 {
finished = true
return false
}
// Which index was hit
if ev.Released {
continue
}
index, err := config.KeyColumn(ev.Code, chart.Difficulty.NKeys)
if nil != err {
continue
}
input := game.Input{
Index: index,
HitTime: time.Unix(0, ev.Time.Nano()).Sub(startTime),
}
// get the key inputs that occured so far
for i := 0; i < len(in); i++ {
ev := <-in
if ev.Code == C.KEY_ESC {
return false
}
inputs = append(inputs, input)
// Get the column to render the hit splash at
col := getColumn(chart.Difficulty.NKeys, middleColumn, input.Index)
r.AddDecoration(col, totalRows-*config.BarOffsetFromBottom, "*", 120)
// Which index was hit
if ev.Released {
continue
}
index, err := config.KeyColumn(ev.Code, chart.Difficulty.NKeys)
if nil != err {
continue
}
input := game.Input{
Index: index,
HitTime: time.Unix(0, ev.Time.Nano()).Sub(startTime),
}
note, distance, abs := scorer.ApplyInputToChart(chart, &input, *config.Rate)
if note == nil {
continue
}
inputs = append(inputs, input)
// Get the column to render the hit splash at
col := getColumn(chart.Difficulty.NKeys, middleColumn, input.Index)
r.AddDecoration(col, totalRows-*config.BarOffsetFromBottom, "*", 120)
note, distance, abs := scorer.ApplyInputToChart(chart, &input, *config.Rate)
if note == nil {
continue
}
distanceError += abs
totalHits += 1
sumOfDistance += distance
// because distance is < missDistance, this should never be nil
idx, judgement := judge(abs)
r.AddDecoration(middleColumn, middleRow, judgement.Name, 120)
counts[idx]++
if totalHits > 1 {
stdev = 0.0
mean = float64(sumOfDistance) / float64(totalHits)
for _, n := range chart.Notes {
if n.HitTime == 0 {
continue
distanceError += abs
totalHits += 1
sumOfDistance += distance
// because distance is < missDistance, this should never be nil
idx, judgement := judge(abs)
r.AddDecoration(middleColumn, middleRow, judgement.Name, 120)
counts[idx]++
if totalHits > 1 {
stdev = 0.0
mean = float64(sumOfDistance) / float64(totalHits)
for _, n := range chart.Notes {
if n.HitTime == 0 {
continue
}
diff := scorer.Distance(*config.Rate, n.Time, n.HitTime)
xi := float64(diff) - mean
xi2 := xi * xi
stdev += xi2
}
diff := scorer.Distance(*config.Rate, n.Time, n.HitTime)
xi := float64(diff) - mean
xi2 := xi * xi
stdev += xi2
stdev /= float64(totalHits - 1)
stdev = math.Sqrt(stdev)
}
stdev /= float64(totalHits - 1)
stdev = math.Sqrt(stdev)
// Update stats
r.Fill(10, sideColData, fmt.Sprintf("%6.0f ms", float64(distanceError)/float64(time.Millisecond)))
r.Fill(11, sideColData, fmt.Sprintf("%6.2f ms", stdev/float64(time.Millisecond)))
r.Fill(12, sideColData, fmt.Sprintf("%6.2f ms", mean/float64(time.Millisecond)))
r.Fill(uint16(18+idx), sideColData, fmt.Sprintf("%6v", counts[idx]))
}
}
// Adjust the active note range
// The first time this is called, the active slice is empty
// and start, end = 0, 0
active, start, end := chart.Active()
startOffset := 0
endOffset := 0
// Render notes
for _, note := range active {
// Clear all existing notes
col := getColumn(chart.Difficulty.NKeys, middleColumn, note.Index)
// rowCount = 60 = bottom of screen
// BarRow = 8 = 8 from bottom of screen
// nr = hitRow
// Calculate the new row based on time
// This is the main use of the Distance function
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// Check if this note will be rendered
if rowOffsetFromHitRow > int64(hitRow) {
// This is too far in the future and off the top of the screen
log.Fatalln("Active note should not be active: top")
} else if rowOffsetFromHitRow < -int64(*config.BarOffsetFromBottom) {
// This is scrolled past the bottom of the screen
// Mark the active window to slide forward 1
startOffset++
// Mark the render loop to clear this note
r.Fill(note.Row, col, " ")
// TODO: probably do not need this anymore
note.Row = math.MaxUint16
} else {
// This is still an active note
renderRow := hitRow - uint16(rowOffsetFromHitRow)
// Only if this has changed position do we clear and render anew
if note.Row != renderRow && note.HitTime == 0 {
// TODO: there might be an optimization here
// Adjust the active note range
// The first time this is called, the active slice is empty
// and start, end = 0, 0
active, start, end := chart.Active()
startOffset := 0
endOffset := 0
// Render notes
for _, note := range active {
col := getColumn(chart.Difficulty.NKeys, middleColumn, note.Index)
// rowCount = 60 = bottom of screen
// BarRow = 8 = 8 from bottom of screen
// nr = hitRow
// Calculate the new row based on time
// This is the main use of the Distance function
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// Check if this note will be rendered
if rowOffsetFromHitRow > int64(hitRow) {
// This is too far in the future and off the top of the screen
log.Fatalln("Active note should not be active: top")
} else if rowOffsetFromHitRow < -int64(*config.BarOffsetFromBottom) {
// This is scrolled past the bottom of the screen
// Check to see if the note was missed
if note.HitTime == 0 && !note.IsMine {
eidx := len(counts) - 1
counts[eidx] += 1
r.Fill(uint16(18+eidx), sideColData, fmt.Sprintf("%6v", counts[eidx]))
r.AddDecoration(col-1, middleRow-1, "\033[1;31m╭", 240)
r.AddDecoration(col+1, middleRow-1, "\033[1;31m╮", 240)
r.AddDecoration(col-1, middleRow, "\033[1;31m╰", 240)
r.AddDecoration(col+1, middleRow, "\033[1;31m╯", 240)
}
// Mark the active window to slide forward 1
startOffset++
// Mark the render loop to clear this note
r.Fill(note.Row, col, " ")
note.Row = renderRow
if note.IsMine {
r.Fill(note.Row, col, th.RenderMine(col, note.Denom))
} else {
r.Fill(note.Row, col, th.RenderNote(col, note.Denom))
// TODO: probably do not need this anymore
note.Row = math.MaxUint16
} else {
// This is still an active note
renderRow := hitRow - uint16(rowOffsetFromHitRow)
// Only if this has changed position do we clear and render anew
if note.Row != renderRow && note.HitTime == 0 {
// TODO: there might be an optimization here
r.Fill(note.Row, col, " ")
note.Row = renderRow
if note.IsMine {
r.Fill(note.Row, col, th.RenderMine(col, note.Denom))
} else {
r.Fill(note.Row, col, th.RenderNote(col, note.Denom))
}
}
}
}
// Is this row within the playing field?
if !note.Miss && note.HitTime == 0 && !note.IsMine && d < -config.Judgements[len(config.Judgements)-2].Time {
counts[len(counts)-1] += 1
note.Miss = true
r.AddDecoration(col-1, middleRow-1, "\033[1;31m╭", 240)
r.AddDecoration(col+1, middleRow-1, "\033[1;31m╮", 240)
r.AddDecoration(col-1, middleRow, "\033[1;31m╰", 240)
r.AddDecoration(col+1, middleRow, "\033[1;31m╯", 240)
}
}
// At the end of this render loop I want to see which notes will require rendering next frame and slide the window
for _, note := range chart.Notes[end:] {
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// At the end of this render loop I want to see which notes will require rendering next frame and slide the window
for _, note := range chart.Notes[end:] {
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// Check if this note will be rendered
if rowOffsetFromHitRow < int64(hitRow) {
endOffset++
} else {
break
// Check if this note will be rendered
if rowOffsetFromHitRow < int64(hitRow) {
endOffset++
} else {
break
}
}
}
// Update the sliding window
chart.SetActive(start+startOffset, end+endOffset)
r.Fill(10, sideCol, fmt.Sprintf(" Error dt: %6v", distanceError))
r.Fill(11, sideCol, fmt.Sprintf(" Stdev: %6.2f", stdev))
r.Fill(12, sideCol, fmt.Sprintf(" Mean: %6.2f", mean))
r.Fill(13, sideCol, fmt.Sprintf(" Total: %6v", chart.NoteCount))
r.Fill(14, sideCol, fmt.Sprintf(" Mines: %6v", chart.MineCount))
// r.Fill(15, sideCol, fmt.Sprintf(" Window: %v - %v (%v)", start, end, len(active)))
for i, judgement := range config.Judgements {
r.Fill(uint16(18+i), sideCol, fmt.Sprintf("%v: %6v", judgement.Name, counts[i]))
}
// Update the sliding window
chart.SetActive(start+startOffset, end+endOffset)
return true
},
func(renderDuration time.Duration) {
totalRenderDuration += uint64(renderDuration)
totalFrameCounter++
if totalFrameCounter%uint64(*config.DebugUpdateRate) != 0 {
return
}
return true
})
// Print debugging stats
active, start, end := chart.Active()
r.Fill(2, sideCol, fmt.Sprintf(" Window: %v - %v (%v)", start, end, len(active)))
r.Fill(3, sideColData, strconv.FormatUint(totalRenderDuration/totalFrameCounter, 10)+" ")
},
)
if finished {
if finished && len(quitChannel) == 0 {
scorer.Save(chart, &inputs, *config.Rate)
log.Println("saved")
_, _ = <-in, <-in
}
_, _ = <-in, <-in
return nil
}
Loading…
Cancel
Save