buffered: Allow Set before the game runs

Fixes #949
This commit is contained in:
Hajime Hoshi 2019-10-12 03:09:43 +09:00
parent 1b011e3864
commit 5027bc1af5
4 changed files with 159 additions and 66 deletions

View File

@ -40,8 +40,6 @@ type Image struct {
bounds image.Rectangle bounds image.Rectangle
original *Image original *Image
pendingPixels []byte
filter Filter filter Filter
} }
@ -92,8 +90,6 @@ func (i *Image) Fill(clr color.Color) error {
panic("ebiten: render to a subimage is not implemented (Fill)") panic("ebiten: render to a subimage is not implemented (Fill)")
} }
i.invalidatePendingPixels()
i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA)) i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA))
return nil return nil
} }
@ -150,9 +146,6 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error {
panic("ebiten: render to a subimage is not implemented (drawImage)") panic("ebiten: render to a subimage is not implemented (drawImage)")
} }
img.resolvePendingPixels(true)
i.resolvePendingPixels(false)
// Calculate vertices before locking because the user can do anything in // Calculate vertices before locking because the user can do anything in
// options.ImageParts interface without deadlock (e.g. Call Image functions). // options.ImageParts interface without deadlock (e.g. Call Image functions).
if options == nil { if options == nil {
@ -293,9 +286,6 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
panic("ebiten: render to a subimage is not implemented (DrawTriangles)") panic("ebiten: render to a subimage is not implemented (DrawTriangles)")
} }
img.resolvePendingPixels(true)
i.resolvePendingPixels(false)
if len(indices)%3 != 0 { if len(indices)%3 != 0 {
panic("ebiten: len(indices) % 3 must be 0") panic("ebiten: len(indices) % 3 must be 0")
} }
@ -390,8 +380,6 @@ func (i *Image) At(x, y int) color.Color {
if i.isSubImage() && !image.Pt(x, y).In(i.bounds) { if i.isSubImage() && !image.Pt(x, y).In(i.bounds) {
return color.RGBA{} return color.RGBA{}
} }
// TODO: Use pending pixels
i.resolvePendingPixels(true)
r, g, b, a := i.mipmap.at(x, y) r, g, b, a := i.mipmap.at(x, y)
return color.RGBA{r, g, b, a} return color.RGBA{r, g, b, a}
} }
@ -400,66 +388,23 @@ func (i *Image) At(x, y int) color.Color {
// //
// Set loads pixels from GPU to system memory if necessary, which means that Set can be slow. // Set loads pixels from GPU to system memory if necessary, which means that Set can be slow.
// //
// Set can't be called outside the main loop (ebiten.Run's updating function) starts. // In the current implementation, successive calls of Set invokes loading pixels at most once, so this is efficient.
// //
// If the image is disposed, Set does nothing. // If the image is disposed, Set does nothing.
func (img *Image) Set(x, y int, clr color.Color) { func (i *Image) Set(x, y int, clr color.Color) {
img.copyCheck() i.copyCheck()
if img.isDisposed() { if i.isDisposed() {
return return
} }
if !image.Pt(x, y).In(img.Bounds()) { if !image.Pt(x, y).In(i.Bounds()) {
return return
} }
if img.isSubImage() { if i.isSubImage() {
img = img.original i = i.original
} }
w, h := img.Size()
if img.pendingPixels == nil {
pix := make([]byte, 4*w*h)
idx := 0
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, g, b, a := img.mipmap.at(i, j)
pix[4*idx] = r
pix[4*idx+1] = g
pix[4*idx+2] = b
pix[4*idx+3] = a
idx++
}
}
img.pendingPixels = pix
}
r, g, b, a := clr.RGBA() r, g, b, a := clr.RGBA()
img.pendingPixels[4*(x+y*w)] = byte(r >> 8) i.mipmap.set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8))
img.pendingPixels[4*(x+y*w)+1] = byte(g >> 8)
img.pendingPixels[4*(x+y*w)+2] = byte(b >> 8)
img.pendingPixels[4*(x+y*w)+3] = byte(a >> 8)
}
func (i *Image) invalidatePendingPixels() {
if i.isSubImage() {
i.original.invalidatePendingPixels()
return
}
i.pendingPixels = nil
}
func (i *Image) resolvePendingPixels(keepPendingPixels bool) {
if i.isSubImage() {
i.original.resolvePendingPixels(keepPendingPixels)
return
}
if i.pendingPixels == nil {
return
}
i.mipmap.replacePixels(i.pendingPixels)
if !keepPendingPixels {
i.pendingPixels = nil
}
} }
// Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values. // Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values.
@ -480,7 +425,6 @@ func (i *Image) Dispose() error {
return nil return nil
} }
i.mipmap.dispose() i.mipmap.dispose()
i.invalidatePendingPixels()
return nil return nil
} }
@ -505,7 +449,6 @@ func (i *Image) ReplacePixels(p []byte) error {
if i.isSubImage() { if i.isSubImage() {
panic("ebiten: render to a subimage is not implemented (ReplacePixels)") panic("ebiten: render to a subimage is not implemented (ReplacePixels)")
} }
i.invalidatePendingPixels()
s := i.Bounds().Size() s := i.Bounds().Size()
if l := 4 * s.X * s.Y; len(p) != l { 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)) panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l))

View File

@ -24,6 +24,10 @@ import (
type Image struct { type Image struct {
img *shareable.Image img *shareable.Image
width int
height int
pendingPixels []byte
} }
func BeginFrame() error { func BeginFrame() error {
@ -44,6 +48,8 @@ func NewImage(width, height int, volatile bool) *Image {
if needsToDelayCommands { if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() { delayedCommands = append(delayedCommands, func() {
i.img = shareable.NewImage(width, height, volatile) i.img = shareable.NewImage(width, height, volatile)
i.width = width
i.height = height
}) })
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
return i return i
@ -51,6 +57,8 @@ func NewImage(width, height int, volatile bool) *Image {
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.img = shareable.NewImage(width, height, volatile) i.img = shareable.NewImage(width, height, volatile)
i.width = width
i.height = height
return i return i
} }
@ -60,6 +68,8 @@ func NewScreenFramebufferImage(width, height int) *Image {
if needsToDelayCommands { if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() { delayedCommands = append(delayedCommands, func() {
i.img = shareable.NewScreenFramebufferImage(width, height) i.img = shareable.NewScreenFramebufferImage(width, height)
i.width = width
i.height = height
}) })
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
return i return i
@ -67,17 +77,38 @@ func NewScreenFramebufferImage(width, height int) *Image {
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.img = shareable.NewScreenFramebufferImage(width, height) i.img = shareable.NewScreenFramebufferImage(width, height)
i.width = width
i.height = height
return i return i
} }
func (i *Image) invalidatePendingPixels() {
i.pendingPixels = nil
}
func (i *Image) resolvePendingPixels(keepPendingPixels bool) {
if i.pendingPixels == nil {
return
}
i.img.ReplacePixels(i.pendingPixels)
if !keepPendingPixels {
i.pendingPixels = nil
}
}
func (i *Image) MarkDisposed() { func (i *Image) MarkDisposed() {
delayedCommandsM.Lock() delayedCommandsM.Lock()
if needsToDelayCommands { if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() { delayedCommands = append(delayedCommands, func() {
i.img.MarkDisposed() i.img.MarkDisposed()
}) })
delayedCommandsM.Unlock()
return
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.invalidatePendingPixels()
} }
func (i *Image) At(x, y int) (r, g, b, a byte) { func (i *Image) At(x, y int) (r, g, b, a byte) {
@ -86,9 +117,48 @@ func (i *Image) At(x, y int) (r, g, b, a byte) {
if needsToDelayCommands { if needsToDelayCommands {
panic("buffered: the command queue is not available yet at At") panic("buffered: the command queue is not available yet at At")
} }
// TODO: Use pending pixels
i.resolvePendingPixels(true)
return i.img.At(x, y) return i.img.At(x, y)
} }
func (i *Image) Set(x, y int, r, g, b, a byte) {
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
i.set(x, y, r, g, b, a)
})
delayedCommandsM.Unlock()
return
}
delayedCommandsM.Unlock()
i.set(x, y, r, g, b, a)
}
func (img *Image) set(x, y int, r, g, b, a byte) {
w, h := img.width, img.height
if img.pendingPixels == nil {
pix := make([]byte, 4*w*h)
idx := 0
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, g, b, a := img.img.At(i, j)
pix[4*idx] = r
pix[4*idx+1] = g
pix[4*idx+2] = b
pix[4*idx+3] = a
idx++
}
}
img.pendingPixels = pix
}
img.pendingPixels[4*(x+y*w)] = r
img.pendingPixels[4*(x+y*w)+1] = g
img.pendingPixels[4*(x+y*w)+2] = b
img.pendingPixels[4*(x+y*w)+3] = a
}
func (i *Image) Dump(name string) error { func (i *Image) Dump(name string) error {
delayedCommandsM.Lock() delayedCommandsM.Lock()
defer delayedCommandsM.Unlock() defer delayedCommandsM.Unlock()
@ -109,6 +179,7 @@ func (i *Image) Fill(clr color.RGBA) {
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.invalidatePendingPixels()
i.img.Fill(clr) i.img.Fill(clr)
} }
@ -137,6 +208,7 @@ func (i *Image) ReplacePixels(pix []byte) {
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
i.invalidatePendingPixels()
i.img.ReplacePixels(pix) i.img.ReplacePixels(pix)
} }
@ -155,5 +227,7 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
} }
delayedCommandsM.Unlock() delayedCommandsM.Unlock()
src.resolvePendingPixels(true)
i.resolvePendingPixels(false)
i.img.DrawTriangles(src.img, vertices, indices, colorm, mode, filter, address) i.img.DrawTriangles(src.img, vertices, indices, colorm, mode, filter, address)
} }

View File

@ -0,0 +1,72 @@
// 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_test
import (
"errors"
"image/color"
"os"
"testing"
"github.com/hajimehoshi/ebiten"
)
var mainCh = make(chan func())
func runOnMainThread(f func()) {
ch := make(chan struct{})
mainCh <- func() {
f()
close(ch)
}
<-ch
}
func TestMain(m *testing.M) {
go func() {
os.Exit(m.Run())
}()
for {
select {
case f := <-mainCh:
f()
}
}
}
func TestSetBeforeRun(t *testing.T) {
clr := color.RGBA{1, 2, 3, 4}
img, _ := ebiten.NewImage(16, 16, ebiten.FilterDefault)
img.Set(0, 0, clr)
want := clr
var got color.RGBA
runOnMainThread(func() {
quit := errors.New("quit")
if err := ebiten.Run(func(*ebiten.Image) error {
got = img.At(0, 0).(color.RGBA)
return quit
}, 320, 240, 1, ""); err != nil && err != quit {
t.Fatal(err)
}
})
if got != want {
t.Errorf("got: %v, want: %v", got, want)
}
}

View File

@ -77,6 +77,10 @@ func (m *mipmap) at(x, y int) (r, g, b, a byte) {
return m.orig.At(x, y) return m.orig.At(x, y)
} }
func (m *mipmap) set(x, y int, r, g, b, a byte) {
m.orig.Set(x, y, r, g, b, a)
}
func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) { func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) {
if det := geom.det(); det == 0 { if det := geom.det(); det == 0 {
return return