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/color"
"math"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/internal/driver"
@ -26,6 +27,31 @@ import (
"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.
// The pixel format is alpha-premultiplied RGBA.
// 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.
func (i *Image) Fill(clr color.Color) error {
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() {
return nil
}
@ -147,6 +187,15 @@ func (i *Image) disposeMipmaps() {
// DrawImage always returns nil as of 1.5.0-alpha.
func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
op := *options
i.DrawImage(img, &op)
})
return nil
}
if img.isDisposed() {
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.
func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions) {
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() {
return
}
@ -556,6 +618,14 @@ func (i *Image) resolvePendingPixels(draw bool) {
// Dipose always return nil as of 1.5.0-alpha.
func (i *Image) Dispose() error {
i.copyCheck()
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
i.Dispose()
})
return nil
}
if i.isDisposed() {
return nil
}
@ -580,6 +650,16 @@ func (i *Image) Dispose() error {
// ReplacePixels always returns nil as of 1.5.0-alpha.
func (i *Image) ReplacePixels(p []byte) error {
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() {
return nil
}
@ -663,6 +743,13 @@ func NewImage(width, height int, filter Filter) (*Image, error) {
//
// When the image is disposed, makeVolatile does nothing.
func (i *Image) makeVolatile() {
if atomic.LoadInt32(&isImageAvailable) == 0 {
enqueueImageOp(func() {
i.makeVolatile()
})
return
}
if i.isDisposed() {
return
}

View File

@ -128,8 +128,7 @@ var (
// backendsM is a mutex for critical sections of the backend and packing.Node objects.
backendsM sync.Mutex
backendsOnce sync.Once
initOnce sync.Once
initOnce sync.Once
// theBackends is a set of actually shared images.
theBackends = []*backend{}
@ -139,13 +138,8 @@ var (
deferred []func()
)
// isShareable reports whether the new allocation can use the shareable backends.
//
// 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
func init() {
backendsM.Lock()
}
type Image struct {
@ -472,7 +466,7 @@ func (i *Image) IsVolatile() bool {
}
func NewImage(width, height int) *Image {
// Actual allocation is done lazily.
// Actual allocation is done lazily, and the lock is not needed.
return &Image{
width: width,
height: height,
@ -480,8 +474,8 @@ func NewImage(width, height int) *Image {
}
func (i *Image) shareable() bool {
if !isShareable() {
return false
if minSize == 0 || maxSize == 0 {
panic("shareable: minSize or maxSize must be initialized")
}
if i.neverShared {
return false
@ -575,8 +569,6 @@ func EndFrame() error {
}
func BeginFrame() error {
// Unlock except for the first time.
//
// In each frame, restoring images and resolving images happen respectively:
//
// [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
// (#913).
defer func() {
firsttime := false
backendsOnce.Do(func() {
firsttime = true
})
if !firsttime {
backendsM.Unlock()
}
backendsM.Unlock()
}()
var err error

View File

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