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
This commit is contained in:
Hajime Hoshi 2020-01-08 00:15:46 +09:00
parent 7f4a82ddf0
commit c99fd1a742
7 changed files with 143 additions and 100 deletions

View File

@ -16,5 +16,4 @@ package ebiten
var (
CopyImage = copyImage
MipmapLevelForDownscale = mipmapLevelForDownscale
)

View File

@ -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,7 +349,7 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
}
img := &Image{
mipmap: i.mipmap,
buffered: i.buffered,
filter: i.filter,
}
@ -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),
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),
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),
buffered: buffered.NewScreenFramebufferImage(width, height),
filter: FilterDefault,
bounds: image.Rect(0, 0, width, height),
}
i.addr = i
return i

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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) {

View File

@ -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