Compare commits

...

2 Commits

Author SHA1 Message Date
Hajime Hoshi
4b1ae72f59 ebiten: add Draw{Image,Triangles}Options.DisableMipmaps
Mipmaps could be unexpectedly expensive even when we don't need mipmaps.
In order to improve performance, let's add an option to disable mipmaps.

Closes #3095
2024-09-12 22:40:16 +09:00
Hajime Hoshi
355dd453bd internal/mipmap: refactoring 2024-09-12 17:36:17 +09:00
3 changed files with 70 additions and 25 deletions

View File

@ -32,7 +32,7 @@ import (
)
const (
screenWidth = 800
screenWidth = 1000
screenHeight = 480
)
@ -44,20 +44,28 @@ type Game struct {
rotate bool
clip bool
counter int
pause bool
}
func (g *Game) Update() error {
g.counter++
if g.counter == 480 {
g.counter = 0
}
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
g.rotate = !g.rotate
}
if inpututil.IsKeyJustPressed(ebiten.KeyC) {
g.clip = !g.clip
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
g.pause = !g.pause
}
if g.pause {
return nil
}
g.counter++
if g.counter == 480 {
g.counter = 0
}
return nil
}
@ -65,7 +73,8 @@ func (g *Game) Draw(screen *ebiten.Image) {
s := 1.5 / math.Pow(1.01, float64(g.counter))
clippedGophersImage := gophersImage.SubImage(image.Rect(100, 100, 200, 200)).(*ebiten.Image)
for i, f := range []ebiten.Filter{ebiten.FilterNearest, ebiten.FilterLinear} {
for i := range 3 {
//for i, f := range []ebiten.Filter{ebiten.FilterNearest, ebiten.FilterLinear} {
w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy()
op := &ebiten.DrawImageOptions{}
@ -75,8 +84,15 @@ func (g *Game) Draw(screen *ebiten.Image) {
op.GeoM.Translate(float64(w)/2, float64(h)/2)
}
op.GeoM.Scale(s, s)
op.GeoM.Translate(32+float64(i*w)*s+float64(i*4), 64)
op.Filter = f
op.GeoM.Translate(32+float64(i*w)*s+float64(i*4), 100)
if i == 0 {
op.Filter = ebiten.FilterNearest
} else {
op.Filter = ebiten.FilterLinear
}
if i == 2 {
op.DisableMipmaps = true
}
if g.clip {
screen.DrawImage(clippedGophersImage, op)
} else {
@ -84,9 +100,10 @@ func (g *Game) Draw(screen *ebiten.Image) {
}
}
msg := fmt.Sprintf(`Minifying images (Nearest filter vs Linear filter):
msg := fmt.Sprintf(`Minifying images (Nearest filter, Linear filter (w/ mipmaps), and Linear Filter (w/o mipmaps)):
Press R to rotate the images.
Press C to clip the images.
Click to pause and resume.
Scale: %0.2f`, s)
ebitenutil.DebugPrint(screen, msg)
}

View File

@ -144,6 +144,16 @@ type DrawImageOptions struct {
// Filter is a type of texture filter.
// The default (zero) value is FilterNearest.
Filter Filter
// DisableMipmaps disables mipmaps.
// When Filter is FilterLinear and GeoM shrinks the image, mipmaps are used by default.
// Mipmap is useful to render a shrunk image with high quality.
// However, mipmaps can be expensive, especially on mobiles.
// When DisableMipmaps is true, mipmap is not used.
// When Filter is not FilterLinear, DisableMipmaps is ignored.
//
// The default (zero) value is false.
DisableMipmaps bool
}
// adjustPosition converts the position in the *ebiten.Image coordinate to the *ui.Image coordinate.
@ -273,7 +283,11 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
hint = restorable.HintOverwriteDstRegion
}
i.image.DrawTriangles(srcs, vs, is, blend, dr, [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRuleFillAll, canSkipMipmap(geoM, filter), false, hint)
skipMipmap := options.DisableMipmaps
if !skipMipmap {
skipMipmap = canSkipMipmap(geoM, filter)
}
i.image.DrawTriangles(srcs, vs, is, blend, dr, [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRuleFillAll, skipMipmap, false, hint)
}
// overwritesDstRegion reports whether the given parameters overwrite the destination region completely.
@ -447,6 +461,16 @@ type DrawTrianglesOptions struct {
//
// The default (zero) value is false.
AntiAlias bool
// DisableMipmaps disables mipmaps.
// When Filter is FilterLinear and GeoM shrinks the image, mipmaps are used by default.
// Mipmap is useful to render a shrunk image with high quality.
// However, mipmaps can be expensive, especially on mobiles.
// When DisableMipmaps is true, mipmap is not used.
// When Filter is not FilterLinear, DisableMipmaps is ignored.
//
// The default (zero) value is false.
DisableMipmaps bool
}
// MaxIndicesCount is the maximum number of indices for DrawTriangles and DrawTrianglesShader.
@ -576,7 +600,11 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
})
}
i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedBounds(), [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), filter != builtinshader.FilterLinear, options.AntiAlias, restorable.HintNone)
skipMipmap := options.DisableMipmaps
if !skipMipmap {
skipMipmap = filter != builtinshader.FilterLinear
}
i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedBounds(), [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), skipMipmap, options.AntiAlias, restorable.HintNone)
}
// DrawTrianglesShaderOptions represents options for DrawTrianglesShader.

View File

@ -164,40 +164,40 @@ func (m *Mipmap) level(level int) *buffered.Image {
return img.img
}
var w, h int
var srcW, srcH int
var src *buffered.Image
vs := make([]float32, 4*graphics.VertexFloatCount)
switch {
case level == 1:
src = m.orig
w = m.width
h = m.height
srcW = m.width
srcH = m.height
case level > 1:
src = m.level(level - 1)
if src == nil {
m.setImg(level, nil)
return nil
}
w = sizeForLevel(m.width, level-1)
h = sizeForLevel(m.height, level-1)
srcW = sizeForLevel(m.width, level-1)
srcH = sizeForLevel(m.height, level-1)
default:
panic(fmt.Sprintf("mipmap: invalid level: %d", level))
}
graphics.QuadVerticesFromSrcAndMatrix(vs, 0, 0, float32(w), float32(h), 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1)
graphics.QuadVerticesFromSrcAndMatrix(vs, 0, 0, float32(srcW), float32(srcH), 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1)
is := graphics.QuadIndices()
w2 := sizeForLevel(m.width, level)
h2 := sizeForLevel(m.height, level)
if w2 == 0 || h2 == 0 {
dstW := sizeForLevel(m.width, level)
dstH := sizeForLevel(m.height, level)
if dstW == 0 || dstH == 0 {
m.setImg(level, nil)
return nil
}
// buffered.NewImage panics with a too big size when actual allocation happens.
// 4096 should be a safe size in most environments (#1399).
// Unfortunately a precise max image size cannot be obtained here since this requires GPU access.
if w2 > 4096 || h2 > 4096 {
if dstW > 4096 || dstH > 4096 {
m.setImg(level, nil)
return nil
}
@ -207,11 +207,11 @@ func (m *Mipmap) level(level int) *buffered.Image {
// As s is overwritten, this doesn't have to be cleared.
s = img.img
} else {
s = buffered.NewImage(w2, h2, m.imageType)
s = buffered.NewImage(dstW, dstH, m.imageType)
}
dstRegion := image.Rect(0, 0, w2, h2)
srcRegion := image.Rect(0, 0, w, h)
dstRegion := image.Rect(0, 0, dstW, dstH)
srcRegion := image.Rect(0, 0, srcW, srcH)
s.DrawTriangles([graphics.ShaderSrcImageCount]*buffered.Image{src}, vs, is, graphicsdriver.BlendCopy, dstRegion, [graphics.ShaderSrcImageCount]image.Rectangle{srcRegion}, atlas.LinearFilterShader, nil, graphicsdriver.FillRuleFillAll, restorable.HintOverwriteDstRegion)
m.setImg(level, s)