Skip to content

Commit

Permalink
Optimized paint request and throttling algorithm
Browse files Browse the repository at this point in the history
Previously a repaint was requested through method Paint() and was
synchronous. Frame throttling was computed using time.Now(), which is a
relatively expensive call. It also led to a bug were a dropped frame
would not be redrawn if repaint wasn't explicitly requested after the
frame had been dropped.

Now tuikit.Init() returns a sending channel. The client writes to the
channel to request a repaint. This decouples painting from application
logic and allows for a more fine-grained throttling algorithm. As a
side-effect the bug mentioned earlier is fixed and there isn't any call
to time.Now() anymore.

The examples were also adjusted to use the new channel.
  • Loading branch information
sgeb committed Jun 8, 2014
1 parent 27d0421 commit e6eb5fa
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 27 deletions.
6 changes: 2 additions & 4 deletions examples/databinding/databinding.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ func main() {
fmt.Fprintln(os.Stderr, http.ListenAndServe("0.0.0.0:6060", nil))
}()

repaint := make(chan struct{}, 1)
quit := make(chan struct{}, 1)

if err := tuikit.Init(); err != nil {
repaint, err := tuikit.Init()
if err != nil {
panic(err)
}
defer tuikit.Close()
Expand All @@ -54,8 +54,6 @@ func main() {
if ev.Ch == 'q' {
quit <- struct{}{}
}
case <-repaint:
tuikit.Paint()
case <-quit:
return
}
Expand Down
6 changes: 2 additions & 4 deletions examples/widgets/widgets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ func main() {
fmt.Fprintln(os.Stderr, http.ListenAndServe("0.0.0.0:6060", nil))
}()

repaint := make(chan struct{}, 1)
quit := make(chan struct{}, 1)

if err := tuikit.Init(); err != nil {
repaint, err := tuikit.Init()
if err != nil {
panic(err)
}
defer tuikit.Close()
Expand All @@ -50,8 +50,6 @@ func main() {
case ev.Ch == 'q' || ev.Key == termbox.KeyCtrlQ:
quit <- struct{}{}
}
case <-repaint:
tuikit.Paint()
case <-quit:
return
}
Expand Down
59 changes: 40 additions & 19 deletions tuikit/tuikit.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type Event struct {
}

const (
MaxFps = 40
MaxFps = 40
frameInterval = time.Second / MaxFps
)

var (
Expand All @@ -40,29 +41,35 @@ var (
// Event polling channel
Events chan Event = make(chan Event, 20)

// Paint channel. Clients write to it to request repaint
paintChan chan struct{} = make(chan struct{}, 1)

// Controls event polling
internalEvents chan termbox.Event = make(chan termbox.Event, 20)
stopPolling chan struct{} = make(chan struct{}, 1)

// Lock on screen drawing
mutex sync.Mutex

fpsCounter *FpsCounter
paintTimeT0 time.Time
paintTimeT1 time.Time
framesSkipped uint64
errFrameSkip = fmt.Errorf("Above %v FPS, skipping frame", MaxFps)
fpsCounter *FpsCounter
fpsSkipped uint
shouldSkipFrame bool
didSkipFrame bool

errFrameSkip = fmt.Errorf("Above %v FPS, skipping frame", MaxFps)
)

func Init() error {
err := termbox.Init()
func Init() (paint chan<- struct{}, err error) {
err = termbox.Init()
if err != nil {
return fmt.Errorf("Could not init terminal: %v", err)
err = fmt.Errorf("Could not init terminal: %v", err)
return
}

err = clearWithDefaultColors()
if err != nil {
return fmt.Errorf("Could not clear terminal: %v", err)
err = fmt.Errorf("Could not clear terminal: %v", err)
return
}
termbox.SetInputMode(termbox.InputAlt)
hideCursor()
Expand All @@ -73,12 +80,19 @@ func Init() error {
fpsCounter = NewFpsCounter(time.Second)
go func() {
for fps := range fpsCounter.Fps {
log.Debug.Printf("FPS: %v (%v frames skipped)", fps, framesSkipped)
framesSkipped = 0
log.Debug.Printf("FPS: %v (%v frames skipped)", fps, fpsSkipped)
fpsSkipped = 0
}
}()

return nil
go func() {
for _ = range paintChan {
paintThrottled()
}
}()

paint = paintChan
return
}

func internalEventProxying() {
Expand Down Expand Up @@ -133,13 +147,22 @@ func SetFirstResponder(eh Responder) {
firstResponder = eh
}

func Paint() error {
paintTimeT1 = time.Now()
if paintTimeT1.Sub(paintTimeT0) < time.Second/MaxFps {
framesSkipped++
func paintThrottled() error {
if shouldSkipFrame {
didSkipFrame = true
fpsSkipped++
return errFrameSkip
}

shouldSkipFrame = true
time.AfterFunc(frameInterval, func() {
shouldSkipFrame = false
if didSkipFrame {
didSkipFrame = false
paintChan <- struct{}{}
}
})

return paintForced()
}

Expand All @@ -159,8 +182,6 @@ func paintForced() error {
}

fpsCounter.Ticks <- struct{}{}
paintTimeT0 = paintTimeT1

return nil
}

Expand Down

0 comments on commit e6eb5fa

Please sign in to comment.