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) { if i.isSubImage() && !image.Pt(x, y).In(i.bounds) {
return color.RGBA{} 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} 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() 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. // 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 // 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 // (BeginFrame). Before the game starts, the package shareable doesn't determine the minimum/maximum texture
// sizes (#879). // sizes (#879).
delayedCommands []func() delayedCommands []func() error
delayedCommandsM sync.Mutex delayedCommandsM sync.Mutex
) )
func flushDelayedCommands() { func flushDelayedCommands() error {
delayedCommandsM.Lock() delayedCommandsM.Lock()
defer delayedCommandsM.Unlock() defer delayedCommandsM.Unlock()
if !needsToDelayCommands { if !needsToDelayCommands {
return return nil
} }
for _, c := range delayedCommands { for _, c := range delayedCommands {
c() if err := c(); err != nil {
return err
}
} }
delayedCommands = delayedCommands[:0] delayedCommands = delayedCommands[:0]
needsToDelayCommands = false needsToDelayCommands = false
return nil
} }

View File

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

View File

@ -174,11 +174,7 @@ func fract(x float32) float32 {
} }
// Flush flushes the command queue. // Flush flushes the command queue.
func (q *commandQueue) Flush() { func (q *commandQueue) Flush() error {
if q.err != nil {
return
}
es := q.indices es := q.indices
vs := q.vertices vs := q.vertices
if recordLog() { if recordLog() {
@ -263,8 +259,7 @@ func (q *commandQueue) Flush() {
indexOffset := 0 indexOffset := 0
for _, c := range cs[:nc] { for _, c := range cs[:nc] {
if err := c.Exec(indexOffset); err != nil { if err := c.Exec(indexOffset); err != nil {
q.err = err return err
return
} }
if recordLog() { if recordLog() {
fmt.Printf("%s\n", c) fmt.Printf("%s\n", c)
@ -282,16 +277,12 @@ func (q *commandQueue) Flush() {
q.nindices = 0 q.nindices = 0
q.tmpNumIndices = 0 q.tmpNumIndices = 0
q.nextIndex = 0 q.nextIndex = 0
} return nil
// Error returns an OpenGL error for the last command.
func Error() error {
return theCommandQueue.err
} }
// FlushCommands flushes the command queue. // FlushCommands flushes the command queue.
func FlushCommands() { func FlushCommands() error {
theCommandQueue.Flush() return theCommandQueue.Flush()
} }
// drawTrianglesCommand represents a drawing command to draw an image on another image. // 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 returns the image's pixels.
// Pixels might return nil when OpenGL error happens. // Pixels might return nil when OpenGL error happens.
func (i *Image) Pixels() []byte { func (i *Image) Pixels() ([]byte, error) {
i.resolveBufferedReplacePixels() i.resolveBufferedReplacePixels()
c := &pixelsCommand{ c := &pixelsCommand{
result: nil, result: nil,
img: i, img: i,
} }
theCommandQueue.Enqueue(c) theCommandQueue.Enqueue(c)
theCommandQueue.Flush() if err := theCommandQueue.Flush(); err != nil {
return c.result return nil, err
}
return c.result, nil
} }
func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) { 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() defer f.Close()
pix, err := i.Pixels()
if err != nil {
return err
}
if err := png.Encode(f, &image.RGBA{ if err := png.Encode(f, &image.RGBA{
Pix: i.Pixels(), Pix: pix,
Stride: 4 * i.width, Stride: 4 * i.width,
Rect: image.Rect(0, 0, i.width, i.height), Rect: image.Rect(0, 0, i.width, i.height),
}); err != nil { }); err != nil {

View File

@ -61,7 +61,10 @@ func TestClear(t *testing.T) {
is := graphics.QuadIndices() is := graphics.QuadIndices()
dst.DrawTriangles(src, vs, is, nil, driver.CompositeModeClear, driver.FilterNearest, driver.AddressClampToZero) 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 j := 0; j < h/2; j++ {
for i := 0; i < w/2; i++ { for i := 0; i < w/2; i++ {
idx := 4 * (i + w*j) idx := 4 * (i + w*j)

View File

@ -92,7 +92,7 @@ func (m *Mipmap) ReplacePixels(pix []byte) {
m.disposeMipmaps() 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) 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) i.drawTrianglesHistory = append(i.drawTrianglesHistory, item)
} }
func (i *Image) readPixelsFromGPUIfNeeded() { func (i *Image) readPixelsFromGPUIfNeeded() error {
if len(i.drawTrianglesHistory) > 0 || i.stale { if len(i.drawTrianglesHistory) > 0 || i.stale {
graphicscommand.FlushCommands() if err := graphicscommand.FlushCommands(); err != nil {
i.readPixelsFromGPU() return err
}
if err := i.readPixelsFromGPU(); err != nil {
return err
}
i.drawTrianglesHistory = nil i.drawTrianglesHistory = nil
i.stale = false i.stale = false
} }
return nil
} }
// At returns a color value at (x, y). // At returns a color value at (x, y).
// //
// Note that this must not be called until context is available. // 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 { 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. // 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. // 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 = 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.drawTrianglesHistory = nil
i.stale = false i.stale = false
return nil
} }
// resolveStale resolves the image's 'stale' state. // resolveStale resolves the image's 'stale' state.
func (i *Image) resolveStale() { func (i *Image) resolveStale() error {
if !needsRestoring() { if !needsRestoring() {
return return nil
} }
if i.volatile { if i.volatile {
return return nil
} }
if i.screen { if i.screen {
return return nil
} }
if !i.stale { if !i.stale {
return return nil
} }
i.readPixelsFromGPU() return i.readPixelsFromGPU()
} }
// dependsOn returns a boolean value indicating whether the image depends on target. // 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. // 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 w, h := i.width, i.height
// Do not dispose the image here. The image should be already disposed. // Do not dispose the image here. The image should be already disposed.
@ -499,12 +512,12 @@ func (i *Image) restore() {
i.basePixels = Pixels{} i.basePixels = Pixels{}
i.drawTrianglesHistory = nil i.drawTrianglesHistory = nil
i.stale = false i.stale = false
return return nil
} }
if i.volatile { if i.volatile {
i.image = graphicscommand.NewImage(w, h) i.image = graphicscommand.NewImage(w, h)
fillImage(i.image, color.RGBA{}) fillImage(i.image, color.RGBA{})
return return nil
} }
if i.stale { if i.stale {
panic("restorable: pixels must not be stale when restoring") panic("restorable: pixels must not be stale when restoring")
@ -528,12 +541,17 @@ func (i *Image) restore() {
if len(i.drawTrianglesHistory) > 0 { if len(i.drawTrianglesHistory) > 0 {
i.basePixels = Pixels{} 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.image = gimg
i.drawTrianglesHistory = nil i.drawTrianglesHistory = nil
i.stale = false i.stale = false
return nil
} }
// Dispose disposes the image. // Dispose disposes the image.
@ -551,10 +569,12 @@ func (i *Image) Dispose() {
// isInvalidated returns a boolean value indicating whether the image is invalidated. // 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. // 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. // FlushCommands is required because c.offscreen.impl might not have an actual texture.
graphicscommand.FlushCommands() if err := graphicscommand.FlushCommands(); err != nil {
return i.image.IsInvalidated() return false, err
}
return i.image.IsInvalidated(), nil
} }
func (i *Image) Dump(path string) error { func (i *Image) Dump(path string) error {

View File

@ -51,12 +51,14 @@ var theImages = &images{
// all stale images. // all stale images.
// //
// ResolveStaleImages is intended to be called at the end of a frame. // ResolveStaleImages is intended to be called at the end of a frame.
func ResolveStaleImages() { func ResolveStaleImages() error {
graphicscommand.FlushCommands() if err := graphicscommand.FlushCommands(); err != nil {
if !needsRestoring() { return err
return
} }
theImages.resolveStaleImages() if !needsRestoring() {
return nil
}
return theImages.resolveStaleImages()
} }
// RestoreIfNeeded restores the images. // RestoreIfNeeded restores the images.
@ -76,7 +78,11 @@ func RestoreIfNeeded() error {
if img.screen { if img.screen {
continue continue
} }
r = img.isInvalidated() var err error
r, err = img.isInvalidated()
if err != nil {
return err
}
break break
} }
if !r { if !r {
@ -87,8 +93,7 @@ func RestoreIfNeeded() error {
if err := graphicscommand.ResetGraphicsDriverState(); err != nil { if err := graphicscommand.ResetGraphicsDriverState(); err != nil {
return err return err
} }
theImages.restore() return theImages.restore()
return nil
} }
// DumpImages dumps all the current images to the specified directory. // DumpImages dumps all the current images to the specified directory.
@ -115,12 +120,15 @@ func (i *images) remove(img *Image) {
} }
// resolveStaleImages resolves stale images. // resolveStaleImages resolves stale images.
func (i *images) resolveStaleImages() { func (i *images) resolveStaleImages() error {
i.lastTarget = nil i.lastTarget = nil
for img := range i.images { 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. // makeStaleIfDependingOn makes all the images stale that depend on target.
// //
@ -147,7 +155,7 @@ func (i *images) makeStaleIfDependingOnImpl(target *Image) {
// restore restores the images. // restore restores the images.
// //
// Restoring means to make all *graphicscommand.Image objects have their textures and framebuffers. // Restoring means to make all *graphicscommand.Image objects have their textures and framebuffers.
func (i *images) restore() { func (i *images) restore() error {
if !needsRestoring() { if !needsRestoring() {
panic("restorable: restore cannot be called when restoring is disabled") panic("restorable: restore cannot be called when restoring is disabled")
} }
@ -211,15 +219,14 @@ func (i *images) restore() {
} }
for _, img := range sorted { for _, img := range sorted {
img.restore() if err := img.restore(); err != nil {
return err
} }
} }
return nil
}
// InitializeGraphicsDriverState initializes the graphics driver state. // InitializeGraphicsDriverState initializes the graphics driver state.
func InitializeGraphicsDriverState() error { func InitializeGraphicsDriverState() error {
return graphicscommand.ResetGraphicsDriverState() 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} clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
img0.ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, 0, 0, 1, 1) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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. // 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -144,7 +148,9 @@ func TestRestoreChain(t *testing.T) {
is := graphics.QuadIndices() is := graphics.QuadIndices()
imgs[i+1].DrawTriangles(imgs[i], vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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) 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) 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) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -320,7 +330,9 @@ func TestRestoreComplexGraph(t *testing.T) {
img7.DrawTriangles(img2, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero) img7.DrawTriangles(img2, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero)
vs = quadVertices(w, h, 2, 0) vs = quadVertices(w, h, 2, 0)
img7.DrawTriangles(img3, vs, is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -411,7 +423,9 @@ func TestRestoreRecursive(t *testing.T) {
is := graphics.QuadIndices() is := graphics.QuadIndices()
img1.DrawTriangles(img0, quadVertices(w, h, 1, 0), is, nil, driver.CompositeModeSourceOver, driver.FilterNearest, driver.AddressClampToZero) 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) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -457,7 +471,10 @@ func TestReplacePixels(t *testing.T) {
// Check the region (5, 7)-(9, 11). Outside state is indeterministic. // Check the region (5, 7)-(9, 11). Outside state is indeterministic.
for j := 7; j < 11; j++ { for j := 7; j < 11; j++ {
for i := 5; i < 9; i++ { 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} got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff} want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want { 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
for j := 7; j < 11; j++ { for j := 7; j < 11; j++ {
for i := 5; i < 9; i++ { 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} got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff} want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want { 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.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) 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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} got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff} want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if !sameColors(got, want, 1) { 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) img0.DrawTriangles(img1, quadVertices(1, 1, 0, 0), is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressClampToZero)
img1.Dispose() img1.Dispose()
ResolveStaleImages() if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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} got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0xff, 0xff, 0xff} want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if !sameColors(got, want, 1) { 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 { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) 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 // Read the pixels. If the implementation is correct, dst tries to read its pixels from GPU due to being
// stale. // stale.
want := byte(0xff) want := byte(0xff)
got, _, _, _ := dst.At(0, 0) got, _, _, _, err := dst.At(0, 0)
if err != nil {
t.Fatal(err)
}
if got != want { if got != want {
t.Errorf("got: %v, want: %v", 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 j := 0; j < h*2; j++ {
for i := 0; i < w*2; i++ { 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) want := byte(0)
if i < w && j < h { if i < w && j < h {
want = pixAt(i, j) want = pixAt(i, j)
@ -773,13 +813,18 @@ func TestFill(t *testing.T) {
const w, h = 16, 16 const w, h = 16, 16
img := NewImage(w, h, false) img := NewImage(w, h, false)
img.Fill(color.RGBA{0xff, 0, 0, 0xff}) img.Fill(color.RGBA{0xff, 0, 0, 0xff})
ResolveStaleImages() if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
for j := 0; j < h; j++ { for j := 0; j < h; j++ {
for i := 0; i < w; i++ { 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} got := color.RGBA{r, g, b, a}
want := color.RGBA{0xff, 0, 0, 0xff} want := color.RGBA{0xff, 0, 0, 0xff}
if got != want { if got != want {
@ -812,16 +857,24 @@ func TestMutateSlices(t *testing.T) {
for i := range is { for i := range is {
is[i] = 0 is[i] = 0
} }
ResolveStaleImages() if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
}
if err := RestoreIfNeeded(); err != nil { if err := RestoreIfNeeded(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
for j := 0; j < h; j++ { for j := 0; j < h; j++ {
for i := 0; i < w; i++ { 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} 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} got := color.RGBA{r, g, b, a}
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("(%d, %d): got %v, want %v", i, j, got, want) t.Errorf("(%d, %d): got %v, want %v", i, j, got, want)

View File

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

View File

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

View File

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

View File

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