ebiten/mipmap.go
Hajime Hoshi 9cb631e30f Reland: graphics: Reuse vertices backend and reduce allocating
This is a reland of 74f1e5519f.

The vertex slice is now copied before saving as the drawing
history items.
2019-09-29 01:36:00 +09:00

424 lines
9.6 KiB
Go

// Copyright 2018 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ebiten
import (
"fmt"
"image"
"image/color"
"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"
)
type levelToImage map[int]*buffered.Image
type mipmap struct {
width int
height int
volatile bool
orig *buffered.Image
imgs map[image.Rectangle]levelToImage
}
func newMipmap(width, height int, volatile bool) *mipmap {
return &mipmap{
width: width,
height: height,
volatile: volatile,
orig: buffered.NewImage(width, height, volatile),
imgs: map[image.Rectangle]levelToImage{},
}
}
func newScreenFramebufferMipmap(width, height int) *mipmap {
return &mipmap{
width: width,
height: height,
orig: buffered.NewScreenFramebufferImage(width, height),
imgs: map[image.Rectangle]levelToImage{},
}
}
func (m *mipmap) dump(name string) error {
return m.orig.Dump(name)
}
func (m *mipmap) fill(clr color.RGBA) {
m.orig.Fill(clr)
m.disposeMipmaps()
}
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) {
return m.orig.At(x, y)
}
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)) {
return
}
level := src.mipmapLevel(geom, bounds.Dx(), bounds.Dy(), filter)
if level > 0 {
// If the image can be scaled into 0 size, adjust the level. (#839)
w, h := bounds.Dx(), bounds.Dy()
for level >= 0 {
s := 1 << uint(level)
if w/s == 0 || h/s == 0 {
level--
continue
}
break
}
if level < 0 {
// As the render source is too small, nothing is rendered.
return
}
}
if level > 6 {
level = 6
}
if level < -6 {
level = -6
}
cr, cg, cb, ca := float32(1), float32(1), float32(1), float32(1)
if colorm.ScaleOnly() {
body, _ := colorm.UnsafeElements()
cr = body[0]
cg = body[5]
cb = body[10]
ca = body[15]
colorm = nil
}
screen := filter == driver.FilterScreen
if screen && level != 0 {
panic("ebiten: Mipmap must not be used when the filter is FilterScreen")
}
a, b, c, d, tx, ty := geom.elements()
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()
m.orig.DrawTriangles(src.orig, vs, is, colorm, mode, filter, driver.AddressClampToZero)
} else if buf := src.level(bounds, level); buf != nil {
w, h := sizeForLevel(bounds.Dx(), bounds.Dy(), level)
s := pow2(level)
a *= s
b *= s
c *= s
d *= s
vs := quadVertices(0, 0, w, h, a, b, c, d, tx, ty, cr, cg, cb, ca, false)
is := graphics.QuadIndices()
m.orig.DrawTriangles(buf, vs, is, colorm, mode, filter, driver.AddressClampToZero)
}
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)
m.disposeMipmaps()
}
func (m *mipmap) level(r image.Rectangle, level int) *buffered.Image {
if level == 0 {
panic("ebiten: level must be non-zero at level")
}
if m.volatile {
panic("ebiten: mipmap images for a volatile image is not implemented yet")
}
if _, ok := m.imgs[r]; !ok {
m.imgs[r] = levelToImage{}
}
imgs := m.imgs[r]
if img, ok := imgs[level]; ok {
return img
}
var src *buffered.Image
var vs []float32
var filter driver.Filter
switch {
case level == 1:
src = m.orig
vs = quadVertices(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterLinear
case level > 1:
src = m.level(r, level-1)
if src == nil {
imgs[level] = nil
return nil
}
w, h := sizeForLevel(r.Dx(), r.Dy(), level-1)
vs = quadVertices(0, 0, w, h, 0.5, 0, 0, 0.5, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterLinear
case level == -1:
src = m.orig
vs = quadVertices(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y, 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterNearest
case level < -1:
src = m.level(r, level+1)
if src == nil {
imgs[level] = nil
return nil
}
w, h := sizeForLevel(r.Dx(), r.Dy(), level+1)
vs = quadVertices(0, 0, w, h, 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, false)
filter = driver.FilterNearest
default:
panic(fmt.Sprintf("ebiten: invalid level: %d", level))
}
is := graphics.QuadIndices()
w2, h2 := sizeForLevel(r.Dx(), r.Dy(), level)
if w2 == 0 || h2 == 0 {
imgs[level] = nil
return nil
}
s := buffered.NewImage(w2, h2, m.volatile)
s.DrawTriangles(src, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressClampToZero)
imgs[level] = s
return imgs[level]
}
func sizeForLevel(origWidth, origHeight int, level int) (width, height int) {
width = origWidth
height = origHeight
if level > 0 {
for i := 0; i < level; i++ {
width /= 2
height /= 2
if width == 0 || height == 0 {
return 0, 0
}
}
} else {
for i := 0; i < -level; i++ {
width *= 2
height *= 2
}
}
return
}
func (m *mipmap) isDisposed() bool {
return m.orig == nil
}
func (m *mipmap) dispose() {
m.disposeMipmaps()
m.orig.MarkDisposed()
m.orig = nil
}
func (m *mipmap) disposeMipmaps() {
for _, a := range m.imgs {
for _, img := range a {
img.MarkDisposed()
}
}
for k := range m.imgs {
delete(m.imgs, k)
}
}
func (m *mipmap) clearFramebuffer() {
m.orig.ClearFramebuffer()
}
// 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 {
det := geom.det()
if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevel")
}
if det == 0 {
panic("ebiten: dst must be non zero at mipmapLevel")
}
if filter == driver.FilterScreen {
return 0
}
// Use 'negative' mipmap to render edges correctly (#611, #907).
// It looks like 128 is the enlargement factor that causes edge missings to pass the test TestImageStretch.
const tooBigScale = 128
if sx, sy := geomScaleSize(geom); sx >= tooBigScale || sy >= tooBigScale {
// If the filter is not nearest, the target needs to be rendered with graduation. Don't use mipmaps.
if filter != driver.FilterNearest {
return 0
}
const mipmapMaxSize = 1024
w, h := width, height
if w >= mipmapMaxSize || h >= mipmapMaxSize {
return 0
}
level := 0
for sx >= tooBigScale || sy >= tooBigScale {
level--
sx /= 2
sy /= 2
w *= 2
h *= 2
if w >= mipmapMaxSize || h >= mipmapMaxSize {
break
}
}
return level
}
if filter != driver.FilterLinear {
return 0
}
if m.volatile {
return 0
}
// This is a separate function for testing.
return mipmapLevelForDownscale(det)
}
func mipmapLevelForDownscale(det float32) int {
if math.IsNaN(float64(det)) {
panic("ebiten: det must be finite at mipmapLevelForDownscale")
}
if det == 0 {
panic("ebiten: dst must be non zero at mipmapLevelForDownscale")
}
// TODO: Should this be determined by x/y scales instead of det?
d := math.Abs(float64(det))
level := 0
for d < 0.25 {
level++
d *= 4
}
return level
}
func pow2(power int) float32 {
if power >= 0 {
x := 1
return float32(x << uint(power))
}
x := float32(1)
for i := 0; i < -power; i++ {
x /= 2
}
return x
}
func maxf32(a, b, c, d float32) float32 {
max := a
if max < b {
max = b
}
if max < c {
max = c
}
if max < d {
max = d
}
return max
}
func minf32(a, b, c, d float32) float32 {
min := a
if min > b {
min = b
}
if min > c {
min = c
}
if min > d {
min = d
}
return min
}
func geomScaleSize(geom *GeoM) (sx, sy float32) {
a, b, c, d, _, _ := geom.elements()
// (0, 1)
x0 := 0*a + 1*b
y0 := 0*c + 1*d
// (1, 0)
x1 := 1*a + 0*b
y1 := 1*c + 0*d
// (1, 1)
x2 := 1*a + 1*b
y2 := 1*c + 1*d
maxx := maxf32(0, x0, x1, x2)
maxy := maxf32(0, y0, y1, y2)
minx := minf32(0, x0, x1, x2)
miny := minf32(0, y0, y1, y2)
return maxx - minx, maxy - miny
}