From f269b619032fa7953800b173f2973a09ca0ac233 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 8 Oct 2023 22:59:35 +0900 Subject: [PATCH] internal/atlas: introduce a managed byte slice pool A managed byte slice from the new byte slice pool has a function to release and put it back to the pool explicitly, and this doesn't rely on GCs. Updates #1681 Closes #2804 --- internal/atlas/image.go | 41 ++++--- internal/graphics/bytes.go | 134 +++++++++++++++++++++++ internal/graphicscommand/command.go | 41 +++++-- internal/graphicscommand/commandqueue.go | 14 ++- internal/graphicscommand/image.go | 11 +- internal/graphicscommand/image_test.go | 7 +- internal/restorable/image.go | 29 ++++- internal/restorable/images_test.go | 69 +++++++----- internal/restorable/pixelrecords.go | 25 ++++- internal/restorable/shader_test.go | 12 +- 10 files changed, 296 insertions(+), 87 deletions(-) create mode 100644 internal/graphics/bytes.go diff --git a/internal/atlas/image.go b/internal/atlas/image.go index d5ac68a5c..a75ebe924 100644 --- a/internal/atlas/image.go +++ b/internal/atlas/image.go @@ -482,39 +482,38 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) { } // Copy pixels in the case when pix is modified before the graphics command is executed. - // TODO: Create byte slices from a pool. - pix2 := make([]byte, len(pix)) - copy(pix2, pix) + pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) { + copy(bs, pix) + }) i.backend.restorable.WritePixels(pix2, region) return } - // TODO: Create byte slices from a pool. - pixb := make([]byte, 4*r.Dx()*r.Dy()) - - // Clear the edges. pixb might not be zero-cleared. // TODO: These loops assume that paddingSize is 1. // TODO: Is clearing edges explicitly really needed? const paddingSize = 1 if paddingSize != i.paddingSize() { panic(fmt.Sprintf("atlas: writePixels assumes the padding is always 1 but the actual padding was %d", i.paddingSize())) } - rowPixels := 4 * r.Dx() - for i := 0; i < rowPixels; i++ { - pixb[rowPixels*(r.Dy()-1)+i] = 0 - } - for j := 1; j < r.Dy(); j++ { - pixb[rowPixels*j-4] = 0 - pixb[rowPixels*j-3] = 0 - pixb[rowPixels*j-2] = 0 - pixb[rowPixels*j-1] = 0 - } - // Copy the content. - for j := 0; j < region.Dy(); j++ { - copy(pixb[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()]) - } + pixb := graphics.NewManagedBytes(4*r.Dx()*r.Dy(), func(bs []byte) { + // Clear the edges. bs might not be zero-cleared. + rowPixels := 4 * r.Dx() + for i := 0; i < rowPixels; i++ { + bs[rowPixels*(r.Dy()-1)+i] = 0 + } + for j := 1; j < r.Dy(); j++ { + bs[rowPixels*j-4] = 0 + bs[rowPixels*j-3] = 0 + bs[rowPixels*j-2] = 0 + bs[rowPixels*j-1] = 0 + } + // Copy the content. + for j := 0; j < region.Dy(); j++ { + copy(bs[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()]) + } + }) i.backend.restorable.WritePixels(pixb, r) } diff --git a/internal/graphics/bytes.go b/internal/graphics/bytes.go new file mode 100644 index 000000000..69e7ecc99 --- /dev/null +++ b/internal/graphics/bytes.go @@ -0,0 +1,134 @@ +// Copyright 2023 The Ebitengine 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 graphics + +import ( + "runtime" + "sync" +) + +// ManagedBytes is a managed byte slice. +// The internal byte alice are managed in a pool. +// ManagedBytes is useful when its lifetime is explicit, as the underlying byte slice can be reused for another ManagedBytes later. +// This can redduce allocations and GCs. +type ManagedBytes struct { + bytes []byte + pool *bytesPool +} + +// Len returns the length of the slice. +func (m *ManagedBytes) Len() int { + return len(m.bytes) +} + +// Read reads the byte slice's content to dst. +func (m *ManagedBytes) Read(dst []byte, from, to int) { + copy(dst, m.bytes[from:to]) +} + +// Clone creates a new ManagedBytes with the same content. +func (m *ManagedBytes) Clone() *ManagedBytes { + return NewManagedBytes(len(m.bytes), func(bs []byte) { + copy(bs, m.bytes) + }) +} + +// GetAndRelease returns the raw byte slice and a finalizer. +// A finalizer should be called when you can ensure that the slice is no longer used, +// e.g. when a graphics command using this slice is sent and executed. +// +// After GetAndRelease is called, the underlying byte slice is no longer available. +func (m *ManagedBytes) GetAndRelease() ([]byte, func()) { + bs := m.bytes + m.bytes = nil + return bs, func() { + m.pool.put(bs) + runtime.SetFinalizer(m, nil) + } +} + +// NewManagedBytes returns a managed byte slice initialized by the given constructor f. +// +// The byte slice is not zero-cleared at the constructor. +func NewManagedBytes(size int, f func([]byte)) *ManagedBytes { + bs := theBytesPool.get(size) + f(bs.bytes) + return bs +} + +type bytesPool struct { + pool [][]byte + + m sync.Mutex +} + +var theBytesPool bytesPool + +func (b *bytesPool) get(size int) *ManagedBytes { + bs := b.getFromCache(size) + if bs == nil { + bs = make([]byte, size) + } + m := &ManagedBytes{ + bytes: bs, + pool: b, + } + runtime.SetFinalizer(m, func(m *ManagedBytes) { + b.put(m.bytes) + }) + return m +} + +func (b *bytesPool) getFromCache(size int) []byte { + b.m.Lock() + defer b.m.Unlock() + + for i, bs := range b.pool { + if cap(bs) < size { + continue + } + + copy(b.pool[i:], b.pool[i+1:]) + b.pool[len(b.pool)-1] = nil + b.pool = b.pool[:len(b.pool)-1] + return bs[:size] + } + + return nil +} + +func (b *bytesPool) put(bs []byte) { + if len(bs) == 0 { + return + } + + b.m.Lock() + defer b.m.Unlock() + + b.pool = append(b.pool, bs) + + // GC the pool. The size limitation is arbitrary. + for len(b.pool) >= 32 || b.totalSize() >= 1024*1024*1024 { + b.pool = b.pool[1:] + } +} + +func (b *bytesPool) totalSize() int { + var s int + for _, bs := range b.pool { + s += len(bs) + } + return s +} diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 42e07871a..dc92561ea 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -16,6 +16,7 @@ package graphicscommand import ( "fmt" + "image" "math" "strings" @@ -33,7 +34,7 @@ import ( type command interface { fmt.Stringer - Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error + Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error NeedsSync() bool } @@ -101,7 +102,7 @@ func (c *drawTrianglesCommand) String() string { } // Exec executes the drawTrianglesCommand. -func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *drawTrianglesCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { // TODO: Is it ok not to bind any framebuffer here? if len(c.dstRegions) == 0 { return nil @@ -211,7 +212,12 @@ func mightOverlapDstRegions(vertices1, vertices2 []float32) bool { // writePixelsCommand represents a command to replace pixels of an image. type writePixelsCommand struct { dst *Image - args []graphicsdriver.PixelsArgs + args []writePixelsCommandArgs +} + +type writePixelsCommandArgs struct { + pixels *graphics.ManagedBytes + region image.Rectangle } func (c *writePixelsCommand) String() string { @@ -219,11 +225,24 @@ func (c *writePixelsCommand) String() string { } // Exec executes the writePixelsCommand. -func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *writePixelsCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { if len(c.args) == 0 { return nil } - if err := c.dst.image.WritePixels(c.args); err != nil { + args := make([]graphicsdriver.PixelsArgs, 0, len(c.args)) + for _, a := range c.args { + pix, f := a.pixels.GetAndRelease() + // A finalizer is executed when flushing the queue at the end of the frame. + // At the end of the frame, the last command is rendering triangles onto the screen, + // so the bytes are already sent to GPU and synced. + // TODO: This might be fragile. When is the better time to call finalizers by a command queue? + commandQueue.addFinalizer(f) + args = append(args, graphicsdriver.PixelsArgs{ + Pixels: pix, + Region: a.region, + }) + } + if err := c.dst.image.WritePixels(args); err != nil { return err } return nil @@ -239,7 +258,7 @@ type readPixelsCommand struct { } // Exec executes a readPixelsCommand. -func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *readPixelsCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { if err := c.img.image.ReadPixels(c.args); err != nil { return err } @@ -264,7 +283,7 @@ func (c *disposeImageCommand) String() string { } // Exec executes the disposeImageCommand. -func (c *disposeImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *disposeImageCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { c.target.image.Dispose() return nil } @@ -283,7 +302,7 @@ func (c *disposeShaderCommand) String() string { } // Exec executes the disposeShaderCommand. -func (c *disposeShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *disposeShaderCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { c.target.shader.Dispose() return nil } @@ -305,7 +324,7 @@ func (c *newImageCommand) String() string { } // Exec executes a newImageCommand. -func (c *newImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *newImageCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { var err error if c.screen { c.result.image, err = graphicsDriver.NewScreenFramebufferImage(c.width, c.height) @@ -330,7 +349,7 @@ func (c *newShaderCommand) String() string { } // Exec executes a newShaderCommand. -func (c *newShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *newShaderCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { s, err := graphicsDriver.NewShader(c.ir) if err != nil { return err @@ -352,7 +371,7 @@ func (c *isInvalidatedCommand) String() string { return fmt.Sprintf("is-invalidated: image: %d", c.image.id) } -func (c *isInvalidatedCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error { +func (c *isInvalidatedCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error { c.result = c.image.image.IsInvalidated() return nil } diff --git a/internal/graphicscommand/commandqueue.go b/internal/graphicscommand/commandqueue.go index 3446443be..f522caa1c 100644 --- a/internal/graphicscommand/commandqueue.go +++ b/internal/graphicscommand/commandqueue.go @@ -50,10 +50,17 @@ type commandQueue struct { drawTrianglesCommandPool drawTrianglesCommandPool uint32sBuffer uint32sBuffer + finalizers []func() err atomic.Value } +// addFinalizer adds a finalizer function to this queue. +// A finalizer is executed when the command queue is flushed at the end of the frame. +func (q *commandQueue) addFinalizer(f func()) { + q.finalizers = append(q.finalizers, f) +} + func (q *commandQueue) appendIndices(indices []uint16, offset uint16) { n := len(q.indices) q.indices = append(q.indices, indices...) @@ -220,6 +227,11 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo if endFrame { q.uint32sBuffer.reset() + for i, f := range q.finalizers { + f() + q.finalizers[i] = nil + } + q.finalizers = q.finalizers[:0] } }() @@ -247,7 +259,7 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo } indexOffset := 0 for _, c := range cs[:nc] { - if err := c.Exec(graphicsDriver, indexOffset); err != nil { + if err := c.Exec(q, graphicsDriver, indexOffset); err != nil { return err } logger.Logf(" %s\n", c) diff --git a/internal/graphicscommand/image.go b/internal/graphicscommand/image.go index 8b2612ef9..df42afab6 100644 --- a/internal/graphicscommand/image.go +++ b/internal/graphicscommand/image.go @@ -43,7 +43,7 @@ type Image struct { // have its graphicsdriver.Image. id int - bufferedWritePixelsArgs []graphicsdriver.PixelsArgs + bufferedWritePixelsArgs []writePixelsCommandArgs } var nextID = 1 @@ -78,6 +78,7 @@ func (i *Image) flushBufferedWritePixels() { if len(i.bufferedWritePixelsArgs) == 0 { return } + c := &writePixelsCommand{ dst: i, args: i.bufferedWritePixelsArgs, @@ -159,10 +160,10 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, args []graphi return nil } -func (i *Image) WritePixels(pixels []byte, region image.Rectangle) { - i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs, graphicsdriver.PixelsArgs{ - Pixels: pixels, - Region: region, +func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) { + i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs, writePixelsCommandArgs{ + pixels: pixels, + region: region, }) } diff --git a/internal/graphicscommand/image_test.go b/internal/graphicscommand/image_test.go index c4be518e0..ae55efc9a 100644 --- a/internal/graphicscommand/image_test.go +++ b/internal/graphicscommand/image_test.go @@ -92,7 +92,12 @@ func TestWritePixelsPartAfterDrawTriangles(t *testing.T) { dr := image.Rect(0, 0, w, h) dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{clr}, vs, is, graphicsdriver.BlendClear, dr, [graphics.ShaderImageCount]image.Rectangle{}, nearestFilterShader, nil, false) dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, nearestFilterShader, nil, false) - dst.WritePixels(make([]byte, 4), image.Rect(0, 0, 1, 1)) + bs := graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }) + dst.WritePixels(bs, image.Rect(0, 0, 1, 1)) // TODO: Check the result. } diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 89e7e19da..2e21c1fab 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -37,7 +37,7 @@ func (p *Pixels) Apply(img *graphicscommand.Image) { p.pixelsRecords.apply(img) } -func (p *Pixels) AddOrReplace(pix []byte, region image.Rectangle) { +func (p *Pixels) AddOrReplace(pix *graphics.ManagedBytes, region image.Rectangle) { if p.pixelsRecords == nil { p.pixelsRecords = &pixelsRecords{} } @@ -70,6 +70,13 @@ func (p *Pixels) AppendRegion(regions []image.Rectangle) []image.Rectangle { return p.pixelsRecords.appendRegions(regions) } +func (p *Pixels) Dispose() { + if p.pixelsRecords == nil { + return + } + p.pixelsRecords.dispose() +} + // drawTrianglesHistoryItem is an item for history of draw-image commands. type drawTrianglesHistoryItem struct { images [graphics.ShaderImageCount]*Image @@ -251,7 +258,7 @@ func (i *Image) needsRestoring() bool { // WritePixels replaces the image pixels with the given pixels slice. // // The specified region must not be overlapped with other regions by WritePixels. -func (i *Image) WritePixels(pixels []byte, region image.Rectangle) { +func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) { if region.Dx() <= 0 || region.Dy() <= 0 { panic("restorable: width/height must be positive") } @@ -278,7 +285,8 @@ func (i *Image) WritePixels(pixels []byte, region image.Rectangle) { if region.Eq(image.Rect(0, 0, w, h)) { if pixels != nil { - i.basePixels.AddOrReplace(pixels, image.Rect(0, 0, w, h)) + // Clone a ManagedBytes as the package graphicscommand has a different lifetime management. + i.basePixels.AddOrReplace(pixels.Clone(), image.Rect(0, 0, w, h)) } else { i.basePixels.Clear(image.Rect(0, 0, w, h)) } @@ -295,7 +303,8 @@ func (i *Image) WritePixels(pixels []byte, region image.Rectangle) { } if pixels != nil { - i.basePixels.AddOrReplace(pixels, region) + // Clone a ManagedBytes as the package graphicscommand has a different lifetime management. + i.basePixels.AddOrReplace(pixels.Clone(), region) } else { i.basePixels.Clear(region) } @@ -483,7 +492,10 @@ func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error } for _, a := range args { - i.basePixels.AddOrReplace(a.Pixels, a.Region) + bs := graphics.NewManagedBytes(len(a.Pixels), func(bs []byte) { + copy(bs, a.Pixels) + }) + i.basePixels.AddOrReplace(bs, a.Region) } i.clearDrawTrianglesHistory() @@ -563,6 +575,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error { // The screen image should also be recreated because framebuffer might // be changed. i.image = graphicscommand.NewImage(w, h, true) + i.basePixels.Dispose() i.basePixels = Pixels{} i.clearDrawTrianglesHistory() i.stale = false @@ -633,7 +646,10 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error { } for _, a := range args { - i.basePixels.AddOrReplace(a.Pixels, a.Region) + bs := graphics.NewManagedBytes(len(a.Pixels), func(bs []byte) { + copy(bs, a.Pixels) + }) + i.basePixels.AddOrReplace(bs, a.Region) } } @@ -651,6 +667,7 @@ func (i *Image) Dispose() { theImages.remove(i) i.image.Dispose() i.image = nil + i.basePixels.Dispose() i.basePixels = Pixels{} i.pixelsCache = nil i.clearDrawTrianglesHistory() diff --git a/internal/restorable/images_test.go b/internal/restorable/images_test.go index 9b1f456e3..4c02e7fe9 100644 --- a/internal/restorable/images_test.go +++ b/internal/restorable/images_test.go @@ -37,6 +37,15 @@ func pixelsToColor(p *restorable.Pixels, i, j, imageWidth, imageHeight int) colo return color.RGBA{R: pix[0], G: pix[1], B: pix[2], A: pix[3]} } +func bytesToManagedBytes(src []byte) *graphics.ManagedBytes { + if len(src) == 0 { + panic("restorable: len(src) must be > 0") + } + return graphics.NewManagedBytes(len(src), func(dst []byte) { + copy(dst, src) + }) +} + func abs(x int) int { if x < 0 { return -x @@ -61,7 +70,7 @@ func TestRestore(t *testing.T) { defer img0.Dispose() clr0 := color.RGBA{A: 0xff} - img0.WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, 1, 1)) + img0.WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, 1, 1)) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) } @@ -129,7 +138,7 @@ func TestRestoreChain(t *testing.T) { } }() clr := color.RGBA{A: 0xff} - imgs[0].WritePixels([]byte{clr.R, clr.G, clr.B, clr.A}, image.Rect(0, 0, 1, 1)) + imgs[0].WritePixels(bytesToManagedBytes([]byte{clr.R, clr.G, clr.B, clr.A}), image.Rect(0, 0, 1, 1)) for i := 0; i < num-1; i++ { vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() @@ -169,11 +178,11 @@ func TestRestoreChain2(t *testing.T) { }() clr0 := color.RGBA{R: 0xff, A: 0xff} - imgs[0].WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, w, h)) + imgs[0].WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, w, h)) clr7 := color.RGBA{G: 0xff, A: 0xff} - imgs[7].WritePixels([]byte{clr7.R, clr7.G, clr7.B, clr7.A}, image.Rect(0, 0, w, h)) + imgs[7].WritePixels(bytesToManagedBytes([]byte{clr7.R, clr7.G, clr7.B, clr7.A}), image.Rect(0, 0, w, h)) clr8 := color.RGBA{B: 0xff, A: 0xff} - imgs[8].WritePixels([]byte{clr8.R, clr8.G, clr8.B, clr8.A}, image.Rect(0, 0, w, h)) + imgs[8].WritePixels(bytesToManagedBytes([]byte{clr8.R, clr8.G, clr8.B, clr8.A}), image.Rect(0, 0, w, h)) is := graphics.QuadIndices() dr := image.Rect(0, 0, w, h) @@ -218,12 +227,12 @@ func TestRestoreOverrideSource(t *testing.T) { }() clr0 := color.RGBA{A: 0xff} clr1 := color.RGBA{B: 0x01, A: 0xff} - img1.WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, w, h)) + img1.WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, w, h)) is := graphics.QuadIndices() dr := image.Rect(0, 0, w, h) img2.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) - img0.WritePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, image.Rect(0, 0, w, h)) + img0.WritePixels(bytesToManagedBytes([]byte{clr1.R, clr1.G, clr1.B, clr1.A}), image.Rect(0, 0, w, h)) img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) @@ -390,7 +399,7 @@ func TestRestoreComplexGraph(t *testing.T) { func newImageFromImage(rgba *image.RGBA) *restorable.Image { s := rgba.Bounds().Size() img := restorable.NewImage(s.X, s.Y, restorable.ImageTypeRegular) - img.WritePixels(rgba.Pix, image.Rect(0, 0, s.X, s.Y)) + img.WritePixels(bytesToManagedBytes(rgba.Pix), image.Rect(0, 0, s.X, s.Y)) return img } @@ -459,7 +468,7 @@ func TestWritePixels(t *testing.T) { for i := range pix { pix[i] = 0xff } - img.WritePixels(pix, image.Rect(5, 7, 9, 11)) + img.WritePixels(bytesToManagedBytes(pix), image.Rect(5, 7, 9, 11)) // Check the region (5, 7)-(9, 11). Outside state is indeterminate. pix = make([]byte, 4*4*4) for i := range pix { @@ -514,7 +523,7 @@ func TestDrawTrianglesAndWritePixels(t *testing.T) { is := graphics.QuadIndices() dr := image.Rect(0, 0, 2, 1) img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) - img1.WritePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, image.Rect(0, 0, 2, 1)) + img1.WritePixels(bytesToManagedBytes([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}), image.Rect(0, 0, 2, 1)) if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { t.Fatal(err) @@ -580,7 +589,7 @@ func TestWritePixelsPart(t *testing.T) { img := restorable.NewImage(4, 4, restorable.ImageTypeRegular) // This doesn't make the image stale. Its base pixels are available. - img.WritePixels(pix, image.Rect(1, 1, 3, 3)) + img.WritePixels(bytesToManagedBytes(pix), image.Rect(1, 1, 3, 3)) cases := []struct { i int @@ -655,14 +664,14 @@ func TestWritePixelsOnly(t *testing.T) { defer img1.Dispose() for i := 0; i < w*h; i += 5 { - img0.WritePixels([]byte{1, 2, 3, 4}, image.Rect(i%w, i/w, i%w+1, i/w+1)) + img0.WritePixels(bytesToManagedBytes([]byte{1, 2, 3, 4}), image.Rect(i%w, i/w, i%w+1, i/w+1)) } vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := image.Rect(0, 0, 1, 1) img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) - img0.WritePixels([]byte{5, 6, 7, 8}, image.Rect(0, 0, 1, 1)) + img0.WritePixels(bytesToManagedBytes([]byte{5, 6, 7, 8}), image.Rect(0, 0, 1, 1)) // BasePixelsForTesting is available without GPU accessing. for j := 0; j < h; j++ { @@ -704,14 +713,14 @@ func TestReadPixelsFromVolatileImage(t *testing.T) { src := restorable.NewImage(w, h, restorable.ImageTypeRegular) // First, make sure that dst has pixels - dst.WritePixels(make([]byte, 4*w*h), image.Rect(0, 0, w, h)) + dst.WritePixels(bytesToManagedBytes(make([]byte, 4*w*h)), image.Rect(0, 0, w, h)) // Second, draw src to dst. If the implementation is correct, dst becomes stale. pix := make([]byte, 4*w*h) for i := range pix { pix[i] = 0xff } - src.WritePixels(pix, image.Rect(0, 0, w, h)) + src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h)) vs := quadVertices(1, 1, 0, 0) is := graphics.QuadIndices() dr := image.Rect(0, 0, w, h) @@ -740,7 +749,7 @@ func TestAllowWritePixelsAfterDrawTriangles(t *testing.T) { is := graphics.QuadIndices() dr := image.Rect(0, 0, w, h) dst.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) - dst.WritePixels(make([]byte, 4*w*h), image.Rect(0, 0, w, h)) + dst.WritePixels(bytesToManagedBytes(make([]byte, 4*w*h)), image.Rect(0, 0, w, h)) // WritePixels for a whole image doesn't panic. } @@ -753,13 +762,13 @@ func TestAllowWritePixelsForPartAfterDrawTriangles(t *testing.T) { for i := range pix { pix[i] = 0xff } - src.WritePixels(pix, image.Rect(0, 0, w, h)) + src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h)) vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() dr := image.Rect(0, 0, w, h) dst.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false) - dst.WritePixels(make([]byte, 4*2*2), image.Rect(0, 0, 2, 2)) + dst.WritePixels(bytesToManagedBytes(make([]byte, 4*2*2)), image.Rect(0, 0, 2, 2)) // WritePixels for a part of image doesn't panic. if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil { @@ -806,7 +815,7 @@ func TestExtend(t *testing.T) { } } - orig.WritePixels(pix, image.Rect(0, 0, w, h)) + orig.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h)) extended := orig.Extend(w*2, h*2) // After this, orig is already disposed. result := make([]byte, 4*(w*2)*(h*2)) @@ -846,7 +855,7 @@ func TestDrawTrianglesAndExtend(t *testing.T) { pix[4*idx+3] = v } } - src.WritePixels(pix, image.Rect(0, 0, w, h)) + src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h)) orig := restorable.NewImage(w, h, restorable.ImageTypeRegular) vs := quadVertices(w, h, 0, 0) @@ -876,13 +885,13 @@ func TestDrawTrianglesAndExtend(t *testing.T) { func TestClearPixels(t *testing.T) { const w, h = 16, 16 img := restorable.NewImage(w, h, restorable.ImageTypeRegular) - img.WritePixels(make([]byte, 4*4*4), image.Rect(0, 0, 4, 4)) - img.WritePixels(make([]byte, 4*4*4), image.Rect(4, 0, 8, 4)) + img.WritePixels(bytesToManagedBytes(make([]byte, 4*4*4)), image.Rect(0, 0, 4, 4)) + img.WritePixels(bytesToManagedBytes(make([]byte, 4*4*4)), image.Rect(4, 0, 8, 4)) img.ClearPixels(image.Rect(0, 0, 4, 4)) img.ClearPixels(image.Rect(4, 0, 8, 4)) // After clearing, the regions will be available again. - img.WritePixels(make([]byte, 4*8*4), image.Rect(0, 0, 8, 4)) + img.WritePixels(bytesToManagedBytes(make([]byte, 4*8*4)), image.Rect(0, 0, 8, 4)) } func TestMutateSlices(t *testing.T) { @@ -896,7 +905,7 @@ func TestMutateSlices(t *testing.T) { pix[4*i+2] = byte(i) pix[4*i+3] = 0xff } - src.WritePixels(pix, image.Rect(0, 0, w, h)) + src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h)) vs := quadVertices(w, h, 0, 0) is := make([]uint16, len(graphics.QuadIndices())) @@ -950,7 +959,7 @@ func TestOverlappedPixels(t *testing.T) { pix0[idx+3] = 0xff } } - dst.WritePixels(pix0, image.Rect(0, 0, 2, 2)) + dst.WritePixels(bytesToManagedBytes(pix0), image.Rect(0, 0, 2, 2)) pix1 := make([]byte, 4*2*2) for j := 0; j < 2; j++ { @@ -962,7 +971,7 @@ func TestOverlappedPixels(t *testing.T) { pix1[idx+3] = 0xff } } - dst.WritePixels(pix1, image.Rect(1, 1, 3, 3)) + dst.WritePixels(bytesToManagedBytes(pix1), image.Rect(1, 1, 3, 3)) wantColors := []color.RGBA{ {0xff, 0, 0, 0xff}, @@ -1032,7 +1041,7 @@ func TestOverlappedPixels(t *testing.T) { pix2[idx+3] = 0xff } } - dst.WritePixels(pix2, image.Rect(1, 1, 3, 3)) + dst.WritePixels(bytesToManagedBytes(pix2), image.Rect(1, 1, 3, 3)) wantColors = []color.RGBA{ {0xff, 0, 0, 0xff}, @@ -1089,7 +1098,7 @@ func TestDrawTrianglesAndReadPixels(t *testing.T) { src := restorable.NewImage(w, h, restorable.ImageTypeRegular) dst := restorable.NewImage(w, h, restorable.ImageTypeRegular) - src.WritePixels([]byte{0x80, 0x80, 0x80, 0x80}, image.Rect(0, 0, 1, 1)) + src.WritePixels(bytesToManagedBytes([]byte{0x80, 0x80, 0x80, 0x80}), image.Rect(0, 0, 1, 1)) vs := quadVertices(w, h, 0, 0) is := graphics.QuadIndices() @@ -1109,10 +1118,10 @@ func TestWritePixelsAndDrawTriangles(t *testing.T) { src := restorable.NewImage(1, 1, restorable.ImageTypeRegular) dst := restorable.NewImage(2, 1, restorable.ImageTypeRegular) - src.WritePixels([]byte{0x80, 0x80, 0x80, 0x80}, image.Rect(0, 0, 1, 1)) + src.WritePixels(bytesToManagedBytes([]byte{0x80, 0x80, 0x80, 0x80}), image.Rect(0, 0, 1, 1)) // Call WritePixels first. - dst.WritePixels([]byte{0x40, 0x40, 0x40, 0x40}, image.Rect(0, 0, 1, 1)) + dst.WritePixels(bytesToManagedBytes([]byte{0x40, 0x40, 0x40, 0x40}), image.Rect(0, 0, 1, 1)) // Call DrawTriangles at a different region second. vs := quadVertices(1, 1, 1, 0) diff --git a/internal/restorable/pixelrecords.go b/internal/restorable/pixelrecords.go index 249542b86..b97446d4f 100644 --- a/internal/restorable/pixelrecords.go +++ b/internal/restorable/pixelrecords.go @@ -18,12 +18,13 @@ import ( "fmt" "image" + "github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" ) type pixelsRecord struct { rect image.Rectangle - pix []byte + pix *graphics.ManagedBytes } func (p *pixelsRecord) readPixels(pixels []byte, region image.Rectangle, imageWidth, imageHeight int) { @@ -41,7 +42,7 @@ func (p *pixelsRecord) readPixels(pixels []byte, region image.Rectangle, imageWi for j := 0; j < r.Dy(); j++ { dstX := 4 * ((dstBaseY+j)*region.Dx() + dstBaseX) srcX := 4 * ((srcBaseY+j)*p.rect.Dx() + srcBaseX) - copy(pixels[dstX:dstX+lineWidth], p.pix[srcX:srcX+lineWidth]) + p.pix.Read(pixels[dstX:dstX+lineWidth], srcX, srcX+lineWidth) } } else { for j := 0; j < r.Dy(); j++ { @@ -57,9 +58,9 @@ type pixelsRecords struct { records []*pixelsRecord } -func (pr *pixelsRecords) addOrReplace(pixels []byte, region image.Rectangle) { - if len(pixels) != 4*region.Dx()*region.Dy() { - msg := fmt.Sprintf("restorable: len(pixels) must be 4*%d*%d = %d but %d", region.Dx(), region.Dy(), 4*region.Dx()*region.Dy(), len(pixels)) +func (pr *pixelsRecords) addOrReplace(pixels *graphics.ManagedBytes, region image.Rectangle) { + if pixels.Len() != 4*region.Dx()*region.Dy() { + msg := fmt.Sprintf("restorable: len(pixels) must be 4*%d*%d = %d but %d", region.Dx(), region.Dy(), 4*region.Dx()*region.Dy(), pixels.Len()) if pixels == nil { msg += " (nil)" } @@ -128,7 +129,8 @@ func (pr *pixelsRecords) apply(img *graphicscommand.Image) { // TODO: Isn't this too heavy? Can we merge the operations? for _, r := range pr.records { if r.pix != nil { - img.WritePixels(r.pix, r.rect) + // Clone a ManagedBytes as the package graphicscommand has a different lifetime management. + img.WritePixels(r.pix.Clone(), r.rect) } else { clearImage(img, r.rect) } @@ -144,3 +146,14 @@ func (pr *pixelsRecords) appendRegions(regions []image.Rectangle) []image.Rectan } return regions } + +func (pr *pixelsRecords) dispose() { + for _, r := range pr.records { + if r.pix == nil { + continue + } + // As the package graphicscommands already has cloned ManagedBytes objects, it is OK to release it. + _, f := r.pix.GetAndRelease() + f() + } +} diff --git a/internal/restorable/shader_test.go b/internal/restorable/shader_test.go index 4fd674005..0c3c9442f 100644 --- a/internal/restorable/shader_test.go +++ b/internal/restorable/shader_test.go @@ -80,7 +80,7 @@ func TestShaderChain(t *testing.T) { imgs = append(imgs, img) } - imgs[0].WritePixels([]byte{0xff, 0, 0, 0xff}, image.Rect(0, 0, 1, 1)) + imgs[0].WritePixels(bytesToManagedBytes([]byte{0xff, 0, 0, 0xff}), image.Rect(0, 0, 1, 1)) s := restorable.NewShader(etesting.ShaderProgramImages(1)) for i := 0; i < num-1; i++ { @@ -109,9 +109,9 @@ func TestShaderMultipleSources(t *testing.T) { for i := range srcs { srcs[i] = restorable.NewImage(1, 1, restorable.ImageTypeRegular) } - srcs[0].WritePixels([]byte{0x40, 0, 0, 0xff}, image.Rect(0, 0, 1, 1)) - srcs[1].WritePixels([]byte{0, 0x80, 0, 0xff}, image.Rect(0, 0, 1, 1)) - srcs[2].WritePixels([]byte{0, 0, 0xc0, 0xff}, image.Rect(0, 0, 1, 1)) + srcs[0].WritePixels(bytesToManagedBytes([]byte{0x40, 0, 0, 0xff}), image.Rect(0, 0, 1, 1)) + srcs[1].WritePixels(bytesToManagedBytes([]byte{0, 0x80, 0, 0xff}), image.Rect(0, 0, 1, 1)) + srcs[2].WritePixels(bytesToManagedBytes([]byte{0, 0, 0xc0, 0xff}), image.Rect(0, 0, 1, 1)) dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular) @@ -138,11 +138,11 @@ func TestShaderMultipleSources(t *testing.T) { func TestShaderMultipleSourcesOnOneTexture(t *testing.T) { src := restorable.NewImage(3, 1, restorable.ImageTypeRegular) - src.WritePixels([]byte{ + src.WritePixels(bytesToManagedBytes([]byte{ 0x40, 0, 0, 0xff, 0, 0x80, 0, 0xff, 0, 0, 0xc0, 0xff, - }, image.Rect(0, 0, 3, 1)) + }), image.Rect(0, 0, 3, 1)) srcs := [graphics.ShaderImageCount]*restorable.Image{src, src, src} dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)