internal/restorable: optimize removeDuplicatedRegions

Updates #2375
Updates #2626
Updates #3083
This commit is contained in:
Hajime Hoshi 2024-09-06 21:15:00 +09:00
parent 35f4884a74
commit d50a438c07
3 changed files with 49 additions and 204 deletions

View File

@ -15,8 +15,6 @@
package restorable
import (
"image"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
@ -28,7 +26,3 @@ func EnableRestoringForTesting() {
func ResolveStaleImages(graphicsDriver graphicsdriver.Graphics) error {
return resolveStaleImages(graphicsDriver, false)
}
func AppendRegionRemovingDuplicates(regions *[]image.Rectangle, region image.Rectangle) {
appendRegionRemovingDuplicates(regions, region)
}

View File

@ -17,6 +17,7 @@ package restorable
import (
"fmt"
"image"
"sort"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
@ -215,20 +216,16 @@ func (i *Image) makeStale(rect image.Rectangle) {
return
}
var addedRegions []image.Rectangle
i.appendRegionsForDrawTriangles(&addedRegions)
origSize := len(i.staleRegions)
i.staleRegions = i.appendRegionsForDrawTriangles(i.staleRegions)
if !rect.Empty() {
appendRegionRemovingDuplicates(&addedRegions, rect)
}
for _, rect := range addedRegions {
appendRegionRemovingDuplicates(&i.staleRegions, rect)
i.staleRegions = append(i.staleRegions, rect)
}
i.clearDrawTrianglesHistory()
// Clear pixels to save memory.
for _, r := range addedRegions {
for _, r := range i.staleRegions[origSize:] {
i.basePixels.Clear(r)
}
@ -450,13 +447,16 @@ func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error
if i.stale {
rs = i.staleRegions
} else {
i.appendRegionsForDrawTriangles(&i.regionsCache)
i.regionsCache = i.appendRegionsForDrawTriangles(i.regionsCache)
defer func() {
i.regionsCache = i.regionsCache[:0]
}()
rs = i.regionsCache
}
// Remove duplications. Is this heavy?
rs = rs[:removeDuplicatedRegions(rs)]
args := make([]graphicsdriver.PixelsArgs, 0, len(rs))
for _, r := range rs {
if r.Empty() {
@ -607,7 +607,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
// In order to clear the draw-triangles history, read pixels from GPU.
if len(i.drawTrianglesHistory) > 0 {
i.appendRegionsForDrawTriangles(&i.regionsCache)
i.regionsCache = i.appendRegionsForDrawTriangles(i.regionsCache)
defer func() {
i.regionsCache = i.regionsCache[:0]
}()
@ -683,38 +683,54 @@ func (i *Image) InternalSize() (int, int) {
return i.image.InternalSize()
}
func (i *Image) appendRegionsForDrawTriangles(regions *[]image.Rectangle) {
func (i *Image) appendRegionsForDrawTriangles(regions []image.Rectangle) []image.Rectangle {
for _, d := range i.drawTrianglesHistory {
if d.dstRegion.Empty() {
continue
}
appendRegionRemovingDuplicates(regions, d.dstRegion)
regions = append(regions, d.dstRegion)
}
return regions
}
// appendRegionRemovingDuplicates adds a region to a given list of regions,
// but removes any duplicate between the newly added region and any existing regions.
//
// In case the newly added region is fully contained in any pre-existing region, this function does nothing.
// Otherwise, any pre-existing regions that are fully contained in the newly added region are removed.
//
// This is done to avoid unnecessary reading pixels from GPU.
func appendRegionRemovingDuplicates(regions *[]image.Rectangle, region image.Rectangle) {
for _, r := range *regions {
if region.In(r) {
// The newly added rectangle is fully contained in one of the input regions.
// Nothing to add.
return
}
}
// Separate loop, as regions must not get mutated before above return.
n := 0
for _, r := range *regions {
if r.In(region) {
// removeDuplicatedRegions removes duplicated regions and returns a shrunk slice.
// If a region covers preceding regions, the covered regions are removed.
func removeDuplicatedRegions(regions []image.Rectangle) int {
// Sweep and prune algorithm
sort.Slice(regions, func(i, j int) bool {
return regions[i].Min.X < regions[j].Min.X
})
for i, r := range regions {
if r.Empty() {
continue
}
(*regions)[n] = r
for j := i + 1; j < len(regions); j++ {
rr := regions[j]
if rr.Empty() {
continue
}
if r.Max.X <= rr.Min.X {
break
}
if rr.In(r) {
regions[j] = image.Rectangle{}
} else if r.In(rr) {
regions[i] = image.Rectangle{}
break
}
}
}
var n int
for _, r := range regions {
if r.Empty() {
continue
}
regions[n] = r
n++
}
*regions = append((*regions)[:n], region)
return n
}

View File

@ -1,165 +0,0 @@
// 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 restorable_test
import (
"image"
"testing"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)
func areEqualRectangles(a, b []image.Rectangle) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestRemoveDuplicatedRegions(t *testing.T) {
cases := []struct {
Regions []image.Rectangle
NewRegions []image.Rectangle
Expected []image.Rectangle
}{
{
Regions: nil,
NewRegions: nil,
Expected: nil,
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 2, 2),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 2, 2),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 1, 1),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 2, 2),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 1),
image.Rect(0, 0, 2, 2),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 2, 2),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 4, 4),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
image.Rect(1, 1, 2, 2),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 4, 4),
},
},
{
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
image.Rect(0, 0, 5, 5),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 5, 5),
},
},
{
Regions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
},
NewRegions: []image.Rectangle{
image.Rect(0, 0, 5, 5),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 5, 5),
},
},
{
Regions: []image.Rectangle{
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
image.Rect(0, 0, 5, 5),
},
NewRegions: []image.Rectangle{
image.Rect(0, 0, 1, 3),
},
Expected: []image.Rectangle{
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 3, 1),
image.Rect(0, 0, 4, 4),
image.Rect(0, 0, 5, 5),
},
},
}
for _, c := range cases {
got := c.Regions
for _, r := range c.NewRegions {
restorable.AppendRegionRemovingDuplicates(&got, r)
}
want := c.Expected
if !areEqualRectangles(got, want) {
t.Errorf("restorable.RemoveDuplicatedRegions(%#v): got: %#v, want: %#v", c.NewRegions, got, want)
}
}
}