From 9c408dce7c86219cf5380d8da364458dfbba5f78 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 29 Apr 2018 18:51:48 +0900 Subject: [PATCH] shareable: Lazy allocation (#592) Clear is called when an image becomes a render target, and it looks like Clear causes the memory spikes. Before this change, when an image is created, shared region on a GL texture is allocated. After that, the image's region is copied to non-shared region when the image becomes a rendering target. After this change, an image is not allocated on a GL texture first, and allocated only when it is necessary. Then, Clear calls can be avoided as much as possible. --- internal/shareable/shareable.go | 88 ++++++++++++++++++---------- internal/shareable/shareable_test.go | 23 -------- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/internal/shareable/shareable.go b/internal/shareable/shareable.go index f9b0938c7..13f7f4f8d 100644 --- a/internal/shareable/shareable.go +++ b/internal/shareable/shareable.go @@ -82,6 +82,10 @@ var ( ) type Image struct { + allocated bool + width int + height int + backend *backend // If node is nil, the image is not shared. @@ -89,11 +93,17 @@ type Image struct { } func (i *Image) ensureNotShared() { + if !i.allocated { + i.allocate(false) + return + } + if i.node == nil { return } x, y, w, h := i.region() + println(x, y, w, h) newImg := restorable.NewImage(w, h, false) newImg.DrawImage(i.backend.restorable, x, y, x+w, y+h, nil, nil, opengl.CompositeModeCopy, graphics.FilterNearest) @@ -104,6 +114,9 @@ func (i *Image) ensureNotShared() { } func (i *Image) region() (x, y, width, height int) { + if !i.allocated { + panic("not reached") + } if i.node == nil { w, h := i.backend.restorable.Size() return 0, 0, w, h @@ -112,15 +125,17 @@ func (i *Image) region() (x, y, width, height int) { } func (i *Image) Size() (width, height int) { - backendsM.Lock() - defer backendsM.Unlock() - _, _, w, h := i.region() - return w, h + return i.width, i.height } func (i *Image) DrawImage(img *Image, sx0, sy0, sx1, sy1 int, geom *affine.GeoM, colorm *affine.ColorM, mode opengl.CompositeMode, filter graphics.Filter) { backendsM.Lock() defer backendsM.Unlock() + + if !img.allocated { + img.allocate(true) + } + i.ensureNotShared() // Compare i and img after ensuring i is not shared, or @@ -141,6 +156,10 @@ func (i *Image) ReplacePixels(p []byte) { backendsM.Lock() defer backendsM.Unlock() + if !i.allocated { + i.allocate(true) + } + x, y, w, h := i.region() if l := 4 * w * h; len(p) != l { panic(fmt.Sprintf("shareable: len(p) was %d but must be %d", len(p), l)) @@ -152,6 +171,10 @@ func (i *Image) At(x, y int) (color.Color, error) { backendsM.Lock() defer backendsM.Unlock() + if !i.allocated { + return color.RGBA{}, nil + } + ox, oy, w, h := i.region() if x < 0 || y < 0 || x >= w || y >= h { return color.RGBA{}, nil @@ -217,33 +240,37 @@ func (i *Image) IsInvalidated() (bool, error) { } func NewImage(width, height int) *Image { + // Actual allocation is done lazily. + return &Image{ + width: width, + height: height, + } +} + +func (i *Image) allocate(shareable bool) { const ( initSize = 1024 maxSize = 4096 ) - backendsM.Lock() - defer backendsM.Unlock() - - if width > maxSize || height > maxSize { - b := &backend{ - restorable: restorable.NewImage(width, height, false), - } - return &Image{ - backend: b, + if !shareable || i.width > maxSize || i.height > maxSize { + i.allocated = true + i.backend = &backend{ + restorable: restorable.NewImage(i.width, i.height, false), } + return } for _, b := range theBackends { - if n, ok := b.TryAlloc(width, height); ok { - return &Image{ - backend: b, - node: n, - } + if n, ok := b.TryAlloc(i.width, i.height); ok { + i.allocated = true + i.backend = b + i.node = n + return } } size := initSize - for width > size || height > size { + for i.width > size || i.height > size { if size == maxSize { panic("not reached") } @@ -256,16 +283,15 @@ func NewImage(width, height int) *Image { } theBackends = append(theBackends, b) - n := b.page.Alloc(width, height) + n := b.page.Alloc(i.width, i.height) if n == nil { panic("not reached") } - i := &Image{ - backend: b, - node: n, - } + i.allocated = true + i.backend = b + i.node = n runtime.SetFinalizer(i, (*Image).Dispose) - return i + return } func NewVolatileImage(width, height int) *Image { @@ -274,6 +300,9 @@ func NewVolatileImage(width, height int) *Image { r := restorable.NewImage(width, height, true) i := &Image{ + allocated: true, + width: width, + height: height, backend: &backend{ restorable: r, }, @@ -288,6 +317,9 @@ func NewScreenFramebufferImage(width, height int) *Image { r := restorable.NewScreenFramebufferImage(width, height) i := &Image{ + allocated: true, + width: width, + height: height, backend: &backend{ restorable: r, }, @@ -319,12 +351,6 @@ func Restore() error { return restorable.Restore() } -func BackendNumForTesting() int { - backendsM.Lock() - defer backendsM.Unlock() - return len(theBackends) -} - func Images() ([]image.Image, error) { backendsM.Lock() defer backendsM.Unlock() diff --git a/internal/shareable/shareable_test.go b/internal/shareable/shareable_test.go index 55638d6af..1a4b1441b 100644 --- a/internal/shareable/shareable_test.go +++ b/internal/shareable/shareable_test.go @@ -103,26 +103,3 @@ func TestEnsureNotShared(t *testing.T) { } } } - -func TestDispose(t *testing.T) { - // There are already backend image for the offscreen or something. - // Creating a big image and the next image should be created at a new backend. - img1 := NewImage(bigSize, bigSize) - defer img1.Dispose() - - if BackendNumForTesting() != 1 { - t.Errorf("BackendNumForTesting(): got: %d, want: %d", BackendNumForTesting(), 1) - } - - img2 := NewImage(bigSize, bigSize) - - if BackendNumForTesting() != 2 { - t.Errorf("BackendNumForTesting(): got: %d, want: %d", BackendNumForTesting(), 2) - } - - img2.Dispose() - - if BackendNumForTesting() != 1 { - t.Errorf("BackendNumForTesting(): got: %d, want: %d", BackendNumForTesting(), 1) - } -}