diff --git a/image.go b/image.go index 050b46818..776f58e58 100644 --- a/image.go +++ b/image.go @@ -37,11 +37,8 @@ type Image struct { bounds image.Rectangle original *Image - subs map[image.Rectangle]*Image filter Filter - - disposed bool } func (i *Image) copyCheck() { @@ -57,11 +54,7 @@ func (i *Image) Size() (width, height int) { } func (i *Image) isDisposed() bool { - return i.disposed -} - -func (i *Image) isEmpty() bool { - return i.bounds.Dx() == 0 || i.bounds.Dy() == 0 + return i.buffered == nil } func (i *Image) isSubImage() bool { @@ -89,18 +82,13 @@ func (i *Image) Fill(clr color.Color) error { if i.isDisposed() { return nil } - if i.isEmpty() { - return nil - } // TODO: Implement this. if i.isSubImage() { panic("ebiten: render to a subimage is not implemented (Fill)") } - i.desyncSubImages() i.buffered.Fill(color.RGBAModel.Convert(clr).(color.RGBA)) - return nil } @@ -147,27 +135,15 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error { if img.isDisposed() { panic("ebiten: the given image to DrawImage must not be disposed") } - if img.isEmpty() { - return nil - } if i.isDisposed() { return nil } - if i.isEmpty() { - return nil - } // TODO: Implement this. if i.isSubImage() { panic("ebiten: render to a subimage is not implemented (drawImage)") } - if err := img.syncWithOriginalIfNeeded(); err != nil { - theUIContext.setError(err) - return nil - } - i.desyncSubImages() - // Calculate vertices before locking because the user can do anything in // options.ImageParts interface without deadlock (e.g. Call Image functions). if options == nil { @@ -222,9 +198,7 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error { } a, b, c, d, tx, ty := geom.elements32() - w, h := img.Size() - i.buffered.DrawImage(img.buffered, image.Rect(0, 0, w, h), a, b, c, d, tx, ty, options.ColorM.impl, mode, filter) - + i.buffered.DrawImage(img.buffered, img.Bounds(), a, b, c, d, tx, ty, options.ColorM.impl, mode, filter) return nil } @@ -306,12 +280,10 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o if img.isDisposed() { panic("ebiten: the given image to DrawTriangles must not be disposed") } - if img.isEmpty() { - return - } if i.isDisposed() { return } + if i.isSubImage() { panic("ebiten: render to a subimage is not implemented (DrawTriangles)") } @@ -324,12 +296,6 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o } // TODO: Check the maximum value of indices and len(vertices)? - if err := img.syncWithOriginalIfNeeded(); err != nil { - theUIContext.setError(err) - return - } - i.desyncSubImages() - if options == nil { options = &DrawTrianglesOptions{} } @@ -343,19 +309,18 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o filter = driver.Filter(img.filter) } - w, h := img.Size() - bx0 := float32(0) - by0 := float32(0) - bx1 := float32(w) - by1 := float32(h) + b := img.Bounds() + bx0 := float32(b.Min.X) + by0 := float32(b.Min.Y) + bx1 := float32(b.Max.X) + by1 := float32(b.Max.Y) - dx, dy := float32(img.Bounds().Min.X), float32(img.Bounds().Min.Y) vs := make([]float32, len(vertices)*graphics.VertexFloatNum) for i, v := range vertices { vs[i*graphics.VertexFloatNum] = v.DstX vs[i*graphics.VertexFloatNum+1] = v.DstY - vs[i*graphics.VertexFloatNum+2] = v.SrcX - dx - vs[i*graphics.VertexFloatNum+3] = v.SrcY - dy + vs[i*graphics.VertexFloatNum+2] = v.SrcX + vs[i*graphics.VertexFloatNum+3] = v.SrcY vs[i*graphics.VertexFloatNum+4] = bx0 vs[i*graphics.VertexFloatNum+5] = by0 vs[i*graphics.VertexFloatNum+6] = bx1 @@ -395,8 +360,6 @@ func (i *Image) DrawTrianglesWithShader(vertices []Vertex, indices []uint16, sha panic("ebiten: len(indices) must be <= MaxIndicesNum") } - i.desyncSubImages() - if options == nil { options = &DrawTrianglesWithShaderOptions{} } @@ -411,25 +374,16 @@ func (i *Image) DrawTrianglesWithShader(vertices []Vertex, indices []uint16, sha if v.isDisposed() { panic("ebiten: the given image to DrawTriangles must not be disposed") } - if v.isEmpty() { - // TODO: Fix this - panic("ebiten: zero-sized image for DrawTrianglesWithShader is not implemented so far") - } - if err := v.syncWithOriginalIfNeeded(); err != nil { - theUIContext.setError(err) - return - } - us = append(us, v.buffered) if firstImage == nil { firstImage = v } else { - w, h := v.Size() + b := v.Bounds() us = append(us, []float32{ - 0, - 0, - float32(w), - float32(h), + float32(b.Min.X), + float32(b.Min.Y), + float32(b.Max.X), + float32(b.Max.Y), }) } default: @@ -441,24 +395,21 @@ func (i *Image) DrawTrianglesWithShader(vertices []Vertex, indices []uint16, sha // The actual value is set at graphicscommand package. us = append([]interface{}{[]float32{0, 0}}, us...) - var dx, dy, bx0, by0, bx1, by1 float32 + var bx0, by0, bx1, by1 float32 if firstImage != nil { - dx = float32(firstImage.Bounds().Min.X) - dy = float32(firstImage.Bounds().Min.Y) - - w, h := firstImage.Size() - bx0 = float32(0) - by0 = float32(0) - bx1 = float32(w) - by1 = float32(h) + b := firstImage.Bounds() + bx0 = float32(b.Min.X) + by0 = float32(b.Min.Y) + bx1 = float32(b.Max.X) + by1 = float32(b.Max.Y) } vs := make([]float32, len(vertices)*graphics.VertexFloatNum) for i, v := range vertices { vs[i*graphics.VertexFloatNum] = v.DstX vs[i*graphics.VertexFloatNum+1] = v.DstY - vs[i*graphics.VertexFloatNum+2] = v.SrcX - dx - vs[i*graphics.VertexFloatNum+3] = v.SrcY - dy + vs[i*graphics.VertexFloatNum+2] = v.SrcX + vs[i*graphics.VertexFloatNum+3] = v.SrcY vs[i*graphics.VertexFloatNum+4] = bx0 vs[i*graphics.VertexFloatNum+5] = by0 vs[i*graphics.VertexFloatNum+6] = bx1 @@ -488,8 +439,6 @@ func (i *Image) SubImage(r image.Rectangle) image.Image { return nil } - // TODO: Check that SubImage cannot be called on a screen image. - r = r.Intersect(i.Bounds()) // Need to check Empty explicitly. See the standard image package implementations. if r.Empty() { @@ -502,53 +451,17 @@ func (i *Image) SubImage(r image.Rectangle) image.Image { orig = i.original } - if sub, ok := orig.subs[r]; ok { - return sub - } - - // In the initial state, the image doesn't have its own pixels. - // Sync with its original image when necessary. img := &Image{ + buffered: i.buffered, filter: i.filter, bounds: r, original: orig, } img.addr = img - orig.subs[img.Bounds()] = img return img } -func (i *Image) desyncSubImages() { - if i.isSubImage() { - panic("ebiten: desyncSubImages must be called on an original image") - } - - for _, s := range i.subs { - if s.buffered == nil { - continue - } - s.buffered.MarkDisposed() - s.buffered = nil - } -} - -func (i *Image) syncWithOriginalIfNeeded() error { - if !i.isSubImage() { - return nil - } - if i.buffered != nil { - return nil - } - - x, y, width, height := i.bounds.Min.X, i.bounds.Min.Y, i.bounds.Dx(), i.bounds.Dy() - i.buffered = buffered.NewImage(width, height, false) - if err := i.buffered.CopyPixels(i.original.buffered, x, y, width, height); err != nil { - return err - } - return nil -} - // Bounds returns the bounds of the image. func (i *Image) Bounds() image.Rectangle { if i.isDisposed() { @@ -579,11 +492,6 @@ func (i *Image) At(x, y int) color.Color { if !image.Pt(x, y).In(i.Bounds()) { return color.RGBA{} } - - if i.isSubImage() { - return i.original.At(x, y) - } - pix, err := i.buffered.Pixels(x, y, 1, 1) if err != nil { theUIContext.setError(err) @@ -607,27 +515,20 @@ func (i *Image) Set(x, y int, clr color.Color) { if !image.Pt(x, y).In(i.Bounds()) { return } - if i.isSubImage() { - i.original.Set(x, y, clr) - return + i = i.original } r, g, b, a := clr.RGBA() pix := []byte{byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8)} if err := i.buffered.ReplacePixels(pix, x, y, 1, 1); err != nil { theUIContext.setError(err) - return } - i.desyncSubImages() } // Dispose disposes the image data. // After disposing, most of image functions do nothing and returns meaningless values. // -// If the callee is a sub-image, Dispose disposes only the callee. -// If the callee is not a sub-image, Dispose also diposes all its related sub-images. -// // Calling Dispose is not mandatory. GC automatically collects internal resources that no objects refer to. // However, calling Dispose explicitly is helpful if memory usage matters. // @@ -640,25 +541,11 @@ func (i *Image) Dispose() error { if i.isDisposed() { return nil } - - // Get the bound before the image is disposed. - b := i.Bounds() - - if i.buffered != nil { - i.buffered.MarkDisposed() - i.buffered = nil - } - i.disposed = true - - // If a sub-image is disposed, dispose only this image. if i.isSubImage() { - delete(i.original.subs, b) return nil } - - for _, s := range i.subs { - s.Dispose() - } + i.buffered.MarkDisposed() + i.buffered = nil return nil } @@ -675,35 +562,16 @@ func (i *Image) Dispose() error { // // ReplacePixels always returns nil as of 1.5.0-alpha. func (i *Image) ReplacePixels(pix []byte) error { - // TODO: This is not very efficient. Fix this. - if i.isSubImage() { - i.original.replacePixels(pix, i.Bounds().Min.X, i.Bounds().Min.Y, i.Bounds().Dx(), i.Bounds().Dy()) - return nil - } - - w, h := i.Size() - i.replacePixels(pix, 0, 0, w, h) - return nil -} - -func (i *Image) replacePixels(pix []byte, x, y, width, height int) { i.copyCheck() if i.isDisposed() { - return + return nil } - if i.isEmpty() { - return - } - if i.isSubImage() { - panic("ebiten: replacePixels must be called on an original image") - } - - if err := i.buffered.ReplacePixels(pix, x, y, width, height); err != nil { + r := i.Bounds() + if err := i.buffered.ReplacePixels(pix, r.Min.X, r.Min.Y, r.Dx(), r.Dy()); err != nil { theUIContext.setError(err) - return } - i.desyncSubImages() + return nil } // A DrawImageOptions represents options to render an image on an image. @@ -759,7 +627,6 @@ func newImage(width, height int, filter Filter, volatile bool) *Image { buffered: buffered.NewImage(width, height, volatile), filter: filter, bounds: image.Rect(0, 0, width, height), - subs: map[image.Rectangle]*Image{}, } i.addr = i return i @@ -782,7 +649,6 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) { buffered: buffered.NewImage(width, height, false), filter: filter, bounds: image.Rect(0, 0, width, height), - subs: map[image.Rectangle]*Image{}, } i.addr = i diff --git a/image_test.go b/image_test.go index 77d62bd54..065bd0051 100644 --- a/image_test.go +++ b/image_test.go @@ -26,7 +26,6 @@ import ( "testing" . "github.com/hajimehoshi/ebiten" - "github.com/hajimehoshi/ebiten/ebitenutil" "github.com/hajimehoshi/ebiten/examples/resources/images" "github.com/hajimehoshi/ebiten/internal/graphics" t "github.com/hajimehoshi/ebiten/internal/testing" @@ -548,18 +547,18 @@ func TestImageClear(t *testing.T) { // Issue #317, #558, #724 func TestImageEdge(t *testing.T) { + // TODO: This test is not so meaningful after #1218. Do we remove this? + if isGopherJS() { t.Skip("too slow on GopherJS") return } const ( - img0Width = 16 - img0Height = 16 - img0InnerWidth = 10 - img0InnerHeight = 10 - img0OffsetWidth = (img0Width - img0InnerWidth) / 2 - img0OffsetHeight = (img0Height - img0InnerHeight) / 2 + img0Width = 10 + img0Height = 10 + img0InnerWidth = 10 + img0InnerHeight = 10 img1Width = 32 img1Height = 32 @@ -569,19 +568,10 @@ func TestImageEdge(t *testing.T) { for j := 0; j < img0Height; j++ { for i := 0; i < img0Width; i++ { idx := 4 * (i + j*img0Width) - switch { - case img0OffsetWidth <= i && i < img0Width-img0OffsetWidth && - img0OffsetHeight <= j && j < img0Height-img0OffsetHeight: - pixels[idx] = 0xff - pixels[idx+1] = 0 - pixels[idx+2] = 0 - pixels[idx+3] = 0xff - default: - pixels[idx] = 0 - pixels[idx+1] = 0xff - pixels[idx+2] = 0 - pixels[idx+3] = 0xff - } + pixels[idx] = 0xff + pixels[idx+1] = 0 + pixels[idx+2] = 0 + pixels[idx+3] = 0xff } } img0.ReplacePixels(pixels) @@ -598,15 +588,13 @@ func TestImageEdge(t *testing.T) { angles = append(angles, float64(a)/4096*2*math.Pi) } - img0Sub := img0.SubImage(image.Rect(img0OffsetWidth, img0OffsetHeight, img0Width-img0OffsetWidth, img0Height-img0OffsetHeight)).(*Image) - for _, s := range []float64{1, 0.5, 0.25} { for _, f := range []Filter{FilterNearest, FilterLinear} { for _, a := range angles { for _, testDrawTriangles := range []bool{false, true} { img1.Clear() - w, h := img0Sub.Size() - b := img0Sub.Bounds() + w, h := img0.Size() + b := img0.Bounds() var geo GeoM geo.Translate(-float64(w)/2, -float64(h)/2) geo.Scale(s, s) @@ -616,7 +604,7 @@ func TestImageEdge(t *testing.T) { op := &DrawImageOptions{} op.GeoM = geo op.Filter = f - img1.DrawImage(img0Sub, op) + img1.DrawImage(img0, op) } else { op := &DrawTrianglesOptions{} dx0, dy0 := geo.Apply(0, 0) @@ -667,7 +655,7 @@ func TestImageEdge(t *testing.T) { } is := graphics.QuadIndices() op.Filter = f - img1.DrawTriangles(vs, is, img0Sub, op) + img1.DrawTriangles(vs, is, img0, op) } allTransparent := true for j := 0; j < img1Height; j++ { @@ -763,30 +751,6 @@ func TestImageLinearGradiation(t *testing.T) { } } -func TestImageLinearEdges(t *testing.T) { - src, _ := NewImage(32, 32, FilterDefault) - dst, _ := NewImage(64, 64, FilterDefault) - src.Fill(color.RGBA{0, 0xff, 0, 0xff}) - ebitenutil.DrawRect(src, 8, 8, 16, 16, color.RGBA{0xff, 0, 0, 0xff}) - - op := &DrawImageOptions{} - op.GeoM.Translate(8, 8) - op.GeoM.Scale(2, 2) - op.Filter = FilterLinear - dst.DrawImage(src.SubImage(image.Rect(8, 8, 24, 24)).(*Image), op) - - for j := 0; j < 64; j++ { - for i := 0; i < 64; i++ { - c := dst.At(i, j).(color.RGBA) - got := c.G - want := uint8(0) - if abs(int(c.G)-int(want)) > 1 { - t.Errorf("dst At(%d, %d).G: got %#v, want: %#v", i, j, got, want) - } - } - } -} - func TestImageOutside(t *testing.T) { src, _ := NewImage(5, 10, FilterNearest) // internal texture size is 8x16. dst, _ := NewImage(4, 4, FilterNearest) diff --git a/internal/buffered/image.go b/internal/buffered/image.go index c1e8d2500..354a95d7f 100644 --- a/internal/buffered/image.go +++ b/internal/buffered/image.go @@ -198,9 +198,7 @@ func (i *Image) ReplacePixels(pix []byte, x, y, width, height int) error { i.invalidatePendingPixels() // Don't call (*mipmap.Mipmap).ReplacePixels here. Let's defer it to reduce GPU operations as much as - // posssible. This is a necessary optimization for sub-images: as sub-images are actually used and, - // have to allocate their region on a texture atlas, while their original image doesn't have to - // allocate its region on a texture atlas (#896). + // posssible. copied := make([]byte, len(pix)) copy(copied, pix) i.pixels = copied @@ -229,24 +227,6 @@ func (i *Image) replacePendingPixels(pix []byte, x, y, width, height int) { i.needsToResolvePixels = true } -func (i *Image) CopyPixels(img *Image, x, y, width, height int) error { - if tryAddDelayedCommand(func(obj interface{}) error { - i.CopyPixels(img, x, y, width, height) - return nil - }, nil) { - return nil - } - - pix, err := img.Pixels(x, y, width, height) - if err != nil { - return err - } - if err := i.ReplacePixels(pix, 0, 0, width, height); err != nil { - return err - } - return nil -} - func (i *Image) DrawImage(src *Image, bounds image.Rectangle, a, b, c, d, tx, ty float32, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) { if i == src { panic("buffered: Image.DrawImage: src must be different from the receiver")