diff --git a/export_test.go b/export_test.go index ec70535fb..323c9fbf2 100644 --- a/export_test.go +++ b/export_test.go @@ -15,6 +15,5 @@ package ebiten var ( - CopyImage = copyImage - MipmapLevelForDownscale = mipmapLevelForDownscale + CopyImage = copyImage ) diff --git a/image.go b/image.go index 1cacc341b..9dfda9ab9 100644 --- a/image.go +++ b/image.go @@ -19,6 +19,7 @@ import ( "image" "image/color" + "github.com/hajimehoshi/ebiten/internal/buffered" "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/graphics" ) @@ -33,9 +34,7 @@ type Image struct { // See strings.Builder for similar examples. addr *Image - // mipmap is a set of shareable.Image sorted by the order of mipmap level. - // The level 0 image is a regular image and higher-level images are used for mipmap. - mipmap *mipmap + buffered *buffered.Image bounds image.Rectangle original *Image @@ -56,7 +55,7 @@ func (i *Image) Size() (width, height int) { } func (i *Image) isDisposed() bool { - return i.mipmap.isDisposed() + return i.buffered == nil } func (i *Image) isSubImage() bool { @@ -90,7 +89,7 @@ func (i *Image) Fill(clr color.Color) error { panic("ebiten: render to a subimage is not implemented (Fill)") } - i.mipmap.fill(color.RGBAModel.Convert(clr).(color.RGBA)) + i.buffered.Fill(color.RGBAModel.Convert(clr).(color.RGBA)) return nil } @@ -199,7 +198,8 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) error { filter = driver.Filter(img.filter) } - i.mipmap.drawImage(img.mipmap, img.Bounds(), geom, options.ColorM.impl, mode, filter) + a, b, c, d, tx, ty := geom.elements() + i.buffered.DrawImage(img.buffered, img.Bounds(), a, b, c, d, tx, ty, options.ColorM.impl, mode, filter) return nil } @@ -307,7 +307,32 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o filter = driver.Filter(img.filter) } - i.mipmap.drawTriangles(img.mipmap, img.Bounds(), vertices, indices, options.ColorM.impl, mode, filter, driver.Address(options.Address)) + b := img.Bounds() + bx0 := float32(b.Min.X) + by0 := float32(b.Min.Y) + bx1 := float32(b.Max.X) + by1 := float32(b.Max.Y) + + // TODO: Should we use mipmap.verticesBackend? + 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 + 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 + vs[i*graphics.VertexFloatNum+7] = by1 + vs[i*graphics.VertexFloatNum+8] = v.ColorR + vs[i*graphics.VertexFloatNum+9] = v.ColorG + vs[i*graphics.VertexFloatNum+10] = v.ColorB + vs[i*graphics.VertexFloatNum+11] = v.ColorA + } + is := make([]uint16, len(indices)) + copy(is, indices) + + i.buffered.DrawTriangles(img.buffered, vs, is, options.ColorM.impl, mode, filter, driver.Address(options.Address)) } // SubImage returns an image representing the portion of the image p visible through r. The returned value shares pixels with the original image. @@ -324,8 +349,8 @@ func (i *Image) SubImage(r image.Rectangle) image.Image { } img := &Image{ - mipmap: i.mipmap, - filter: i.filter, + buffered: i.buffered, + filter: i.filter, } // Keep the original image's reference not to dispose that by GC. @@ -351,10 +376,6 @@ func (i *Image) Bounds() image.Rectangle { if i.isDisposed() { panic("ebiten: the image is already disposed") } - if !i.isSubImage() { - w, h := i.mipmap.size() - return image.Rect(0, 0, w, h) - } return i.bounds } @@ -380,7 +401,7 @@ func (i *Image) At(x, y int) color.Color { if i.isSubImage() && !image.Pt(x, y).In(i.bounds) { return color.RGBA{} } - r, g, b, a := i.mipmap.at(x, y) + r, g, b, a := i.buffered.At(x, y) return color.RGBA{r, g, b, a} } @@ -404,7 +425,7 @@ func (i *Image) Set(x, y int, clr color.Color) { } r, g, b, a := clr.RGBA() - i.mipmap.set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8)) + i.buffered.Set(x, y, byte(r>>8), byte(g>>8), byte(b>>8), byte(a>>8)) } // Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values. @@ -424,7 +445,8 @@ func (i *Image) Dispose() error { if i.isSubImage() { return nil } - i.mipmap.dispose() + i.buffered.MarkDisposed() + i.buffered = nil return nil } @@ -454,7 +476,7 @@ func (i *Image) ReplacePixels(p []byte) error { panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l)) } - i.mipmap.replacePixels(p) + i.buffered.ReplacePixels(p) return nil } @@ -508,8 +530,9 @@ func NewImage(width, height int, filter Filter) (*Image, error) { func newImage(width, height int, filter Filter, volatile bool) *Image { i := &Image{ - mipmap: newMipmap(width, height, volatile), - filter: filter, + buffered: buffered.NewImage(width, height, volatile), + filter: filter, + bounds: image.Rect(0, 0, width, height), } i.addr = i return i @@ -529,8 +552,9 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) { width, height := size.X, size.Y i := &Image{ - mipmap: newMipmap(width, height, false), - filter: filter, + buffered: buffered.NewImage(width, height, false), + filter: filter, + bounds: image.Rect(0, 0, width, height), } i.addr = i @@ -540,8 +564,9 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) { func newScreenFramebufferImage(width, height int) *Image { i := &Image{ - mipmap: newScreenFramebufferMipmap(width, height), - filter: FilterDefault, + buffered: buffered.NewScreenFramebufferImage(width, height), + filter: FilterDefault, + bounds: image.Rect(0, 0, width, height), } i.addr = i return i diff --git a/imagedumper_desktop.go b/imagedumper_desktop.go index 94e4012ee..5ac9e3858 100644 --- a/imagedumper_desktop.go +++ b/imagedumper_desktop.go @@ -52,7 +52,7 @@ func takeScreenshot(screen *Image) error { return err } - if err := screen.mipmap.dump(newname); err != nil { + if err := screen.buffered.Dump(newname); err != nil { return err } diff --git a/internal/buffered/image.go b/internal/buffered/image.go index 72baeffbd..b76b1b4b5 100644 --- a/internal/buffered/image.go +++ b/internal/buffered/image.go @@ -15,15 +15,16 @@ package buffered import ( + "image" "image/color" "github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/driver" - "github.com/hajimehoshi/ebiten/internal/shareable" + "github.com/hajimehoshi/ebiten/internal/mipmap" ) type Image struct { - img *shareable.Image + img *mipmap.Mipmap width int height int @@ -32,7 +33,7 @@ type Image struct { } func BeginFrame() error { - if err := shareable.BeginFrame(); err != nil { + if err := mipmap.BeginFrame(); err != nil { return err } flushDelayedCommands() @@ -40,7 +41,7 @@ func BeginFrame() error { } func EndFrame() error { - return shareable.EndFrame() + return mipmap.EndFrame() } func NewImage(width, height int, volatile bool) *Image { @@ -48,7 +49,7 @@ func NewImage(width, height int, volatile bool) *Image { delayedCommandsM.Lock() if needsToDelayCommands { delayedCommands = append(delayedCommands, func() { - i.img = shareable.NewImage(width, height, volatile) + i.img = mipmap.New(width, height, volatile) i.width = width i.height = height }) @@ -57,7 +58,7 @@ func NewImage(width, height int, volatile bool) *Image { } delayedCommandsM.Unlock() - i.img = shareable.NewImage(width, height, volatile) + i.img = mipmap.New(width, height, volatile) i.width = width i.height = height return i @@ -68,7 +69,7 @@ func NewScreenFramebufferImage(width, height int) *Image { delayedCommandsM.Lock() if needsToDelayCommands { delayedCommands = append(delayedCommands, func() { - i.img = shareable.NewScreenFramebufferImage(width, height) + i.img = mipmap.NewScreenFramebufferMipmap(width, height) i.width = width i.height = height }) @@ -77,7 +78,7 @@ func NewScreenFramebufferImage(width, height int) *Image { } delayedCommandsM.Unlock() - i.img = shareable.NewScreenFramebufferImage(width, height) + i.img = mipmap.NewScreenFramebufferMipmap(width, height) i.width = width i.height = height return i @@ -204,6 +205,35 @@ func (i *Image) ReplacePixels(pix []byte) { i.img.ReplacePixels(pix) } +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") + } + + g := &mipmap.GeoM{ + A: a, + B: b, + C: c, + D: d, + Tx: tx, + Ty: ty, + } + + delayedCommandsM.Lock() + if needsToDelayCommands { + delayedCommands = append(delayedCommands, func() { + i.img.DrawImage(src.img, bounds, g, colorm, mode, filter) + }) + delayedCommandsM.Unlock() + return + } + delayedCommandsM.Unlock() + + src.resolvePendingPixels(true) + i.resolvePendingPixels(false) + i.img.DrawImage(src.img, bounds, g, colorm, mode, filter) +} + func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { if i == src { panic("buffered: Image.DrawTriangles: src must be different from the receiver") diff --git a/mipmap.go b/internal/mipmap/mipmap.go similarity index 75% rename from mipmap.go rename to internal/mipmap/mipmap.go index aa8fe158a..43c3cdc6d 100644 --- a/mipmap.go +++ b/internal/mipmap/mipmap.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ebiten +package mipmap import ( "fmt" @@ -21,67 +21,82 @@ import ( "math" "github.com/hajimehoshi/ebiten/internal/affine" - "github.com/hajimehoshi/ebiten/internal/buffered" "github.com/hajimehoshi/ebiten/internal/driver" "github.com/hajimehoshi/ebiten/internal/graphics" + "github.com/hajimehoshi/ebiten/internal/shareable" ) -type levelToImage map[int]*buffered.Image +func BeginFrame() error { + return shareable.BeginFrame() +} -type mipmap struct { +func EndFrame() error { + return shareable.EndFrame() +} + +type GeoM struct { + A float32 + B float32 + C float32 + D float32 + Tx float32 + Ty float32 +} + +func (g *GeoM) det() float32 { + return g.A*g.D - g.B*g.C +} + +type levelToImage map[int]*shareable.Image + +// Mipmap is a set of shareable.Image sorted by the order of mipmap level. +// The level 0 image is a regular image and higher-level images are used for mipmap. +type Mipmap struct { width int height int volatile bool - orig *buffered.Image + orig *shareable.Image imgs map[image.Rectangle]levelToImage } -func newMipmap(width, height int, volatile bool) *mipmap { - return &mipmap{ +func New(width, height int, volatile bool) *Mipmap { + return &Mipmap{ width: width, height: height, volatile: volatile, - orig: buffered.NewImage(width, height, volatile), + orig: shareable.NewImage(width, height, volatile), imgs: map[image.Rectangle]levelToImage{}, } } -func newScreenFramebufferMipmap(width, height int) *mipmap { - return &mipmap{ +func NewScreenFramebufferMipmap(width, height int) *Mipmap { + return &Mipmap{ width: width, height: height, - orig: buffered.NewScreenFramebufferImage(width, height), + orig: shareable.NewScreenFramebufferImage(width, height), imgs: map[image.Rectangle]levelToImage{}, } } -func (m *mipmap) dump(name string) error { +func (m *Mipmap) Dump(name string) error { return m.orig.Dump(name) } -func (m *mipmap) fill(clr color.RGBA) { +func (m *Mipmap) Fill(clr color.RGBA) { m.orig.Fill(clr) m.disposeMipmaps() } -func (m *mipmap) replacePixels(pix []byte) { +func (m *Mipmap) ReplacePixels(pix []byte) { m.orig.ReplacePixels(pix) m.disposeMipmaps() } -func (m *mipmap) size() (int, int) { - return m.width, m.height -} - -func (m *mipmap) at(x, y int) (r, g, b, a byte) { +func (m *Mipmap) At(x, y int) (r, g, b, a byte) { return m.orig.At(x, y) } -func (m *mipmap) set(x, y int, r, g, b, a byte) { - m.orig.Set(x, y, r, g, b, a) -} - -func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) { +func (m *Mipmap) DrawImage(src *Mipmap, bounds image.Rectangle, geom *GeoM, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter) { if det := geom.det(); det == 0 { return } else if math.IsNaN(float64(det)) { @@ -130,7 +145,7 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo panic("ebiten: Mipmap must not be used when the filter is FilterScreen") } - a, b, c, d, tx, ty := geom.elements() + a, b, c, d, tx, ty := geom.A, geom.B, geom.C, geom.D, geom.Tx, geom.Ty if level == 0 { vs := quadVertices(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, a, b, c, d, tx, ty, cr, cg, cb, ca, screen) is := graphics.QuadIndices() @@ -149,37 +164,12 @@ func (m *mipmap) drawImage(src *mipmap, bounds image.Rectangle, geom *GeoM, colo m.disposeMipmaps() } -func (m *mipmap) drawTriangles(src *mipmap, bounds image.Rectangle, vertices []Vertex, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { - bx0 := float32(bounds.Min.X) - by0 := float32(bounds.Min.Y) - bx1 := float32(bounds.Max.X) - by1 := float32(bounds.Max.Y) - - // TODO: Needs boundary check optimization? - // See https://go101.org/article/bounds-check-elimination.html - - vs := vertexSlice(len(vertices), false) - 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 - 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 - vs[i*graphics.VertexFloatNum+7] = by1 - vs[i*graphics.VertexFloatNum+8] = v.ColorR - vs[i*graphics.VertexFloatNum+9] = v.ColorG - vs[i*graphics.VertexFloatNum+10] = v.ColorB - vs[i*graphics.VertexFloatNum+11] = v.ColorA - } - is := make([]uint16, len(indices)) - copy(is, indices) - m.orig.DrawTriangles(src.orig, vs, is, colorm, mode, filter, address) +func (m *Mipmap) DrawTriangles(src *Mipmap, vertices []float32, indices []uint16, colorm *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) { + m.orig.DrawTriangles(src.orig, vertices, indices, colorm, mode, filter, address) m.disposeMipmaps() } -func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image { +func (m *Mipmap) level(r image.Rectangle, level int) *shareable.Image { if level == 0 { panic("ebiten: level must be non-zero at level") } @@ -197,7 +187,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image { return img } - var src *buffered.Image + var src *shareable.Image var vs []float32 var filter driver.Filter switch { @@ -237,7 +227,7 @@ func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image { imgs[level] = nil return nil } - s := buffered.NewImage(w2, h2, m.volatile) + s := shareable.NewImage(w2, h2, m.volatile) s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero) imgs[level] = s @@ -264,17 +254,17 @@ func sizeForLevel(origWidth, origHeight int, level int) (width, height int) { return } -func (m *mipmap) isDisposed() bool { +func (m *Mipmap) isDisposed() bool { return m.orig == nil } -func (m *mipmap) dispose() { +func (m *Mipmap) MarkDisposed() { m.disposeMipmaps() m.orig.MarkDisposed() m.orig = nil } -func (m *mipmap) disposeMipmaps() { +func (m *Mipmap) disposeMipmaps() { for _, a := range m.imgs { for _, img := range a { img.MarkDisposed() @@ -288,7 +278,7 @@ func (m *mipmap) disposeMipmaps() { // mipmapLevel returns an appropriate mipmap level for the given determinant of a geometry matrix. // // mipmapLevel panics if det is NaN or 0. -func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int { +func (m *Mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter) int { det := geom.det() if math.IsNaN(float64(det)) { panic("ebiten: det must be finite at mipmapLevel") @@ -338,10 +328,10 @@ func (m *mipmap) mipmapLevel(geom *GeoM, width, height int, filter driver.Filter } // This is a separate function for testing. - return mipmapLevelForDownscale(det) + return MipmapLevelForDownscale(det) } -func mipmapLevelForDownscale(det float32) int { +func MipmapLevelForDownscale(det float32) int { if math.IsNaN(float64(det)) { panic("ebiten: det must be finite at mipmapLevelForDownscale") } @@ -401,7 +391,7 @@ func minf32(a, b, c, d float32) float32 { } func geomScaleSize(geom *GeoM) (sx, sy float32) { - a, b, c, d, _, _ := geom.elements() + a, b, c, d := geom.A, geom.B, geom.C, geom.D // (0, 1) x0 := 0*a + 1*b y0 := 0*c + 1*d diff --git a/mipmap_test.go b/internal/mipmap/mipmap_test.go similarity index 95% rename from mipmap_test.go rename to internal/mipmap/mipmap_test.go index 2dc44a236..1c53525f3 100644 --- a/mipmap_test.go +++ b/internal/mipmap/mipmap_test.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ebiten_test +package mipmap_test import ( "math" "testing" - . "github.com/hajimehoshi/ebiten" + . "github.com/hajimehoshi/ebiten/internal/mipmap" ) func TestMipmapLevelForDownscale(t *testing.T) { diff --git a/vertex.go b/internal/mipmap/vertex.go similarity index 96% rename from vertex.go rename to internal/mipmap/vertex.go index 40858f623..630e8db88 100644 --- a/vertex.go +++ b/internal/mipmap/vertex.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ebiten +package mipmap import ( "sync" @@ -70,7 +70,6 @@ func quadVertices(sx0, sy0, sx1, sy1 int, a, b, c, d, tx, ty float32, cr, cg, cb vs := vertexSlice(4, last) _ = vs[:48] - // For each values, see the comment at shareable.(*Image).DrawTriangles. vs[0] = tx vs[1] = ty vs[2] = u0