Skip to content

Commit

Permalink
internal/driver: free dirty textures as much as possible in a frame
Browse files Browse the repository at this point in the history
  • Loading branch information
changkun authored and andydotxyz committed Oct 18, 2021
1 parent ed29f48 commit da91097
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 16 deletions.
40 changes: 25 additions & 15 deletions internal/driver/common/canvas.go
Expand Up @@ -46,6 +46,7 @@ type Canvas struct {
// the refreshQueue is an unbounded channel which is bale to cache
// arbitrary number of fyne.CanvasObject for the rendering.
refreshQueue *async.UnboundedCanvasObjectChan
refreshCount uint64 // atomic
dirty uint32 // atomic

mWindowHeadTree, contentTree, menuTree *renderCacheTree
Expand Down Expand Up @@ -192,25 +193,33 @@ func (c *Canvas) FocusPrevious() {
mgr.FocusPrevious()
}

// FreeDirtyTextures frees dirty textures.
func (c *Canvas) FreeDirtyTextures() bool {
freed := false
for {
select {
case object := <-c.refreshQueue.Out():
freed = true
freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
// FreeDirtyTextures frees dirty textures and returns the number of freed textures.
func (c *Canvas) FreeDirtyTextures() uint64 {
freed := uint64(0)

// Within a frame, refresh tasks are requested from the Refresh method,
// and we desire to process all requested operations as much as possible
// in a frame. Use a counter to guarantee that all desired tasks are
// processed. See https://github.com/fyne-io/fyne/issues/2548.
for atomic.LoadUint64(&c.refreshCount) > 0 {
object := <-c.refreshQueue.Out()
atomic.AddUint64(&c.refreshCount, ^uint64(0))
freed++
freeWalked := func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool {
if c.painter != nil {
c.painter.Free(obj)
return false
}
driver.WalkCompleteObjectTree(object, freeWalked, nil)
default:
cache.RangeExpiredTexturesFor(c.impl, func(obj fyne.CanvasObject) {
c.painter.Free(obj)
})
return freed
return false
}
driver.WalkCompleteObjectTree(object, freeWalked, nil)
}

cache.RangeExpiredTexturesFor(c.impl, func(obj fyne.CanvasObject) {
if c.painter != nil {
c.painter.Free(obj)
}
})
return freed
}

// Initialize initializes the canvas.
Expand Down Expand Up @@ -260,6 +269,7 @@ func (c *Canvas) Painter() gl.Painter {

// Refresh refreshes a canvas object.
func (c *Canvas) Refresh(obj fyne.CanvasObject) {
atomic.AddUint64(&c.refreshCount, 1)
c.refreshQueue.In() <- obj // never block
c.SetDirty(true)
}
Expand Down
30 changes: 30 additions & 0 deletions internal/driver/common/canvas_test.go
@@ -1,6 +1,7 @@
package common

import (
"errors"
"image/color"
"testing"

Expand Down Expand Up @@ -373,3 +374,32 @@ func Prepend(c *fyne.Container, object fyne.CanvasObject) {
c.Objects = append([]fyne.CanvasObject{object}, c.Objects...)
c.Refresh()
}

func TestRefreshCount(t *testing.T) { // Issue 2548.
var (
c = &Canvas{}
errCh = make(chan error)
freed uint64 = 0
refresh uint64 = 1000
)
c.Initialize(nil, func() {})
for i := uint64(0); i < refresh; i++ {
c.Refresh(canvas.NewRectangle(color.Gray16{Y: 1}))
}

go func() {
freed = c.FreeDirtyTextures()
if freed == 0 {
errCh <- errors.New("expected to free dirty textures but actually not freed")
return
}
errCh <- nil
}()
err := <-errCh
if err != nil {
t.Fatal(err)
}
if freed != refresh {
t.Fatalf("FreeDirtyTextures left refresh tasks behind in a frame, got %v, want %v", freed, refresh)
}
}
2 changes: 1 addition & 1 deletion internal/driver/mobile/driver.go
Expand Up @@ -249,7 +249,7 @@ func (d *mobileDriver) handlePaint(e paint.Event, w fyne.Window) {
c.Painter().Init() // we cannot init until the context is set above
}

canvasNeedRefresh := c.FreeDirtyTextures() || c.IsDirty()
canvasNeedRefresh := c.FreeDirtyTextures() > 0 || c.IsDirty()
if canvasNeedRefresh {
newSize := fyne.NewSize(float32(d.currentSize.WidthPx)/c.scale, float32(d.currentSize.HeightPx)/c.scale)

Expand Down

0 comments on commit da91097

Please sign in to comment.