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"
"math"
"strings"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
@ -36,6 +37,7 @@ type command interface {
fmt.Stringer
Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error
NeedsSync() bool
}
type drawTrianglesCommandPool struct {
@ -72,6 +74,8 @@ type commandQueue struct {
drawTrianglesCommandPool drawTrianglesCommandPool
uint32sBuffer uint32sBuffer
err atomic.Value
}
// 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.
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() {
err = q.flush(graphicsDriver, endFrame)
if err != nil {
if err := q.flush(graphicsDriver, endFrame); err != nil {
if sync {
return
}
q.err.Store(err)
return
}
if endFrame && swapBuffersForGL != nil {
swapBuffersForGL()
}
})
return
}, sync)
if sync && flushErr != nil {
return flushErr
}
return nil
}
// 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)
}
func (c *drawTrianglesCommand) NeedsSync() bool {
return false
}
func (c *drawTrianglesCommand) numVertices() int {
return len(c.vertices)
}
@ -452,6 +481,10 @@ func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexO
return nil
}
func (c *writePixelsCommand) NeedsSync() bool {
return false
}
type readPixelsCommand struct {
result []byte
img *Image
@ -466,6 +499,10 @@ func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOf
return nil
}
func (c *readPixelsCommand) NeedsSync() bool {
return true
}
func (c *readPixelsCommand) String() string {
return fmt.Sprintf("read-pixels: image: %d", c.img.id)
}
@ -485,6 +522,10 @@ func (c *disposeImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, index
return nil
}
func (c *disposeImageCommand) NeedsSync() bool {
return false
}
// disposeShaderCommand represents a command to dispose a shader.
type disposeShaderCommand struct {
target *Shader
@ -500,6 +541,10 @@ func (c *disposeShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
return nil
}
func (c *disposeShaderCommand) NeedsSync() bool {
return false
}
// newImageCommand represents a command to create an empty image with given width and height.
type newImageCommand struct {
result *Image
@ -523,6 +568,10 @@ func (c *newImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffs
return err
}
func (c *newImageCommand) NeedsSync() bool {
return true
}
// newShaderCommand is a command to create a shader.
type newShaderCommand struct {
result *Shader
@ -543,6 +592,10 @@ func (c *newShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOff
return nil
}
func (c *newShaderCommand) NeedsSync() bool {
return true
}
type isInvalidatedCommand struct {
result bool
image *Image
@ -557,11 +610,15 @@ func (c *isInvalidatedCommand) Exec(graphicsDriver graphicsdriver.Graphics, inde
return nil
}
func (c *isInvalidatedCommand) NeedsSync() bool {
return true
}
// InitializeGraphicsDriverState initialize the current graphics driver state.
func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error) {
runOnRenderThread(func() {
err = graphicsDriver.Initialize()
})
}, true)
return
}
@ -571,7 +628,7 @@ func ResetGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error
if r, ok := graphicsDriver.(graphicsdriver.Resetter); ok {
runOnRenderThread(func() {
err = r.Reset()
})
}, true)
}
return nil
}
@ -581,7 +638,7 @@ func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
var size int
runOnRenderThread(func() {
size = graphicsDriver.MaxImageSize()
})
}, true)
return size
}

View File

@ -28,6 +28,14 @@ func SetRenderThread(thread thread.Thread) {
}
// runOnRenderThread calls f on the rendering thread.
func runOnRenderThread(f func()) {
theRenderThread.Call(f)
func runOnRenderThread(f func(), sync bool) {
if sync {
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 {
Loop(ctx context.Context) error
Call(f func())
CallAsync(f func())
private()
}
type queueItem struct {
f func()
sync bool
}
// OSThread represents an OS thread.
type OSThread struct {
funcs chan func()
funcs chan queueItem
done chan struct{}
}
// NewOSThread creates a new thread.
//
// queueSize indicates the function queue size. This matters when you use CallAsync.
func NewOSThread() *OSThread {
return &OSThread{
funcs: make(chan func()),
funcs: make(chan queueItem),
done: make(chan struct{}),
}
}
// 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 {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
for {
select {
case fn := <-t.funcs:
case item := <-t.funcs:
func() {
defer func() {
t.done <- struct{}{}
}()
fn()
if item.sync {
defer func() {
t.done <- struct{}{}
}()
}
item.f()
}()
case <-ctx.Done():
return ctx.Err()
@ -65,17 +74,26 @@ func (t *OSThread) Loop(ctx context.Context) error {
// 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.
func (t *OSThread) Call(f func()) {
t.funcs <- f
t.funcs <- queueItem{f: f, sync: true}
<-t.done
}
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.
type NoopThread struct{}
@ -94,5 +112,10 @@ func (t *NoopThread) Call(f func()) {
f()
}
// CallAsync executes the func immediately.
func (t *NoopThread) CallAsync(f func()) {
f()
}
func (t *NoopThread) private() {
}

View File

@ -1037,9 +1037,14 @@ func (u *userInterfaceImpl) update() (float64, float64, error) {
}
func (u *userInterfaceImpl) loopGame() error {
defer u.mainThread.Call(func() {
glfw.Terminate()
})
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()
})
}()
u.renderThread.Call(func() {
if u.graphicsDriver.IsGL() {