restorable: Enable to set the 'volatile' state later

Updates #1309
This commit is contained in:
Hajime Hoshi 2020-08-19 00:54:21 +09:00
parent bebad644ad
commit 8aef1f9080
9 changed files with 140 additions and 98 deletions

View File

@ -814,12 +814,12 @@ func (i *Image) ReplacePixels(pixels []byte) error {
//
// Error returned by NewImage is always nil as of 1.5.0.
func NewImage(width, height int, filter Filter) (*Image, error) {
return newImage(width, height, filter, false), nil
return newImage(width, height, filter), nil
}
func newImage(width, height int, filter Filter, volatile bool) *Image {
func newImage(width, height int, filter Filter) *Image {
i := &Image{
mipmap: mipmap.New(width, height, volatile),
mipmap: mipmap.New(width, height),
filter: filter,
bounds: image.Rect(0, 0, width, height),
}
@ -841,7 +841,7 @@ func NewImageFromImage(source image.Image, filter Filter) (*Image, error) {
width, height := size.X, size.Y
i := &Image{
mipmap: mipmap.New(width, height, false),
mipmap: mipmap.New(width, height),
filter: filter,
bounds: image.Rect(0, 0, width, height),
}

View File

@ -49,26 +49,38 @@ func EndFrame() error {
return shareable.EndFrame()
}
func NewImage(width, height int, volatile bool) *Image {
func NewImage(width, height int) *Image {
i := &Image{}
i.initialize(width, height, volatile)
i.initialize(width, height)
return i
}
func (i *Image) initialize(width, height int, volatile bool) {
func (i *Image) initialize(width, height int) {
if maybeCanAddDelayedCommand() {
if tryAddDelayedCommand(func() error {
i.initialize(width, height, volatile)
i.initialize(width, height)
return nil
}) {
return
}
}
i.img = shareable.NewImage(width, height, volatile)
i.img = shareable.NewImage(width, height)
i.width = width
i.height = height
}
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)

View File

@ -50,12 +50,11 @@ type Mipmap struct {
imgs map[int]*buffered.Image
}
func New(width, height int, volatile bool) *Mipmap {
func New(width, height int) *Mipmap {
return &Mipmap{
width: width,
height: height,
volatile: volatile,
orig: buffered.NewImage(width, height, volatile),
orig: buffered.NewImage(width, height),
imgs: map[int]*buffered.Image{},
}
}
@ -69,6 +68,14 @@ func NewScreenFramebufferMipmap(width, height int) *Mipmap {
}
}
func (m *Mipmap) SetVolatile(volatile bool) {
m.volatile = volatile
if m.volatile {
m.disposeMipmaps()
}
m.orig.SetVolatile(volatile)
}
func (m *Mipmap) Dump(name string, blackbg bool) error {
return m.orig.Dump(name, blackbg)
}
@ -229,7 +236,8 @@ func (m *Mipmap) level(level int) *buffered.Image {
m.imgs[level] = nil
return nil
}
s := buffered.NewImage(w2, h2, m.volatile)
s := buffered.NewImage(w2, h2)
s.SetVolatile(m.volatile)
s.DrawTriangles([graphics.ShaderImageNum]*buffered.Image{src}, vs, is, nil, driver.CompositeModeCopy, filter, driver.AddressUnsafe, driver.Region{}, nil, nil)
m.imgs[level] = s

View File

@ -133,26 +133,30 @@ func init() {
// NewImage creates an empty image with the given size.
//
// volatile indicates whether the image is volatile. Regular non-volatile images need to record drawing history or
// read its pixels from GPU if necessary so that all the images can be restored automatically from the context lost.
// However, such recording the drawing history or reading pixels from GPU are expensive operations. Volatile images
// can skip such oprations, but the image content is cleared every frame instead.
//
// The returned image is cleared.
//
// Note that Dispose is not called automatically.
func NewImage(width, height int, volatile bool) *Image {
func NewImage(width, height int) *Image {
i := &Image{
image: graphicscommand.NewImage(width, height),
width: width,
height: height,
volatile: volatile,
}
fillImage(i.image, color.RGBA{})
theImages.add(i)
return i
}
// SetVolatile sets the volatile state of the image.
//
// Regular non-volatile images need to record drawing history or read its pixels from GPU if necessary so that all
// the images can be restored automatically from the context lost. However, such recording the drawing history or
// reading pixels from GPU are expensive operations. Volatile images can skip such oprations, but the image content
// is cleared every frame instead.
func (i *Image) SetVolatile(volatile bool) {
i.volatile = volatile
}
// Extend extends the image by the given size.
// Extend creates a new image with the given size and copies the pixels of the given source image.
// Extend disposes itself after its call.
@ -175,7 +179,8 @@ func (i *Image) Extend(width, height int) *Image {
panic("restorable: Extend after DrawTriangles is forbidden")
}
newImg := NewImage(width, height, i.volatile)
newImg := NewImage(width, height)
newImg.SetVolatile(i.volatile)
i.basePixels.Apply(newImg.image)
if i.basePixels.baseColor != (color.RGBA{}) {

View File

@ -55,7 +55,7 @@ func sameColors(c1, c2 color.RGBA, delta int) bool {
}
func TestRestore(t *testing.T) {
img0 := NewImage(1, 1, false)
img0 := NewImage(1, 1)
defer img0.Dispose()
clr0 := color.RGBA{0x00, 0x00, 0x00, 0xff}
@ -74,7 +74,7 @@ func TestRestore(t *testing.T) {
}
func TestRestoreWithoutDraw(t *testing.T) {
img0 := NewImage(1024, 1024, false)
img0 := NewImage(1024, 1024)
defer img0.Dispose()
// If there is no drawing command on img0, img0 is cleared when restored.
@ -118,7 +118,7 @@ func TestRestoreChain(t *testing.T) {
const num = 10
imgs := []*Image{}
for i := 0; i < num; i++ {
img := NewImage(1, 1, false)
img := NewImage(1, 1)
imgs = append(imgs, img)
}
defer func() {
@ -156,7 +156,7 @@ func TestRestoreChain2(t *testing.T) {
)
imgs := []*Image{}
for i := 0; i < num; i++ {
img := NewImage(w, h, false)
img := NewImage(w, h)
imgs = append(imgs, img)
}
defer func() {
@ -202,10 +202,10 @@ func TestRestoreOverrideSource(t *testing.T) {
w = 1
h = 1
)
img0 := NewImage(w, h, false)
img1 := NewImage(w, h, false)
img2 := NewImage(w, h, false)
img3 := NewImage(w, h, false)
img0 := NewImage(w, h)
img1 := NewImage(w, h)
img2 := NewImage(w, h)
img3 := NewImage(w, h)
defer func() {
img3.Dispose()
img2.Dispose()
@ -281,11 +281,11 @@ func TestRestoreComplexGraph(t *testing.T) {
img0 := newImageFromImage(base)
img1 := newImageFromImage(base)
img2 := newImageFromImage(base)
img3 := NewImage(w, h, false)
img4 := NewImage(w, h, false)
img5 := NewImage(w, h, false)
img6 := NewImage(w, h, false)
img7 := NewImage(w, h, false)
img3 := NewImage(w, h)
img4 := NewImage(w, h)
img5 := NewImage(w, h)
img6 := NewImage(w, h)
img7 := NewImage(w, h)
defer func() {
img7.Dispose()
img6.Dispose()
@ -384,7 +384,7 @@ func TestRestoreComplexGraph(t *testing.T) {
func newImageFromImage(rgba *image.RGBA) *Image {
s := rgba.Bounds().Size()
img := NewImage(s.X, s.Y, false)
img := NewImage(s.X, s.Y)
img.ReplacePixels(rgba.Pix, 0, 0, s.X, s.Y)
return img
}
@ -401,7 +401,7 @@ func TestRestoreRecursive(t *testing.T) {
base.Pix[3] = 0xff
img0 := newImageFromImage(base)
img1 := NewImage(w, h, false)
img1 := NewImage(w, h)
defer func() {
img1.Dispose()
img0.Dispose()
@ -446,7 +446,7 @@ func TestRestoreRecursive(t *testing.T) {
}
func TestReplacePixels(t *testing.T) {
img := NewImage(17, 31, false)
img := NewImage(17, 31)
defer img.Dispose()
pix := make([]byte, 4*4*4)
@ -497,7 +497,7 @@ func TestDrawTrianglesAndReplacePixels(t *testing.T) {
base.Pix[3] = 0xff
img0 := newImageFromImage(base)
defer img0.Dispose()
img1 := NewImage(2, 1, false)
img1 := NewImage(2, 1)
defer img1.Dispose()
vs := quadVertices(1, 1, 0, 0)
@ -566,7 +566,7 @@ func TestReplacePixelsPart(t *testing.T) {
pix[i] = 0xff
}
img := NewImage(4, 4, false)
img := NewImage(4, 4)
// This doesn't make the image stale. Its base pixels are available.
img.ReplacePixels(pix, 1, 1, 2, 2)
@ -637,9 +637,9 @@ func TestReplacePixelsPart(t *testing.T) {
func TestReplacePixelsOnly(t *testing.T) {
const w, h = 128, 128
img0 := NewImage(w, h, false)
img0 := NewImage(w, h)
defer img0.Dispose()
img1 := NewImage(1, 1, false)
img1 := NewImage(1, 1)
defer img1.Dispose()
for i := 0; i < w*h; i += 5 {
@ -687,8 +687,9 @@ func TestReplacePixelsOnly(t *testing.T) {
// Issue #793
func TestReadPixelsFromVolatileImage(t *testing.T) {
const w, h = 16, 16
dst := NewImage(w, h, true /* volatile */)
src := NewImage(w, h, false)
dst := NewImage(w, h)
dst.SetVolatile(true)
src := NewImage(w, h)
// First, make sure that dst has pixels
dst.ReplacePixels(make([]byte, 4*w*h), 0, 0, w, h)
@ -717,8 +718,8 @@ func TestReadPixelsFromVolatileImage(t *testing.T) {
func TestAllowReplacePixelsAfterDrawTriangles(t *testing.T) {
const w, h = 16, 16
src := NewImage(w, h, false)
dst := NewImage(w, h, false)
src := NewImage(w, h)
dst := NewImage(w, h)
vs := quadVertices(w, h, 0, 0)
is := graphics.QuadIndices()
@ -735,8 +736,8 @@ func TestDisallowReplacePixelsForPartAfterDrawTriangles(t *testing.T) {
}()
const w, h = 16, 16
src := NewImage(w, h, false)
dst := NewImage(w, h, false)
src := NewImage(w, h)
dst := NewImage(w, h)
vs := quadVertices(w, h, 0, 0)
is := graphics.QuadIndices()
@ -750,7 +751,7 @@ func TestExtend(t *testing.T) {
}
const w, h = 16, 16
orig := NewImage(w, h, false)
orig := NewImage(w, h)
pix := make([]byte, 4*w*h)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
@ -785,7 +786,7 @@ func TestExtend(t *testing.T) {
func TestClearPixels(t *testing.T) {
const w, h = 16, 16
img := NewImage(w, h, false)
img := NewImage(w, h)
img.ReplacePixels(make([]byte, 4*4*4), 0, 0, 4, 4)
img.ReplacePixels(make([]byte, 4*4*4), 4, 0, 4, 4)
img.ClearPixels(0, 0, 4, 4)
@ -797,7 +798,7 @@ func TestClearPixels(t *testing.T) {
func TestFill(t *testing.T) {
const w, h = 16, 16
img := NewImage(w, h, false)
img := NewImage(w, h)
img.Fill(color.RGBA{0xff, 0, 0, 0xff})
if err := ResolveStaleImages(); err != nil {
t.Fatal(err)
@ -823,10 +824,10 @@ func TestFill(t *testing.T) {
// Issue #1170
func TestFill2(t *testing.T) {
const w, h = 16, 16
src := NewImage(w, h, false)
src := NewImage(w, h)
src.Fill(color.RGBA{0xff, 0, 0, 0xff})
dst := NewImage(w, h, false)
dst := NewImage(w, h)
vs := quadVertices(w, h, 0, 0)
is := graphics.QuadIndices()
dst.DrawTriangles([graphics.ShaderImageNum]*Image{src}, [graphics.ShaderImageNum - 1][2]float32{}, vs, is, nil, driver.CompositeModeCopy, driver.FilterNearest, driver.AddressUnsafe, driver.Region{}, nil, nil)
@ -854,8 +855,8 @@ func TestFill2(t *testing.T) {
func TestMutateSlices(t *testing.T) {
const w, h = 16, 16
dst := NewImage(w, h, false)
src := NewImage(w, h, false)
dst := NewImage(w, h)
src := NewImage(w, h)
pix := make([]byte, 4*w*h)
for i := 0; i < w*h; i++ {
pix[4*i] = byte(i)

View File

@ -25,7 +25,7 @@ import (
)
func TestShader(t *testing.T) {
img := NewImage(1, 1, false)
img := NewImage(1, 1)
defer img.Dispose()
ir := etesting.ShaderProgramFill(0xff, 0, 0, 0xff)
@ -50,7 +50,7 @@ func TestShaderChain(t *testing.T) {
const num = 10
imgs := []*Image{}
for i := 0; i < num; i++ {
img := NewImage(1, 1, false)
img := NewImage(1, 1)
defer img.Dispose()
imgs = append(imgs, img)
}
@ -82,13 +82,13 @@ func TestShaderChain(t *testing.T) {
func TestShaderMultipleSources(t *testing.T) {
var srcs [graphics.ShaderImageNum]*Image
for i := range srcs {
srcs[i] = NewImage(1, 1, false)
srcs[i] = NewImage(1, 1)
}
srcs[0].ReplacePixels([]byte{0x40, 0, 0, 0xff}, 0, 0, 1, 1)
srcs[1].ReplacePixels([]byte{0, 0x80, 0, 0xff}, 0, 0, 1, 1)
srcs[2].ReplacePixels([]byte{0, 0, 0xc0, 0xff}, 0, 0, 1, 1)
dst := NewImage(1, 1, false)
dst := NewImage(1, 1)
ir := etesting.ShaderProgramImages(3)
s := NewShader(&ir)
@ -113,7 +113,7 @@ func TestShaderMultipleSources(t *testing.T) {
}
func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
src := NewImage(3, 1, false)
src := NewImage(3, 1)
src.ReplacePixels([]byte{
0x40, 0, 0, 0xff,
0, 0x80, 0, 0xff,
@ -121,7 +121,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
}, 0, 0, 3, 1)
srcs := [graphics.ShaderImageNum]*Image{src, src, src}
dst := NewImage(1, 1, false)
dst := NewImage(1, 1)
ir := etesting.ShaderProgramImages(3)
s := NewShader(&ir)
@ -149,7 +149,7 @@ func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
}
func TestShaderDispose(t *testing.T) {
img := NewImage(1, 1, false)
img := NewImage(1, 1)
defer img.Dispose()
ir := etesting.ShaderProgramFill(0xff, 0, 0, 0xff)

View File

@ -212,7 +212,8 @@ func (i *Image) ensureNotShared() {
sy0 := float32(oy)
sx1 := float32(ox + w)
sy1 := float32(oy + h)
newImg := restorable.NewImage(w, h, i.volatile)
newImg := restorable.NewImage(w, h)
newImg.SetVolatile(i.volatile)
vs := []float32{
dx0, dy0, sx0, sy0, 1, 1, 1, 1,
dx1, dy0, sx1, sy0, 1, 1, 1, 1,
@ -244,7 +245,8 @@ func (i *Image) makeShared() error {
panic("shareable: makeShared cannot be called on a non-shareable image")
}
newI := NewImage(i.width, i.height, i.volatile)
newI := NewImage(i.width, i.height)
newI.SetVolatile(i.volatile)
pixels := make([]byte, 4*i.width*i.height)
for y := 0; y < i.height; y++ {
for x := 0; x < i.width; x++ {
@ -545,15 +547,25 @@ func (i *Image) dispose(markDisposed bool) {
theBackends = append(theBackends[:index], theBackends[index+1:]...)
}
func NewImage(width, height int, volatile bool) *Image {
func NewImage(width, height int) *Image {
// Actual allocation is done lazily, and the lock is not needed.
return &Image{
width: width,
height: height,
volatile: volatile,
}
}
func (i *Image) SetVolatile(volatile bool) {
i.volatile = volatile
if i.backend == nil {
return
}
if i.volatile {
i.ensureNotShared()
}
i.backend.restorable.SetVolatile(i.volatile)
}
func (i *Image) shareable() bool {
if minSize == 0 || maxSize == 0 {
panic("shareable: minSize or maxSize must be initialized")
@ -584,8 +596,9 @@ func (i *Image) allocate(shareable bool) {
if !shareable || !i.shareable() {
i.backend = &backend{
restorable: restorable.NewImage(i.width+2*paddingSize, i.height+2*paddingSize, i.volatile),
restorable: restorable.NewImage(i.width+2*paddingSize, i.height+2*paddingSize),
}
i.backend.restorable.SetVolatile(i.volatile)
return
}
@ -605,9 +618,10 @@ func (i *Image) allocate(shareable bool) {
}
b := &backend{
restorable: restorable.NewImage(size, size, i.volatile),
restorable: restorable.NewImage(size, size),
page: packing.NewPage(size, maxSize),
}
b.restorable.SetVolatile(i.volatile)
theBackends = append(theBackends, b)
n := b.page.Alloc(i.width+2*paddingSize, i.height+2*paddingSize)

View File

@ -58,22 +58,22 @@ const bigSize = 2049
func TestEnsureNotShared(t *testing.T) {
// Create img1 and img2 with this size so that the next images are allocated
// with non-upper-left location.
img1 := NewImage(bigSize, 100, false)
img1 := NewImage(bigSize, 100)
defer img1.MarkDisposed()
// Ensure img1's region is allocated.
img1.ReplacePixels(make([]byte, 4*bigSize*100))
img2 := NewImage(100, bigSize, false)
img2 := NewImage(100, bigSize)
defer img2.MarkDisposed()
img2.ReplacePixels(make([]byte, 4*100*bigSize))
const size = 32
img3 := NewImage(size/2, size/2, false)
img3 := NewImage(size/2, size/2)
defer img3.MarkDisposed()
img3.ReplacePixels(make([]byte, (size/2)*(size/2)*4))
img4 := NewImage(size, size, false)
img4 := NewImage(size, size)
defer img4.MarkDisposed()
pix := make([]byte, size*size*4)
@ -132,18 +132,18 @@ func TestEnsureNotShared(t *testing.T) {
func TestReshared(t *testing.T) {
const size = 16
img0 := NewImage(size, size, false)
img0 := NewImage(size, size)
defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*size*size))
img1 := NewImage(size, size, false)
img1 := NewImage(size, size)
defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size))
if got, want := img1.IsSharedForTesting(), true; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
img2 := NewImage(size, size, false)
img2 := NewImage(size, size)
defer img2.MarkDisposed()
pix := make([]byte, 4*size*size)
for j := 0; j < size; j++ {
@ -156,7 +156,8 @@ func TestReshared(t *testing.T) {
}
img2.ReplacePixels(pix)
img3 := NewImage(size, size, true /* volatile */)
img3 := NewImage(size, size)
img3.SetVolatile(true)
defer img3.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*size*size))
if got, want := img3.IsSharedForTesting(), false; got != want {
@ -242,7 +243,7 @@ func TestReshared(t *testing.T) {
func TestExtend(t *testing.T) {
const w0, h0 = 100, 100
img0 := NewImage(w0, h0, false)
img0 := NewImage(w0, h0)
defer img0.MarkDisposed()
p0 := make([]byte, 4*w0*h0)
for i := 0; i < w0*h0; i++ {
@ -254,7 +255,7 @@ func TestExtend(t *testing.T) {
img0.ReplacePixels(p0)
const w1, h1 = 1025, 100
img1 := NewImage(w1, h1, false)
img1 := NewImage(w1, h1)
defer img1.MarkDisposed()
p1 := make([]byte, 4*w1*h1)
for i := 0; i < w1*h1; i++ {
@ -310,9 +311,9 @@ func TestExtend(t *testing.T) {
func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
const w, h = 256, 256
src := NewImage(w, h, false)
src := NewImage(w, h)
defer src.MarkDisposed()
dst := NewImage(w, h, false)
dst := NewImage(w, h)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -352,9 +353,9 @@ func TestReplacePixelsAfterDrawTriangles(t *testing.T) {
// Issue #887
func TestSmallImages(t *testing.T) {
const w, h = 4, 8
src := NewImage(w, h, false)
src := NewImage(w, h)
defer src.MarkDisposed()
dst := NewImage(w, h, false)
dst := NewImage(w, h)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -391,11 +392,11 @@ func TestSmallImages(t *testing.T) {
// Issue #887
func TestLongImages(t *testing.T) {
const w, h = 1, 6
src := NewImage(w, h, false)
src := NewImage(w, h)
defer src.MarkDisposed()
const dstW, dstH = 256, 256
dst := NewImage(dstW, dstH, false)
dst := NewImage(dstW, dstH)
defer dst.MarkDisposed()
pix := make([]byte, 4*w*h)
@ -433,10 +434,10 @@ func TestLongImages(t *testing.T) {
func TestDisposeImmediately(t *testing.T) {
// This tests restorable.Image.ClearPixels is called but ReplacePixels is not called.
img0 := NewImage(16, 16, false)
img0 := NewImage(16, 16)
img0.EnsureNotSharedForTesting()
img1 := NewImage(16, 16, false)
img1 := NewImage(16, 16)
img1.EnsureNotSharedForTesting()
// img0 and img1 should share the same backend in 99.9999% possibility.
@ -447,12 +448,12 @@ func TestDisposeImmediately(t *testing.T) {
// Issue #1028
func TestExtendWithBigImage(t *testing.T) {
img0 := NewImage(1, 1, false)
img0 := NewImage(1, 1)
defer img0.MarkDisposed()
img0.ReplacePixels(make([]byte, 4*1*1))
img1 := NewImage(1025, 1025, false)
img1 := NewImage(1025, 1025)
defer img1.MarkDisposed()
img1.ReplacePixels(make([]byte, 4*1025*1025))
@ -462,7 +463,7 @@ func TestExtendWithBigImage(t *testing.T) {
func TestMaxImageSize(t *testing.T) {
// This tests that a too-big image is allocated correctly.
s := maxImageSizeForTesting
img := NewImage(s, s, false)
img := NewImage(s, s)
defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s))
}
@ -474,7 +475,7 @@ func 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 := NewImage(s, s, false)
img := NewImage(s, s)
defer img.MarkDisposed()
img.ReplacePixels(make([]byte, 4*s*s))
}

View File

@ -175,7 +175,8 @@ func (c *uiContext) updateOffscreen() {
}
}
if c.offscreen == nil {
c.offscreen = newImage(sw, sh, FilterDefault, true)
c.offscreen = newImage(sw, sh, FilterDefault)
c.offscreen.mipmap.SetVolatile(true)
}
// The window size is automatically adjusted when Run is used.