diff --git a/internal/atlas/image.go b/internal/atlas/image.go index e06622305..853dd7d5b 100644 --- a/internal/atlas/image.go +++ b/internal/atlas/image.go @@ -572,45 +572,20 @@ func (i *Image) replacePixels(pix []byte, x, y, width, height int) { i.backend.restorable.ReplacePixels(pixb, x, y, pw, ph) } -func (img *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte) error { +func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte) error { backendsM.Lock() defer backendsM.Unlock() - x := img.paddingSize() - y := img.paddingSize() - - if got, want := len(pixels), 4*img.width*img.height; got != want { - return fmt.Errorf("atlas: len(pixels) must be %d but %d", want, got) - } - - idx := 0 - for j := y; j < y+img.height; j++ { - for i := x; i < x+img.width; i++ { - r, g, b, a, err := img.at(graphicsDriver, i, j) - if err != nil { - return err - } - pixels[4*idx] = r - pixels[4*idx+1] = g - pixels[4*idx+2] = b - pixels[4*idx+3] = a - idx++ + if i.backend == nil || i.backend.restorable == nil { + for i := range pixels { + pixels[i] = 0 } - } - return nil -} - -func (i *Image) at(graphicsDriver graphicsdriver.Graphics, x, y int) (byte, byte, byte, byte, error) { - if i.backend == nil { - return 0, 0, 0, 0, nil + return nil } + ps := i.paddingSize() ox, oy, w, h := i.regionWithPadding() - if x < 0 || y < 0 || x >= w || y >= h { - return 0, 0, 0, 0, nil - } - - return i.backend.restorable.At(graphicsDriver, x+ox, y+oy) + return i.backend.restorable.ReadPixels(graphicsDriver, pixels, ox+ps, oy+ps, w-ps*2, h-ps*2) } // MarkDisposed marks the image as disposed. The actual operation is deferred. diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 83d5dc4f2..b71ad3832 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -54,13 +54,14 @@ func (p *Pixels) Clear(x, y, width, height int) { p.pixelsRecords.clear(x, y, width, height) } -func (p *Pixels) At(i, j int) (byte, byte, byte, byte) { - if p.pixelsRecords != nil { - if r, g, b, a, ok := p.pixelsRecords.at(i, j); ok { - return r, g, b, a +func (p *Pixels) ReadPixels(pixels []byte, x, y, width, height, imageWidth, imageHeight int) { + if p.pixelsRecords == nil { + for i := range pixels { + pixels[i] = 0 } + return } - return 0, 0, 0, 0 + p.pixelsRecords.readPixels(pixels, x, y, width, height, imageWidth, imageHeight) } // drawTrianglesHistoryItem is an item for history of draw-image commands. @@ -447,20 +448,15 @@ func (i *Image) readPixelsFromGPUIfNeeded(graphicsDriver graphicsdriver.Graphics return nil } -// At returns a color value at (x, y). -// -// Note that this must not be called until context is available. -func (i *Image) At(graphicsDriver graphicsdriver.Graphics, x, y int) (byte, byte, byte, byte, error) { - if x < 0 || y < 0 || i.width <= x || i.height <= y { - return 0, 0, 0, 0, nil - } - +func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, x, y, width, height int) error { if err := i.readPixelsFromGPUIfNeeded(graphicsDriver); err != nil { - return 0, 0, 0, 0, err + return err } - - r, g, b, a := i.basePixels.At(x, y) - return r, g, b, a, nil + if got, want := len(pixels), 4*width*height; got != want { + return fmt.Errorf("restorable: len(pixels) must be %d but %d at ReadPixels", want, got) + } + i.basePixels.ReadPixels(pixels, x, y, width, height, i.width, i.height) + return nil } // makeStaleIfDependingOn makes the image stale if the image depends on target. diff --git a/internal/restorable/images_test.go b/internal/restorable/images_test.go index 4e103468a..7df2d08de 100644 --- a/internal/restorable/images_test.go +++ b/internal/restorable/images_test.go @@ -32,9 +32,10 @@ func TestMain(m *testing.M) { etesting.MainWithRunLoop(m) } -func pixelsToColor(p *restorable.Pixels, i, j int) color.RGBA { - r, g, b, a := p.At(i, j) - return color.RGBA{r, g, b, a} +func pixelsToColor(p *restorable.Pixels, i, j, imageWidth, imageHeight int) color.RGBA { + var pix [4]byte + p.ReadPixels(pix[:], i, j, 1, 1, imageWidth, imageHeight) + return color.RGBA{pix[0], pix[1], pix[2], pix[3]} } func abs(x int) int { @@ -69,7 +70,7 @@ func TestRestore(t *testing.T) { t.Fatal(err) } want := clr0 - got := pixelsToColor(img0.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img0.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("got %v, want %v", got, want) } @@ -91,7 +92,7 @@ func TestRestoreWithoutDraw(t *testing.T) { for j := 0; j < 1024; j++ { for i := 0; i < 1024; i++ { want := color.RGBA{0x00, 0x00, 0x00, 0x00} - got := pixelsToColor(img0.BasePixelsForTesting(), i, j) + got := pixelsToColor(img0.BasePixelsForTesting(), i, j, 1024, 1024) if !sameColors(got, want, 0) { t.Errorf("got %v, want %v", got, want) } @@ -155,7 +156,7 @@ func TestRestoreChain(t *testing.T) { } want := clr for i, img := range imgs { - got := pixelsToColor(img.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("%d: got %v, want %v", i, got, want) } @@ -210,7 +211,7 @@ func TestRestoreChain2(t *testing.T) { if i == 8 || i == 9 { want = clr7 } - got := pixelsToColor(img.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, w, h) if !sameColors(got, want, 1) { t.Errorf("%d: got %v, want %v", i, got, want) } @@ -260,22 +261,22 @@ func TestRestoreOverrideSource(t *testing.T) { { "0", clr1, - pixelsToColor(img0.BasePixelsForTesting(), 0, 0), + pixelsToColor(img0.BasePixelsForTesting(), 0, 0, w, h), }, { "1", clr1, - pixelsToColor(img1.BasePixelsForTesting(), 0, 0), + pixelsToColor(img1.BasePixelsForTesting(), 0, 0, w, h), }, { "2", clr0, - pixelsToColor(img2.BasePixelsForTesting(), 0, 0), + pixelsToColor(img2.BasePixelsForTesting(), 0, 0, w, h), }, { "3", clr0, - pixelsToColor(img3.BasePixelsForTesting(), 0, 0), + pixelsToColor(img3.BasePixelsForTesting(), 0, 0, w, h), }, } for _, c := range testCases { @@ -406,7 +407,7 @@ func TestRestoreComplexGraph(t *testing.T) { if c.out[i] == '*' { want = color.RGBA{0xff, 0xff, 0xff, 0xff} } - got := pixelsToColor(c.image.BasePixelsForTesting(), i, 0) + got := pixelsToColor(c.image.BasePixelsForTesting(), i, 0, w, h) if !sameColors(got, want, 1) { t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want) } @@ -475,7 +476,7 @@ func TestRestoreRecursive(t *testing.T) { if c.out[i] == '*' { want = color.RGBA{0xff, 0xff, 0xff, 0xff} } - got := pixelsToColor(c.image.BasePixelsForTesting(), i, 0) + got := pixelsToColor(c.image.BasePixelsForTesting(), i, 0, w, h) if !sameColors(got, want, 1) { t.Errorf("%s[%d]: got %v, want %v", c.name, i, got, want) } @@ -493,16 +494,19 @@ func TestReplacePixels(t *testing.T) { } img.ReplacePixels(pix, 5, 7, 4, 4) // Check the region (5, 7)-(9, 11). Outside state is indeterministic. + for i := range pix { + pix[i] = 0 + } + if err := img.ReadPixels(ui.GraphicsDriverForTesting(), pix, 5, 7, 4, 4); err != nil { + t.Fatal(err) + } for j := 7; j < 11; j++ { for i := 5; i < 9; i++ { - r, g, b, a, err := img.At(ui.GraphicsDriverForTesting(), i, j) - if err != nil { - t.Fatal(err) - } - got := color.RGBA{r, g, b, a} + idx := 4 * ((j-7)*4 + i - 5) + got := color.RGBA{pix[idx], pix[idx+1], pix[idx+2], pix[idx+3]} want := color.RGBA{0xff, 0xff, 0xff, 0xff} if got != want { - t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want) + t.Errorf("(%d, %d): got: %v, want: %v", i, j, got, want) } } } @@ -512,16 +516,16 @@ func TestReplacePixels(t *testing.T) { if err := restorable.RestoreIfNeeded(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) } + if err := img.ReadPixels(ui.GraphicsDriverForTesting(), pix, 5, 7, 4, 4); err != nil { + t.Fatal(err) + } for j := 7; j < 11; j++ { for i := 5; i < 9; i++ { - r, g, b, a, err := img.At(ui.GraphicsDriverForTesting(), i, j) - if err != nil { - t.Fatal(err) - } - got := color.RGBA{r, g, b, a} + idx := 4 * ((j-7)*4 + i - 5) + got := color.RGBA{pix[idx], pix[idx+1], pix[idx+2], pix[idx+3]} want := color.RGBA{0xff, 0xff, 0xff, 0xff} if got != want { - t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want) + t.Errorf("(%d, %d): got: %v, want: %v", i, j, got, want) } } } @@ -555,11 +559,11 @@ func TestDrawTrianglesAndReplacePixels(t *testing.T) { if err := restorable.RestoreIfNeeded(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) } - r, g, b, a, err := img1.At(ui.GraphicsDriverForTesting(), 0, 0) - if err != nil { + var pix [4]byte + if err := img1.ReadPixels(ui.GraphicsDriverForTesting(), pix[:], 0, 0, 1, 1); err != nil { t.Fatal(err) } - got := color.RGBA{r, g, b, a} + got := color.RGBA{pix[0], pix[1], pix[2], pix[3]} want := color.RGBA{0xff, 0xff, 0xff, 0xff} if !sameColors(got, want, 1) { t.Errorf("got: %v, want: %v", got, want) @@ -599,11 +603,11 @@ func TestDispose(t *testing.T) { if err := restorable.RestoreIfNeeded(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) } - r, g, b, a, err := img0.At(ui.GraphicsDriverForTesting(), 0, 0) - if err != nil { + var pix [4]byte + if err := img0.ReadPixels(ui.GraphicsDriverForTesting(), pix[:], 0, 0, 1, 1); err != nil { t.Fatal(err) } - got := color.RGBA{r, g, b, a} + got := color.RGBA{pix[0], pix[1], pix[2], pix[3]} want := color.RGBA{0xff, 0xff, 0xff, 0xff} if !sameColors(got, want, 1) { t.Errorf("got: %v, want: %v", got, want) @@ -677,7 +681,7 @@ func TestReplacePixelsPart(t *testing.T) { }, } for _, c := range cases { - got := pixelsToColor(img.BasePixelsForTesting(), c.i, c.j) + got := pixelsToColor(img.BasePixelsForTesting(), c.i, c.j, 4, 4) want := c.want if got != want { t.Errorf("base pixel (%d, %d): got %v, want %v", c.i, c.j, got, want) @@ -718,7 +722,7 @@ func TestReplacePixelsOnly(t *testing.T) { case idx%5 == 0: want = color.RGBA{1, 2, 3, 4} } - got := pixelsToColor(img0.BasePixelsForTesting(), i, j) + got := pixelsToColor(img0.BasePixelsForTesting(), i, j, w, h) if !sameColors(got, want, 0) { t.Errorf("got %v, want %v", got, want) } @@ -732,7 +736,7 @@ func TestReplacePixelsOnly(t *testing.T) { t.Fatal(err) } want := color.RGBA{1, 2, 3, 4} - got := pixelsToColor(img1.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img1.BasePixelsForTesting(), 0, 0, w, h) if !sameColors(got, want, 0) { t.Errorf("got %v, want %v", got, want) } @@ -768,10 +772,12 @@ func TestReadPixelsFromVolatileImage(t *testing.T) { // Read the pixels. If the implementation is correct, dst tries to read its pixels from GPU due to being // stale. want := byte(0xff) - got, _, _, _, err := dst.At(ui.GraphicsDriverForTesting(), 0, 0) - if err != nil { + + var result [4]byte + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), result[:], 0, 0, 1, 1); err != nil { t.Fatal(err) } + got := result[0] if got != want { t.Errorf("got: %v, want: %v", got, want) } @@ -840,12 +846,13 @@ func TestExtend(t *testing.T) { orig.ReplacePixels(pix, 0, 0, w, h) extended := orig.Extend(w*2, h*2) // After this, orig is already disposed. + result := make([]byte, 4*(w*2)*(h*2)) + if err := extended.ReadPixels(ui.GraphicsDriverForTesting(), result, 0, 0, w*2, h*2); err != nil { + t.Fatal(err) + } for j := 0; j < h*2; j++ { for i := 0; i < w*2; i++ { - got, _, _, _, err := extended.At(ui.GraphicsDriverForTesting(), i, j) - if err != nil { - t.Fatal(err) - } + got := result[4*(j*(w*2)+i)] want := byte(0) if i < w && j < h { want = pixAt(i, j) @@ -905,18 +912,20 @@ func TestMutateSlices(t *testing.T) { t.Fatal(err) } + srcPix := make([]byte, 4*w*h) + if err := src.ReadPixels(ui.GraphicsDriverForTesting(), srcPix, 0, 0, w, h); err != nil { + t.Fatal(err) + } + dstPix := make([]byte, 4*w*h) + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), dstPix, 0, 0, w, h); err != nil { + t.Fatal(err) + } + for j := 0; j < h; j++ { for i := 0; i < w; i++ { - r, g, b, a, err := src.At(ui.GraphicsDriverForTesting(), i, j) - if err != nil { - t.Fatal(err) - } - want := color.RGBA{r, g, b, a} - r, g, b, a, err = dst.At(ui.GraphicsDriverForTesting(), i, j) - if err != nil { - t.Fatal(err) - } - got := color.RGBA{r, g, b, a} + idx := 4 * (j*w + i) + want := color.RGBA{srcPix[idx], srcPix[idx+1], srcPix[idx+2], srcPix[idx+3]} + got := color.RGBA{dstPix[idx], dstPix[idx+1], dstPix[idx+2], dstPix[idx+3]} if !sameColors(got, want, 1) { t.Errorf("(%d, %d): got %v, want %v", i, j, got, want) } @@ -964,13 +973,15 @@ func TestOverlappedPixels(t *testing.T) { {0, 0xff, 0, 0xff}, {0, 0xff, 0, 0xff}, } + + result := make([]byte, 4*3*3) + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), result, 0, 0, 3, 3); 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} + idx := 4 * (j*3 + i) + got := color.RGBA{result[idx], result[idx+1], result[idx+2], result[idx+3]} want := wantColors[3*j+i] if got != want { t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want) @@ -993,13 +1004,13 @@ func TestOverlappedPixels(t *testing.T) { {0, 0xff, 0, 0xff}, {0, 0xff, 0, 0xff}, } + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), result, 0, 0, 3, 3); 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} + idx := 4 * (j*3 + i) + got := color.RGBA{result[idx], result[idx+1], result[idx+2], result[idx+3]} want := wantColors[3*j+i] if got != want { t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want) @@ -1032,13 +1043,13 @@ func TestOverlappedPixels(t *testing.T) { {0, 0, 0xff, 0xff}, {0, 0, 0xff, 0xff}, } + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), result, 0, 0, 3, 3); 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} + idx := 4 * (j*3 + i) + got := color.RGBA{result[idx], result[idx+1], result[idx+2], result[idx+3]} want := wantColors[3*j+i] if got != want { t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want) @@ -1053,13 +1064,13 @@ func TestOverlappedPixels(t *testing.T) { t.Fatal(err) } + if err := dst.ReadPixels(ui.GraphicsDriverForTesting(), result, 0, 0, 3, 3); 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} + idx := 4 * (j*3 + i) + got := color.RGBA{result[idx], result[idx+1], result[idx+2], result[idx+3]} want := wantColors[3*j+i] if got != want { t.Errorf("color at (%d, %d): got %v, want: %v", i, j, got, want) diff --git a/internal/restorable/rect.go b/internal/restorable/rect.go index 3528cb881..fa2295af0 100644 --- a/internal/restorable/rect.go +++ b/internal/restorable/rect.go @@ -42,12 +42,22 @@ func (p *pixelsRecord) clearIfOverlapped(rect image.Rectangle) { } } -func (p *pixelsRecord) at(x, y int) (r, g, b, a byte, ok bool) { - if !image.Pt(x, y).In(p.rect) { - return 0, 0, 0, 0, false +func (p *pixelsRecord) readPixels(pixels []byte, x, y, width, height, imageWidth, imageHeight int) { + r := p.rect.Intersect(image.Rect(x, y, x+width, y+height)).Intersect(image.Rect(0, 0, imageWidth, imageHeight)) + if r.Empty() { + return + } + + dstBaseX := r.Min.X - x + dstBaseY := r.Min.Y - y + srcBaseX := r.Min.X - p.rect.Min.X + srcBaseY := r.Min.Y - p.rect.Min.Y + lineWidth := 4 * r.Dx() + for j := 0; j < r.Dy(); j++ { + dstX := 4 * ((dstBaseY+j)*width + dstBaseX) + srcX := 4 * ((srcBaseY+j)*p.rect.Dx() + srcBaseX) + copy(pixels[dstX:dstX+lineWidth], p.pix[srcX:srcX+lineWidth]) } - idx := ((y-p.rect.Min.Y)*p.rect.Dx() + (x - p.rect.Min.X)) - return p.pix[4*idx], p.pix[4*idx+1], p.pix[4*idx+2], p.pix[4*idx+3], true } type pixelsRecords struct { @@ -103,16 +113,13 @@ func (pr *pixelsRecords) clear(x, y, width, height int) { pr.records = pr.records[:n] } -func (pr *pixelsRecords) at(i, j int) (r, g, b, a byte, ok bool) { - // Traverse the slice in the reversed order. - for idx := len(pr.records) - 1; idx >= 0; idx-- { - r, g, b, a, ok := pr.records[idx].at(i, j) - if ok { - return r, g, b, a, true - } +func (pr *pixelsRecords) readPixels(pixels []byte, x, y, width, height, imageWidth, imageHeight int) { + for i := range pixels { + pixels[i] = 0 + } + for _, r := range pr.records { + r.readPixels(pixels, x, y, width, height, imageWidth, imageHeight) } - - return 0, 0, 0, 0, false } func (pr *pixelsRecords) apply(img *graphicscommand.Image) { diff --git a/internal/restorable/shader_test.go b/internal/restorable/shader_test.go index 051432d78..91edcfceb 100644 --- a/internal/restorable/shader_test.go +++ b/internal/restorable/shader_test.go @@ -75,7 +75,7 @@ func TestShader(t *testing.T) { } want := color.RGBA{0xff, 0, 0, 0xff} - got := pixelsToColor(img.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("got %v, want %v", got, want) } @@ -112,7 +112,7 @@ func TestShaderChain(t *testing.T) { for i, img := range imgs { want := color.RGBA{0xff, 0, 0, 0xff} - got := pixelsToColor(img.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("%d: got %v, want %v", i, got, want) } @@ -151,7 +151,7 @@ func TestShaderMultipleSources(t *testing.T) { } want := color.RGBA{0x40, 0x80, 0xc0, 0xff} - got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("got %v, want %v", got, want) } @@ -192,7 +192,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) { } want := color.RGBA{0x40, 0x80, 0xc0, 0xff} - got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("got %v, want %v", got, want) } @@ -223,7 +223,7 @@ func TestShaderDispose(t *testing.T) { } want := color.RGBA{0xff, 0, 0, 0xff} - got := pixelsToColor(img.BasePixelsForTesting(), 0, 0) + got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1) if !sameColors(got, want, 1) { t.Errorf("got %v, want %v", got, want) }