Skip to content

Commit

Permalink
internal/graphicsdriver: flush commands asynchronously whenever possible
Browse files Browse the repository at this point in the history
Closes #2664
  • Loading branch information
hajimehoshi committed Jul 30, 2023
1 parent a7e4665 commit f81dbd9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 24 deletions.
73 changes: 65 additions & 8 deletions internal/graphicscommand/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"image"
"math"
"strings"
"sync/atomic"

"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
Expand All @@ -36,6 +37,7 @@ type command interface {
fmt.Stringer

Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error
NeedsSync() bool
}

type drawTrianglesCommandPool struct {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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
}
Expand All @@ -581,7 +638,7 @@ func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
var size int
runOnRenderThread(func() {
size = graphicsDriver.MaxImageSize()
})
}, true)
return size
}

Expand Down
12 changes: 10 additions & 2 deletions internal/graphicscommand/thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
45 changes: 34 additions & 11 deletions internal/thread/thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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{}

Expand All @@ -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() {
}
11 changes: 8 additions & 3 deletions internal/ui/ui_glfw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit f81dbd9

Please sign in to comment.