mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2026-04-22 15:57:15 +08:00
ebiten: add RecyclableSubImage and restrict Recycle to recyclable images
The pool was previously used by both NewImage and SubImage, but only the sub-image churn case benefits from pooling. Any image could be recycled, including originals — a documented footgun. Add RecyclableSubImage that explicitly creates pooled sub-images, and restrict Recycle to only work on images created by RecyclableSubImage. Closes #3423 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,10 @@ type Image struct {
|
||||
// atime needs to be an atomic value since a sub-image atime can be accessed from its original image.
|
||||
atime atomic.Int64
|
||||
|
||||
// recyclable reports whether the image was created via [Image.RecyclableSubImage]
|
||||
// and can be returned to the pool via [Image.Recycle].
|
||||
recyclable bool
|
||||
|
||||
// usageCallbacks are callbacks that are invoked when the image is used.
|
||||
// usageCallbacks is valid only when the image is not a sub-image.
|
||||
usageCallbacks map[int64]usageCallback
|
||||
@@ -86,7 +90,7 @@ type Image struct {
|
||||
}
|
||||
|
||||
// theImagePool is a global pool of Image structs to reduce allocations.
|
||||
// Both [NewImage] and [Image.SubImage] draw from this pool; [Image.Recycle] returns to it.
|
||||
// [Image.RecyclableSubImage] draws from this pool; [Image.Recycle] returns to it.
|
||||
var theImagePool = sync.Pool{
|
||||
New: func() any { return &Image{} },
|
||||
}
|
||||
@@ -1152,7 +1156,7 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
|
||||
}
|
||||
}
|
||||
|
||||
img := theImagePool.Get().(*Image)
|
||||
img := &Image{}
|
||||
img.image = i.image
|
||||
img.bounds = r
|
||||
img.original = i
|
||||
@@ -1167,6 +1171,42 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
|
||||
return img
|
||||
}
|
||||
|
||||
// RecyclableSubImage returns a sub-image of the image from a global pool.
|
||||
// The returned sub-image can be returned to the pool by calling [Image.Recycle].
|
||||
//
|
||||
// RecyclableSubImage is useful when you need to create many sub-images with different bounds,
|
||||
// and want to avoid repeated allocations.
|
||||
//
|
||||
// Unlike [Image.SubImage], the returned sub-image is not cached internally.
|
||||
// The caller is responsible for managing the lifecycle of the returned image.
|
||||
//
|
||||
// If the image is disposed, RecyclableSubImage panics.
|
||||
func (i *Image) RecyclableSubImage(r image.Rectangle) *Image {
|
||||
i.copyCheck()
|
||||
if i.isDisposed() {
|
||||
panic("ebiten: the image is already disposed")
|
||||
}
|
||||
|
||||
if i.isSubImage() {
|
||||
return i.original.RecyclableSubImage(r.Intersect(i.Bounds()))
|
||||
}
|
||||
|
||||
r = r.Intersect(i.Bounds())
|
||||
// Need to check Empty explicitly. See the standard image package implementations.
|
||||
if r.Empty() {
|
||||
r = image.Rectangle{}
|
||||
}
|
||||
|
||||
img := theImagePool.Get().(*Image)
|
||||
img.image = i.image
|
||||
img.bounds = r
|
||||
img.original = i
|
||||
img.addr = img
|
||||
img.recyclable = true
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
// Bounds returns the bounds of the image.
|
||||
//
|
||||
// Bounds implements the standard image.Image's Bounds.
|
||||
@@ -1359,26 +1399,16 @@ func (i *Image) Deallocate() {
|
||||
// Recycle puts the Image struct back into a global pool for reuse, reducing allocations.
|
||||
// After Recycle is called, the image must not be used; the behavior is undefined.
|
||||
//
|
||||
// In most cases, you don't have to call Recycle.
|
||||
// Recycle is useful when you need to create many sub-images with different bounds,
|
||||
// and want to avoid repeated allocations.
|
||||
//
|
||||
// Be careful when calling Recycle on a sub-image obtained from [Image.SubImage].
|
||||
// If the sub-image's bounds cover the original image's bounds, [Image.SubImage] may return
|
||||
// the original image itself, and calling Recycle on it would invalidate the original.
|
||||
// Recycle can only be called on images created by [Image.RecyclableSubImage].
|
||||
// Calling Recycle on any other image causes a panic.
|
||||
func (i *Image) Recycle() {
|
||||
i.copyCheck()
|
||||
if i.inUsageCallbacks.Load() {
|
||||
panic("ebiten: Recycle cannot be called from within a usage callback")
|
||||
if !i.recyclable {
|
||||
panic("ebiten: Recycle can only be called on an image created by RecyclableSubImage")
|
||||
}
|
||||
i.Deallocate()
|
||||
|
||||
// Clear all fields to release references and reset state.
|
||||
if i.isSubImage() {
|
||||
i.original.subImageCacheM.Lock()
|
||||
delete(i.original.subImageCache, i.bounds)
|
||||
i.original.subImageCacheM.Unlock()
|
||||
}
|
||||
i.addr = nil
|
||||
i.image = nil
|
||||
i.original = nil
|
||||
i.bounds = image.Rectangle{}
|
||||
@@ -1389,6 +1419,7 @@ func (i *Image) Recycle() {
|
||||
i.subImageGCLastTick = 0
|
||||
i.atime.Store(0)
|
||||
clear(i.usageCallbacks)
|
||||
i.recyclable = false
|
||||
|
||||
theImagePool.Put(i)
|
||||
}
|
||||
@@ -1488,7 +1519,7 @@ func newImage(bounds image.Rectangle, imageType atlas.ImageType) *Image {
|
||||
panic(fmt.Sprintf("ebiten: height at NewImage must be positive but %d", height))
|
||||
}
|
||||
|
||||
i := theImagePool.Get().(*Image)
|
||||
i := &Image{}
|
||||
i.image = ui.Get().NewImage(width, height, imageType)
|
||||
i.bounds = bounds
|
||||
i.addr = i
|
||||
|
||||
@@ -4867,3 +4867,72 @@ func TestSubImageRaceConditionWithSubImage(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestRecyclableSubImage(t *testing.T) {
|
||||
img := ebiten.NewImage(16, 16)
|
||||
img.Fill(color.White)
|
||||
|
||||
sub := img.RecyclableSubImage(image.Rect(0, 0, 8, 8))
|
||||
if got := sub.Bounds(); got != image.Rect(0, 0, 8, 8) {
|
||||
t.Errorf("Bounds(): got %v, want %v", got, image.Rect(0, 0, 8, 8))
|
||||
}
|
||||
|
||||
want := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
|
||||
if got := sub.At(0, 0); got != want {
|
||||
t.Errorf("At(0,0): got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
// Recycle should not panic on a recyclable sub-image.
|
||||
sub.Recycle()
|
||||
}
|
||||
|
||||
func TestRecyclableSubImageFromSubImage(t *testing.T) {
|
||||
img := ebiten.NewImage(16, 16)
|
||||
img.Fill(color.White)
|
||||
|
||||
sub := img.SubImage(image.Rect(0, 0, 12, 12)).(*ebiten.Image)
|
||||
rsub := sub.RecyclableSubImage(image.Rect(0, 0, 8, 8))
|
||||
if got := rsub.Bounds(); got != image.Rect(0, 0, 8, 8) {
|
||||
t.Errorf("Bounds(): got %v, want %v", got, image.Rect(0, 0, 8, 8))
|
||||
}
|
||||
|
||||
want := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
|
||||
if got := rsub.At(0, 0); got != want {
|
||||
t.Errorf("At(0,0): got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
rsub.Recycle()
|
||||
}
|
||||
|
||||
func TestRecycleOnNewImage(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("Recycle on NewImage must panic but did not")
|
||||
}
|
||||
}()
|
||||
img := ebiten.NewImage(16, 16)
|
||||
img.Recycle()
|
||||
}
|
||||
|
||||
func TestRecycleOnSubImage(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("Recycle on SubImage must panic but did not")
|
||||
}
|
||||
}()
|
||||
img := ebiten.NewImage(16, 16)
|
||||
sub := img.SubImage(image.Rect(0, 0, 8, 8)).(*ebiten.Image)
|
||||
sub.Recycle()
|
||||
}
|
||||
|
||||
func TestUseAfterRecycle(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("using a recycled image must panic but did not")
|
||||
}
|
||||
}()
|
||||
img := ebiten.NewImage(16, 16)
|
||||
sub := img.RecyclableSubImage(image.Rect(0, 0, 8, 8))
|
||||
sub.Recycle()
|
||||
sub.Fill(color.White)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user