graphicscommand: Return the error immediately (#1060)

Now grpahicscommand saves the error and shows the error after a
while. This was good to simplify the API but was the cause to hide
some issues.

This change fixes all the errors to be returned immediately, and
buffer this in the ebiten package instead.

Fixes #971
This commit is contained in:
Hajime Hoshi 2020-01-19 01:18:56 +09:00 committed by GitHub
parent 5c285de3db
commit b3bdf51905
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 132 deletions

View File

@ -401,7 +401,10 @@ func (i *Image) At(x, y int) color.Color {
if i.isSubImage() && !image.Pt(x, y).In(i.bounds) {
return color.RGBA{}
}
r, g, b, a := i.buffered.At(x, y)
r, g, b, a, err := i.buffered.At(x, y)
if err != nil {
theUIContext.setError(err)
}
return color.RGBA{r, g, b, a}
}
@ -425,7 +428,9 @@ func (i *Image) Set(x, y int, clr color.Color) {
}
r, g, b, a := clr.RGBA()
i.buffered.Set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8))
if err := i.buffered.Set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8)); err != nil {
theUIContext.setError(err)
}
}
// Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values.

View File

@ -24,21 +24,24 @@ var (
// 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).
delayedCommands []func()
delayedCommands []func() error
delayedCommandsM sync.Mutex
)
func flushDelayedCommands() {
func flushDelayedCommands() error {
delayedCommandsM.Lock()
defer delayedCommandsM.Unlock()
if !needsToDelayCommands {
return
return nil
}
for _, c := range delayedCommands {
c()
if err := c(); err != nil {
return err
}
}
delayedCommands = delayedCommands[:0]
needsToDelayCommands = false
return nil
}

View File

@ -36,8 +36,7 @@ func BeginFrame() error {
if err := mipmap.BeginFrame(); err != nil {
return err
}
flushDelayedCommands()
return nil
return flushDelayedCommands()
}
func EndFrame() error {
@ -48,10 +47,11 @@ func NewImage(width, height int, volatile bool) *Image {
i := &Image{}
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.img = mipmap.New(width, height, volatile)
i.width = width
i.height = height
return nil
})
delayedCommandsM.Unlock()
return i
@ -68,10 +68,11 @@ func NewScreenFramebufferImage(width, height int) *Image {
i := &Image{}
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.img = mipmap.NewScreenFramebufferMipmap(width, height)
i.width = width
i.height = height
return nil
})
delayedCommandsM.Unlock()
return i
@ -104,8 +105,9 @@ func (i *Image) resolvePendingPixels(keepPendingPixels bool) {
func (i *Image) MarkDisposed() {
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.img.MarkDisposed()
return nil
})
delayedCommandsM.Unlock()
return
@ -115,7 +117,7 @@ func (i *Image) MarkDisposed() {
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, err error) {
delayedCommandsM.Lock()
defer delayedCommandsM.Unlock()
if needsToDelayCommands {
@ -126,28 +128,31 @@ func (i *Image) At(x, y int) (r, g, b, a byte) {
return i.img.At(x, y)
}
func (i *Image) Set(x, y int, r, g, b, a byte) {
func (i *Image) Set(x, y int, r, g, b, a byte) error {
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
i.set(x, y, r, g, b, a)
delayedCommands = append(delayedCommands, func() error {
return i.set(x, y, r, g, b, a)
})
delayedCommandsM.Unlock()
return
return nil
}
delayedCommandsM.Unlock()
i.set(x, y, r, g, b, a)
return i.set(x, y, r, g, b, a)
}
func (img *Image) set(x, y int, r, g, b, a byte) {
func (img *Image) set(x, y int, r, g, b, a byte) error {
w, h := img.width, img.height
if img.pixels == 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)
r, g, b, a, err := img.img.At(i, j)
if err != nil {
return err
}
pix[4*idx] = r
pix[4*idx+1] = g
pix[4*idx+2] = b
@ -162,6 +167,7 @@ func (img *Image) set(x, y int, r, g, b, a byte) {
img.pixels[4*(x+y*w)+2] = b
img.pixels[4*(x+y*w)+3] = a
img.needsToResolvePixels = true
return nil
}
func (i *Image) Dump(name string) error {
@ -176,8 +182,9 @@ func (i *Image) Dump(name string) error {
func (i *Image) Fill(clr color.RGBA) {
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.img.Fill(clr)
return nil
})
delayedCommandsM.Unlock()
return
@ -191,10 +198,11 @@ func (i *Image) Fill(clr color.RGBA) {
func (i *Image) ReplacePixels(pix []byte) {
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
copied := make([]byte, len(pix))
copy(copied, pix)
i.img.ReplacePixels(copied)
return nil
})
delayedCommandsM.Unlock()
return
@ -221,8 +229,9 @@ func (i *Image) DrawImage(src *Image, bounds image.Rectangle, a, b, c, d, tx, ty
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.drawImage(src, bounds, g, colorm, mode, filter)
return nil
})
delayedCommandsM.Unlock()
return
@ -245,8 +254,9 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
delayedCommandsM.Lock()
if needsToDelayCommands {
delayedCommands = append(delayedCommands, func() {
delayedCommands = append(delayedCommands, func() error {
i.drawTriangles(src, vertices, indices, colorm, mode, filter, address)
return nil
})
delayedCommandsM.Unlock()
return

View File

@ -174,11 +174,7 @@ func fract(x float32) float32 {
}
// Flush flushes the command queue.
func (q *commandQueue) Flush() {
if q.err != nil {
return
}
func (q *commandQueue) Flush() error {
es := q.indices
vs := q.vertices
if recordLog() {
@ -263,8 +259,7 @@ func (q *commandQueue) Flush() {
indexOffset := 0
for _, c := range cs[:nc] {
if err := c.Exec(indexOffset); err != nil {
q.err = err
return
return err
}
if recordLog() {
fmt.Printf("%s\n", c)
@ -282,16 +277,12 @@ func (q *commandQueue) Flush() {
q.nindices = 0
q.tmpNumIndices = 0
q.nextIndex = 0
}
// Error returns an OpenGL error for the last command.
func Error() error {
return theCommandQueue.err
return nil
}
// FlushCommands flushes the command queue.
func FlushCommands() {
theCommandQueue.Flush()
func FlushCommands() error {
return theCommandQueue.Flush()
}
// drawTrianglesCommand represents a drawing command to draw an image on another image.

View File

@ -162,15 +162,17 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
// Pixels returns the image's pixels.
// Pixels might return nil when OpenGL error happens.
func (i *Image) Pixels() []byte {
func (i *Image) Pixels() ([]byte, error) {
i.resolveBufferedReplacePixels()
c := &pixelsCommand{
result: nil,
img: i,
}
theCommandQueue.Enqueue(c)
theCommandQueue.Flush()
return c.result
if err := theCommandQueue.Flush(); err != nil {
return nil, err
}
return c.result, nil
}
func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
@ -221,8 +223,12 @@ func (i *Image) Dump(path string) error {
}
defer f.Close()
pix, err := i.Pixels()
if err != nil {
return err
}
if err := png.Encode(f, &image.RGBA{
Pix: i.Pixels(),
Pix: pix,
Stride: 4 * i.width,
Rect: image.Rect(0, 0, i.width, i.height),
}); err != nil {

View File

@ -61,7 +61,10 @@ func TestClear(t *testing.T) {
is := graphics.QuadIndices()
dst.DrawTriangles(src, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressClampToZero)
pix := dst.Pixels()
pix, err := dst.Pixels()
if err != nil {
t.Fatal(err)
}
for j := 0; j < h/2; j++ {
for i := 0; i < w/2; i++ {
idx := 4 * (i + w*j)

View File

@ -92,7 +92,7 @@ func (m *Mipmap) ReplacePixels(pix []byte) {
m.disposeMipmaps()
}
func (m *Mipmap) At(x, y int) (r, g, b, a byte) {
func (m *Mipmap) At(x, y int) (r, g, b, a byte, err error) {
return m.orig.At(x, y)
}

View File

@ -402,26 +402,34 @@ func (i *Image) appendDrawTrianglesHistory(image *Image, vertices []float32, ind
i.drawTrianglesHistory = append(i.drawTrianglesHistory, item)
}
func (i *Image) readPixelsFromGPUIfNeeded() {
func (i *Image) readPixelsFromGPUIfNeeded() error {
if len(i.drawTrianglesHistory) > 0 || i.stale {
graphicscommand.FlushCommands()
i.readPixelsFromGPU()
if err := graphicscommand.FlushCommands(); err != nil {
return err
}
if err := i.readPixelsFromGPU(); err != nil {
return err
}
i.drawTrianglesHistory = nil
i.stale = false
}
return nil
}
// At returns a color value at (x, y).
//
// Note that this must not be called until context is available.
func (i *Image) At(x, y int) (byte, byte, byte, byte) {
func (i *Image) At(x, y int) (byte, byte, byte, byte, error) {
if x < 0 || y < 0 || i.width <= x || i.height <= y {
return 0, 0, 0, 0
return 0, 0, 0, 0, nil
}
i.readPixelsFromGPUIfNeeded()
if err := i.readPixelsFromGPUIfNeeded(); err != nil {
return 0, 0, 0, 0, err
}
return i.basePixels.At(x, y)
r, g, b, a := i.basePixels.At(x, y)
return r, g, b, a, nil
}
// makeStaleIfDependingOn makes the image stale if the image depends on target.
@ -435,29 +443,34 @@ func (i *Image) makeStaleIfDependingOn(target *Image) {
}
// readPixelsFromGPU reads the pixels from GPU and resolves the image's 'stale' state.
func (i *Image) readPixelsFromGPU() {
func (i *Image) readPixelsFromGPU() error {
pix, err := i.image.Pixels()
if err != nil {
return err
}
i.basePixels = Pixels{}
i.basePixels.AddOrReplace(i.image.Pixels(), 0, 0, i.width, i.height)
i.basePixels.AddOrReplace(pix, 0, 0, i.width, i.height)
i.drawTrianglesHistory = nil
i.stale = false
return nil
}
// resolveStale resolves the image's 'stale' state.
func (i *Image) resolveStale() {
func (i *Image) resolveStale() error {
if !needsRestoring() {
return
return nil
}
if i.volatile {
return
return nil
}
if i.screen {
return
return nil
}
if !i.stale {
return
return nil
}
i.readPixelsFromGPU()
return i.readPixelsFromGPU()
}
// dependsOn returns a boolean value indicating whether the image depends on target.
@ -488,7 +501,7 @@ func (i *Image) hasDependency() bool {
}
// Restore restores *graphicscommand.Image from the pixels using its state.
func (i *Image) restore() {
func (i *Image) restore() error {
w, h := i.width, i.height
// Do not dispose the image here. The image should be already disposed.
@ -499,12 +512,12 @@ func (i *Image) restore() {
i.basePixels = Pixels{}
i.drawTrianglesHistory = nil
i.stale = false
return
return nil
}
if i.volatile {
i.image = graphicscommand.NewImage(w, h)
fillImage(i.image, color.RGBA{})
return
return nil
}
if i.stale {
panic("restorable: pixels must not be stale when restoring")
@ -528,12 +541,17 @@ func (i *Image) restore() {
if len(i.drawTrianglesHistory) > 0 {
i.basePixels = Pixels{}
i.basePixels.AddOrReplace(gimg.Pixels(), 0, 0, w, h)
pix, err := gimg.Pixels()
if err != nil {
return err
}
i.basePixels.AddOrReplace(pix, 0, 0, w, h)
}
i.image = gimg
i.drawTrianglesHistory = nil
i.stale = false
return nil
}
// Dispose disposes the image.
@ -551,10 +569,12 @@ func (i *Image) Dispose() {
// isInvalidated returns a boolean value indicating whether the image is invalidated.
//
// If an image is invalidated, GL context is lost and all the images should be restored asap.
func (i *Image) isInvalidated() bool {
func (i *Image) isInvalidated() (bool, error) {
// FlushCommands is required because c.offscreen.impl might not have an actual texture.
graphicscommand.FlushCommands()
return i.image.IsInvalidated()
if err := graphicscommand.FlushCommands(); err != nil {
return false, err
}
return i.image.IsInvalidated(), nil
}
func (i *Image) Dump(path string) error {

View File

@ -51,12 +51,14 @@ var theImages = &images{
// all stale images.
//
// ResolveStaleImages is intended to be called at the end of a frame.
func ResolveStaleImages() {
graphicscommand.FlushCommands()
if !needsRestoring() {
return
func ResolveStaleImages() error {
if err := graphicscommand.FlushCommands(); err != nil {
return err
}
theImages.resolveStaleImages()
if !needsRestoring() {
return nil
}
return theImages.resolveStaleImages()
}
// RestoreIfNeeded restores the images.
@ -76,7 +78,11 @@ func RestoreIfNeeded() error {
if img.screen {
continue
}
r = img.isInvalidated()
var err error
r, err = img.isInvalidated()
if err != nil {
return err
}
break
}
if !r {
@ -87,8 +93,7 @@ func RestoreIfNeeded() error {
if err := graphicscommand.ResetGraphicsDriverState(); err != nil {
return err
}
theImages.restore()
return nil
return theImages.restore()
}
// DumpImages dumps all the current images to the specified directory.
@ -115,11 +120,14 @@ func (i *images) remove(img *Image) {
}
// resolveStaleImages resolves stale images.
func (i *images) resolveStaleImages() {
func (i *images) resolveStaleImages() error {
i.lastTarget = nil
for img := range i.images {
img.resolveStale()
if err := img.resolveStale(); err != nil {
return err
}
}
return nil
}
// makeStaleIfDependingOn makes all the images stale that depend on target.
@ -147,7 +155,7 @@ func (i *images) makeStaleIfDependingOnImpl(target *Image) {
// restore restores the images.
//
// Restoring means to make all *graphicscommand.Image objects have their textures and framebuffers.
func (i *images) restore() {
func (i *images) restore() error {
if !needsRestoring() {
panic("restorable: restore cannot be called when restoring is disabled")
}
@ -211,15 +219,14 @@ func (i *images) restore() {
}
for _, img := range sorted {
img.restore()
if err := img.restore(); err != nil {
return err
}
}
return nil
}
// InitializeGraphicsDriverState initializes the graphics driver state.
func InitializeGraphicsDriverState() error {
return graphicscommand.ResetGraphicsDriverState()
}
func Error() error {
return graphicscommand.Error()
}

View File

@ -75,7 +75,9 @@ func TestRestore(t *testing.T) {
clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
img0.ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, 0, 0, 1, 1)
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -92,7 +94,9 @@ func TestRestoreWithoutDraw(t *testing.T) {
// If there is no drawing command on img0, img0 is cleared when restored.
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -144,7 +148,9 @@ func TestRestoreChain(t *testing.T) {
is := graphics.QuadIndices()
imgs[i+1].DrawTriangles(imgs[i], vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
}
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -188,7 +194,9 @@ func TestRestoreChain2(t *testing.T) {
imgs[i+1].DrawTriangles(imgs[i], quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
}
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -227,7 +235,9 @@ func TestRestoreOverrideSource(t *testing.T) {
img3.DrawTriangles(img2, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
img0.ReplacePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, 0, 0, w, h)
img1.DrawTriangles(img0, quadVertices(w, h, 0, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -320,7 +330,9 @@ func TestRestoreComplexGraph(t *testing.T) {
img7.DrawTriangles(img2, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
vs = quadVertices(w, h, 2, 0)
img7.DrawTriangles(img3, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -411,7 +423,9 @@ func TestRestoreRecursive(t *testing.T) {
is := graphics.QuadIndices()
img1.DrawTriangles(img0, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
img0.DrawTriangles(img1, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -457,7 +471,10 @@ func TestReplacePixels(t *testing.T) {
// Check the region (5, 7)-(9, 11). Outside state is indeterministic.
for j := 7; j < 11; j++ {
for i := 5; i < 9; i++ {
r, g, b, a := img.At(i, j)
r, g, b, a, err := img.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want {
@ -465,13 +482,18 @@ func TestReplacePixels(t *testing.T) {
}
}
}
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
for j := 7; j < 11; j++ {
for i := 5; i < 9; i++ {
r, g, b, a := img.At(i, j)
r, g, b, a, err := img.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want {
@ -497,11 +519,16 @@ func TestDrawTrianglesAndReplacePixels(t *testing.T) {
img1.DrawTriangles(img0, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
img1.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 0, 0, 2, 1)
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
r, g, b, a := img1.At(0, 0)
r, g, b, a, err := img1.At(0, 0)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if !sameColors(got, want, 1) {
@ -530,11 +557,16 @@ func TestDispose(t *testing.T) {
img0.DrawTriangles(img1, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
img1.Dispose()
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
r, g, b, a := img0.At(0, 0)
r, g, b, a, err := img0.At(0, 0)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if !sameColors(got, want, 1) {
@ -651,7 +683,9 @@ func TestReplacePixelsOnly(t *testing.T) {
}
}
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
@ -686,7 +720,10 @@ func TestReadPixelsFromVolatileImage(t *testing.T) {
// Read the pixels. If the implementation is correct, dst tries to read its pixels from GPU due to being
// stale.
want := byte(0xff)
got, _, _, _ := dst.At(0, 0)
got, _, _, _, err := dst.At(0, 0)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("got: %v, want: %v", got, want)
}
@ -745,7 +782,10 @@ func TestExtend(t *testing.T) {
for j := 0; j < h*2; j++ {
for i := 0; i < w*2; i++ {
got, _, _, _ := extended.At(i, j)
got, _, _, _, err := extended.At(i, j)
if err != nil {
t.Fatal(err)
}
want := byte(0)
if i < w && j < h {
want = pixAt(i, j)
@ -773,13 +813,18 @@ func TestFill(t *testing.T) {
const w, h = 16, 16
img := NewImage(w, h, false)
img.Fill(color.RGBA{0xff, 0, 0, 0xff})
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, g, b, a := img.At(i, j)
r, g, b, a, err := img.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0, 0, 0xff}
if got != want {
@ -812,16 +857,24 @@ func TestMutateSlices(t *testing.T) {
for i := range is {
is[i] = 0
}
ResolveStaleImages()
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil {
t.Fatal(err)
}
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, g, b, a := src.At(i, j)
r, g, b, a, err := src.At(i, j)
if err != nil {
t.Fatal(err)
}
want := color.RGBA{r, g, b, a}
r, g, b, a = dst.At(i, j)
r, g, b, a, err = dst.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
if !sameColors(got, want, 1) {
t.Errorf("(%d, %d): got %v, want %v", i, j, got, want)

View File

@ -14,8 +14,8 @@
package shareable
func MakeImagesSharedForTesting() {
makeImagesShared()
func MakeImagesSharedForTesting() error {
return makeImagesShared()
}
func (i *Image) IsSharedForTesting() bool {

View File

@ -52,8 +52,7 @@ func init() {
defer backendsM.Unlock()
resolveDeferred()
makeImagesShared()
return nil
return makeImagesShared()
})
}
@ -73,14 +72,17 @@ func resolveDeferred() {
// This value is exported for testing.
const MaxCountForShare = 10
func makeImagesShared() {
func makeImagesShared() error {
for i := range imagesToMakeShared {
i.nonUpdatedCount++
if i.nonUpdatedCount >= MaxCountForShare {
i.makeShared()
if err := i.makeShared(); err != nil {
return err
}
}
delete(imagesToMakeShared, i)
}
return nil
}
type backend struct {
@ -220,14 +222,14 @@ func (i *Image) ensureNotShared() {
}
}
func (i *Image) makeShared() {
func (i *Image) makeShared() error {
if i.backend == nil {
i.allocate(true)
return
return nil
}
if i.isShared() {
return
return nil
}
if !i.shareable() {
@ -238,7 +240,10 @@ func (i *Image) makeShared() {
pixels := make([]byte, 4*i.width*i.height)
for y := 0; y < i.height; y++ {
for x := 0; x < i.width; x++ {
r, g, b, a := i.at(x, y)
r, g, b, a, err := i.at(x, y)
if err != nil {
return err
}
pixels[4*(x+i.width*y)] = r
pixels[4*(x+i.width*y)+1] = g
pixels[4*(x+i.width*y)+2] = b
@ -248,6 +253,7 @@ func (i *Image) makeShared() {
newI.replacePixels(pixels)
newI.moveTo(i)
i.nonUpdatedCount = 0
return nil
}
func (i *Image) region() (x, y, width, height int) {
@ -370,21 +376,21 @@ func (i *Image) replacePixels(p []byte) {
i.backend.restorable.ReplacePixels(p, x, y, w, h)
}
func (i *Image) At(x, y int) (byte, byte, byte, byte) {
func (i *Image) At(x, y int) (byte, byte, byte, byte, error) {
backendsM.Lock()
r, g, b, a := i.at(x, y)
r, g, b, a, err := i.at(x, y)
backendsM.Unlock()
return r, g, b, a
return r, g, b, a, err
}
func (i *Image) at(x, y int) (byte, byte, byte, byte) {
func (i *Image) at(x, y int) (byte, byte, byte, byte, error) {
if i.backend == nil {
return 0, 0, 0, 0
return 0, 0, 0, 0, nil
}
ox, oy, w, h := i.region()
if x < 0 || y < 0 || x >= w || y >= h {
return 0, 0, 0, 0
return 0, 0, 0, 0, nil
}
return i.backend.restorable.At(x+ox, y+oy)
@ -543,8 +549,7 @@ func NewScreenFramebufferImage(width, height int) *Image {
func EndFrame() error {
backendsM.Lock()
restorable.ResolveStaleImages()
return restorable.Error()
return restorable.ResolveStaleImages()
}
func BeginFrame() error {

View File

@ -112,7 +112,10 @@ func TestEnsureNotShared(t *testing.T) {
for j := 0; j < size; j++ {
for i := 0; i < size; i++ {
r, g, b, a := img4.At(i, j)
r, g, b, a, err := img4.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
var want color.RGBA
if i < dx0 || dx1 <= i || j < dy0 || dy1 <= j {
@ -174,18 +177,25 @@ func TestReshared(t *testing.T) {
// Use img1 as a render source.
for i := 0; i < MaxCountForShare; i++ {
MakeImagesSharedForTesting()
if err := MakeImagesSharedForTesting(); err != nil {
t.Fatal(err)
}
img0.DrawTriangles(img1, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
if got, want := img1.IsSharedForTesting(), false; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
}
MakeImagesSharedForTesting()
if err := MakeImagesSharedForTesting(); err != nil {
t.Fatal(err)
}
for j := 0; j < size; j++ {
for i := 0; i < size; i++ {
want := color.RGBA{byte(i + j), byte(i + j), byte(i + j), byte(i + j)}
r, g, b, a := img1.At(i, j)
r, g, b, a, err := img1.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
if got != want {
t.Errorf("got: %v, want: %v", got, want)
@ -201,7 +211,10 @@ func TestReshared(t *testing.T) {
for j := 0; j < size; j++ {
for i := 0; i < size; i++ {
want := color.RGBA{byte(i + j), byte(i + j), byte(i + j), byte(i + j)}
r, g, b, a := img1.At(i, j)
r, g, b, a, err := img1.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
if got != want {
t.Errorf("got: %v, want: %v", got, want)
@ -211,7 +224,9 @@ func TestReshared(t *testing.T) {
// Use img3 as a render source. img3 never uses a shared texture.
for i := 0; i < MaxCountForShare*2; i++ {
MakeImagesSharedForTesting()
if err := MakeImagesSharedForTesting(); err != nil {
t.Fatal(err)
}
img0.DrawTriangles(img3, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
if got, want := img3.IsSharedForTesting(), false; got != want {
t.Errorf("got: %v, want: %v", got, want)
@ -249,7 +264,10 @@ func TestExtend(t *testing.T) {
for j := 0; j < h0; j++ {
for i := 0; i < w0; i++ {
r, g, b, a := img0.At(i, j)
r, g, b, a, err := img0.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
c := byte(i + w0*j)
want := color.RGBA{c, c, c, c}
@ -261,7 +279,10 @@ func TestExtend(t *testing.T) {
for j := 0; j < h1; j++ {
for i := 0; i < w1; i++ {
r, g, b, a := img1.At(i, j)
r, g, b, a, err := img1.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
c := byte(i + w1*j)
want := color.RGBA{c, c, c, c}
@ -298,7 +319,10 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, g, b, a := dst.At(i, j)
r, g, b, a, err := dst.At(i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
c := byte(i + w*j)
want := color.RGBA{c, c, c, c}
@ -332,7 +356,10 @@ func TestSmallImages(t *testing.T) {
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
r, _, _, a := dst.At(i, j)
r, _, _, a, err := dst.At(i, j)
if err != nil {
t.Fatal(err)
}
if got, want := r, byte(0xff); got != want {
t.Errorf("At(%d, %d) red: got: %d, want: %d", i, j, got, want)
}
@ -367,7 +394,10 @@ func TestLongImages(t *testing.T) {
for j := 0; j < h; j++ {
for i := 0; i < w*scale; i++ {
r, _, _, a := dst.At(i, j)
r, _, _, a, err := dst.At(i, j)
if err != nil {
t.Fatal(err)
}
if got, want := r, byte(0xff); got != want {
t.Errorf("At(%d, %d) red: got: %d, want: %d", i, j, got, want)
}

View File

@ -18,6 +18,7 @@ import (
"fmt"
"math"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/internal/buffered"
"github.com/hajimehoshi/ebiten/internal/clock"
@ -69,6 +70,8 @@ type uiContext struct {
outsideWidth float64
outsideHeight float64
err atomic.Value
m sync.Mutex
}
@ -85,6 +88,10 @@ func (c *uiContext) set(game Game, scaleForWindow float64) {
}
}
func (c *uiContext) setError(err error) {
c.err.Store(err)
}
func (c *uiContext) setScaleForWindow(scale float64) {
c.m.Lock()
defer c.m.Unlock()
@ -233,6 +240,9 @@ func (c *uiContext) offsets() (float64, float64) {
func (c *uiContext) Update(afterFrameUpdate func()) error {
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
if err, ok := c.err.Load().(error); ok && err != nil {
return err
}
if err := buffered.BeginFrame(); err != nil {
return err
}