Skip to content

Commit

Permalink
add Image.SubImageInto method
Browse files Browse the repository at this point in the history
Instead of allocating the result on heap for the user,
allow the caller side to decide whether it should be heap-allocated
or stack-allocated.

This new method allows us to have a zero-alloc SubImage routine
for the use cases where we don't want to make an extra allocation.

The performance difference is measurable (comparison is done with benchstat):

	name             old time/op    new time/op    delta
	SubImage-12     203ns ± 2%      33ns ± 2%   -83.91%  (p=0.000 n=20+19)

	name             old alloc/op   new alloc/op   delta
	SubImage-12      112B ± 0%        0B       -100.00%  (p=0.000 n=20+20)

Using SubImage is `~203ns`, SubImageInto is `~33ns`.
SubImage does 1 allocation, SubImageInto does none.

SubImage is rewritten in a way to avoid the code duplication.
It should work identically to the previous contract (it returns nil
if `i` is disposed, etc.)

Fixes hajimehoshi#2902
  • Loading branch information
quasilyte committed Feb 4, 2024
1 parent e3b54b4 commit 1ea88db
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 4 deletions.
36 changes: 32 additions & 4 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -827,9 +827,37 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
// Successive uses of multiple various regions as rendering destination is still efficient
// when all the underlying images are the same, but some platforms like browsers might not work efficiently.
func (i *Image) SubImage(r image.Rectangle) image.Image {
var s Image
if !i.SubImageInto(&s, r) {
return nil
}
return &s
}

// SubImageInto is like SubImage, but it returns the result through the s parameter.
// The s image will be initialized to a result of taking the SubImage from i.
//
// This code is semantically equivalent to:
//
// s := i.SubImage(r)
//
// You might want to use this method to avoid a new Image object heap allocations.
// Use it like this:
//
// var s ebiten.Image
// i.SubImageInto(&s, r)
//
// And then use s as your sub image.
//
// This method returns false if i is disposed.
func (i *Image) SubImageInto(s *Image, r image.Rectangle) bool {
// Try to keep this method zero alloc.
// There is a test for that (TestSubImageIntoZeroAlloc).
// There is also a benchmark for the comparison.

i.copyCheck()
if i.isDisposed() {
return nil
return false
}

r = r.Intersect(i.Bounds())
Expand All @@ -843,14 +871,14 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
orig = i.original
}

img := &Image{
*s = Image{
image: i.image,
bounds: r,
original: orig,
}
img.addr = img
s.addr = s

return img
return true
}

// Bounds returns the bounds of the image.
Expand Down
26 changes: 26 additions & 0 deletions image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4582,3 +4582,29 @@ func TestImageDrawImageAfterDeallocation(t *testing.T) {
}
}
}

func TestSubImageIntoZeroAlloc(t *testing.T) {
i := ebiten.NewImage(1, 1)
var s ebiten.Image
allocs := testing.AllocsPerRun(1, func() {
i.SubImageInto(&s, image.Rect(0, 0, 1, 1))
})
if allocs != 0 {
t.Fatalf("have %d allocs, wanted 0", int(allocs))
}
}

func BenchmarkSubImageInto(b *testing.B) {
img := ebiten.NewImage(1, 1)
var subimg ebiten.Image
for i := 0; i < b.N; i++ {
img.SubImageInto(&subimg, image.Rect(0, 0, 1, 1))
}
}

func BenchmarkSubImage(b *testing.B) {
img := ebiten.NewImage(1, 1)
for i := 0; i < b.N; i++ {
img.SubImage(image.Rect(0, 0, 1, 1))
}
}

0 comments on commit 1ea88db

Please sign in to comment.