shareable: Lock before BeginFrame

This make package shareable more consistent. The mutex is lock
after EndFrame and before BeginFrame, and the similar rule will be
applied at launching (BeginFrame unlocks the lock in any cases).

Instead, package ebiten queues image operations if BeginFrame and
doesn't create provisional non-shared images. This should improve
performance at launching since this reduces the number of draw
calls, especifally for creating new images.

Updates #879.
Updates #921.
This commit is contained in:
Hajime Hoshi 2019-08-24 01:06:14 +09:00
parent 7907bb43ce
commit d2312f1450
3 changed files with 95 additions and 21 deletions

View File

@ -19,6 +19,7 @@ import (
"image" "image"
"image/color" "image/color"
"math" "math"
"sync"
"sync/atomic" "sync/atomic"
"github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/driver"
@ -26,6 +27,31 @@ import (
"github.com/hajimehoshi/ebiten/internal/shareable" "github.com/hajimehoshi/ebiten/internal/shareable"
) )
var (
// imageQueue represents a queue for image operations that are ordered before the game starts (BeginFrame).
// Before the game starts, the package shareable doesn't determine the minimum/maximum texture sizes (#879).
// Instead of accessing the package shareable, defer the image operations until the game starts (#921).
imageQueue []func()
imageQueueM sync.Mutex
)
func enqueueImageOp(f func()) {
imageQueueM.Lock()
defer imageQueueM.Unlock()
imageQueue = append(imageQueue, f)
}
func flushImageOps() {
imageQueueM.Lock()
defer imageQueueM.Unlock()
for _, f := range imageQueue {
f()
}
imageQueue = nil
}
// Image represents a rectangle set of pixels. // Image represents a rectangle set of pixels.
// The pixel format is alpha-premultiplied RGBA. // The pixel format is alpha-premultiplied RGBA.
// Image implements image.Image and draw.Image. // Image implements image.Image and draw.Image.
@ -85,6 +111,20 @@ func (i *Image) Clear() error {
// Fill always returns nil as of 1.5.0-alpha. // Fill always returns nil as of 1.5.0-alpha.
func (i *Image) Fill(clr color.Color) error { func (i *Image) Fill(clr color.Color) error {
i.copyCheck() i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
r, g, b, a := clr.RGBA()
i.Fill(color.RGBA64{
R: uint16(r),
G: uint16(g),
B: uint16(b),
A: uint16(a),
})
})
return nil
}
if i.isDisposed() { if i.isDisposed() {
return nil return nil
} }
@ -147,6 +187,15 @@ func (i *Image) disposeMipmaps() {
// DrawImage always returns nil as of 1.5.0-alpha. // DrawImage always returns nil as of 1.5.0-alpha.
func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error { func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
i.copyCheck() i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
op := *options
i.DrawImage(img, &op)
})
return nil
}
if img.isDisposed() { if img.isDisposed() {
panic("ebiten: the given image to DrawImage must not be disposed") panic("ebiten: the given image to DrawImage must not be disposed")
} }
@ -357,6 +406,19 @@ const MaxIndicesNum = graphics.IndicesNum
// Note that this API is experimental. // Note that this API is experimental.
func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions) { func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions) {
i.copyCheck() i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
vs := make([]Vertex, len(vertices))
copy(vs, vertices)
is := make([]uint16, len(indices))
copy(is, indices)
op := *options
i.DrawTriangles(vs, is, img, &op)
})
return
}
if i.isDisposed() { if i.isDisposed() {
return return
} }
@ -556,6 +618,14 @@ func (i *Image) resolvePendingPixels(draw bool) {
// Dipose always return nil as of 1.5.0-alpha. // Dipose always return nil as of 1.5.0-alpha.
func (i *Image) Dispose() error { func (i *Image) Dispose() error {
i.copyCheck() i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
i.Dispose()
})
return nil
}
if i.isDisposed() { if i.isDisposed() {
return nil return nil
} }
@ -580,6 +650,16 @@ func (i *Image) Dispose() error {
// ReplacePixels always returns nil as of 1.5.0-alpha. // ReplacePixels always returns nil as of 1.5.0-alpha.
func (i *Image) ReplacePixels(p []byte) error { func (i *Image) ReplacePixels(p []byte) error {
i.copyCheck() i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
px := make([]byte, len(p))
copy(px, p)
i.ReplacePixels(px)
})
return nil
}
if i.isDisposed() { if i.isDisposed() {
return nil return nil
} }
@ -663,6 +743,13 @@ func NewImage(width, height int, filter Filter) (*Image, error) {
// //
// When the image is disposed, makeVolatile does nothing. // When the image is disposed, makeVolatile does nothing.
func (i *Image) makeVolatile() { func (i *Image) makeVolatile() {
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
i.makeVolatile()
})
return
}
if i.isDisposed() { if i.isDisposed() {
return return
} }

View File

@ -128,8 +128,7 @@ var (
// backendsM is a mutex for critical sections of the backend and packing.Node objects. // backendsM is a mutex for critical sections of the backend and packing.Node objects.
backendsM sync.Mutex backendsM sync.Mutex
backendsOnce sync.Once initOnce sync.Once
initOnce sync.Once
// theBackends is a set of actually shared images. // theBackends is a set of actually shared images.
theBackends = []*backend{} theBackends = []*backend{}
@ -139,13 +138,8 @@ var (
deferred []func() deferred []func()
) )
// isShareable reports whether the new allocation can use the shareable backends. func init() {
// backendsM.Lock()
// isShareable retruns false before the graphics driver is available.
// After the graphics driver is available, read-only images will be automatically on the shareable backends by
// (*Image).makeShared().
func isShareable() bool {
return minSize > 0 && maxSize > 0
} }
type Image struct { type Image struct {
@ -472,7 +466,7 @@ func (i *Image) IsVolatile() bool {
} }
func NewImage(width, height int) *Image { func NewImage(width, height int) *Image {
// Actual allocation is done lazily. // Actual allocation is done lazily, and the lock is not needed.
return &Image{ return &Image{
width: width, width: width,
height: height, height: height,
@ -480,8 +474,8 @@ func NewImage(width, height int) *Image {
} }
func (i *Image) shareable() bool { func (i *Image) shareable() bool {
if !isShareable() { if minSize == 0 || maxSize == 0 {
return false panic("shareable: minSize or maxSize must be initialized")
} }
if i.neverShared { if i.neverShared {
return false return false
@ -575,8 +569,6 @@ func EndFrame() error {
} }
func BeginFrame() error { func BeginFrame() error {
// Unlock except for the first time.
//
// In each frame, restoring images and resolving images happen respectively: // In each frame, restoring images and resolving images happen respectively:
// //
// [Restore -> Resolve] -> [Restore -> Resolve] -> ... // [Restore -> Resolve] -> [Restore -> Resolve] -> ...
@ -584,13 +576,7 @@ func BeginFrame() error {
// Between each frame, any image operations are not permitted, or stale images would remain when restoring // Between each frame, any image operations are not permitted, or stale images would remain when restoring
// (#913). // (#913).
defer func() { defer func() {
firsttime := false backendsM.Unlock()
backendsOnce.Do(func() {
firsttime = true
})
if !firsttime {
backendsM.Unlock()
}
}() }()
var err error var err error

View File

@ -86,6 +86,7 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
// Images are available after shareable is initialized. // Images are available after shareable is initialized.
atomic.StoreInt32(&isImageAvailable, 1) atomic.StoreInt32(&isImageAvailable, 1)
flushImageOps()
for i := 0; i < updateCount; i++ { for i := 0; i < updateCount; i++ {
c.offscreen.Clear() c.offscreen.Clear()