internal/graphicsdriver: flush commands asynchronously whenever possible

Closes #2664
This commit is contained in:
Hajime Hoshi 2023-07-30 17:36:50 +09:00
parent a7e4665f71
commit f81dbd9288
4 changed files with 117 additions and 24 deletions

View File

@ -19,6 +19,7 @@ import (
"image" "image"
"math" "math"
"strings" "strings"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/debug" "github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphics"
@ -36,6 +37,7 @@ type command interface {
fmt.Stringer fmt.Stringer
Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error
NeedsSync() bool
} }
type drawTrianglesCommandPool struct { type drawTrianglesCommandPool struct {
@ -72,6 +74,8 @@ type commandQueue struct {
drawTrianglesCommandPool drawTrianglesCommandPool drawTrianglesCommandPool drawTrianglesCommandPool
uint32sBuffer uint32sBuffer uint32sBuffer uint32sBuffer
err atomic.Value
} }
// theCommandQueues is the set of command queues for the current process. // theCommandQueues is the set of command queues for the current process.
@ -179,18 +183,39 @@ func (q *commandQueue) Enqueue(command command) {
} }
// Flush flushes the command queue. // Flush flushes the command queue.
func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bool, swapBuffersForGL func()) (err error) { func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bool, swapBuffersForGL func()) error {
if err := q.err.Load(); err != nil {
return err.(error)
}
var sync bool
for _, c := range q.commands {
if c.NeedsSync() {
sync = true
break
}
}
var flushErr error
runOnRenderThread(func() { runOnRenderThread(func() {
err = q.flush(graphicsDriver, endFrame) if err := q.flush(graphicsDriver, endFrame); err != nil {
if err != nil { if sync {
return
}
q.err.Store(err)
return return
} }
if endFrame && swapBuffersForGL != nil { if endFrame && swapBuffersForGL != nil {
swapBuffersForGL() swapBuffersForGL()
} }
}) }, sync)
return
if sync && flushErr != nil {
return flushErr
}
return nil
} }
// flush must be called the main thread. // flush must be called the main thread.
@ -346,6 +371,10 @@ func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
return graphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.shader.shader.ID(), c.dstRegions, indexOffset, c.blend, c.uniforms, c.evenOdd) return graphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.shader.shader.ID(), c.dstRegions, indexOffset, c.blend, c.uniforms, c.evenOdd)
} }
func (c *drawTrianglesCommand) NeedsSync() bool {
return false
}
func (c *drawTrianglesCommand) numVertices() int { func (c *drawTrianglesCommand) numVertices() int {
return len(c.vertices) return len(c.vertices)
} }
@ -452,6 +481,10 @@ func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexO
return nil return nil
} }
func (c *writePixelsCommand) NeedsSync() bool {
return false
}
type readPixelsCommand struct { type readPixelsCommand struct {
result []byte result []byte
img *Image img *Image
@ -466,6 +499,10 @@ func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOf
return nil return nil
} }
func (c *readPixelsCommand) NeedsSync() bool {
return true
}
func (c *readPixelsCommand) String() string { func (c *readPixelsCommand) String() string {
return fmt.Sprintf("read-pixels: image: %d", c.img.id) return fmt.Sprintf("read-pixels: image: %d", c.img.id)
} }
@ -485,6 +522,10 @@ func (c *disposeImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, index
return nil return nil
} }
func (c *disposeImageCommand) NeedsSync() bool {
return false
}
// disposeShaderCommand represents a command to dispose a shader. // disposeShaderCommand represents a command to dispose a shader.
type disposeShaderCommand struct { type disposeShaderCommand struct {
target *Shader target *Shader
@ -500,6 +541,10 @@ func (c *disposeShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
return nil return nil
} }
func (c *disposeShaderCommand) NeedsSync() bool {
return false
}
// newImageCommand represents a command to create an empty image with given width and height. // newImageCommand represents a command to create an empty image with given width and height.
type newImageCommand struct { type newImageCommand struct {
result *Image result *Image
@ -523,6 +568,10 @@ func (c *newImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffs
return err return err
} }
func (c *newImageCommand) NeedsSync() bool {
return true
}
// newShaderCommand is a command to create a shader. // newShaderCommand is a command to create a shader.
type newShaderCommand struct { type newShaderCommand struct {
result *Shader result *Shader
@ -543,6 +592,10 @@ func (c *newShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOff
return nil return nil
} }
func (c *newShaderCommand) NeedsSync() bool {
return true
}
type isInvalidatedCommand struct { type isInvalidatedCommand struct {
result bool result bool
image *Image image *Image
@ -557,11 +610,15 @@ func (c *isInvalidatedCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
return nil return nil
} }
func (c *isInvalidatedCommand) NeedsSync() bool {
return true
}
// InitializeGraphicsDriverState initialize the current graphics driver state. // InitializeGraphicsDriverState initialize the current graphics driver state.
func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error) { func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error) {
runOnRenderThread(func() { runOnRenderThread(func() {
err = graphicsDriver.Initialize() err = graphicsDriver.Initialize()
}) }, true)
return return
} }
@ -571,7 +628,7 @@ func ResetGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error
if r, ok := graphicsDriver.(graphicsdriver.Resetter); ok { if r, ok := graphicsDriver.(graphicsdriver.Resetter); ok {
runOnRenderThread(func() { runOnRenderThread(func() {
err = r.Reset() err = r.Reset()
}) }, true)
} }
return nil return nil
} }
@ -581,7 +638,7 @@ func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
var size int var size int
runOnRenderThread(func() { runOnRenderThread(func() {
size = graphicsDriver.MaxImageSize() size = graphicsDriver.MaxImageSize()
}) }, true)
return size return size
} }

View File

@ -28,6 +28,14 @@ func SetRenderThread(thread thread.Thread) {
} }
// runOnRenderThread calls f on the rendering thread. // runOnRenderThread calls f on the rendering thread.
func runOnRenderThread(f func()) { func runOnRenderThread(f func(), sync bool) {
if sync {
theRenderThread.Call(f) theRenderThread.Call(f)
return
}
// As the current thread doesn't have a capacity in a channel,
// CallAsync should block when the previously-queued task is not executed yet.
// This blocking is expected as double-buffering is used.
theRenderThread.CallAsync(f)
} }

View File

@ -22,40 +22,49 @@ import (
type Thread interface { type Thread interface {
Loop(ctx context.Context) error Loop(ctx context.Context) error
Call(f func()) Call(f func())
CallAsync(f func())
private() private()
} }
type queueItem struct {
f func()
sync bool
}
// OSThread represents an OS thread. // OSThread represents an OS thread.
type OSThread struct { type OSThread struct {
funcs chan func() funcs chan queueItem
done chan struct{} done chan struct{}
} }
// NewOSThread creates a new thread. // NewOSThread creates a new thread.
//
// queueSize indicates the function queue size. This matters when you use CallAsync.
func NewOSThread() *OSThread { func NewOSThread() *OSThread {
return &OSThread{ return &OSThread{
funcs: make(chan func()), funcs: make(chan queueItem),
done: make(chan struct{}), done: make(chan struct{}),
} }
} }
// Loop starts the thread loop until Stop is called on the current OS thread. // Loop starts the thread loop until Stop is called on the current OS thread.
// //
// Loop must be called on the thread. // Loop must be called on the OS thread.
func (t *OSThread) Loop(ctx context.Context) error { func (t *OSThread) Loop(ctx context.Context) error {
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
for { for {
select { select {
case fn := <-t.funcs: case item := <-t.funcs:
func() { func() {
if item.sync {
defer func() { defer func() {
t.done <- struct{}{} t.done <- struct{}{}
}() }()
}
fn() item.f()
}() }()
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
@ -65,17 +74,26 @@ func (t *OSThread) Loop(ctx context.Context) error {
// Call calls f on the thread. // Call calls f on the thread.
// //
// Do not call this from the same thread. This would block forever. // Do not call Call from the same thread. Call would block forever.
// //
// Call blocks if Loop is not called. // Call blocks if Loop is not called.
func (t *OSThread) Call(f func()) { func (t *OSThread) Call(f func()) {
t.funcs <- f t.funcs <- queueItem{f: f, sync: true}
<-t.done <-t.done
} }
func (t *OSThread) private() { func (t *OSThread) private() {
} }
// CallAsync tries to queue f.
// CallAsync returns immediately if f can be queued.
// CallAsync blocks if f cannot be queued.
//
// Do not call CallAsync from the same thread. CallAsync would block forever.
func (t *OSThread) CallAsync(f func()) {
t.funcs <- queueItem{f: f, sync: false}
}
// NoopThread is used to disable threading. // NoopThread is used to disable threading.
type NoopThread struct{} type NoopThread struct{}
@ -94,5 +112,10 @@ func (t *NoopThread) Call(f func()) {
f() f()
} }
// CallAsync executes the func immediately.
func (t *NoopThread) CallAsync(f func()) {
f()
}
func (t *NoopThread) private() { func (t *NoopThread) private() {
} }

View File

@ -1037,9 +1037,14 @@ func (u *userInterfaceImpl) update() (float64, float64, error) {
} }
func (u *userInterfaceImpl) loopGame() error { func (u *userInterfaceImpl) loopGame() error {
defer u.mainThread.Call(func() { defer func() {
// Post a task to the render thread to ensure all the queued functions are executed.
// glfw.Terminate will remove the context and any graphics calls after that will be invalidated.
u.renderThread.Call(func() {})
u.mainThread.Call(func() {
glfw.Terminate() glfw.Terminate()
}) })
}()
u.renderThread.Call(func() { u.renderThread.Call(func() {
if u.graphicsDriver.IsGL() { if u.graphicsDriver.IsGL() {