From c99fd1a742abb00125ac686582b40a50ab4f082e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Wed, 8 Jan 2020 00:15:46 +0900 Subject: [PATCH] mipmap: Create mipmap package and bufferd.Image uses it Mipmap calculation must be executed after the main loop starts because the graphics driver's HasHighPrecisionFloat is needed. Then, operations on mipmap images must be called from images in buffered package. Updates #1044 --- export_test.go | 3 +- image.go | 71 +++++++---- imagedumper_desktop.go | 2 +- internal/buffered/image.go | 46 +++++-- mipmap.go => internal/mipmap/mipmap.go | 114 ++++++++---------- .../mipmap/mipmap_test.go | 4 +- vertex.go => internal/mipmap/vertex.go | 3 +- 7 files changed, 143 insertions(+), 100 deletions(-) rename mipmap.go => internal/mipmap/mipmap.go (75%) rename mipmap_test.go => internal/mipmap/mipmap_test.go (95%) rename vertex.go => internal/mipmap/vertex.go (96%) 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