internal/graphicscommand: bug fix: replacePixelsCommand should not read pixels

Reading pixels, writing pixels, and using the image as a source might
cause a flaky behavior with Metal. Stop reading pixels if possible.

Closes #2180
This commit is contained in:
Hajime Hoshi 2022-07-05 13:26:45 +09:00
parent dd805c9414
commit 8522bfd0bf
13 changed files with 195 additions and 477 deletions

View File

@ -495,17 +495,21 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f
} }
// ReplacePixels replaces the pixels on the image. // ReplacePixels replaces the pixels on the image.
func (i *Image) ReplacePixels(pix []byte, mask []byte) { func (i *Image) ReplacePixels(pix []byte, x, y, width, height int) {
backendsM.Lock() backendsM.Lock()
defer backendsM.Unlock() defer backendsM.Unlock()
i.replacePixels(pix, mask) i.replacePixels(pix, x, y, width, height)
} }
func (i *Image) replacePixels(pix []byte, mask []byte) { func (i *Image) replacePixels(pix []byte, x, y, width, height int) {
if i.disposed { if i.disposed {
panic("atlas: the image must not be disposed at replacePixels") panic("atlas: the image must not be disposed at replacePixels")
} }
if l := 4 * width * height; len(pix) != l {
panic(fmt.Sprintf("atlas: len(p) must be %d but %d", l, len(pix)))
}
i.resetUsedAsSourceCount() i.resetUsedAsSourceCount()
if i.backend == nil { if i.backend == nil {
@ -516,29 +520,20 @@ func (i *Image) replacePixels(pix []byte, mask []byte) {
} }
px, py, pw, ph := i.regionWithPadding() px, py, pw, ph := i.regionWithPadding()
if x != 0 || y != 0 || width != i.width || height != i.height || i.paddingSize() == 0 {
x += px + i.paddingSize()
y += py + i.paddingSize()
if pix == nil { if pix == nil {
if mask != nil { i.backend.restorable.ReplacePixels(nil, x, y, width, height)
panic("atlas: mask must be nil when pix is nil")
}
i.backend.restorable.ReplacePixels(nil, nil, px, py, pw, ph)
return return
} }
ow, oh := pw-2*i.paddingSize(), ph-2*i.paddingSize()
if l := 4 * ow * oh; len(pix) != l {
panic(fmt.Sprintf("atlas: len(p) must be %d but %d", l, len(pix)))
}
if i.paddingSize() == 0 {
// Copy pixels in the case when pix is modified before the graphics command is executed. // Copy pixels in the case when pix is modified before the graphics command is executed.
pix2 := make([]byte, len(pix)) pix2 := theTemporaryBytes.alloc(len(pix))
copy(pix2, pix) copy(pix2, pix)
var mask2 []byte i.backend.restorable.ReplacePixels(pix2, x, y, width, height)
if mask != nil {
mask2 = make([]byte, len(mask))
copy(mask2, mask)
}
i.backend.restorable.ReplacePixels(pix2, mask2, px, py, pw, ph)
return return
} }
@ -546,6 +541,7 @@ func (i *Image) replacePixels(pix []byte, mask []byte) {
// Clear the edges. pixb might not be zero-cleared. // Clear the edges. pixb might not be zero-cleared.
// TODO: These loops assume that paddingSize is 1. // TODO: These loops assume that paddingSize is 1.
// TODO: Is clearing edges explicitly really needed?
const paddingSize = 1 const paddingSize = 1
if paddingSize != i.paddingSize() { if paddingSize != i.paddingSize() {
panic(fmt.Sprintf("atlas: replacePixels assumes the padding is always 1 but the actual padding was %d", i.paddingSize())) panic(fmt.Sprintf("atlas: replacePixels assumes the padding is always 1 but the actual padding was %d", i.paddingSize()))
@ -567,45 +563,13 @@ func (i *Image) replacePixels(pix []byte, mask []byte) {
} }
// Copy the content. // Copy the content.
for j := 0; j < oh; j++ { for j := 0; j < height; j++ {
copy(pixb[4*((j+paddingSize)*pw+paddingSize):], pix[4*j*ow:4*(j+1)*ow]) copy(pixb[4*((j+paddingSize)*pw+paddingSize):], pix[4*j*width:4*(j+1)*width])
} }
// Add the paddings to the mask if needed. x += px
if mask != nil { y += py
origMask := mask i.backend.restorable.ReplacePixels(pixb, x, y, pw, ph)
mask = theTemporaryBytes.alloc((pw*ph-1)/8 + 1)
// Clear the allocated region explicitly (#2089).
for i := range mask {
mask[i] = 0
}
for i := 0; i < pw; i++ {
// Top edge
idx := i
mask[idx/8] |= 1 << (idx % 8)
// Bottom edge
idx = (ph-1)*pw + i
mask[idx/8] |= 1 << (idx % 8)
}
for j := 1; j < ph-1; j++ {
// Left edge
idx := j * pw
mask[idx/8] |= 1 << (idx % 8)
// Right edge
idx = j*pw + pw - 1
mask[idx/8] |= 1 << (idx % 8)
// Content
for i := 1; i < pw-1; i++ {
idx := j*pw + i
origIdx := (j-paddingSize)*(pw-paddingSize*2) + i - paddingSize
origValue := (origMask[origIdx/8] >> (origIdx % 8)) & 1
mask[idx/8] |= origValue << (idx % 8)
}
}
}
i.backend.restorable.ReplacePixels(pixb, mask, px, py, pw, ph)
} }
func (img *Image) Pixels(graphicsDriver graphicsdriver.Graphics) ([]byte, error) { func (img *Image) Pixels(graphicsDriver graphicsdriver.Graphics) ([]byte, error) {

View File

@ -63,17 +63,17 @@ func TestEnsureIsolated(t *testing.T) {
img1 := atlas.NewImage(bigSize, 100, atlas.ImageTypeRegular) img1 := atlas.NewImage(bigSize, 100, atlas.ImageTypeRegular)
defer img1.MarkDisposed() defer img1.MarkDisposed()
// Ensure img1's region is allocated. // Ensure img1's region is allocated.
img1.ReplacePixels(make([]byte, 4*bigSize*100), nil) img1.ReplacePixels(make([]byte, 4*bigSize*100), 0, 0, bigSize, 100)
img2 := atlas.NewImage(100, bigSize, atlas.ImageTypeRegular) img2 := atlas.NewImage(100, bigSize, atlas.ImageTypeRegular)
defer img2.MarkDisposed() defer img2.MarkDisposed()
img2.ReplacePixels(make([]byte, 4*100*bigSize), nil) img2.ReplacePixels(make([]byte, 4*100*bigSize), 0, 0, 100, bigSize)
const size = 32 const size = 32
img3 := atlas.NewImage(size/2, size/2, atlas.ImageTypeRegular) img3 := atlas.NewImage(size/2, size/2, atlas.ImageTypeRegular)
defer img3.MarkDisposed() defer img3.MarkDisposed()
img3.ReplacePixels(make([]byte, (size/2)*(size/2)*4), nil) img3.ReplacePixels(make([]byte, (size/2)*(size/2)*4), 0, 0, size/2, size/2)
img4 := atlas.NewImage(size, size, atlas.ImageTypeRegular) img4 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img4.MarkDisposed() defer img4.MarkDisposed()
@ -90,7 +90,7 @@ func TestEnsureIsolated(t *testing.T) {
pix[4*(i+j*size)+3] = byte(i + j) pix[4*(i+j*size)+3] = byte(i + j)
} }
} }
img4.ReplacePixels(pix, nil) img4.ReplacePixels(pix, 0, 0, size, size)
const ( const (
dx0 = size / 4 dx0 = size / 4
@ -157,11 +157,11 @@ func TestReputOnAtlas(t *testing.T) {
img0 := atlas.NewImage(size, size, atlas.ImageTypeRegular) img0 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img0.MarkDisposed() defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*size*size), nil) img0.ReplacePixels(make([]byte, 4*size*size), 0, 0, size, size)
img1 := atlas.NewImage(size, size, atlas.ImageTypeRegular) img1 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img1.MarkDisposed() defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size), nil) img1.ReplacePixels(make([]byte, 4*size*size), 0, 0, size, size)
if got, want := img1.IsOnAtlasForTesting(), true; got != want { if got, want := img1.IsOnAtlasForTesting(), true; got != want {
t.Errorf("got: %v, want: %v", got, want) t.Errorf("got: %v, want: %v", got, want)
} }
@ -177,12 +177,12 @@ func TestReputOnAtlas(t *testing.T) {
pix[4*(i+j*size)+3] = byte(i + j) pix[4*(i+j*size)+3] = byte(i + j)
} }
} }
img2.ReplacePixels(pix, nil) img2.ReplacePixels(pix, 0, 0, size, size)
// Create a volatile image. This should always be isolated. // Create a volatile image. This should always be isolated.
img3 := atlas.NewImage(size, size, atlas.ImageTypeVolatile) img3 := atlas.NewImage(size, size, atlas.ImageTypeVolatile)
defer img3.MarkDisposed() defer img3.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size), nil) img1.ReplacePixels(make([]byte, 4*size*size), 0, 0, size, size)
if got, want := img3.IsOnAtlasForTesting(), false; got != want { if got, want := img3.IsOnAtlasForTesting(), false; got != want {
t.Errorf("got: %v, want: %v", got, want) t.Errorf("got: %v, want: %v", got, want)
} }
@ -271,7 +271,7 @@ func TestReputOnAtlas(t *testing.T) {
if err := atlas.PutImagesOnAtlasForTesting(ui.GraphicsDriverForTesting()); err != nil { if err := atlas.PutImagesOnAtlasForTesting(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
img1.ReplacePixels(make([]byte, 4*size*size), nil) img1.ReplacePixels(make([]byte, 4*size*size), 0, 0, size, size)
img0.DrawTriangles([graphics.ShaderImageNum]*atlas.Image{img1}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) img0.DrawTriangles([graphics.ShaderImageNum]*atlas.Image{img1}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false)
if got, want := img1.IsOnAtlasForTesting(), false; got != want { if got, want := img1.IsOnAtlasForTesting(), false; got != want {
t.Errorf("got: %v, want: %v", got, want) t.Errorf("got: %v, want: %v", got, want)
@ -313,7 +313,7 @@ func TestExtend(t *testing.T) {
p0[4*i+2] = byte(i) p0[4*i+2] = byte(i)
p0[4*i+3] = byte(i) p0[4*i+3] = byte(i)
} }
img0.ReplacePixels(p0, nil) img0.ReplacePixels(p0, 0, 0, w0, h0)
const w1, h1 = minImageSizeForTesting + 1, 100 const w1, h1 = minImageSizeForTesting + 1, 100
img1 := atlas.NewImage(w1, h1, atlas.ImageTypeRegular) img1 := atlas.NewImage(w1, h1, atlas.ImageTypeRegular)
@ -327,7 +327,7 @@ func TestExtend(t *testing.T) {
p1[4*i+3] = byte(i) p1[4*i+3] = byte(i)
} }
// Ensure to allocate // Ensure to allocate
img1.ReplacePixels(p1, nil) img1.ReplacePixels(p1, 0, 0, w1, h1)
pix0, err := img0.Pixels(ui.GraphicsDriverForTesting()) pix0, err := img0.Pixels(ui.GraphicsDriverForTesting())
if err != nil { if err != nil {
@ -382,7 +382,7 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
pix[4*i+2] = byte(i) pix[4*i+2] = byte(i)
pix[4*i+3] = byte(i) pix[4*i+3] = byte(i)
} }
src.ReplacePixels(pix, nil) src.ReplacePixels(pix, 0, 0, w, h)
vs := quadVertices(w, h, 0, 0, 1) vs := quadVertices(w, h, 0, 0, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
@ -393,7 +393,7 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
Height: h, Height: h,
} }
dst.DrawTriangles([graphics.ShaderImageNum]*atlas.Image{src}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false) dst.DrawTriangles([graphics.ShaderImageNum]*atlas.Image{src}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false)
dst.ReplacePixels(pix, nil) dst.ReplacePixels(pix, 0, 0, w, h)
pix, err := dst.Pixels(ui.GraphicsDriverForTesting()) pix, err := dst.Pixels(ui.GraphicsDriverForTesting())
if err != nil { if err != nil {
@ -430,7 +430,7 @@ func TestSmallImages(t *testing.T) {
pix[4*i+2] = 0xff pix[4*i+2] = 0xff
pix[4*i+3] = 0xff pix[4*i+3] = 0xff
} }
src.ReplacePixels(pix, nil) src.ReplacePixels(pix, 0, 0, w, h)
vs := quadVertices(w, h, 0, 0, 1) vs := quadVertices(w, h, 0, 0, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
@ -477,7 +477,7 @@ func TestLongImages(t *testing.T) {
pix[4*i+2] = 0xff pix[4*i+2] = 0xff
pix[4*i+3] = 0xff pix[4*i+3] = 0xff
} }
src.ReplacePixels(pix, nil) src.ReplacePixels(pix, 0, 0, w, h)
const scale = 120 const scale = 120
vs := quadVertices(w, h, 0, 0, scale) vs := quadVertices(w, h, 0, 0, scale)
@ -527,12 +527,12 @@ func TestExtendWithBigImage(t *testing.T) {
img0 := atlas.NewImage(1, 1, atlas.ImageTypeRegular) img0 := atlas.NewImage(1, 1, atlas.ImageTypeRegular)
defer img0.MarkDisposed() defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*1*1), nil) img0.ReplacePixels(make([]byte, 4*1*1), 0, 0, 1, 1)
img1 := atlas.NewImage(minImageSizeForTesting+1, minImageSizeForTesting+1, atlas.ImageTypeRegular) img1 := atlas.NewImage(minImageSizeForTesting+1, minImageSizeForTesting+1, atlas.ImageTypeRegular)
defer img1.MarkDisposed() defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*(minImageSizeForTesting+1)*(minImageSizeForTesting+1)), nil) img1.ReplacePixels(make([]byte, 4*(minImageSizeForTesting+1)*(minImageSizeForTesting+1)), 0, 0, minImageSizeForTesting+1, minImageSizeForTesting+1)
} }
// Issue #1217 // Issue #1217
@ -545,7 +545,7 @@ func TestMaxImageSize(t *testing.T) {
s := maxImageSizeForTesting - 2*paddingSize s := maxImageSizeForTesting - 2*paddingSize
img1 := atlas.NewImage(s, s, atlas.ImageTypeRegular) img1 := atlas.NewImage(s, s, atlas.ImageTypeRegular)
defer img1.MarkDisposed() defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*s*s), nil) img1.ReplacePixels(make([]byte, 4*s*s), 0, 0, s, s)
} }
// Issue #1217 (disabled) // Issue #1217 (disabled)
@ -558,7 +558,7 @@ func Disable_TestMinImageSize(t *testing.T) {
s := minImageSizeForTesting s := minImageSizeForTesting
img := atlas.NewImage(s, s, atlas.ImageTypeRegular) img := atlas.NewImage(s, s, atlas.ImageTypeRegular)
defer img.MarkDisposed() defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s), nil) img.ReplacePixels(make([]byte, 4*s*s), 0, 0, s, s)
} }
func TestMaxImageSizeJust(t *testing.T) { func TestMaxImageSizeJust(t *testing.T) {
@ -567,7 +567,7 @@ func TestMaxImageSizeJust(t *testing.T) {
// TODO: Should we allow such this size for ImageTypeRegular? // TODO: Should we allow such this size for ImageTypeRegular?
img := atlas.NewImage(s, s, atlas.ImageTypeUnmanaged) img := atlas.NewImage(s, s, atlas.ImageTypeUnmanaged)
defer img.MarkDisposed() defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s), nil) img.ReplacePixels(make([]byte, 4*s*s), 0, 0, s, s)
} }
func TestMaxImageSizeExceeded(t *testing.T) { func TestMaxImageSizeExceeded(t *testing.T) {
@ -582,7 +582,7 @@ func TestMaxImageSizeExceeded(t *testing.T) {
} }
}() }()
img.ReplacePixels(make([]byte, 4*(s+1)*s), nil) img.ReplacePixels(make([]byte, 4*(s+1)*s), 0, 0, s+1, s)
} }
// Issue #1421 // Issue #1421
@ -704,7 +704,7 @@ func TestImageReplacePixelsModify(t *testing.T) {
pix[4*(i+j*size)+3] = byte(i + j) pix[4*(i+j*size)+3] = byte(i + j)
} }
} }
img.ReplacePixels(pix, nil) img.ReplacePixels(pix, 0, 0, size, size)
// Modify pix after ReplacePixels. // Modify pix after ReplacePixels.
for j := 0; j < size; j++ { for j := 0; j < size; j++ {

View File

@ -62,9 +62,9 @@ func TestImageDrawTwice(t *testing.T) {
dst := atlas.NewImage(w, h, atlas.ImageTypeRegular) dst := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src0 := atlas.NewImage(w, h, atlas.ImageTypeRegular) src0 := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src0.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff}, nil) src0.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff}, 0, 0, w, h)
src1 := atlas.NewImage(w, h, atlas.ImageTypeRegular) src1 := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src1.ReplacePixels([]byte{0x80, 0x80, 0x80, 0xff}, nil) src1.ReplacePixels([]byte{0x80, 0x80, 0x80, 0xff}, 0, 0, w, h)
vs := quadVertices(w, h, 0, 0, 1) vs := quadVertices(w, h, 0, 0, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()

View File

@ -30,8 +30,6 @@ type Image struct {
height int height int
pixels []byte pixels []byte
mask []byte
needsToResolvePixels bool
} }
func BeginFrame(graphicsDriver graphicsdriver.Graphics) error { func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
@ -68,21 +66,6 @@ func (i *Image) initialize(imageType atlas.ImageType) {
func (i *Image) invalidatePixels() { func (i *Image) invalidatePixels() {
i.pixels = nil i.pixels = nil
i.mask = nil
i.needsToResolvePixels = false
}
func (i *Image) resolvePendingPixels(keepPendingPixels bool) {
if !i.needsToResolvePixels {
return
}
i.img.ReplacePixels(i.pixels, i.mask)
if !keepPendingPixels {
i.pixels = nil
i.mask = nil
}
i.needsToResolvePixels = false
} }
func (i *Image) MarkDisposed() { func (i *Image) MarkDisposed() {
@ -102,23 +85,14 @@ func (img *Image) At(graphicsDriver graphicsdriver.Graphics, x, y int) (r, g, b,
idx := (y*img.width + x) idx := (y*img.width + x)
if img.pixels != nil { if img.pixels != nil {
if img.mask == nil {
return img.pixels[4*idx], img.pixels[4*idx+1], img.pixels[4*idx+2], img.pixels[4*idx+3], nil return img.pixels[4*idx], img.pixels[4*idx+1], img.pixels[4*idx+2], img.pixels[4*idx+3], nil
} }
if img.mask[idx/8]<<(idx%8)&1 != 0 {
return img.pixels[4*idx], img.pixels[4*idx+1], img.pixels[4*idx+2], img.pixels[4*idx+3], nil
}
img.resolvePendingPixels(false)
}
pix, err := img.img.Pixels(graphicsDriver) pix, err := img.img.Pixels(graphicsDriver)
if err != nil { if err != nil {
return 0, 0, 0, 0, err return 0, 0, 0, 0, err
} }
img.pixels = pix img.pixels = pix
// When pixels represents the whole pixels, the mask is not needed.
img.mask = nil
return img.pixels[4*idx], img.pixels[4*idx+1], img.pixels[4*idx+2], img.pixels[4*idx+3], nil return img.pixels[4*idx], img.pixels[4*idx+1], img.pixels[4*idx+2], img.pixels[4*idx+3], nil
} }
@ -143,40 +117,8 @@ func (i *Image) ReplacePixels(pix []byte, x, y, width, height int) {
} }
} }
if x == 0 && y == 0 && width == i.width && height == i.height {
i.invalidatePixels() i.invalidatePixels()
i.img.ReplacePixels(pix, nil) i.img.ReplacePixels(pix, x, y, width, height)
return
}
// TODO: If width/height is big enough, ReplacePixels can be called instead of replacePendingPixels.
// Check if this is efficient.
i.replacePendingPixels(pix, x, y, width, height)
}
func (img *Image) replacePendingPixels(pix []byte, x, y, width, height int) {
if img.pixels == nil {
img.pixels = make([]byte, 4*img.width*img.height)
if img.mask == nil {
img.mask = make([]byte, (img.width*img.height-1)/8+1)
}
}
for j := 0; j < height; j++ {
copy(img.pixels[4*((j+y)*img.width+x):], pix[4*j*width:4*(j+1)*width])
}
// A mask is created only when partial regions are replaced by replacePendingPixels.
if img.mask != nil {
for j := 0; j < height; j++ {
for i := 0; i < width; i++ {
idx := (y+j)*img.width + x + i
img.mask[idx/8] |= 1 << (idx % 8)
}
}
}
img.needsToResolvePixels = true
} }
// DrawTriangles draws the src image with the given vertices. // DrawTriangles draws the src image with the given vertices.
@ -203,22 +145,19 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f
if shader == nil { if shader == nil {
// Fast path for rendering without a shader (#1355). // Fast path for rendering without a shader (#1355).
img := srcs[0] img := srcs[0]
img.resolvePendingPixels(true)
imgs[0] = img.img imgs[0] = img.img
} else { } else {
for i, img := range srcs { for i, img := range srcs {
if img == nil { if img == nil {
continue continue
} }
img.resolvePendingPixels(true)
imgs[i] = img.img imgs[i] = img.img
} }
s = shader.shader s = shader.shader
} }
i.resolvePendingPixels(false)
i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms, evenOdd)
i.invalidatePixels() i.invalidatePixels()
i.img.DrawTriangles(imgs, vertices, indices, colorm, mode, filter, address, dstRegion, srcRegion, subimageOffsets, s, uniforms, evenOdd)
} }
type Shader struct { type Shader struct {

View File

@ -523,38 +523,12 @@ func (c *replacePixelsCommand) String() string {
// Exec executes the replacePixelsCommand. // Exec executes the replacePixelsCommand.
func (c *replacePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { func (c *replacePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
var lastArgIdx int if len(c.args) == 0 {
for i, a := range c.args { return nil
if a.Mask == nil {
continue
} }
if len(c.args[lastArgIdx:i]) > 0 { if err := c.dst.image.ReplacePixels(c.args); err != nil {
c.dst.image.ReplacePixels(c.args[lastArgIdx:i])
lastArgIdx = i
}
orig := make([]byte, 4*c.dst.width*c.dst.height)
if err := c.dst.image.ReadPixels(orig); err != nil {
return err return err
} }
for j := 0; j < a.Height; j++ {
for i := 0; i < a.Width; i++ {
idx := j*a.Width + i
if a.Mask[idx/8]>>(idx%8)&1 == 0 {
srcIdx := (a.Y+j)*c.dst.width + a.X + i
copy(a.Pixels[4*idx:4*(idx+1)], orig[4*srcIdx:4*(srcIdx+1)])
}
}
}
a.Mask = nil
}
if len(c.args[lastArgIdx:]) > 0 {
if err := c.dst.image.ReplacePixels(c.args[lastArgIdx:]); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -166,10 +166,9 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, buf []byte) e
return nil return nil
} }
func (i *Image) ReplacePixels(pixels []byte, mask []byte, x, y, width, height int) { func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
i.bufferedRP = append(i.bufferedRP, &graphicsdriver.ReplacePixelsArgs{ i.bufferedRP = append(i.bufferedRP, &graphicsdriver.ReplacePixelsArgs{
Pixels: pixels, Pixels: pixels,
Mask: mask,
X: x, X: x,
Y: y, Y: y,
Width: width, Width: width,

View File

@ -87,60 +87,11 @@ func TestReplacePixelsPartAfterDrawTriangles(t *testing.T) {
} }
dst.DrawTriangles([graphics.ShaderImageNum]*graphicscommand.Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeClear, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) dst.DrawTriangles([graphics.ShaderImageNum]*graphicscommand.Image{clr}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeClear, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
dst.DrawTriangles([graphics.ShaderImageNum]*graphicscommand.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) dst.DrawTriangles([graphics.ShaderImageNum]*graphicscommand.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
dst.ReplacePixels(make([]byte, 4), nil, 0, 0, 1, 1) dst.ReplacePixels(make([]byte, 4), 0, 0, 1, 1)
// TODO: Check the result. // TODO: Check the result.
} }
func TestReplacePixelsWithMask(t *testing.T) {
const w, h = 4, 3
src := graphicscommand.NewImage(w, h, false)
dst := graphicscommand.NewImage(w, h, false)
vs := quadVertices(src, w, h)
is := graphics.QuadIndices()
dr := graphicsdriver.Region{
X: 0,
Y: 0,
Width: w,
Height: h,
}
dst.DrawTriangles([graphics.ShaderImageNum]*graphicscommand.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeClear, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
pix0 := make([]byte, 4*w*h)
for i := range pix0 {
pix0[i] = 0x40
}
dst.ReplacePixels(pix0, nil, 0, 0, w, h)
pix1 := make([]byte, 4*w*h)
for i := range pix1 {
pix1[i] = 0x80
}
mask1 := []byte{0b11110110, 0b00000110}
dst.ReplacePixels(pix1, mask1, 0, 0, w, h)
readPix := make([]byte, 4*w*h)
if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), readPix); err != nil {
t.Fatal(err)
}
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
idx := 4 * (i + w*j)
got := color.RGBA{readPix[idx], readPix[idx+1], readPix[idx+2], readPix[idx+3]}
var want color.RGBA
if (i != 0 && i != w-1) || (j != 0 && j != h-1) {
want = color.RGBA{0x80, 0x80, 0x80, 0x80}
} else {
want = color.RGBA{0x40, 0x40, 0x40, 0x40}
}
if got != want {
t.Errorf("dst.At(%d, %d) after ReplacePixels: got %v, want: %v", i, j, got, want)
}
}
}
}
func TestShader(t *testing.T) { func TestShader(t *testing.T) {
const w, h = 16, 16 const w, h = 16, 16
clr := graphicscommand.NewImage(w, h, false) clr := graphicscommand.NewImage(w, h, false)

View File

@ -77,7 +77,6 @@ type ImageID int
type ReplacePixelsArgs struct { type ReplacePixelsArgs struct {
Pixels []byte Pixels []byte
Mask []byte
X int X int
Y int Y int
Width int Width int

View File

@ -0,0 +1,93 @@
// Copyright 2022 The Ebitengine 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.
//go:build ignore
// +build ignore
package main
import (
"errors"
"fmt"
"image"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
var regularTermination = errors.New("regular termination")
var (
baseImage *ebiten.Image
derivedImage *ebiten.Image
)
func init() {
const (
w = 36
h = 40
)
baseImage = ebiten.NewImage(w, h)
derivedImage = ebiten.NewImage(w, h)
baseImage.Fill(color.White)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
baseImage.SubImage(image.Rect(i, j, i+1, j+1)).(*ebiten.Image).ReplacePixels([]byte{0, 0, 0, 0xff})
}
}
derivedImage.DrawImage(baseImage, nil)
}
type Game struct {
count int
}
func (g *Game) Update() error {
g.count++
if g.count == 16 {
return regularTermination
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.Fill(color.White)
screen.DrawImage(derivedImage, nil)
if g.count >= 8 {
if got, want := screen.At(0, 0), (color.RGBA{0, 0, 0, 0xff}); got != want {
panic(fmt.Sprintf("got: %v, want: %v", got, want))
}
}
// The blow 3 line matters to reproduce #2154.
mx, my := ebiten.CursorPosition()
msg := fmt.Sprintf("TPS: %.01f; FPS: %.01f; cursor: (%d, %d)", ebiten.CurrentTPS(), ebiten.CurrentFPS(), mx, my)
ebitenutil.DebugPrint(screen, msg)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return 640, 480
}
func main() {
ebiten.SetWindowTitle("Test")
if err := ebiten.RunGame(&Game{}); err != nil && err != regularTermination {
panic(err)
}
}

View File

@ -38,11 +38,11 @@ func (p *Pixels) Apply(img *graphicscommand.Image) {
p.pixelsRecords.apply(img) p.pixelsRecords.apply(img)
} }
func (p *Pixels) AddOrReplace(pix []byte, mask []byte, x, y, width, height int) { func (p *Pixels) AddOrReplace(pix []byte, x, y, width, height int) {
if p.pixelsRecords == nil { if p.pixelsRecords == nil {
p.pixelsRecords = &pixelsRecords{} p.pixelsRecords = &pixelsRecords{}
} }
p.pixelsRecords.addOrReplace(pix, mask, x, y, width, height) p.pixelsRecords.addOrReplace(pix, x, y, width, height)
} }
func (p *Pixels) Clear(x, y, width, height int) { func (p *Pixels) Clear(x, y, width, height int) {
@ -144,7 +144,7 @@ func ensureEmptyImage() *Image {
// As emptyImage is the source at clearImage, initialize this with ReplacePixels, not clearImage. // As emptyImage is the source at clearImage, initialize this with ReplacePixels, not clearImage.
// This operation is also important when restoring emptyImage. // This operation is also important when restoring emptyImage.
emptyImage.ReplacePixels(pix, nil, 0, 0, w, h) emptyImage.ReplacePixels(pix, 0, 0, w, h)
theImages.add(emptyImage) theImages.add(emptyImage)
return emptyImage return emptyImage
} }
@ -267,7 +267,7 @@ func (i *Image) makeStale() {
// ClearPixels clears the specified region by ReplacePixels. // ClearPixels clears the specified region by ReplacePixels.
func (i *Image) ClearPixels(x, y, width, height int) { func (i *Image) ClearPixels(x, y, width, height int) {
i.ReplacePixels(nil, nil, x, y, width, height) i.ReplacePixels(nil, x, y, width, height)
} }
func (i *Image) needsRestoring() bool { func (i *Image) needsRestoring() bool {
@ -277,7 +277,7 @@ func (i *Image) needsRestoring() bool {
// ReplacePixels replaces the image pixels with the given pixels slice. // ReplacePixels replaces the image pixels with the given pixels slice.
// //
// The specified region must not be overlapped with other regions by ReplacePixels. // The specified region must not be overlapped with other regions by ReplacePixels.
func (i *Image) ReplacePixels(pixels []byte, mask []byte, x, y, width, height int) { func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
panic("restorable: width/height must be positive") panic("restorable: width/height must be positive")
} }
@ -291,12 +291,12 @@ func (i *Image) ReplacePixels(pixels []byte, mask []byte, x, y, width, height in
theImages.makeStaleIfDependingOn(i) theImages.makeStaleIfDependingOn(i)
if pixels != nil { if pixels != nil {
i.image.ReplacePixels(pixels, mask, x, y, width, height) i.image.ReplacePixels(pixels, x, y, width, height)
} else { } else {
// TODO: When pixels == nil, we don't have to care the pixel state there. In such cases, the image // TODO: When pixels == nil, we don't have to care the pixel state there. In such cases, the image
// accepts only ReplacePixels and not Fill or DrawTriangles. // accepts only ReplacePixels and not Fill or DrawTriangles.
// TODO: Separate Image struct into two: images for only-ReplacePixels, and the others. // TODO: Separate Image struct into two: images for only-ReplacePixels, and the others.
i.image.ReplacePixels(make([]byte, 4*width*height), nil, x, y, width, height) i.image.ReplacePixels(make([]byte, 4*width*height), x, y, width, height)
} }
if !needsRestoring() || !i.needsRestoring() { if !needsRestoring() || !i.needsRestoring() {
@ -310,7 +310,7 @@ func (i *Image) ReplacePixels(pixels []byte, mask []byte, x, y, width, height in
// This function is responsible to copy this. // This function is responsible to copy this.
copiedPixels := make([]byte, len(pixels)) copiedPixels := make([]byte, len(pixels))
copy(copiedPixels, pixels) copy(copiedPixels, pixels)
i.basePixels.AddOrReplace(copiedPixels, mask, 0, 0, w, h) i.basePixels.AddOrReplace(copiedPixels, 0, 0, w, h)
} else { } else {
i.basePixels.Clear(0, 0, w, h) i.basePixels.Clear(0, 0, w, h)
} }
@ -334,7 +334,7 @@ func (i *Image) ReplacePixels(pixels []byte, mask []byte, x, y, width, height in
// This function is responsible to copy this. // This function is responsible to copy this.
copiedPixels := make([]byte, len(pixels)) copiedPixels := make([]byte, len(pixels))
copy(copiedPixels, pixels) copy(copiedPixels, pixels)
i.basePixels.AddOrReplace(copiedPixels, mask, x, y, width, height) i.basePixels.AddOrReplace(copiedPixels, x, y, width, height)
} else { } else {
i.basePixels.Clear(x, y, width, height) i.basePixels.Clear(x, y, width, height)
} }
@ -490,7 +490,7 @@ func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error
return err return err
} }
i.basePixels = Pixels{} i.basePixels = Pixels{}
i.basePixels.AddOrReplace(pix, nil, 0, 0, i.width, i.height) i.basePixels.AddOrReplace(pix, 0, 0, i.width, i.height)
i.clearDrawTrianglesHistory() i.clearDrawTrianglesHistory()
i.stale = false i.stale = false
return nil return nil
@ -615,7 +615,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
if err := gimg.ReadPixels(graphicsDriver, pix); err != nil { if err := gimg.ReadPixels(graphicsDriver, pix); err != nil {
return err return err
} }
i.basePixels.AddOrReplace(pix, nil, 0, 0, w, h) i.basePixels.AddOrReplace(pix, 0, 0, w, h)
} }
i.image = gimg i.image = gimg

View File

@ -61,7 +61,7 @@ func TestRestore(t *testing.T) {
defer img0.Dispose() defer img0.Dispose()
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}, nil, 0, 0, 1, 1) img0.ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, 0, 0, 1, 1)
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -135,7 +135,7 @@ func TestRestoreChain(t *testing.T) {
} }
}() }()
clr := color.RGBA{0x00, 0x00, 0x00, 0xff} clr := color.RGBA{0x00, 0x00, 0x00, 0xff}
imgs[0].ReplacePixels([]byte{clr.R, clr.G, clr.B, clr.A}, nil, 0, 0, 1, 1) imgs[0].ReplacePixels([]byte{clr.R, clr.G, clr.B, clr.A}, 0, 0, 1, 1)
for i := 0; i < num-1; i++ { for i := 0; i < num-1; i++ {
vs := quadVertices(imgs[i], 1, 1, 0, 0) vs := quadVertices(imgs[i], 1, 1, 0, 0)
is := graphics.QuadIndices() is := graphics.QuadIndices()
@ -180,11 +180,11 @@ func TestRestoreChain2(t *testing.T) {
}() }()
clr0 := color.RGBA{0xff, 0x00, 0x00, 0xff} clr0 := color.RGBA{0xff, 0x00, 0x00, 0xff}
imgs[0].ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, nil, 0, 0, w, h) imgs[0].ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, 0, 0, w, h)
clr7 := color.RGBA{0x00, 0xff, 0x00, 0xff} clr7 := color.RGBA{0x00, 0xff, 0x00, 0xff}
imgs[7].ReplacePixels([]byte{clr7.R, clr7.G, clr7.B, clr7.A}, nil, 0, 0, w, h) imgs[7].ReplacePixels([]byte{clr7.R, clr7.G, clr7.B, clr7.A}, 0, 0, w, h)
clr8 := color.RGBA{0x00, 0x00, 0xff, 0xff} clr8 := color.RGBA{0x00, 0x00, 0xff, 0xff}
imgs[8].ReplacePixels([]byte{clr8.R, clr8.G, clr8.B, clr8.A}, nil, 0, 0, w, h) imgs[8].ReplacePixels([]byte{clr8.R, clr8.G, clr8.B, clr8.A}, 0, 0, w, h)
is := graphics.QuadIndices() is := graphics.QuadIndices()
dr := graphicsdriver.Region{ dr := graphicsdriver.Region{
@ -234,7 +234,7 @@ 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}
img1.ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, nil, 0, 0, w, h) img1.ReplacePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, 0, 0, w, h)
is := graphics.QuadIndices() is := graphics.QuadIndices()
dr := graphicsdriver.Region{ dr := graphicsdriver.Region{
X: 0, X: 0,
@ -244,7 +244,7 @@ func TestRestoreOverrideSource(t *testing.T) {
} }
img2.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img1, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) img2.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img1}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img1, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
img3.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img2, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) img3.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img2}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img2, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
img0.ReplacePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, nil, 0, 0, w, h) img0.ReplacePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, 0, 0, w, h)
img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img0, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, quadVertices(img0, w, h, 0, 0), is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err) t.Fatal(err)
@ -417,7 +417,7 @@ func TestRestoreComplexGraph(t *testing.T) {
func newImageFromImage(rgba *image.RGBA) *restorable.Image { func newImageFromImage(rgba *image.RGBA) *restorable.Image {
s := rgba.Bounds().Size() s := rgba.Bounds().Size()
img := restorable.NewImage(s.X, s.Y, restorable.ImageTypeRegular) img := restorable.NewImage(s.X, s.Y, restorable.ImageTypeRegular)
img.ReplacePixels(rgba.Pix, nil, 0, 0, s.X, s.Y) img.ReplacePixels(rgba.Pix, 0, 0, s.X, s.Y)
return img return img
} }
@ -491,7 +491,7 @@ func TestReplacePixels(t *testing.T) {
for i := range pix { for i := range pix {
pix[i] = 0xff pix[i] = 0xff
} }
img.ReplacePixels(pix, nil, 5, 7, 4, 4) img.ReplacePixels(pix, 5, 7, 4, 4)
// 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++ {
@ -547,7 +547,7 @@ func TestDrawTrianglesAndReplacePixels(t *testing.T) {
Height: 1, Height: 1,
} }
img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
img1.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, nil, 0, 0, 2, 1) img1.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 0, 0, 2, 1)
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err) t.Fatal(err)
@ -618,7 +618,7 @@ func TestReplacePixelsPart(t *testing.T) {
img := restorable.NewImage(4, 4, restorable.ImageTypeRegular) img := restorable.NewImage(4, 4, restorable.ImageTypeRegular)
// This doesn't make the image stale. Its base pixels are available. // This doesn't make the image stale. Its base pixels are available.
img.ReplacePixels(pix, nil, 1, 1, 2, 2) img.ReplacePixels(pix, 1, 1, 2, 2)
cases := []struct { cases := []struct {
i int i int
@ -693,7 +693,7 @@ func TestReplacePixelsOnly(t *testing.T) {
defer img1.Dispose() defer img1.Dispose()
for i := 0; i < w*h; i += 5 { for i := 0; i < w*h; i += 5 {
img0.ReplacePixels([]byte{1, 2, 3, 4}, nil, i%w, i/w, 1, 1) img0.ReplacePixels([]byte{1, 2, 3, 4}, i%w, i/w, 1, 1)
} }
vs := quadVertices(img0, 1, 1, 0, 0) vs := quadVertices(img0, 1, 1, 0, 0)
@ -705,7 +705,7 @@ func TestReplacePixelsOnly(t *testing.T) {
Height: 1, Height: 1,
} }
img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) img1.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{img0}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
img0.ReplacePixels([]byte{5, 6, 7, 8}, nil, 0, 0, 1, 1) img0.ReplacePixels([]byte{5, 6, 7, 8}, 0, 0, 1, 1)
// BasePixelsForTesting is available without GPU accessing. // BasePixelsForTesting is available without GPU accessing.
for j := 0; j < h; j++ { for j := 0; j < h; j++ {
@ -747,14 +747,14 @@ func TestReadPixelsFromVolatileImage(t *testing.T) {
src := restorable.NewImage(w, h, restorable.ImageTypeRegular) src := restorable.NewImage(w, h, restorable.ImageTypeRegular)
// First, make sure that dst has pixels // First, make sure that dst has pixels
dst.ReplacePixels(make([]byte, 4*w*h), nil, 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.
pix := make([]byte, 4*w*h) pix := make([]byte, 4*w*h)
for i := range pix { for i := range pix {
pix[i] = 0xff pix[i] = 0xff
} }
src.ReplacePixels(pix, nil, 0, 0, w, h) src.ReplacePixels(pix, 0, 0, w, h)
vs := quadVertices(src, 1, 1, 0, 0) vs := quadVertices(src, 1, 1, 0, 0)
is := graphics.QuadIndices() is := graphics.QuadIndices()
dr := graphicsdriver.Region{ dr := graphicsdriver.Region{
@ -791,7 +791,7 @@ func TestAllowReplacePixelsAfterDrawTriangles(t *testing.T) {
Height: h, Height: h,
} }
dst.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) dst.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
dst.ReplacePixels(make([]byte, 4*w*h), nil, 0, 0, w, h) dst.ReplacePixels(make([]byte, 4*w*h), 0, 0, w, h)
// ReplacePixels for a whole image doesn't panic. // ReplacePixels for a whole image doesn't panic.
} }
@ -815,7 +815,7 @@ func TestDisallowReplacePixelsForPartAfterDrawTriangles(t *testing.T) {
Height: h, Height: h,
} }
dst.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false) dst.DrawTriangles([graphics.ShaderImageNum]*restorable.Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeSourceOver, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dr, graphicsdriver.Region{}, nil, nil, false)
dst.ReplacePixels(make([]byte, 4), nil, 0, 0, 1, 1) dst.ReplacePixels(make([]byte, 4), 0, 0, 1, 1)
} }
func TestExtend(t *testing.T) { func TestExtend(t *testing.T) {
@ -837,7 +837,7 @@ func TestExtend(t *testing.T) {
} }
} }
orig.ReplacePixels(pix, nil, 0, 0, w, h) orig.ReplacePixels(pix, 0, 0, w, h)
extended := orig.Extend(w*2, h*2) // After this, orig is already disposed. extended := orig.Extend(w*2, h*2) // After this, orig is already disposed.
for j := 0; j < h*2; j++ { for j := 0; j < h*2; j++ {
@ -860,13 +860,13 @@ func TestExtend(t *testing.T) {
func TestClearPixels(t *testing.T) { func TestClearPixels(t *testing.T) {
const w, h = 16, 16 const w, h = 16, 16
img := restorable.NewImage(w, h, restorable.ImageTypeRegular) img := restorable.NewImage(w, h, restorable.ImageTypeRegular)
img.ReplacePixels(make([]byte, 4*4*4), nil, 0, 0, 4, 4) img.ReplacePixels(make([]byte, 4*4*4), 0, 0, 4, 4)
img.ReplacePixels(make([]byte, 4*4*4), nil, 4, 0, 4, 4) img.ReplacePixels(make([]byte, 4*4*4), 4, 0, 4, 4)
img.ClearPixels(0, 0, 4, 4) img.ClearPixels(0, 0, 4, 4)
img.ClearPixels(4, 0, 4, 4) img.ClearPixels(4, 0, 4, 4)
// After clearing, the regions will be available again. // After clearing, the regions will be available again.
img.ReplacePixels(make([]byte, 4*8*4), nil, 0, 0, 8, 4) img.ReplacePixels(make([]byte, 4*8*4), 0, 0, 8, 4)
} }
func TestMutateSlices(t *testing.T) { func TestMutateSlices(t *testing.T) {
@ -880,7 +880,7 @@ func TestMutateSlices(t *testing.T) {
pix[4*i+2] = byte(i) pix[4*i+2] = byte(i)
pix[4*i+3] = 0xff pix[4*i+3] = 0xff
} }
src.ReplacePixels(pix, nil, 0, 0, w, h) src.ReplacePixels(pix, 0, 0, w, h)
vs := quadVertices(src, w, h, 0, 0) vs := quadVertices(src, w, h, 0, 0)
is := make([]uint16, len(graphics.QuadIndices())) is := make([]uint16, len(graphics.QuadIndices()))
@ -937,7 +937,7 @@ func TestOverlappedPixels(t *testing.T) {
pix0[idx+3] = 0xff pix0[idx+3] = 0xff
} }
} }
dst.ReplacePixels(pix0, nil, 0, 0, 2, 2) dst.ReplacePixels(pix0, 0, 0, 2, 2)
pix1 := make([]byte, 4*2*2) pix1 := make([]byte, 4*2*2)
for j := 0; j < 2; j++ { for j := 0; j < 2; j++ {
@ -949,7 +949,7 @@ func TestOverlappedPixels(t *testing.T) {
pix1[idx+3] = 0xff pix1[idx+3] = 0xff
} }
} }
dst.ReplacePixels(pix1, nil, 1, 1, 2, 2) dst.ReplacePixels(pix1, 1, 1, 2, 2)
wantColors := []color.RGBA{ wantColors := []color.RGBA{
{0xff, 0, 0, 0xff}, {0xff, 0, 0, 0xff},
@ -978,7 +978,7 @@ func TestOverlappedPixels(t *testing.T) {
} }
} }
dst.ReplacePixels(nil, nil, 1, 0, 2, 2) dst.ReplacePixels(nil, 1, 0, 2, 2)
wantColors = []color.RGBA{ wantColors = []color.RGBA{
{0xff, 0, 0, 0xff}, {0xff, 0, 0, 0xff},
@ -1017,7 +1017,7 @@ func TestOverlappedPixels(t *testing.T) {
pix2[idx+3] = 0xff pix2[idx+3] = 0xff
} }
} }
dst.ReplacePixels(pix2, nil, 1, 1, 2, 2) dst.ReplacePixels(pix2, 1, 1, 2, 2)
wantColors = []color.RGBA{ wantColors = []color.RGBA{
{0xff, 0, 0, 0xff}, {0xff, 0, 0, 0xff},
@ -1067,151 +1067,3 @@ func TestOverlappedPixels(t *testing.T) {
} }
} }
} }
func TestReplacePixelsWithMask(t *testing.T) {
dst := restorable.NewImage(3, 3, restorable.ImageTypeRegular)
pix0 := make([]byte, 4*2*2)
for j := 0; j < 2; j++ {
for i := 0; i < 2; i++ {
idx := 4 * (j*2 + i)
pix0[idx] = 0xff
pix0[idx+1] = 0
pix0[idx+2] = 0
pix0[idx+3] = 0xff
}
}
dst.ReplacePixels(pix0, nil, 0, 0, 2, 2)
pix1 := make([]byte, 4*2*2)
for j := 0; j < 2; j++ {
for i := 0; i < 2; i++ {
idx := 4 * (j*2 + i)
pix1[idx] = 0
pix1[idx+1] = 0xff
pix1[idx+2] = 0
pix1[idx+3] = 0xff
}
}
mask1 := []byte{0b00001110}
dst.ReplacePixels(pix1, mask1, 1, 1, 2, 2)
wantColors := []color.RGBA{
{0xff, 0, 0, 0xff},
{0xff, 0, 0, 0xff},
{0, 0, 0, 0},
{0xff, 0, 0, 0xff},
{0xff, 0, 0, 0xff},
{0, 0xff, 0, 0xff},
{0, 0, 0, 0},
{0, 0xff, 0, 0xff},
{0, 0xff, 0, 0xff},
}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
r, g, b, a, err := dst.At(ui.GraphicsDriverForTesting(), i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := wantColors[3*j+i]
if got != want {
t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want)
}
}
}
dst.ReplacePixels(nil, nil, 1, 0, 2, 2)
wantColors = []color.RGBA{
{0xff, 0, 0, 0xff},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0xff, 0, 0, 0xff},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0, 0xff, 0, 0xff},
{0, 0xff, 0, 0xff},
}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
r, g, b, a, err := dst.At(ui.GraphicsDriverForTesting(), i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := wantColors[3*j+i]
if got != want {
t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want)
}
}
}
// Update the same region with pix1/mask1.
pix2 := make([]byte, 4*2*2)
for j := 0; j < 2; j++ {
for i := 0; i < 2; i++ {
idx := 4 * (j*2 + i)
pix2[idx] = 0
pix2[idx+1] = 0
pix2[idx+2] = 0xff
pix2[idx+3] = 0xff
}
}
mask2 := []byte{0b00000111}
dst.ReplacePixels(pix2, mask2, 1, 1, 2, 2)
wantColors = []color.RGBA{
{0xff, 0, 0, 0xff},
{0, 0, 0, 0},
{0, 0, 0, 0},
{0xff, 0, 0, 0xff},
{0, 0, 0xff, 0xff},
{0, 0, 0xff, 0xff},
{0, 0, 0, 0},
{0, 0, 0xff, 0xff},
{0, 0xff, 0, 0xff},
}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
r, g, b, a, err := dst.At(ui.GraphicsDriverForTesting(), i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := wantColors[3*j+i]
if got != want {
t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want)
}
}
}
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
r, g, b, a, err := dst.At(ui.GraphicsDriverForTesting(), i, j)
if err != nil {
t.Fatal(err)
}
got := color.RGBA{r, g, b, a}
want := wantColors[3*j+i]
if got != want {
t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want)
}
}
}
}

View File

@ -24,7 +24,6 @@ import (
type pixelsRecord struct { type pixelsRecord struct {
rect image.Rectangle rect image.Rectangle
pix []byte pix []byte
mask []byte
} }
func (p *pixelsRecord) clearIfOverlapped(rect image.Rectangle) { func (p *pixelsRecord) clearIfOverlapped(rect image.Rectangle) {
@ -43,40 +42,11 @@ func (p *pixelsRecord) clearIfOverlapped(rect image.Rectangle) {
} }
} }
func (p *pixelsRecord) merge(pix []byte, mask []byte) {
if len(p.pix) != len(pix) {
panic(fmt.Sprintf("restorable: len(p.pix) (%d) and len(pix) (%d) must match but not", len(p.pix), len(pix)))
}
if mask == nil {
p.pix = pix
return
}
if p.mask == nil {
p.mask = mask
}
if len(p.mask) != len(mask) {
panic(fmt.Sprintf("restorable: len(p.mask) (%d) and len(mask) (%d) must match but not", len(p.mask), len(mask)))
}
for i := 0; i < len(p.pix)/4; i++ {
if mask[i/8]>>(i%8)&1 == 0 {
continue
}
p.mask[i/8] |= 1 << (i % 8)
copy(p.pix[4*i:4*(i+1)], pix[4*i:4*(i+1)])
}
}
func (p *pixelsRecord) at(x, y int) (r, g, b, a byte, ok bool) { func (p *pixelsRecord) at(x, y int) (r, g, b, a byte, ok bool) {
if !image.Pt(x, y).In(p.rect) { if !image.Pt(x, y).In(p.rect) {
return 0, 0, 0, 0, false return 0, 0, 0, 0, false
} }
idx := ((y-p.rect.Min.Y)*p.rect.Dx() + (x - p.rect.Min.X)) idx := ((y-p.rect.Min.Y)*p.rect.Dx() + (x - p.rect.Min.X))
if p.mask != nil && p.mask[idx/8]>>(idx%8)&1 == 0 {
return 0, 0, 0, 0, false
}
return p.pix[4*idx], p.pix[4*idx+1], p.pix[4*idx+2], p.pix[4*idx+3], true return p.pix[4*idx], p.pix[4*idx+1], p.pix[4*idx+2], p.pix[4*idx+3], true
} }
@ -84,7 +54,7 @@ type pixelsRecords struct {
records []*pixelsRecord records []*pixelsRecord
} }
func (pr *pixelsRecords) addOrReplace(pixels []byte, mask []byte, x, y, width, height int) { func (pr *pixelsRecords) addOrReplace(pixels []byte, x, y, width, height int) {
if len(pixels) != 4*width*height { if len(pixels) != 4*width*height {
msg := fmt.Sprintf("restorable: len(pixels) must be 4*%d*%d = %d but %d", width, height, 4*width*height, len(pixels)) msg := fmt.Sprintf("restorable: len(pixels) must be 4*%d*%d = %d but %d", width, height, 4*width*height, len(pixels))
if pixels == nil { if pixels == nil {
@ -94,29 +64,6 @@ func (pr *pixelsRecords) addOrReplace(pixels []byte, mask []byte, x, y, width, h
} }
rect := image.Rect(x, y, x+width, y+height) rect := image.Rect(x, y, x+width, y+height)
if mask != nil {
// If a mask is specified, try merging the records.
var merged bool
for idx := len(pr.records) - 1; idx >= 0; idx-- {
if r := pr.records[idx]; r.rect.Overlaps(rect) {
if r.rect == rect {
r.merge(pixels, mask)
merged = true
}
// If there is an overlap with other regions in the existing records,
// give up modifying them and just add a record.
break
}
}
if !merged {
pr.records = append(pr.records, &pixelsRecord{
rect: rect,
pix: pixels,
mask: mask,
})
}
return
}
// Remove or update the duplicated records first. // Remove or update the duplicated records first.
var n int var n int
@ -171,6 +118,6 @@ func (pr *pixelsRecords) at(i, j int) (r, g, b, a byte, ok bool) {
func (pr *pixelsRecords) apply(img *graphicscommand.Image) { func (pr *pixelsRecords) apply(img *graphicscommand.Image) {
// TODO: Isn't this too heavy? Can we merge the operations? // TODO: Isn't this too heavy? Can we merge the operations?
for _, r := range pr.records { for _, r := range pr.records {
img.ReplacePixels(r.pix, r.mask, r.rect.Min.X, r.rect.Min.Y, r.rect.Dx(), r.rect.Dy()) img.ReplacePixels(r.pix, r.rect.Min.X, r.rect.Min.Y, r.rect.Dx(), r.rect.Dy())
} }
} }

View File

@ -90,7 +90,7 @@ func TestShaderChain(t *testing.T) {
imgs = append(imgs, img) imgs = append(imgs, img)
} }
imgs[0].ReplacePixels([]byte{0xff, 0, 0, 0xff}, nil, 0, 0, 1, 1) imgs[0].ReplacePixels([]byte{0xff, 0, 0, 0xff}, 0, 0, 1, 1)
s := restorable.NewShader(etesting.ShaderProgramImages(1)) s := restorable.NewShader(etesting.ShaderProgramImages(1))
for i := 0; i < num-1; i++ { for i := 0; i < num-1; i++ {
@ -124,9 +124,9 @@ func TestShaderMultipleSources(t *testing.T) {
for i := range srcs { for i := range srcs {
srcs[i] = restorable.NewImage(1, 1, restorable.ImageTypeRegular) srcs[i] = restorable.NewImage(1, 1, restorable.ImageTypeRegular)
} }
srcs[0].ReplacePixels([]byte{0x40, 0, 0, 0xff}, nil, 0, 0, 1, 1) srcs[0].ReplacePixels([]byte{0x40, 0, 0, 0xff}, 0, 0, 1, 1)
srcs[1].ReplacePixels([]byte{0, 0x80, 0, 0xff}, nil, 0, 0, 1, 1) srcs[1].ReplacePixels([]byte{0, 0x80, 0, 0xff}, 0, 0, 1, 1)
srcs[2].ReplacePixels([]byte{0, 0, 0xc0, 0xff}, nil, 0, 0, 1, 1) srcs[2].ReplacePixels([]byte{0, 0, 0xc0, 0xff}, 0, 0, 1, 1)
dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular) dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
@ -163,7 +163,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
0x40, 0, 0, 0xff, 0x40, 0, 0, 0xff,
0, 0x80, 0, 0xff, 0, 0x80, 0, 0xff,
0, 0, 0xc0, 0xff, 0, 0, 0xc0, 0xff,
}, nil, 0, 0, 3, 1) }, 0, 0, 3, 1)
srcs := [graphics.ShaderImageNum]*restorable.Image{src, src, src} srcs := [graphics.ShaderImageNum]*restorable.Image{src, src, src}
dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular) dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)