Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert using a separate draw thread #4656

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/animation/animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"fyne.io/fyne/v2"
)

func tick(run *Runner) {
time.Sleep(time.Millisecond * 5) // wait long enough that we are not at 0 time
run.TickAnimations()
}

func TestGLDriver_StartAnimation(t *testing.T) {
done := make(chan float32)
run := &Runner{}
Expand All @@ -22,6 +27,7 @@ func TestGLDriver_StartAnimation(t *testing.T) {
}}

run.Start(a)
go tick(run) // simulate a graphics draw loop
select {
case d := <-done:
assert.Greater(t, d, float32(0))
Expand All @@ -40,6 +46,7 @@ func TestGLDriver_StopAnimation(t *testing.T) {
}}

run.Start(a)
go tick(run) // simulate a graphics draw loop
select {
case d := <-done:
assert.Greater(t, d, float32(0))
Expand All @@ -63,8 +70,13 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Tick: func(f float32) {},
}
run.Start(a)
go tick(run) // simulate a graphics draw loop
run.Stop(a)

run.animationMutex.RLock()
assert.Zero(t, len(run.animations))
run.animationMutex.RUnlock()

// stopping animation inside tick function
for i := 0; i < 10; i++ {
wg.Add(1)
Expand All @@ -85,7 +97,10 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Tick: func(f float32) {},
}
run.Start(c)
go tick(run) // simulate a graphics draw loop

run.Stop(c)
go tick(run) // simulate a graphics draw loop

wg.Wait()
// animations stopped inside tick are really stopped in the next runner cycle
Expand Down
47 changes: 24 additions & 23 deletions internal/animation/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ func (r *Runner) Start(a *fyne.Animation) {
if !r.runnerStarted {
r.runnerStarted = true
r.animations = append(r.animations, newAnim(a))
r.runAnimations()
} else {
r.pendingAnimations = append(r.pendingAnimations, newAnim(a))
}
Expand Down Expand Up @@ -61,32 +60,34 @@ func (r *Runner) Stop(a *fyne.Animation) {
r.pendingAnimations = newList
}

func (r *Runner) runAnimations() {
draw := time.NewTicker(time.Second / 60)

go func() {
for done := false; !done; {
<-draw.C
r.animationMutex.Lock()
oldList := r.animations
r.animationMutex.Unlock()
newList := make([]*anim, 0, len(oldList))
for _, a := range oldList {
if !a.isStopped() && r.tickAnimation(a) {
newList = append(newList, a)
}
}
r.animationMutex.Lock()
r.animations = append(newList, r.pendingAnimations...)
r.pendingAnimations = nil
done = len(r.animations) == 0
r.animationMutex.Unlock()
// TickAnimations progresses all running animations by one tick.
// This will be called from the driver to update objects immediately before next paint.
func (r *Runner) TickAnimations() {
if !r.runnerStarted {
return
}

done := false
r.animationMutex.Lock()
oldList := r.animations
r.animationMutex.Unlock()
newList := make([]*anim, 0, len(oldList))
for _, a := range oldList {
if !a.isStopped() && r.tickAnimation(a) {
newList = append(newList, a)
}
}
r.animationMutex.Lock()
r.animations = append(newList, r.pendingAnimations...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The runner can get a whole lot cleaner once we assume the new threading model. While Start and Stop can still be called when invoking user code within a Tick, we will know they won't be called elsewhere, so we can do some simplification and cleanup around the synchronization between Start, Stop and Tick. That can be a follow-up PR though.

r.pendingAnimations = nil
done = len(r.animations) == 0
r.animationMutex.Unlock()

if done {
r.animationMutex.Lock()
r.runnerStarted = false
r.animationMutex.Unlock()
draw.Stop()
}()
}
}

// tickAnimation will process a frame of animation and return true if this should continue animating
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/glfw/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type glCanvas struct {

func (c *glCanvas) Capture() image.Image {
var img image.Image
runOnDraw(c.context.(*window), func() {
runOnMainWithContext(c.context.(*window), func() {
img = c.Painter().Capture(c)
})
return img
Expand Down
5 changes: 0 additions & 5 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,12 @@ var curWindow *window
// Declare conformity with Driver
var _ fyne.Driver = (*gLDriver)(nil)

// A workaround on Apple M1/M2, just use 1 thread until fixed upstream.
const drawOnMainThread bool = runtime.GOOS == "darwin" && runtime.GOARCH == "arm64"

const doubleTapDelay = 300 * time.Millisecond

type gLDriver struct {
windowLock sync.RWMutex
windows []fyne.Window
done chan struct{}
drawDone chan struct{}
waitForStart chan struct{}

animation animation.Runner
Expand Down Expand Up @@ -181,7 +177,6 @@ func NewGLDriver() *gLDriver {

return &gLDriver{
done: make(chan struct{}),
drawDone: make(chan struct{}),
waitForStart: make(chan struct{}),
}
}
2 changes: 1 addition & 1 deletion internal/driver/glfw/glfw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func repaintWindow(w *window) {
// If we try to paint windows before the context is created, we will end up on the wrong thread.
<-w.driver.waitForStart

runOnDraw(w, func() {
runOnMainWithContext(w, func() {
d.repaintWindow(w)
})

Expand Down
79 changes: 20 additions & 59 deletions internal/driver/glfw/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,8 @@ type funcData struct {
done chan struct{} // Zero allocation signalling channel
}

type drawData struct {
f func()
win *window
done chan struct{} // Zero allocation signalling channel
}

// channel for queuing functions on the main thread
var funcQueue = make(chan funcData)
var drawFuncQueue = make(chan drawData)
var running atomic.Bool
var initOnce = &sync.Once{}

Expand Down Expand Up @@ -55,16 +48,8 @@ func runOnMain(f func()) {
}

// force a function f to run on the draw thread
func runOnDraw(w *window, f func()) {
if drawOnMainThread {
runOnMain(func() { w.RunWithContext(f) })
return
}
done := common.DonePool.Get().(chan struct{})
defer common.DonePool.Put(done)

drawFuncQueue <- drawData{f: f, win: w, done: done}
<-done
func runOnMainWithContext(w *window, f func()) {
runOnMain(func() { w.RunWithContext(f) }) // TODO remove this completely
}

// Preallocate to avoid allocations on every drawSingleFrame.
Expand Down Expand Up @@ -117,12 +102,15 @@ func (d *gLDriver) runGL() {
if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStarted(); f != nil {
go f() // don't block main, we don't have window event queue
}

settingsChange := make(chan fyne.Settings)
fyne.CurrentApp().Settings().AddChangeListener(settingsChange)

eventTick := time.NewTicker(time.Second / 60)
for {
select {
case <-d.done:
eventTick.Stop()
d.drawDone <- struct{}{} // wait for draw thread to stop
d.Terminate()
if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStopped(); f != nil {
go f() // don't block main, we don't have window event queue
Expand Down Expand Up @@ -162,9 +150,8 @@ func (d *gLDriver) runGL() {
}
}

if drawOnMainThread {
d.drawSingleFrame()
}
d.animation.TickAnimations()
d.drawSingleFrame()
}
if windowsToRemove > 0 {
oldWindows := d.windowList()
Expand Down Expand Up @@ -199,6 +186,18 @@ func (d *gLDriver) runGL() {
d.Quit()
}
}
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*glCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
go c.reloadScale()
})

}
}
}
Expand Down Expand Up @@ -227,44 +226,6 @@ func (d *gLDriver) repaintWindow(w *window) {
})
}

func (d *gLDriver) startDrawThread() {
settingsChange := make(chan fyne.Settings)
fyne.CurrentApp().Settings().AddChangeListener(settingsChange)
var drawCh <-chan time.Time
if drawOnMainThread {
drawCh = make(chan time.Time) // don't tick when on M1
} else {
drawCh = time.NewTicker(time.Second / 60).C
}

go func() {
runtime.LockOSThread()

for {
select {
case <-d.drawDone:
return
case f := <-drawFuncQueue:
f.win.RunWithContext(f.f)
f.done <- struct{}{}
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*glCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
go c.reloadScale()
})
case <-drawCh:
d.drawSingleFrame()
}
}
}()
}

// refreshWindow requests that the specified window be redrawn
func refreshWindow(w *window) {
w.canvas.SetDirty()
Expand Down
1 change: 0 additions & 1 deletion internal/driver/glfw/loop_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func (d *gLDriver) initGLFW() {
}

initCursors()
d.startDrawThread()
})
}

Expand Down
6 changes: 2 additions & 4 deletions internal/driver/glfw/loop_goxjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (

"fyne.io/fyne/v2"

gl "github.com/fyne-io/gl-js"
glfw "github.com/fyne-io/glfw-js"
"github.com/fyne-io/gl-js"
"github.com/fyne-io/glfw-js"
)

func (d *gLDriver) initGLFW() {
Expand All @@ -18,8 +18,6 @@ func (d *gLDriver) initGLFW() {
fyne.LogError("failed to initialise GLFW", err)
return
}

d.startDrawThread()
})
}

Expand Down
2 changes: 1 addition & 1 deletion internal/driver/glfw/loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ func BenchmarkRunOnDraw(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
runOnDraw(w, f)
runOnMainWithContext(w, f)
}
}
4 changes: 2 additions & 2 deletions internal/driver/glfw/window.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (w *window) doShow() {
if content := w.canvas.Content(); content != nil {
content.Show()

runOnDraw(w, func() {
runOnMainWithContext(w, func() {
w.driver.repaintWindow(w)
})
}
Expand Down Expand Up @@ -209,7 +209,7 @@ func (w *window) Close() {
}

// set w.closing flag inside draw thread to ensure we can free textures
runOnDraw(w, func() {
runOnMainWithContext(w, func() {
w.viewLock.Lock()
w.closing = true
w.viewLock.Unlock()
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/glfw/window_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ func (w *window) create() {
}

// run the GL init on the draw thread
runOnDraw(w, func() {
runOnMainWithContext(w, func() {
w.canvas.SetPainter(gl.NewPainter(w.canvas, w))
w.canvas.Painter().Init()
})
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/glfw/window_goxjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ func (w *window) create() {
}

// run the GL init on the draw thread
runOnDraw(w, func() {
runOnMainWithContext(w, func() {
w.canvas.SetPainter(gl.NewPainter(w.canvas, w))
w.canvas.Painter().Init()
})
Expand Down
11 changes: 2 additions & 9 deletions internal/driver/glfw/window_notxdg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,8 @@ func (w *window) platformResize(canvasSize fyne.Size) {
return
}

if drawOnMainThread {
w.canvas.Resize(canvasSize)
d.repaintWindow(w)
} else {
runOnDraw(w, func() {
w.canvas.Resize(canvasSize)
d.repaintWindow(w)
})
}
w.canvas.Resize(canvasSize)
d.repaintWindow(w)
}

// GetWindowHandle returns the window handle. Only implemented for X11 currently.
Expand Down
1 change: 1 addition & 0 deletions internal/driver/mobile/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ func (d *mobileDriver) handlePaint(e paint.Event, w fyne.Window) {
c.Painter().Init() // we cannot init until the context is set above
}

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