diff --git a/internal/graphics/bytes.go b/internal/graphics/bytes.go index a92e299ac..6bd9a6b2b 100644 --- a/internal/graphics/bytes.go +++ b/internal/graphics/bytes.go @@ -59,6 +59,15 @@ func (m *ManagedBytes) GetAndRelease() ([]byte, func()) { } } +// Release releases the underlying byte slice. +// +// After Release is called, the underlying byte slice is no longer available. +func (m *ManagedBytes) Release() { + m.pool.put(m.bytes) + m.bytes = nil + 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. diff --git a/internal/graphicscommand/image.go b/internal/graphicscommand/image.go index 7ef528daa..3f5ed8c07 100644 --- a/internal/graphicscommand/image.go +++ b/internal/graphicscommand/image.go @@ -161,7 +161,23 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, args []graphi } func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) { - i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs, writePixelsCommandArgs{ + // Release the previous pixels if the region is included by the new region. + // Successive WritePixels calls might accumulate the pixels and never release, + // especially when the image is unmanaged (#3036). + var cur int + for idx := 0; idx < len(i.bufferedWritePixelsArgs); idx++ { + arg := i.bufferedWritePixelsArgs[idx] + if arg.region.In(region) { + arg.pixels.Release() + continue + } + i.bufferedWritePixelsArgs[cur] = arg + cur++ + } + for idx := cur; idx < len(i.bufferedWritePixelsArgs); idx++ { + i.bufferedWritePixelsArgs[idx] = writePixelsCommandArgs{} + } + i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs[:cur], writePixelsCommandArgs{ pixels: pixels, region: region, }) diff --git a/internal/graphicscommand/image_test.go b/internal/graphicscommand/image_test.go index 851a416b1..a60175edb 100644 --- a/internal/graphicscommand/image_test.go +++ b/internal/graphicscommand/image_test.go @@ -135,3 +135,54 @@ func TestShader(t *testing.T) { } } } + +// Issue #3036 +func TestSuccessiveWritePixels(t *testing.T) { + const w, h = 32, 32 + dst := graphicscommand.NewImage(w, h, false) + + dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }), image.Rect(0, 0, 1, 1)) + if got, want := len(dst.BufferedWritePixelsArgsForTesting()), 1; got != want { + t.Errorf("len(dst.BufferedWritePixelsArgsForTesting()): got %d, want: %d", got, want) + } + + dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }), image.Rect(1, 1, 2, 2)) + if got, want := len(dst.BufferedWritePixelsArgsForTesting()), 2; got != want { + t.Errorf("len(dst.BufferedWritePixelsArgsForTesting()): got %d, want: %d", got, want) + } + + dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }), image.Rect(0, 0, 1, 1)) + if got, want := len(dst.BufferedWritePixelsArgsForTesting()), 2; got != want { + t.Errorf("len(dst.BufferedWritePixelsArgsForTesting()): got %d, want: %d", got, want) + } + + dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }), image.Rect(0, 0, 1, 1)) + if got, want := len(dst.BufferedWritePixelsArgsForTesting()), 2; got != want { + t.Errorf("len(dst.BufferedWritePixelsArgsForTesting()): got %d, want: %d", got, want) + } + + dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) { + for i := range bs { + bs[i] = 0 + } + }), image.Rect(0, 0, 2, 2)) + if got, want := len(dst.BufferedWritePixelsArgsForTesting()), 1; got != want { + t.Errorf("len(dst.BufferedWritePixelsArgsForTesting()): got %d, want: %d", got, want) + } +}