internal/atlas: refactoring: remove SetVolatile and SetIsolate

Pass an image type to NewImage instead.
This commit is contained in:
Hajime Hoshi 2022-06-07 23:57:56 +09:00
parent b8e8d72377
commit 81f91658ff
9 changed files with 95 additions and 205 deletions

View File

@ -15,6 +15,7 @@
package ebiten
import (
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
@ -34,7 +35,17 @@ func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
c.offscreen.Dispose()
c.offscreen = nil
}
c.offscreen = NewImage(width, height)
// Keep the offscreen an isolated image from an atlas (#1938).
// The shader program for the screen is special and doesn't work well with an image on an atlas.
// An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges.
imageType := atlas.ImageTypeIsolated
if ui.IsScreenClearedEveryFrame() {
// A violatile image is also always isolated.
imageType = atlas.ImageTypeVolatile
}
c.offscreen = newImage(width, height, imageType)
return c.offscreen.image
}

View File

@ -20,6 +20,7 @@ import (
"image/color"
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
@ -787,6 +788,10 @@ func (i *Image) ReplacePixels(pixels []byte) {
//
// NewImage panics if RunGame already finishes.
func NewImage(width, height int) *Image {
return newImage(width, height, atlas.ImageTypeRegular)
}
func newImage(width, height int, imageType atlas.ImageType) *Image {
if isRunGameEnded() {
panic(fmt.Sprintf("ebiten: NewImage cannot be called after RunGame finishes"))
}
@ -797,7 +802,7 @@ func NewImage(width, height int) *Image {
panic(fmt.Sprintf("ebiten: height at NewImage must be positive but %d", height))
}
i := &Image{
image: ui.NewImage(width, height),
image: ui.NewImage(width, height, imageType),
bounds: image.Rect(0, 0, width, height),
}
i.addr = i
@ -839,7 +844,7 @@ func NewImageFromImage(source image.Image) *Image {
}
i := &Image{
image: ui.NewImage(width, height),
image: ui.NewImage(width, height, atlas.ImageTypeRegular),
bounds: image.Rect(0, 0, width, height),
}
i.addr = i

View File

@ -206,14 +206,21 @@ func init() {
backendsM.Lock()
}
type ImageType int
const (
ImageTypeRegular ImageType = iota
ImageTypeScreen
ImageTypeVolatile
ImageTypeIsolated
)
// Image is a rectangle pixel set that might be on an atlas.
type Image struct {
width int
height int
disposed bool
isolated bool
volatile bool
screen bool
width int
height int
imageType ImageType
disposed bool
backend *backend
@ -284,7 +291,7 @@ func (i *Image) ensureIsolated() {
sx1 /= float32(sw)
sy1 /= float32(sh)
typ := restorable.ImageTypeRegular
if i.volatile {
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
newImg := restorable.NewImage(w, h, typ)
@ -326,11 +333,8 @@ func (i *Image) putOnAtlas(graphicsDriver graphicsdriver.Graphics) error {
if !i.canBePutOnAtlas() {
panic("atlas: putOnAtlas cannot be called on a image that cannot be on an atlas")
}
if i.volatile {
panic("atlas: a volatile image cannot be put on an atlas")
}
newI := NewImage(i.width, i.height)
newI := NewImage(i.width, i.height, i.imageType)
if restorable.NeedsRestoring() {
// If the underlying graphics driver requires restoring from the context lost, the pixel data is
@ -433,7 +437,7 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderImageNum]*Image, vertices []f
var dx, dy float32
// A screen image doesn't have its padding.
if !i.screen {
if i.imageType != ImageTypeScreen {
x, y, _, _ := i.regionWithPadding()
dx = float32(x) + paddingSize
dy = float32(y) + paddingSize
@ -708,43 +712,20 @@ func (i *Image) dispose(markDisposed bool) {
theBackends = append(theBackends[:index], theBackends[index+1:]...)
}
func NewImage(width, height int) *Image {
func NewImage(width, height int, imageType ImageType) *Image {
// Actual allocation is done lazily, and the lock is not needed.
return &Image{
width: width,
height: height,
width: width,
height: height,
imageType: imageType,
}
}
func (i *Image) SetIsolated(isolated bool) {
if i.backend != nil {
panic("atlas: SetIsolated must be called before its backend is allocated")
}
i.isolated = isolated
}
func (i *Image) SetVolatile(volatile bool) {
i.volatile = volatile
if i.backend == nil {
return
}
if i.volatile {
i.ensureIsolated()
}
i.backend.restorable.SetVolatile(i.volatile)
}
func (i *Image) canBePutOnAtlas() bool {
if minSize == 0 || maxSize == 0 {
panic("atlas: minSize or maxSize must be initialized")
}
if i.isolated {
return false
}
if i.volatile {
return false
}
if i.screen {
if i.imageType != ImageTypeRegular {
return false
}
return i.width+2*paddingSize <= maxSize && i.height+2*paddingSize <= maxSize
@ -757,7 +738,7 @@ func (i *Image) allocate(putOnAtlas bool) {
runtime.SetFinalizer(i, (*Image).MarkDisposed)
if i.screen {
if i.imageType == ImageTypeScreen {
// A screen image doesn't have a padding.
i.backend = &backend{
restorable: restorable.NewImage(i.width, i.height, restorable.ImageTypeScreen),
@ -767,7 +748,7 @@ func (i *Image) allocate(putOnAtlas bool) {
if !putOnAtlas || !i.canBePutOnAtlas() {
typ := restorable.ImageTypeRegular
if i.volatile {
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
i.backend = &backend{
@ -792,7 +773,7 @@ func (i *Image) allocate(putOnAtlas bool) {
}
typ := restorable.ImageTypeRegular
if i.volatile {
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
b := &backend{
@ -816,16 +797,6 @@ func (i *Image) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, path stri
return i.backend.restorable.Dump(graphicsDriver, path, blackbg, image.Rect(paddingSize, paddingSize, paddingSize+i.width, paddingSize+i.height))
}
func NewScreenFramebufferImage(width, height int) *Image {
// Actual allocation is done lazily.
i := &Image{
width: width,
height: height,
screen: true,
}
return i
}
func EndFrame(graphicsDriver graphicsdriver.Graphics) error {
backendsM.Lock()

View File

@ -60,25 +60,25 @@ const bigSize = 2049
func TestEnsureIsolated(t *testing.T) {
// Create img1 and img2 with this size so that the next images are allocated
// with non-upper-left location.
img1 := atlas.NewImage(bigSize, 100)
img1 := atlas.NewImage(bigSize, 100, atlas.ImageTypeRegular)
defer img1.MarkDisposed()
// Ensure img1's region is allocated.
img1.ReplacePixels(make([]byte, 4*bigSize*100), nil)
img2 := atlas.NewImage(100, bigSize)
img2 := atlas.NewImage(100, bigSize, atlas.ImageTypeRegular)
defer img2.MarkDisposed()
img2.ReplacePixels(make([]byte, 4*100*bigSize), nil)
const size = 32
img3 := atlas.NewImage(size/2, size/2)
img3 := atlas.NewImage(size/2, size/2, atlas.ImageTypeRegular)
defer img3.MarkDisposed()
img3.ReplacePixels(make([]byte, (size/2)*(size/2)*4), nil)
img4 := atlas.NewImage(size, size)
img4 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img4.MarkDisposed()
img5 := atlas.NewImage(size/2, size/2)
img5 := atlas.NewImage(size/2, size/2, atlas.ImageTypeRegular)
defer img3.MarkDisposed()
pix := make([]byte, size*size*4)
@ -155,18 +155,18 @@ func TestEnsureIsolated(t *testing.T) {
func TestReputOnAtlas(t *testing.T) {
const size = 16
img0 := atlas.NewImage(size, size)
img0 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*size*size), nil)
img1 := atlas.NewImage(size, size)
img1 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size), nil)
if got, want := img1.IsOnAtlasForTesting(), true; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
img2 := atlas.NewImage(size, size)
img2 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer img2.MarkDisposed()
pix := make([]byte, 4*size*size)
for j := 0; j < size; j++ {
@ -179,8 +179,8 @@ func TestReputOnAtlas(t *testing.T) {
}
img2.ReplacePixels(pix, nil)
img3 := atlas.NewImage(size, size)
img3.SetVolatile(true)
// Create a volatile image. This should always be isolated.
img3 := atlas.NewImage(size, size, atlas.ImageTypeVolatile)
defer img3.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size), nil)
if got, want := img3.IsOnAtlasForTesting(), false; got != want {
@ -303,7 +303,7 @@ func TestReputOnAtlas(t *testing.T) {
func TestExtend(t *testing.T) {
const w0, h0 = 100, 100
img0 := atlas.NewImage(w0, h0)
img0 := atlas.NewImage(w0, h0, atlas.ImageTypeRegular)
defer img0.MarkDisposed()
p0 := make([]byte, 4*w0*h0)
@ -316,7 +316,7 @@ func TestExtend(t *testing.T) {
img0.ReplacePixels(p0, nil)
const w1, h1 = minImageSizeForTesting + 1, 100
img1 := atlas.NewImage(w1, h1)
img1 := atlas.NewImage(w1, h1, atlas.ImageTypeRegular)
defer img1.MarkDisposed()
p1 := make([]byte, 4*w1*h1)
@ -370,9 +370,9 @@ func TestExtend(t *testing.T) {
func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
const w, h = 256, 256
src := atlas.NewImage(w, h)
src := atlas.NewImage(w, h, atlas.ImageTypeRegular)
defer src.MarkDisposed()
dst := atlas.NewImage(w, h)
dst := atlas.NewImage(w, h, atlas.ImageTypeRegular)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -418,9 +418,9 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
// Issue #887
func TestSmallImages(t *testing.T) {
const w, h = 4, 8
src := atlas.NewImage(w, h)
src := atlas.NewImage(w, h, atlas.ImageTypeRegular)
defer src.MarkDisposed()
dst := atlas.NewImage(w, h)
dst := atlas.NewImage(w, h, atlas.ImageTypeRegular)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -463,11 +463,11 @@ func TestSmallImages(t *testing.T) {
// Issue #887
func TestLongImages(t *testing.T) {
const w, h = 1, 6
src := atlas.NewImage(w, h)
src := atlas.NewImage(w, h, atlas.ImageTypeRegular)
defer src.MarkDisposed()
const dstW, dstH = 256, 256
dst := atlas.NewImage(dstW, dstH)
dst := atlas.NewImage(dstW, dstH, atlas.ImageTypeRegular)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -511,11 +511,11 @@ func TestLongImages(t *testing.T) {
func TestDisposeImmediately(t *testing.T) {
// This tests restorable.Image.ClearPixels is called but ReplacePixels is not called.
img0 := atlas.NewImage(16, 16)
img0 := atlas.NewImage(16, 16, atlas.ImageTypeRegular)
img0.EnsureIsolatedForTesting()
defer img0.MarkDisposed()
img1 := atlas.NewImage(16, 16)
img1 := atlas.NewImage(16, 16, atlas.ImageTypeRegular)
img1.EnsureIsolatedForTesting()
defer img1.MarkDisposed()
@ -524,12 +524,12 @@ func TestDisposeImmediately(t *testing.T) {
// Issue #1028
func TestExtendWithBigImage(t *testing.T) {
img0 := atlas.NewImage(1, 1)
img0 := atlas.NewImage(1, 1, atlas.ImageTypeRegular)
defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*1*1), nil)
img1 := atlas.NewImage(minImageSizeForTesting+1, minImageSizeForTesting+1)
img1 := atlas.NewImage(minImageSizeForTesting+1, minImageSizeForTesting+1, atlas.ImageTypeRegular)
defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*(minImageSizeForTesting+1)*(minImageSizeForTesting+1)), nil)
@ -539,7 +539,7 @@ func TestExtendWithBigImage(t *testing.T) {
func TestMaxImageSize(t *testing.T) {
// This tests that a too-big image is allocated correctly.
s := maxImageSizeForTesting - 2*atlas.PaddingSize
img := atlas.NewImage(s, s)
img := atlas.NewImage(s, s, atlas.ImageTypeRegular)
defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s), nil)
}
@ -552,7 +552,7 @@ func Disable_TestMinImageSize(t *testing.T) {
// This tests that extending a backend works correctly.
// Though the image size is minimum size of the backend, extending the backend happens due to the paddings.
s := minImageSizeForTesting
img := atlas.NewImage(s, s)
img := atlas.NewImage(s, s, atlas.ImageTypeRegular)
defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s), nil)
}
@ -561,11 +561,11 @@ func Disable_TestMinImageSize(t *testing.T) {
func TestDisposedAndReputOnAtlas(t *testing.T) {
const size = 16
src := atlas.NewImage(size, size)
src := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer src.MarkDisposed()
src2 := atlas.NewImage(size, size)
src2 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer src2.MarkDisposed()
dst := atlas.NewImage(size, size)
dst := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer dst.MarkDisposed()
// Use src as a render target so that src is not on an atlas.
@ -609,11 +609,11 @@ func TestDisposedAndReputOnAtlas(t *testing.T) {
func TestImageIsNotReputOnAtlasWithoutUsingAsSource(t *testing.T) {
const size = 16
src := atlas.NewImage(size, size)
src := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer src.MarkDisposed()
src2 := atlas.NewImage(size, size)
src2 := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer src2.MarkDisposed()
dst := atlas.NewImage(size, size)
dst := atlas.NewImage(size, size, atlas.ImageTypeRegular)
defer dst.MarkDisposed()
// Use src as a render target so that src is not on an atlas.

View File

@ -29,7 +29,7 @@ import (
func TestShaderFillTwice(t *testing.T) {
const w, h = 1, 1
dst := atlas.NewImage(w, h)
dst := atlas.NewImage(w, h, atlas.ImageTypeRegular)
vs := quadVertices(w, h, 0, 0, 1)
is := graphics.QuadIndices()
@ -60,10 +60,10 @@ func TestShaderFillTwice(t *testing.T) {
func TestImageDrawTwice(t *testing.T) {
const w, h = 1, 1
dst := atlas.NewImage(w, h)
src0 := atlas.NewImage(w, h)
dst := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src0 := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src0.ReplacePixels([]byte{0xff, 0xff, 0xff, 0xff}, nil)
src1 := atlas.NewImage(w, h)
src1 := atlas.NewImage(w, h, atlas.ImageTypeRegular)
src1.ReplacePixels([]byte{0x80, 0x80, 0x80, 0xff}, nil)
vs := quadVertices(w, h, 0, 0, 1)

View File

@ -45,70 +45,25 @@ func EndFrame(graphicsDriver graphicsdriver.Graphics) error {
return atlas.EndFrame(graphicsDriver)
}
func NewImage(width, height int) *Image {
func NewImage(width, height int, imageType atlas.ImageType) *Image {
i := &Image{
width: width,
height: height,
}
i.initialize()
i.initialize(imageType)
return i
}
func (i *Image) initialize() {
func (i *Image) initialize(imageType atlas.ImageType) {
if maybeCanAddDelayedCommand() {
if tryAddDelayedCommand(func() error {
i.initialize()
i.initialize(imageType)
return nil
}) {
return
}
}
i.img = atlas.NewImage(i.width, i.height)
}
func (i *Image) SetIsolated(isolated bool) {
if maybeCanAddDelayedCommand() {
if tryAddDelayedCommand(func() error {
i.SetIsolated(isolated)
return nil
}) {
return
}
}
i.img.SetIsolated(isolated)
}
func (i *Image) SetVolatile(volatile bool) {
if maybeCanAddDelayedCommand() {
if tryAddDelayedCommand(func() error {
i.SetVolatile(volatile)
return nil
}) {
return
}
}
i.img.SetVolatile(volatile)
}
func NewScreenFramebufferImage(width, height int) *Image {
i := &Image{}
i.initializeAsScreenFramebuffer(width, height)
return i
}
func (i *Image) initializeAsScreenFramebuffer(width, height int) {
if maybeCanAddDelayedCommand() {
if tryAddDelayedCommand(func() error {
i.initializeAsScreenFramebuffer(width, height)
return nil
}) {
return
}
}
i.img = atlas.NewScreenFramebufferImage(width, height)
i.width = width
i.height = height
i.img = atlas.NewImage(i.width, i.height, imageType)
}
func (i *Image) invalidatePixels() {

View File

@ -19,6 +19,7 @@ import (
"math"
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
@ -35,38 +36,15 @@ type Mipmap struct {
imgs map[int]*buffered.Image
}
func New(width, height int) *Mipmap {
func New(width, height int, imageType atlas.ImageType) *Mipmap {
return &Mipmap{
width: width,
height: height,
orig: buffered.NewImage(width, height),
width: width,
height: height,
orig: buffered.NewImage(width, height, imageType),
volatile: imageType == atlas.ImageTypeVolatile,
}
}
func NewScreenFramebufferMipmap(width, height int) *Mipmap {
return &Mipmap{
width: width,
height: height,
orig: buffered.NewScreenFramebufferImage(width, height),
}
}
func (m *Mipmap) SetIsolated(isolated bool) {
m.orig.SetIsolated(isolated)
}
func (m *Mipmap) SetVolatile(volatile bool) {
if m.volatile == volatile {
return
}
m.volatile = volatile
if m.volatile {
m.disposeMipmaps()
}
m.orig.SetVolatile(volatile)
}
func (m *Mipmap) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, name string, blackbg bool) error {
return m.orig.DumpScreenshot(graphicsDriver, name, blackbg)
}
@ -203,8 +181,7 @@ func (m *Mipmap) level(level int) *buffered.Image {
m.setImg(level, nil)
return nil
}
s := buffered.NewImage(w2, h2)
s.SetVolatile(m.volatile)
s := buffered.NewImage(w2, h2, atlas.ImageTypeVolatile)
dstRegion := graphicsdriver.Region{
X: 0,

View File

@ -21,6 +21,7 @@ import (
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
@ -149,13 +150,6 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
w, h := c.offscreen.width, c.offscreen.height
c.offscreen.MarkDisposed()
c.offscreen = c.game.NewOffscreenImage(w, h)
// TODO: Give volatile/isolated property to the constructor.
if theGlobalState.isScreenClearedEveryFrame() {
c.offscreen.setVolatile(true)
} else {
c.offscreen.mipmap.SetIsolated(true)
}
}
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
@ -244,7 +238,7 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac
}
}
if c.screen == nil {
c.screen = newScreenFramebufferImage(sw, sh)
c.screen = NewImage(sw, sh, atlas.ImageTypeScreen)
}
if c.offscreen != nil {
@ -255,17 +249,6 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac
}
if c.offscreen == nil {
c.offscreen = c.game.NewOffscreenImage(ow, oh)
// TODO: Give volatile/isolated property to the constructor.
if theGlobalState.isScreenClearedEveryFrame() {
c.offscreen.setVolatile(true)
} else {
// Keep the offscreen an isolated image from an atlas (#1938).
// The shader program for the screen is special and doesn't work well with an image on an atlas.
// An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges.
c.offscreen.mipmap.SetIsolated(true)
}
}
return ow, oh

View File

@ -16,6 +16,7 @@ package ui
import (
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
@ -36,27 +37,14 @@ type Image struct {
volatile bool
}
func NewImage(width, height int) *Image {
func NewImage(width, height int, imageType atlas.ImageType) *Image {
return &Image{
mipmap: mipmap.New(width, height),
mipmap: mipmap.New(width, height, imageType),
width: width,
height: height,
}
}
func newScreenFramebufferImage(width, height int) *Image {
return &Image{
mipmap: mipmap.NewScreenFramebufferMipmap(width, height),
width: width,
height: height,
}
}
func (i *Image) setVolatile(volatile bool) {
i.volatile = volatile
i.mipmap.SetVolatile(volatile)
}
func (i *Image) MarkDisposed() {
if i.mipmap == nil {
return
@ -112,7 +100,7 @@ func DumpImages(dir string) error {
}
var (
emptyImage = NewImage(3, 3)
emptyImage = NewImage(3, 3, atlas.ImageTypeRegular)
)
func init() {