restorable: Add restorable.Pixels

This commit is contained in:
Hajime Hoshi 2019-02-12 23:31:52 +09:00
parent 93d0b0dd09
commit 6930176fbb
2 changed files with 102 additions and 59 deletions

View File

@ -17,12 +17,67 @@ package restorable
import (
"errors"
"fmt"
"image/color"
"github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/graphicscommand"
)
type Pixels struct {
pixels []byte
length int
// color is used only when pixels == nil
color color.RGBA
}
func (p *Pixels) ensurePixels() {
if p.pixels != nil {
return
}
p.pixels = make([]byte, p.length)
if p.color.A == 0 {
return
}
for i := 0; i < p.length/4; i++ {
p.pixels[4*i] = p.color.R
p.pixels[4*i+1] = p.color.G
p.pixels[4*i+2] = p.color.B
p.pixels[4*i+3] = p.color.A
}
}
func (p *Pixels) CopyFrom(pix []byte, from int) {
p.ensurePixels()
copy(p.pixels[from:from+len(pix)], pix)
}
func (p *Pixels) At(i int) byte {
if i < 0 || p.length <= i {
panic(fmt.Sprintf("restorable: index out of range: %d for length: %d", i, p.length))
}
if p.pixels == nil {
switch i % 4 {
case 0:
return p.color.R
case 1:
return p.color.G
case 2:
return p.color.B
case 3:
return p.color.A
}
}
return p.pixels[i]
}
func (p *Pixels) Slice() []byte {
p.ensurePixels()
return p.pixels
}
// drawImageHistoryItem is an item for history of draw-image commands.
type drawImageHistoryItem struct {
image *Image
@ -38,7 +93,7 @@ type drawImageHistoryItem struct {
type Image struct {
image *graphicscommand.Image
basePixels []byte
basePixels *Pixels
// drawImageHistory is a set of draw-image commands.
// TODO: This should be merged with the similar command queue in package graphics (#433).
@ -150,19 +205,9 @@ func (i *Image) fill(r, g, b, a uint8) {
}
i.image.DrawImage(emptyImage.image, vs, is, nil, c, graphics.FilterNearest, graphics.AddressClampToZero)
if a == 0 {
i.basePixels = nil
} else {
// TODO: Add baseColor?
if i.basePixels == nil {
i.basePixels = make([]byte, 4*w*h)
}
for idx := 0; idx < w*h; idx++ {
i.basePixels[4*idx] = r
i.basePixels[4*idx+1] = g
i.basePixels[4*idx+2] = b
i.basePixels[4*idx+3] = a
}
i.basePixels = &Pixels{
color: color.RGBA{r, g, b, a},
length: 4 * w * h,
}
i.drawImageHistory = nil
i.stale = false
@ -173,7 +218,7 @@ func (i *Image) IsVolatile() bool {
}
// BasePixelsForTesting returns the image's basePixels for testing.
func (i *Image) BasePixelsForTesting() []byte {
func (i *Image) BasePixelsForTesting() *Pixels {
return i.basePixels
}
@ -244,9 +289,11 @@ func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
if x == 0 && y == 0 && width == w && height == h {
if pixels != nil {
if i.basePixels == nil {
i.basePixels = make([]byte, 4*w*h)
i.basePixels = &Pixels{
length: 4 * w * h,
}
copy(i.basePixels, pixels)
}
i.basePixels.CopyFrom(pixels, 0)
} else {
// If basePixels is nil, the restored pixels are cleared.
// See restore() implementation.
@ -268,16 +315,18 @@ func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
idx := 4 * (y*w + x)
if pixels != nil {
if i.basePixels == nil {
i.basePixels = make([]byte, 4*w*h)
i.basePixels = &Pixels{
length: 4 * w * h,
}
}
for j := 0; j < height; j++ {
copy(i.basePixels[idx:idx+4*width], pixels[4*j*width:4*(j+1)*width])
i.basePixels.CopyFrom(pixels[4*j*width:4*(j+1)*width], idx)
idx += 4 * w
}
} else if i.basePixels != nil {
zeros := make([]byte, 4*width)
for j := 0; j < height; j++ {
copy(i.basePixels[idx:idx+4*width], zeros)
i.basePixels.CopyFrom(zeros, idx)
idx += 4 * w
}
}
@ -351,7 +400,7 @@ func (i *Image) At(x, y int) (byte, byte, byte, byte) {
}
idx := 4*x + 4*y*w
return i.basePixels[idx], i.basePixels[idx+1], i.basePixels[idx+2], i.basePixels[idx+3]
return i.basePixels.At(idx), i.basePixels.At(idx + 1), i.basePixels.At(idx + 2), i.basePixels.At(idx + 3)
}
// makeStaleIfDependingOn makes the image stale if the image depends on target.
@ -366,7 +415,10 @@ func (i *Image) makeStaleIfDependingOn(target *Image) {
// readPixelsFromGPU reads the pixels from GPU and resolves the image's 'stale' state.
func (i *Image) readPixelsFromGPU() {
i.basePixels = i.image.Pixels()
i.basePixels = &Pixels{
pixels: i.image.Pixels(),
length: len(i.image.Pixels()),
}
i.drawImageHistory = nil
i.stale = false
}
@ -440,7 +492,7 @@ func (i *Image) restore() error {
gimg := graphicscommand.NewImage(w, h)
if i.basePixels != nil {
gimg.ReplacePixels(i.basePixels, 0, 0, w, h)
gimg.ReplacePixels(i.basePixels.Slice(), 0, 0, w, h)
} else {
// Clear the image explicitly.
pix := make([]uint8, w*h*4)
@ -454,7 +506,10 @@ func (i *Image) restore() error {
}
i.image = gimg
i.basePixels = gimg.Pixels()
i.basePixels = &Pixels{
pixels: gimg.Pixels(),
length: len(gimg.Pixels()),
}
i.drawImageHistory = nil
i.stale = false
return nil

View File

@ -44,9 +44,9 @@ func TestMain(m *testing.M) {
os.Exit(code)
}
func byteSliceToColor(b []byte, index int) color.RGBA {
func pixelsToColor(p *Pixels, index int) color.RGBA {
i := index * 4
return color.RGBA{b[i], b[i+1], b[i+2], b[i+3]}
return color.RGBA{p.At(i), p.At(i + 1), p.At(i + 2), p.At(i + 3)}
}
func abs(x int) int {
@ -68,30 +68,18 @@ func sameColors(c1, c2 color.RGBA, delta int) bool {
abs(int(c1.A)-int(c2.A)) <= delta
}
func fill(img *Image, r, g, b, a uint8) {
w, h := img.Size()
pix := make([]uint8, w*h*4)
for i := 0; i < w*h; i++ {
pix[4*i] = r
pix[4*i+1] = g
pix[4*i+2] = b
pix[4*i+3] = a
}
img.ReplacePixels(pix, 0, 0, w, h)
}
func TestRestore(t *testing.T) {
img0 := NewImage(1, 1)
defer img0.Dispose()
clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
fill(img0, clr0.R, clr0.G, clr0.B, clr0.A)
img0.Fill(clr0.R, clr0.G, clr0.B, clr0.A)
ResolveStaleImages()
if err := Restore(); err != nil {
t.Fatal(err)
}
want := clr0
got := byteSliceToColor(img0.BasePixelsForTesting(), 0)
got := pixelsToColor(img0.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want)
}
@ -110,7 +98,7 @@ func TestRestoreWithoutDraw(t *testing.T) {
for i := 0; i < 1024*1024; i++ {
want := color.RGBA{0x00, 0x00, 0x00, 0x00}
got := byteSliceToColor(img0.BasePixelsForTesting(), i)
got := pixelsToColor(img0.BasePixelsForTesting(), i)
if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want)
}
@ -130,7 +118,7 @@ func TestRestoreChain(t *testing.T) {
}
}()
clr := color.RGBA{0x00, 0x00, 0x00, 0xff}
fill(imgs[0], clr.R, clr.G, clr.B, clr.A)
imgs[0].Fill(clr.R, clr.G, clr.B, clr.A)
for i := 0; i < num-1; i++ {
w, h := imgs[i].Size()
vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
@ -143,7 +131,7 @@ func TestRestoreChain(t *testing.T) {
}
want := clr
for i, img := range imgs {
got := byteSliceToColor(img.BasePixelsForTesting(), 0)
got := pixelsToColor(img.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) {
t.Errorf("%d: got %v, want %v", i, got, want)
}
@ -168,11 +156,11 @@ func TestRestoreChain2(t *testing.T) {
}()
clr0 := color.RGBA{0xff, 0x00, 0x00, 0xff}
fill(imgs[0], clr0.R, clr0.G, clr0.B, clr0.A)
imgs[0].Fill(clr0.R, clr0.G, clr0.B, clr0.A)
clr7 := color.RGBA{0x00, 0xff, 0x00, 0xff}
fill(imgs[7], clr7.R, clr7.G, clr7.B, clr7.A)
imgs[7].Fill(clr7.R, clr7.G, clr7.B, clr7.A)
clr8 := color.RGBA{0x00, 0x00, 0xff, 0xff}
fill(imgs[8], clr8.R, clr8.G, clr8.B, clr8.A)
imgs[8].Fill(clr8.R, clr8.G, clr8.B, clr8.A)
vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices()
@ -191,7 +179,7 @@ func TestRestoreChain2(t *testing.T) {
if i == 8 || i == 9 {
want = clr7
}
got := byteSliceToColor(img.BasePixelsForTesting(), 0)
got := pixelsToColor(img.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) {
t.Errorf("%d: got %v, want %v", i, got, want)
}
@ -215,12 +203,12 @@ func TestRestoreOverrideSource(t *testing.T) {
}()
clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
clr1 := color.RGBA{0x00, 0x00, 0x01, 0xff}
fill(img1, clr0.R, clr0.G, clr0.B, clr0.A)
img1.Fill(clr0.R, clr0.G, clr0.B, clr0.A)
vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices()
img2.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero)
img3.DrawImage(img2, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero)
fill(img0, clr1.R, clr1.G, clr1.B, clr1.A)
img0.Fill(clr1.R, clr1.G, clr1.B, clr1.A)
img1.DrawImage(img0, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero)
ResolveStaleImages()
if err := Restore(); err != nil {
@ -234,22 +222,22 @@ func TestRestoreOverrideSource(t *testing.T) {
{
"0",
clr1,
byteSliceToColor(img0.BasePixelsForTesting(), 0),
pixelsToColor(img0.BasePixelsForTesting(), 0),
},
{
"1",
clr1,
byteSliceToColor(img1.BasePixelsForTesting(), 0),
pixelsToColor(img1.BasePixelsForTesting(), 0),
},
{
"2",
clr0,
byteSliceToColor(img2.BasePixelsForTesting(), 0),
pixelsToColor(img2.BasePixelsForTesting(), 0),
},
{
"3",
clr0,
byteSliceToColor(img3.BasePixelsForTesting(), 0),
pixelsToColor(img3.BasePixelsForTesting(), 0),
},
}
for _, c := range testCases {
@ -371,7 +359,7 @@ func TestRestoreComplexGraph(t *testing.T) {
if c.out[i] == '*' {
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
}
got := byteSliceToColor(c.image.BasePixelsForTesting(), i)
got := pixelsToColor(c.image.BasePixelsForTesting(), i)
if !sameColors(got, want, 1) {
t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want)
}
@ -433,7 +421,7 @@ func TestRestoreRecursive(t *testing.T) {
if c.out[i] == '*' {
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
}
got := byteSliceToColor(c.image.BasePixelsForTesting(), i)
got := pixelsToColor(c.image.BasePixelsForTesting(), i)
if !sameColors(got, want, 1) {
t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want)
}
@ -601,7 +589,7 @@ func TestClear(t *testing.T) {
},
}
for _, c := range cases {
got := byteSliceToColor(img.BasePixelsForTesting(), c.Index)
got := pixelsToColor(img.BasePixelsForTesting(), c.Index)
want := c.Want
if got != want {
t.Errorf("base pixel [%d]: got %v, want %v", c.Index, got, want)
@ -634,7 +622,7 @@ func TestReplacePixelsOnly(t *testing.T) {
case i%5 == 0:
want = color.RGBA{1, 2, 3, 4}
}
got := byteSliceToColor(img0.BasePixelsForTesting(), i)
got := pixelsToColor(img0.BasePixelsForTesting(), i)
if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want)
}
@ -645,7 +633,7 @@ func TestReplacePixelsOnly(t *testing.T) {
t.Fatal(err)
}
want := color.RGBA{1, 2, 3, 4}
got := byteSliceToColor(img1.BasePixelsForTesting(), 0)
got := pixelsToColor(img1.BasePixelsForTesting(), 0)
if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want)
}
@ -664,7 +652,7 @@ func TestReadPixelsFromVolatileImage(t *testing.T) {
dst.ReplacePixels(make([]byte, 4*w*h), 0, 0, w, h)
// Second, draw src to dst. If the implementation is correct, dst becomes stale.
fill(src, 0xff, 0xff, 0xff, 0xff)
src.Fill(0xff, 0xff, 0xff, 0xff)
vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices()
dst.DrawImage(src, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero)