From 5027bc1af578c0e569f487d5d3451fc24188212f Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 12 Oct 2019 03:09:43 +0900 Subject: [PATCH] buffered: Allow Set before the game runs Fixes #949 --- image.go | 73 ++++--------------------------- internal/buffered/image.go | 76 ++++++++++++++++++++++++++++++++- internal/buffered/image_test.go | 72 +++++++++++++++++++++++++++++++ mipmap.go | 4 ++ 4 files changed, 159 insertions(+), 66 deletions(-) create mode 100644 internal/buffered/image_test.go diff --git a/image.go b/image.go index b743624a2..2a44381eb 100644 --- a/image.go +++ b/image.go @@ -40,8 +40,6 @@ type Image struct { bounds image.Rectangle original *Image - pendingPixels []byte - filter Filter } @@ -92,8 +90,6 @@ func (i *Image) Fill(clr color.Color) error { panic("ebiten: render to a subimage is not implemented (Fill)") } - i.invalidatePendingPixels() - i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA)) 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)") } - img.resolvePendingPixels(true) - i.resolvePendingPixels(false) - // Calculate vertices before locking because the user can do anything in // options.ImageParts interface without deadlock (e.g. Call Image functions). 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)") } - img.resolvePendingPixels(true) - i.resolvePendingPixels(false) - if len(indices)%3 != 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) { return color.RGBA{} } - // TODO: Use pending pixels - i.resolvePendingPixels(true) r, g, b, a := i.mipmap.at(x, y) 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 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. -func (img *Image) Set(x, y int, clr color.Color) { - img.copyCheck() - if img.isDisposed() { +func (i *Image) Set(x, y int, clr color.Color) { + i.copyCheck() + if i.isDisposed() { return } - if !image.Pt(x, y).In(img.Bounds()) { + if !image.Pt(x, y).In(i.Bounds()) { return } - if img.isSubImage() { - img = img.original + if i.isSubImage() { + 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() - img.pendingPixels[4*(x+y*w)] = byte(r >> 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 - } + i.mipmap.set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8)) } // 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 } i.mipmap.dispose() - i.invalidatePendingPixels() return nil } @@ -505,7 +449,6 @@ func (i *Image) ReplacePixels(p []byte) error { if i.isSubImage() { panic("ebiten: render to a subimage is not implemented (ReplacePixels)") } - i.invalidatePendingPixels() s := i.Bounds().Size() 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)) diff --git a/internal/buffered/image.go b/internal/buffered/image.go index e219c18b7..75a9b459a 100644 --- a/internal/buffered/image.go +++ b/internal/buffered/image.go @@ -23,7 +23,11 @@ import ( ) type Image struct { - img *shareable.Image + img *shareable.Image + width int + height int + + pendingPixels []byte } func BeginFrame() error { @@ -44,6 +48,8 @@ func NewImage(width, height int, volatile bool) *Image { if needsToDelayCommands { delayedCommands = append(delayedCommands, func() { i.img = shareable.NewImage(width, height, volatile) + i.width = width + i.height = height }) delayedCommandsM.Unlock() return i @@ -51,6 +57,8 @@ func NewImage(width, height int, volatile bool) *Image { delayedCommandsM.Unlock() i.img = shareable.NewImage(width, height, volatile) + i.width = width + i.height = height return i } @@ -60,6 +68,8 @@ func NewScreenFramebufferImage(width, height int) *Image { if needsToDelayCommands { delayedCommands = append(delayedCommands, func() { i.img = shareable.NewScreenFramebufferImage(width, height) + i.width = width + i.height = height }) delayedCommandsM.Unlock() return i @@ -67,17 +77,38 @@ func NewScreenFramebufferImage(width, height int) *Image { delayedCommandsM.Unlock() i.img = shareable.NewScreenFramebufferImage(width, height) + i.width = width + i.height = height 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() { delayedCommandsM.Lock() if needsToDelayCommands { delayedCommands = append(delayedCommands, func() { i.img.MarkDisposed() }) + delayedCommandsM.Unlock() + return } delayedCommandsM.Unlock() + + i.invalidatePendingPixels() } 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 { panic("buffered: the command queue is not available yet at At") } + // TODO: Use pending pixels + i.resolvePendingPixels(true) 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 { delayedCommandsM.Lock() defer delayedCommandsM.Unlock() @@ -109,6 +179,7 @@ func (i *Image) Fill(clr color.RGBA) { } delayedCommandsM.Unlock() + i.invalidatePendingPixels() i.img.Fill(clr) } @@ -137,6 +208,7 @@ func (i *Image) ReplacePixels(pix []byte) { } delayedCommandsM.Unlock() + i.invalidatePendingPixels() i.img.ReplacePixels(pix) } @@ -155,5 +227,7 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, } delayedCommandsM.Unlock() + src.resolvePendingPixels(true) + i.resolvePendingPixels(false) i.img.DrawTriangles(src.img, vertices, indices, colorm, mode, filter, address) } diff --git a/internal/buffered/image_test.go b/internal/buffered/image_test.go new file mode 100644 index 000000000..e8bec3718 --- /dev/null +++ b/internal/buffered/image_test.go @@ -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) + } +} diff --git a/mipmap.go b/mipmap.go index 1968fe8c4..1180fa052 100644 --- a/mipmap.go +++ b/mipmap.go @@ -77,6 +77,10 @@ func (m *mipmap) at(x, y int) (r, g, b, a byte) { 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) { if det := geom.det(); det == 0 { return