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 ( import (
"errors" "errors"
"fmt" "fmt"
"image/color"
"github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/affine"
"github.com/hajimehoshi/ebiten/internal/graphics" "github.com/hajimehoshi/ebiten/internal/graphics"
"github.com/hajimehoshi/ebiten/internal/graphicscommand" "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. // drawImageHistoryItem is an item for history of draw-image commands.
type drawImageHistoryItem struct { type drawImageHistoryItem struct {
image *Image image *Image
@ -38,7 +93,7 @@ type drawImageHistoryItem struct {
type Image struct { type Image struct {
image *graphicscommand.Image image *graphicscommand.Image
basePixels []byte basePixels *Pixels
// drawImageHistory is a set of draw-image commands. // drawImageHistory is a set of draw-image commands.
// TODO: This should be merged with the similar command queue in package graphics (#433). // 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) i.image.DrawImage(emptyImage.image, vs, is, nil, c, graphics.FilterNearest, graphics.AddressClampToZero)
if a == 0 { i.basePixels = &Pixels{
i.basePixels = nil color: color.RGBA{r, g, b, a},
} else { length: 4 * w * h,
// 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.drawImageHistory = nil i.drawImageHistory = nil
i.stale = false i.stale = false
@ -173,7 +218,7 @@ func (i *Image) IsVolatile() bool {
} }
// BasePixelsForTesting returns the image's basePixels for testing. // BasePixelsForTesting returns the image's basePixels for testing.
func (i *Image) BasePixelsForTesting() []byte { func (i *Image) BasePixelsForTesting() *Pixels {
return i.basePixels 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 x == 0 && y == 0 && width == w && height == h {
if pixels != nil { if pixels != nil {
if i.basePixels == 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 { } else {
// If basePixels is nil, the restored pixels are cleared. // If basePixels is nil, the restored pixels are cleared.
// See restore() implementation. // See restore() implementation.
@ -268,16 +315,18 @@ func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
idx := 4 * (y*w + x) idx := 4 * (y*w + x)
if pixels != nil { if pixels != nil {
if i.basePixels == 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++ { 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 idx += 4 * w
} }
} else if i.basePixels != nil { } else if i.basePixels != nil {
zeros := make([]byte, 4*width) zeros := make([]byte, 4*width)
for j := 0; j < height; j++ { for j := 0; j < height; j++ {
copy(i.basePixels[idx:idx+4*width], zeros) i.basePixels.CopyFrom(zeros, idx)
idx += 4 * w idx += 4 * w
} }
} }
@ -351,7 +400,7 @@ func (i *Image) At(x, y int) (byte, byte, byte, byte) {
} }
idx := 4*x + 4*y*w 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. // 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. // readPixelsFromGPU reads the pixels from GPU and resolves the image's 'stale' state.
func (i *Image) readPixelsFromGPU() { 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.drawImageHistory = nil
i.stale = false i.stale = false
} }
@ -440,7 +492,7 @@ func (i *Image) restore() error {
gimg := graphicscommand.NewImage(w, h) gimg := graphicscommand.NewImage(w, h)
if i.basePixels != nil { if i.basePixels != nil {
gimg.ReplacePixels(i.basePixels, 0, 0, w, h) gimg.ReplacePixels(i.basePixels.Slice(), 0, 0, w, h)
} else { } else {
// Clear the image explicitly. // Clear the image explicitly.
pix := make([]uint8, w*h*4) pix := make([]uint8, w*h*4)
@ -454,7 +506,10 @@ func (i *Image) restore() error {
} }
i.image = gimg i.image = gimg
i.basePixels = gimg.Pixels() i.basePixels = &Pixels{
pixels: gimg.Pixels(),
length: len(gimg.Pixels()),
}
i.drawImageHistory = nil i.drawImageHistory = nil
i.stale = false i.stale = false
return nil return nil

View File

@ -44,9 +44,9 @@ func TestMain(m *testing.M) {
os.Exit(code) os.Exit(code)
} }
func byteSliceToColor(b []byte, index int) color.RGBA { func pixelsToColor(p *Pixels, index int) color.RGBA {
i := index * 4 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 { 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 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) { func TestRestore(t *testing.T) {
img0 := NewImage(1, 1) img0 := NewImage(1, 1)
defer img0.Dispose() defer img0.Dispose()
clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff} 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() ResolveStaleImages()
if err := Restore(); err != nil { if err := Restore(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := clr0 want := clr0
got := byteSliceToColor(img0.BasePixelsForTesting(), 0) got := pixelsToColor(img0.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want) t.Errorf("got %v, want %v", got, want)
} }
@ -110,7 +98,7 @@ func TestRestoreWithoutDraw(t *testing.T) {
for i := 0; i < 1024*1024; i++ { for i := 0; i < 1024*1024; i++ {
want := color.RGBA{0x00, 0x00, 0x00, 0x00} want := color.RGBA{0x00, 0x00, 0x00, 0x00}
got := byteSliceToColor(img0.BasePixelsForTesting(), i) got := pixelsToColor(img0.BasePixelsForTesting(), i)
if !sameColors(got, want, 0) { if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want) t.Errorf("got %v, want %v", got, want)
} }
@ -130,7 +118,7 @@ func TestRestoreChain(t *testing.T) {
} }
}() }()
clr := color.RGBA{0x00, 0x00, 0x00, 0xff} 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++ { for i := 0; i < num-1; i++ {
w, h := imgs[i].Size() w, h := imgs[i].Size()
vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) 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 want := clr
for i, img := range imgs { for i, img := range imgs {
got := byteSliceToColor(img.BasePixelsForTesting(), 0) got := pixelsToColor(img.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("%d: got %v, want %v", i, got, want) 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} 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} 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} 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) vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
@ -191,7 +179,7 @@ func TestRestoreChain2(t *testing.T) {
if i == 8 || i == 9 { if i == 8 || i == 9 {
want = clr7 want = clr7
} }
got := byteSliceToColor(img.BasePixelsForTesting(), 0) got := pixelsToColor(img.BasePixelsForTesting(), 0)
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("%d: got %v, want %v", i, got, want) 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} clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
clr1 := color.RGBA{0x00, 0x00, 0x01, 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) vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
img2.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) img2.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero)
img3.DrawImage(img2, 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) img1.DrawImage(img0, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero)
ResolveStaleImages() ResolveStaleImages()
if err := Restore(); err != nil { if err := Restore(); err != nil {
@ -234,22 +222,22 @@ func TestRestoreOverrideSource(t *testing.T) {
{ {
"0", "0",
clr1, clr1,
byteSliceToColor(img0.BasePixelsForTesting(), 0), pixelsToColor(img0.BasePixelsForTesting(), 0),
}, },
{ {
"1", "1",
clr1, clr1,
byteSliceToColor(img1.BasePixelsForTesting(), 0), pixelsToColor(img1.BasePixelsForTesting(), 0),
}, },
{ {
"2", "2",
clr0, clr0,
byteSliceToColor(img2.BasePixelsForTesting(), 0), pixelsToColor(img2.BasePixelsForTesting(), 0),
}, },
{ {
"3", "3",
clr0, clr0,
byteSliceToColor(img3.BasePixelsForTesting(), 0), pixelsToColor(img3.BasePixelsForTesting(), 0),
}, },
} }
for _, c := range testCases { for _, c := range testCases {
@ -371,7 +359,7 @@ func TestRestoreComplexGraph(t *testing.T) {
if c.out[i] == '*' { if c.out[i] == '*' {
want = color.RGBA{0xff, 0xff, 0xff, 0xff} want = color.RGBA{0xff, 0xff, 0xff, 0xff}
} }
got := byteSliceToColor(c.image.BasePixelsForTesting(), i) got := pixelsToColor(c.image.BasePixelsForTesting(), i)
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want) 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] == '*' { if c.out[i] == '*' {
want = color.RGBA{0xff, 0xff, 0xff, 0xff} want = color.RGBA{0xff, 0xff, 0xff, 0xff}
} }
got := byteSliceToColor(c.image.BasePixelsForTesting(), i) got := pixelsToColor(c.image.BasePixelsForTesting(), i)
if !sameColors(got, want, 1) { if !sameColors(got, want, 1) {
t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want) 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 { for _, c := range cases {
got := byteSliceToColor(img.BasePixelsForTesting(), c.Index) got := pixelsToColor(img.BasePixelsForTesting(), c.Index)
want := c.Want want := c.Want
if got != want { if got != want {
t.Errorf("base pixel [%d]: got %v, want %v", c.Index, 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: case i%5 == 0:
want = color.RGBA{1, 2, 3, 4} want = color.RGBA{1, 2, 3, 4}
} }
got := byteSliceToColor(img0.BasePixelsForTesting(), i) got := pixelsToColor(img0.BasePixelsForTesting(), i)
if !sameColors(got, want, 0) { if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want) t.Errorf("got %v, want %v", got, want)
} }
@ -645,7 +633,7 @@ func TestReplacePixelsOnly(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
want := color.RGBA{1, 2, 3, 4} want := color.RGBA{1, 2, 3, 4}
got := byteSliceToColor(img1.BasePixelsForTesting(), 0) got := pixelsToColor(img1.BasePixelsForTesting(), 0)
if !sameColors(got, want, 0) { if !sameColors(got, want, 0) {
t.Errorf("got %v, want %v", got, want) 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) dst.ReplacePixels(make([]byte, 4*w*h), 0, 0, w, h)
// Second, draw src to dst. If the implementation is correct, dst becomes stale. // 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) vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
dst.DrawImage(src, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) dst.DrawImage(src, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero)