graphics: Add buffered package

Moved the command queue to the package.
This commit is contained in:
Hajime Hoshi 2019-09-19 12:02:35 +09:00
parent 0a872b342a
commit 79b32c7601
5 changed files with 191 additions and 123 deletions

112
image.go
View File

@ -18,63 +18,11 @@ import (
"fmt"
"image"
"image/color"
"sync"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphics"
)
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
needsEnqueueImageOps = true
)
func checkNeedsEnqueueImageOp(location string) {
imageQueueM.Lock()
defer imageQueueM.Unlock()
if needsEnqueueImageOps {
panic(fmt.Sprintf("ebiten: %s is not available before the game starts", location))
}
}
func enqueueImageOpIfNeeded(f func() func()) bool {
imageQueueM.Lock()
defer imageQueueM.Unlock()
if !needsEnqueueImageOps {
return false
}
imageQueue = append(imageQueue, f())
return true
}
func flushImageOpsIfNeeded() {
imageQueueM.Lock()
if !needsEnqueueImageOps {
if len(imageQueue) > 0 {
panic("ebiten: len(imageQueue) must be 0 after the game starts")
}
imageQueueM.Unlock()
return
}
// Set this flag false first, or the image operations will be queued again.
needsEnqueueImageOps = false
imageQueueM.Unlock()
// As a new item will not be enqueued any longer, mutex does not have to, or should not be used.
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.
@ -135,15 +83,6 @@ func (i *Image) Clear() error {
func (i *Image) Fill(clr color.Color) error {
i.copyCheck()
rgba := color.RGBAModel.Convert(clr).(color.RGBA)
if enqueueImageOpIfNeeded(func() func() {
return func() {
i.Fill(rgba)
}
}) {
return nil
}
if i.isDisposed() {
return nil
}
@ -155,7 +94,7 @@ func (i *Image) Fill(clr color.Color) error {
i.resolvePendingPixels(false)
i.mipmap.fill(rgba)
i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA))
return nil
}
@ -199,15 +138,6 @@ func (i *Image) Fill(clr color.Color) error {
func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
i.copyCheck()
if enqueueImageOpIfNeeded(func() func() {
op := *options
return func() {
i.DrawImage(img, &op)
}
}) {
return nil
}
if img.isDisposed() {
panic("ebiten: the given image to DrawImage must not be disposed")
}
@ -355,19 +285,6 @@ const MaxIndicesNum = graphics.IndicesNum
func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions) {
i.copyCheck()
if enqueueImageOpIfNeeded(func() func() {
vs := make([]Vertex, len(vertices))
copy(vs, vertices)
is := make([]uint16, len(indices))
copy(is, indices)
op := *options
return func() {
i.DrawTriangles(vs, is, img, &op)
}
}) {
return
}
if i.isDisposed() {
return
}
@ -467,8 +384,6 @@ func (i *Image) ColorModel() color.Model {
//
// At can't be called outside the main loop (ebiten.Run's updating function) starts (as of version 1.4.0-alpha).
func (i *Image) At(x, y int) color.Color {
checkNeedsEnqueueImageOp("(*Image).At")
if i.isDisposed() {
return color.RGBA{}
}
@ -488,8 +403,6 @@ func (i *Image) At(x, y int) color.Color {
//
// If the image is disposed, Set does nothing.
func (img *Image) Set(x, y int, clr color.Color) {
checkNeedsEnqueueImageOp("(*Image).Set")
img.copyCheck()
if img.isDisposed() {
return
@ -554,14 +467,6 @@ func (i *Image) resolvePendingPixels(draw bool) {
func (i *Image) Dispose() error {
i.copyCheck()
if enqueueImageOpIfNeeded(func() func() {
return func() {
i.Dispose()
}
}) {
return nil
}
if i.isDisposed() {
return nil
}
@ -587,16 +492,6 @@ func (i *Image) Dispose() error {
func (i *Image) ReplacePixels(p []byte) error {
i.copyCheck()
if enqueueImageOpIfNeeded(func() func() {
px := make([]byte, len(p))
copy(px, p)
return func() {
i.ReplacePixels(px)
}
}) {
return nil
}
if i.isDisposed() {
return nil
}
@ -609,7 +504,10 @@ func (i *Image) ReplacePixels(p []byte) error {
if l := 4 * s.X * s.Y; len(p) != l {
panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l))
}
i.mipmap.replacePixels(p)
px := make([]byte, len(p))
copy(px, p)
i.mipmap.replacePixels(px)
return nil
}

View File

@ -0,0 +1,64 @@
// Copyright 2019 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buffered
import (
"sync"
)
type command struct {
f func()
}
var (
delayedCommandsFlushable bool
// delayedCommands 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).
//
// TODO: Flush the commands only when necessary (#921).
delayedCommands []*command
delayedCommandsM sync.Mutex
)
func makeDelayedCommandFlushable() {
delayedCommandsM.Lock()
delayedCommandsFlushable = true
delayedCommandsM.Unlock()
}
func enqueueDelayedCommand(f func()) {
delayedCommandsM.Lock()
delayedCommands = append(delayedCommands, &command{
f: f,
})
delayedCommandsM.Unlock()
}
func flushDelayedCommands() bool {
delayedCommandsM.Lock()
defer delayedCommandsM.Unlock()
if !delayedCommandsFlushable {
return false
}
for _, c := range delayedCommands {
c.f()
}
delayedCommands = nil
return true
}

106
internal/buffered/image.go Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2019 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buffered
import (
"image/color"
"github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/shareable"
)
type Image struct {
img *shareable.Image
}
func BeginFrame() error {
if err := shareable.BeginFrame(); err != nil {
return err
}
makeDelayedCommandFlushable()
return nil
}
func EndFrame() error {
if !flushDelayedCommands() {
panic("buffered: the command queue must be available at EndFrame")
}
return shareable.EndFrame()
}
func NewImage(width, height int, volatile bool) *Image {
i := &Image{}
enqueueDelayedCommand(func() {
i.img = shareable.NewImage(width, height, volatile)
})
return i
}
func NewScreenFramebufferImage(width, height int) *Image {
i := &Image{}
enqueueDelayedCommand(func() {
i.img = shareable.NewScreenFramebufferImage(width, height)
})
return i
}
func (i *Image) MarkDisposed() {
enqueueDelayedCommand(func() {
i.img.MarkDisposed()
})
}
func (i *Image) At(x, y int) (r, g, b, a byte) {
if !flushDelayedCommands() {
panic("buffered: the command queue is not available yet at At")
}
return i.img.At(x, y)
}
func (i *Image) Dump(name string) error {
if !flushDelayedCommands() {
panic("buffered: the command queue is not available yet at Dump")
}
return i.img.Dump(name)
}
func (i *Image) Fill(clr color.RGBA) {
enqueueDelayedCommand(func() {
i.img.Fill(clr)
})
}
func (i *Image) ClearFramebuffer() {
enqueueDelayedCommand(func() {
i.img.ClearFramebuffer()
})
}
func (i *Image) ReplacePixels(pix []byte) {
enqueueDelayedCommand(func() {
i.img.ReplacePixels(pix)
})
}
func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) {
if i == src {
panic("buffered: Image.DrawTriangles: src must be different from the receiver")
}
enqueueDelayedCommand(func() {
i.img.DrawTriangles(src.img, vertices, indices, colorm, mode, filter, address)
})
}

View File

@ -21,18 +21,18 @@ import (
"math"
"github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/shareable"
)
type levelToImage map[int]*shareable.Image
type levelToImage map[int]*buffered.Image
type mipmap struct {
width int
height int
volatile bool
orig *shareable.Image
orig *buffered.Image
imgs map[image.Rectangle]levelToImage
}
@ -41,7 +41,7 @@ func newMipmap(width, height int, volatile bool) *mipmap {
width: width,
height: height,
volatile: volatile,
orig: shareable.NewImage(width, height, volatile),
orig: buffered.NewImage(width, height, volatile),
imgs: map[image.Rectangle]levelToImage{},
}
}
@ -50,7 +50,7 @@ func newScreenFramebufferMipmap(width, height int) *mipmap {
return &mipmap{
width: width,
height: height,
orig: shareable.NewScreenFramebufferImage(width, height),
orig: buffered.NewScreenFramebufferImage(width, height),
imgs: map[image.Rectangle]levelToImage{},
}
}
@ -126,7 +126,7 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo
vs := quadVertices(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, a, b, c, d, tx, ty, cr, cg, cb, ca)
is := graphics.QuadIndices()
m.orig.DrawTriangles(src.orig, vs, is, colorm, mode, filter, driver.AddressClampToZero)
} else if shared := src.level(bounds, level); shared != nil {
} else if buf := src.level(bounds, level); buf != nil {
w, h := sizeForLevel(bounds.Dx(), bounds.Dy(), level)
s := pow2(level)
a *= s
@ -135,7 +135,7 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo
d *= s
vs := quadVertices(0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca)
is := graphics.QuadIndices()
m.orig.DrawTriangles(shared, vs, is, colorm, mode, filter, driver.AddressClampToZero)
m.orig.DrawTriangles(buf, vs, is, colorm, mode, filter, driver.AddressClampToZero)
}
m.disposeMipmaps()
}
@ -164,11 +164,13 @@ func (m *mipmap) drawTriangles(src *mipmap, bounds image.Rectangle, vertices []V
vs[i*graphics.VertexFloatNum+10] = v.ColorB
vs[i*graphics.VertexFloatNum+11] = v.ColorA
}
m.orig.DrawTriangles(src.orig, vs, indices, colorm, mode, filter, address)
is := make([]uint16, len(indices))
copy(is, indices)
m.orig.DrawTriangles(src.orig, vs, is, colorm, mode, filter, address)
m.disposeMipmaps()
}
func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image {
if level == 0 {
panic("ebiten: level must be non-zero at level")
}
@ -186,7 +188,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
return img
}
var src *shareable.Image
var src *buffered.Image
var vs []float32
var filter driver.Filter
switch {
@ -226,7 +228,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *shareable.Image {
imgs[level] = nil
return nil
}
s := shareable.NewImage(w2, h2, m.volatile)
s := buffered.NewImage(w2, h2, m.volatile)
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero)
imgs[level] = s

View File

@ -18,6 +18,7 @@ import (
"fmt"
"math"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/clock"
"github.com/hajimehoshi/ebiten/internal/driver"
"github.com/hajimehoshi/ebiten/internal/graphicscommand"
@ -77,13 +78,10 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
if err := shareable.BeginFrame(); err != nil {
if err := buffered.BeginFrame(); err != nil {
return err
}
// Images are available after shareable is initialized.
flushImageOpsIfNeeded()
for i := 0; i < updateCount; i++ {
c.offscreen.Clear()
// Mipmap images should be disposed by fill.
@ -129,7 +127,7 @@ func (c *uiContext) Update(afterFrameUpdate func()) error {
}
_ = c.screen.DrawImage(c.offscreen, op)
if err := shareable.EndFrame(); err != nil {
if err := buffered.EndFrame(); err != nil {
return err
}
return nil