diff --git a/image_test.go b/image_test.go index 687146a97..614aef04f 100644 --- a/image_test.go +++ b/image_test.go @@ -2065,3 +2065,27 @@ func BenchmarkImageDrawOver(b *testing.B) { draw.Draw(dst, dst.Bounds(), src, image.ZP, draw.Over) } } + +// Issue #1171 +func TestImageFloatTranslate(t *testing.T) { + const w, h = 32, 32 + + dst, _ := NewImage(320, 240, FilterDefault) + src, _ := NewImage(w, h, FilterDefault) + src.Fill(color.RGBA{0xff, 0, 0, 0xff}) + + op := &DrawImageOptions{} + op.GeoM.Scale(2, 2) + op.GeoM.Translate(0, 0.501) + dst.DrawImage(src, op) + + for j := 1; j < h*2+1; j++ { + for i := 0; i < w*2; i++ { + got := dst.At(i, j) + want := color.RGBA{0xff, 0, 0, 0xff} + if got != want { + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +} diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 3bb964806..45eaa3a5b 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -208,8 +208,7 @@ func fract(x float32) float32 { } const ( - dstAdjustmentFactor = 1.0 / 256.0 - texelAdjustmentFactor = 1.0 / 512.0 + dstAdjustmentFactor = 1.0 / 256.0 ) // Flush flushes the command queue. @@ -254,10 +253,6 @@ func (q *commandQueue) Flush() error { case 0.5 <= f && f < 0.5+dstAdjustmentFactor: vs[i*graphics.VertexFloatNum+1] += (0.5 + dstAdjustmentFactor) - f } - - // Adjust regions not to violate neighborhoods (#317, #558, #724). - vs[i*graphics.VertexFloatNum+6] -= 1.0 / s.width * texelAdjustmentFactor - vs[i*graphics.VertexFloatNum+7] -= 1.0 / s.height * texelAdjustmentFactor } } else { n := q.nvertices / graphics.VertexFloatNum @@ -442,12 +437,6 @@ func (c *drawTrianglesCommand) Exec(indexOffset int) error { region[3] / float32(h), } - // Adjust regions not to violate neighborhoods (#317, #558, #724). - if theGraphicsDriver.HasHighPrecisionFloat() { - vs[2] -= 1.0 / float32(w) * texelAdjustmentFactor - vs[3] -= 1.0 / float32(h) * texelAdjustmentFactor - } - us[i] = vs default: us[i] = v diff --git a/internal/shareable/image.go b/internal/shareable/image.go index 97a9987e3..1b5bf1387 100644 --- a/internal/shareable/image.go +++ b/internal/shareable/image.go @@ -28,6 +28,12 @@ import ( "github.com/hajimehoshi/ebiten/internal/restorable" ) +const ( + // borderSize represents the size of border around an image. + // Every image or node except for a screen image has its border. + borderSize = 1 +) + var graphicsDriver driver.Graphics func SetGraphicsDriver(graphics driver.Graphics) { @@ -197,7 +203,7 @@ func (i *Image) ensureNotShared() { return } - ox, oy, w, h := i.region() + ox, oy, w, h := i.regionWithBorder() dx0 := float32(0) dy0 := float32(0) dx1 := float32(w) @@ -240,7 +246,7 @@ func (i *Image) makeShared() error { pixels := make([]byte, 4*i.width*i.height) for y := 0; y < i.height; y++ { for x := 0; x < i.width; x++ { - r, g, b, a, err := i.at(x, y) + r, g, b, a, err := i.at(x+borderSize, y+borderSize) if err != nil { return err } @@ -256,12 +262,12 @@ func (i *Image) makeShared() error { return nil } -func (i *Image) region() (x, y, width, height int) { +func (i *Image) regionWithBorder() (x, y, width, height int) { if i.backend == nil { panic("shareable: backend must not be nil: not allocated yet?") } if !i.isShared() { - return 0, 0, i.width, i.height + return 0, 0, i.width + 2*borderSize, i.height + 2*borderSize } return i.node.Region() } @@ -321,12 +327,22 @@ func (i *Image) DrawTriangles(img *Image, vertices []float32, indices []uint16, } } + var dx, dy float32 + // A screen image doesn't have its border. + if !i.screen { + dx = borderSize + dy = borderSize + } var oxf, oyf float32 if len(srcs) > 0 { - ox, oy, _, _ := srcs[0].region() + ox, oy, _, _ := srcs[0].regionWithBorder() + ox += borderSize + oy += borderSize oxf, oyf = float32(ox), float32(oy) n := len(vertices) / graphics.VertexFloatNum for i := 0; i < n; i++ { + vertices[i*graphics.VertexFloatNum+0] += dx + vertices[i*graphics.VertexFloatNum+1] += dy vertices[i*graphics.VertexFloatNum+2] += oxf vertices[i*graphics.VertexFloatNum+3] += oyf vertices[i*graphics.VertexFloatNum+4] += oxf @@ -420,19 +436,33 @@ func (i *Image) replacePixels(pix []byte) { i.allocate(true) } - x, y, w, h := i.region() - if pix != nil { - if l := 4 * w * h; len(pix) != l { - panic(fmt.Sprintf("shareable: len(p) must be %d but %d", l, len(pix))) - } + x, y, w, h := i.regionWithBorder() + if pix == nil { + i.backend.restorable.ReplacePixels(nil, x, y, w, h) + return } - i.backend.restorable.ReplacePixels(pix, x, y, w, h) + + ow, oh := w-2*borderSize, h-2*borderSize + if l := 4 * ow * oh; len(pix) != l { + panic(fmt.Sprintf("shareable: len(p) must be %d but %d", l, len(pix))) + } + + // Add a border around the image. + pixb := make([]byte, 4*w*h) + for j := 0; j < oh; j++ { + copy(pixb[4*((j+borderSize)*w+borderSize):], pix[4*j*ow:4*(j+1)*ow]) + } + + i.backend.restorable.ReplacePixels(pixb, x, y, w, h) } func (img *Image) Pixels(x, y, width, height int) ([]byte, error) { backendsM.Lock() defer backendsM.Unlock() + x += borderSize + y += borderSize + bs := make([]byte, 4*width*height) idx := 0 for j := y; j < y+height; j++ { @@ -456,7 +486,7 @@ func (i *Image) at(x, y int) (byte, byte, byte, byte, error) { return 0, 0, 0, 0, nil } - ox, oy, w, h := i.region() + ox, oy, w, h := i.regionWithBorder() if x < 0 || y < 0 || x >= w || y >= h { return 0, 0, 0, 0, nil } @@ -506,7 +536,7 @@ func (i *Image) dispose(markDisposed bool) { i.backend.page.Free(i.node) if !i.backend.page.IsEmpty() { // As this part can be reused, this should be cleared explicitly. - i.backend.restorable.ClearPixels(i.region()) + i.backend.restorable.ClearPixels(i.regionWithBorder()) return } @@ -554,6 +584,7 @@ func (i *Image) allocate(shareable bool) { runtime.SetFinalizer(i, (*Image).MarkDisposed) if i.screen { + // A screen image doesn't have a border. i.backend = &backend{ restorable: restorable.NewScreenFramebufferImage(i.width, i.height), } @@ -562,13 +593,13 @@ func (i *Image) allocate(shareable bool) { if !shareable || !i.shareable() { i.backend = &backend{ - restorable: restorable.NewImage(i.width, i.height, i.volatile), + restorable: restorable.NewImage(i.width+2*borderSize, i.height+2*borderSize, i.volatile), } return } for _, b := range theBackends { - if n, ok := b.TryAlloc(i.width, i.height); ok { + if n, ok := b.TryAlloc(i.width+2*borderSize, i.height+2*borderSize); ok { i.backend = b i.node = n return @@ -588,7 +619,7 @@ func (i *Image) allocate(shareable bool) { } theBackends = append(theBackends, b) - n := b.page.Alloc(i.width, i.height) + n := b.page.Alloc(i.width+2*borderSize, i.height+2*borderSize) if n == nil { panic("shareable: Alloc result must not be nil at allocate") } diff --git a/internal/shareable/image_test.go b/internal/shareable/image_test.go index a7b394214..f8e5d10ee 100644 --- a/internal/shareable/image_test.go +++ b/internal/shareable/image_test.go @@ -191,7 +191,7 @@ func TestReshared(t *testing.T) { a := pix[4*(size*j+i)+3] got := color.RGBA{r, g, b, a} if got != want { - t.Errorf("got: %v, want: %v", got, want) + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) } } } @@ -214,7 +214,7 @@ func TestReshared(t *testing.T) { a := pix[4*(size*j+i)+3] got := color.RGBA{r, g, b, a} if got != want { - t.Errorf("got: %v, want: %v", got, want) + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) } } }