diff --git a/image_test.go b/image_test.go index d24e16df9..38fefaae9 100644 --- a/image_test.go +++ b/image_test.go @@ -94,7 +94,7 @@ func TestImagePixels(t *testing.T) { w, h := img0.Bounds().Size().X, img0.Bounds().Size().Y // Check out of range part - w2, h2 := graphics.NextPowerOf2Int(w), graphics.NextPowerOf2Int(h) + w2, h2 := graphics.InternalImageSize(w), graphics.InternalImageSize(h) for j := -100; j < h2+100; j++ { for i := -100; i < w2+100; i++ { got := img0.At(i, j) diff --git a/internal/graphics/math.go b/internal/graphics/math.go index ad52c8630..d222aed35 100644 --- a/internal/graphics/math.go +++ b/internal/graphics/math.go @@ -14,10 +14,19 @@ package graphics -// NextPowerOf2Int returns a nearest power of 2 to x. -func NextPowerOf2Int(x int) int { +// minInternalImageSize is the minimum size of internal images (texture/framebuffer). +// +// For example, the image size less than 15 is not supported on some iOS devices. +// See also: https://stackoverflow.com/questions/15935651/certain-framebuffer-sizes-fail-on-ios-devices-gl-framebuffer-unsupported +const minInternalImageSize = 16 + +// InternalImageSize returns a nearest appropriate size as an internal image. +func InternalImageSize(x int) int { if x <= 0 { - panic("x must be positive") + panic("graphics: x must be positive") + } + if x < minInternalImageSize { + return minInternalImageSize } r := 1 for r < x { @@ -25,3 +34,13 @@ func NextPowerOf2Int(x int) int { } return r } + +func isInternalImageSize(x int) bool { + if x <= 0 { + return false + } + if x < minInternalImageSize { + return false + } + return (x & (x - 1)) == 0 +} diff --git a/internal/graphics/math_test.go b/internal/graphics/math_test.go index 39b665d55..65abe8424 100644 --- a/internal/graphics/math_test.go +++ b/internal/graphics/math_test.go @@ -20,7 +20,7 @@ import ( . "github.com/hajimehoshi/ebiten/internal/graphics" ) -func TestNextPowerOf2(t *testing.T) { +func TestInternalImageSize(t *testing.T) { testCases := []struct { expected int arg int @@ -31,7 +31,7 @@ func TestNextPowerOf2(t *testing.T) { } for _, testCase := range testCases { - got := NextPowerOf2Int(testCase.arg) + got := InternalImageSize(testCase.arg) wanted := testCase.expected if wanted != got { t.Errorf("Clp(%d) = %d, wanted %d", testCase.arg, got, wanted) diff --git a/internal/graphics/vertices.go b/internal/graphics/vertices.go index d26a2abb9..a76260cdd 100644 --- a/internal/graphics/vertices.go +++ b/internal/graphics/vertices.go @@ -59,19 +59,14 @@ func (v *verticesBackend) slice(n int) []float32 { return s } -func isPowerOf2(x int) bool { - if x <= 0 { - return false - } - return (x & (x - 1)) == 0 -} - func QuadVertices(width, height int, sx0, sy0, sx1, sy1 int, a, b, c, d, tx, ty float32, cr, cg, cb, ca float32) []float32 { - if !isPowerOf2(width) { - panic(fmt.Sprintf("graphics: width must be power of 2 but not at QuadVertices: %d", width)) + // For performance reason, graphics.InternalImageSize is not applied to width/height here. + + if !isInternalImageSize(width) { + panic(fmt.Sprintf("graphics: width must be an internal image size at QuadVertices: %d", width)) } - if !isPowerOf2(height) { - panic(fmt.Sprintf("graphics: height must be power of 2 but not at QuadVertices: %d", height)) + if !isInternalImageSize(height) { + panic(fmt.Sprintf("graphics: height must be an internal image size at QuadVertices: %d", height)) } if sx0 >= sx1 || sy0 >= sy1 { @@ -166,11 +161,11 @@ func QuadIndices() []uint16 { } func PutVertex(vs []float32, width, height int, dx, dy, su, sv float32, u0, v0, u1, v1 float32, cr, cg, cb, ca float32) { - if !isPowerOf2(width) { - panic(fmt.Sprintf("graphics: width must be power of 2 but not at PutVertices: %d", width)) + if !isInternalImageSize(width) { + panic(fmt.Sprintf("graphics: width must be an internal image size at PutVertices: %d", width)) } - if !isPowerOf2(height) { - panic(fmt.Sprintf("graphics: height must be power of 2 but not at PutVertices: %d", height)) + if !isInternalImageSize(height) { + panic(fmt.Sprintf("graphics: height must be an internal image size at PutVertices: %d", height)) } vs[0] = dx diff --git a/internal/graphicsdriver/metal/driver.go b/internal/graphicsdriver/metal/driver.go index a3a1df79f..51e24b7a1 100644 --- a/internal/graphicsdriver/metal/driver.go +++ b/internal/graphicsdriver/metal/driver.go @@ -340,10 +340,10 @@ func (d *Driver) checkSize(width, height int) { }) if width < 1 { - panic(fmt.Sprintf("metal: width (%d) must be equal or more than 1", width)) + panic(fmt.Sprintf("metal: width (%d) must be equal or more than %d", width, 1)) } if height < 1 { - panic(fmt.Sprintf("metal: height (%d) must be equal or more than 1", height)) + panic(fmt.Sprintf("metal: height (%d) must be equal or more than %d", height, 1)) } if width > m { panic(fmt.Sprintf("metal: width (%d) must be less than or equal to %d", width, m)) @@ -357,8 +357,8 @@ func (d *Driver) NewImage(width, height int) (graphicsdriver.Image, error) { d.checkSize(width, height) td := mtl.TextureDescriptor{ PixelFormat: mtl.PixelFormatRGBA8UNorm, - Width: graphics.NextPowerOf2Int(width), - Height: graphics.NextPowerOf2Int(height), + Width: graphics.InternalImageSize(width), + Height: graphics.InternalImageSize(height), StorageMode: mtl.StorageModeManaged, // MTLTextureUsageRenderTarget might cause a problematic render result. Not sure the reason. @@ -581,8 +581,8 @@ func (d *Driver) Draw(indexLen int, indexOffset int, mode graphics.CompositeMode rce.SetVertexBytes(unsafe.Pointer(&viewportSize[0]), unsafe.Sizeof(viewportSize), 1) sourceSize := [...]float32{ - float32(graphics.NextPowerOf2Int(d.src.width)), - float32(graphics.NextPowerOf2Int(d.src.height)), + float32(graphics.InternalImageSize(d.src.width)), + float32(graphics.InternalImageSize(d.src.height)), } rce.SetFragmentBytes(unsafe.Pointer(&sourceSize[0]), unsafe.Sizeof(sourceSize), 2) @@ -644,7 +644,7 @@ func (i *Image) viewportSize() (int, int) { if i.screen { return i.width, i.height } - return graphics.NextPowerOf2Int(i.width), graphics.NextPowerOf2Int(i.height) + return graphics.InternalImageSize(i.width), graphics.InternalImageSize(i.height) } func (i *Image) Dispose() { diff --git a/internal/graphicsdriver/opengl/driver.go b/internal/graphicsdriver/opengl/driver.go index a9eb2f4ac..25ddaf506 100644 --- a/internal/graphicsdriver/opengl/driver.go +++ b/internal/graphicsdriver/opengl/driver.go @@ -39,10 +39,10 @@ func (d *Driver) SetWindow(window uintptr) { func (d *Driver) checkSize(width, height int) { if width < 1 { - panic(fmt.Sprintf("opengl: width (%d) must be equal or more than 1", width)) + panic(fmt.Sprintf("opengl: width (%d) must be equal or more than %d", width, 1)) } if height < 1 { - panic(fmt.Sprintf("opengl: height (%d) must be equal or more than 1", height)) + panic(fmt.Sprintf("opengl: height (%d) must be equal or more than %d", height, 1)) } m := d.context.getMaxTextureSize() if width > m { @@ -59,8 +59,8 @@ func (d *Driver) NewImage(width, height int) (graphicsdriver.Image, error) { width: width, height: height, } - w := graphics.NextPowerOf2Int(width) - h := graphics.NextPowerOf2Int(height) + w := graphics.InternalImageSize(width) + h := graphics.InternalImageSize(height) d.checkSize(w, h) t, err := d.context.newTexture(w, h) if err != nil { diff --git a/internal/graphicsdriver/opengl/image.go b/internal/graphicsdriver/opengl/image.go index 0bb5e58a2..216f285c4 100644 --- a/internal/graphicsdriver/opengl/image.go +++ b/internal/graphicsdriver/opengl/image.go @@ -76,7 +76,7 @@ func (i *Image) ensureFramebuffer() error { return nil } - w, h := graphics.NextPowerOf2Int(i.width), graphics.NextPowerOf2Int(i.height) + w, h := graphics.InternalImageSize(i.width), graphics.InternalImageSize(i.height) f, err := newFramebufferFromTexture(&i.driver.context, i.textureNative, w, h) if err != nil { return err diff --git a/internal/graphicsdriver/opengl/program.go b/internal/graphicsdriver/opengl/program.go index 99be1717c..9255ea37b 100644 --- a/internal/graphicsdriver/opengl/program.go +++ b/internal/graphicsdriver/opengl/program.go @@ -310,8 +310,8 @@ func (d *Driver) useProgram(mode graphics.CompositeMode, colorM *affine.ColorM, d.state.lastColorMatrixTranslation = esTranslate } - sw := graphics.NextPowerOf2Int(srcW) - sh := graphics.NextPowerOf2Int(srcH) + sw := graphics.InternalImageSize(srcW) + sh := graphics.InternalImageSize(srcH) if d.state.lastSourceWidth != sw || d.state.lastSourceHeight != sh { d.context.uniformFloats(program, "source_size", []float32{float32(sw), float32(sh)}) diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 210d84edc..2bab16cd2 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -230,8 +230,8 @@ func (i *Image) Size() (int, int) { func (i *Image) InternalSize() (int, int) { if i.w2 == 0 || i.h2 == 0 { w, h := i.image.Size() - i.w2 = graphics.NextPowerOf2Int(w) - i.h2 = graphics.NextPowerOf2Int(h) + i.w2 = graphics.InternalImageSize(w) + i.h2 = graphics.InternalImageSize(h) } return i.w2, i.h2 } diff --git a/internal/restorable/images_test.go b/internal/restorable/images_test.go index 80c8b4ddb..1cf4e6a40 100644 --- a/internal/restorable/images_test.go +++ b/internal/restorable/images_test.go @@ -105,6 +105,14 @@ func TestRestoreWithoutDraw(t *testing.T) { } } +func quadVertices(dw, dh, sw, sh, x, y int) []float32 { + // dw/dh must be internal image sizes. + return graphics.QuadVertices(dw, dh, + 0, 0, sw, sh, + 1, 0, 0, 1, float32(x), float32(y), + 1, 1, 1, 1) +} + func TestRestoreChain(t *testing.T) { const num = 10 imgs := []*Image{} @@ -120,8 +128,8 @@ func TestRestoreChain(t *testing.T) { clr := color.RGBA{0x00, 0x00, 0x00, 0xff} imgs[0].Fill(clr.R, clr.G, clr.B, clr.A) for i := 0; i < num-1; i++ { - w, h := imgs[i].Size() - vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + w, h := imgs[i].InternalSize() + vs := quadVertices(w, h, 1, 1, 0, 0) is := graphics.QuadIndices() imgs[i+1].DrawImage(imgs[i], vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) } @@ -162,7 +170,7 @@ func TestRestoreChain2(t *testing.T) { clr8 := color.RGBA{0x00, 0x00, 0xff, 0xff} imgs[8].Fill(clr8.R, clr8.G, clr8.B, clr8.A) - vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(w), graphics.InternalImageSize(h), w, h, 0, 0) is := graphics.QuadIndices() imgs[8].DrawImage(imgs[7], vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) imgs[9].DrawImage(imgs[8], vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) @@ -204,7 +212,7 @@ func TestRestoreOverrideSource(t *testing.T) { clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff} clr1 := color.RGBA{0x00, 0x00, 0x01, 0xff} img1.Fill(clr0.R, clr0.G, clr0.B, clr0.A) - vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(w), graphics.InternalImageSize(h), w, h, 0, 0) is := graphics.QuadIndices() img2.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) img3.DrawImage(img2, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) @@ -284,24 +292,26 @@ func TestRestoreComplexGraph(t *testing.T) { img1.Dispose() img0.Dispose() }() - vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + dw := graphics.InternalImageSize(w) + dh := graphics.InternalImageSize(h) + vs := quadVertices(dw, dh, w, h, 0, 0) is := graphics.QuadIndices() img3.DrawImage(img0, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 1, 0) img3.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 1, 0) img4.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 2, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 2, 0) img4.DrawImage(img2, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 0, 0) img5.DrawImage(img3, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 0, 0) img6.DrawImage(img3, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 1, 0) img6.DrawImage(img4, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 0, 0) img7.DrawImage(img2, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) - vs = graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 2, 0, 1, 1, 1, 1) + vs = quadVertices(dw, dh, w, h, 2, 0) img7.DrawImage(img3, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) ResolveStaleImages() if err := Restore(); err != nil { @@ -391,7 +401,7 @@ func TestRestoreRecursive(t *testing.T) { img1.Dispose() img0.Dispose() }() - vs := graphics.QuadVertices(w, h, 0, 0, w, h, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(w), graphics.InternalImageSize(h), w, h, 1, 0) is := graphics.QuadIndices() img1.DrawImage(img0, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) img0.DrawImage(img1, vs, is, nil, graphics.CompositeModeSourceOver, graphics.FilterNearest, graphics.AddressClampToZero) @@ -481,7 +491,7 @@ func TestDrawImageAndReplacePixels(t *testing.T) { img1 := NewImage(2, 1) defer img1.Dispose() - vs := graphics.QuadVertices(1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(1), graphics.InternalImageSize(1), 1, 1, 0, 0) is := graphics.QuadIndices() img1.DrawImage(img0, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) img1.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 0, 0, 2, 1) @@ -514,7 +524,7 @@ func TestDispose(t *testing.T) { img2 := newImageFromImage(base2) defer img2.Dispose() - vs := graphics.QuadVertices(1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(1), graphics.InternalImageSize(1), 1, 1, 0, 0) is := graphics.QuadIndices() img1.DrawImage(img2, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) img0.DrawImage(img1, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) @@ -608,7 +618,7 @@ func TestReplacePixelsOnly(t *testing.T) { img0.ReplacePixels([]byte{1, 2, 3, 4}, i%w, i/w, 1, 1) } - vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(w), graphics.InternalImageSize(h), 1, 1, 0, 0) is := graphics.QuadIndices() img1.DrawImage(img0, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero) img0.ReplacePixels([]byte{5, 6, 7, 8}, 0, 0, 1, 1) @@ -653,7 +663,7 @@ func TestReadPixelsFromVolatileImage(t *testing.T) { // Second, draw src to dst. If the implementation is correct, dst becomes stale. src.Fill(0xff, 0xff, 0xff, 0xff) - vs := graphics.QuadVertices(w, h, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1) + vs := quadVertices(graphics.InternalImageSize(w), graphics.InternalImageSize(h), 1, 1, 0, 0) is := graphics.QuadIndices() dst.DrawImage(src, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest, graphics.AddressClampToZero)