diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 80864e430..b2c88ed6d 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -187,6 +187,30 @@ func clearImage(i *graphicscommand.Image, region image.Rectangle) { // makeStale makes the image stale. func (i *Image) makeStale(rect image.Rectangle) { i.stale = true + + // If ReadPixels always reads pixels from GPU, staleRegions are never used. + if alwaysReadPixelsFromGPU() { + return + } + + var addedRegions []image.Rectangle + if !rect.Empty() { + appendRegionRemovingDuplicates(&addedRegions, rect) + } + + for _, rect := range addedRegions { + appendRegionRemovingDuplicates(&i.staleRegions, rect) + } + + // Clear pixels to save memory. + for _, r := range addedRegions { + i.basePixels.Clear(r) + } + + // Don't have to call makeStale recursively here. + // Restoring is done after topological sorting is done. + // If an image depends on another stale image, this means that + // the former image can be restored from the latest state of the latter image. } // ClearPixels clears the specified region by WritePixels. @@ -255,15 +279,35 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice i.image.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.shader, uniforms, fillRule) } +func (i *Image) readPixelsFromGPUIfNeeded(graphicsDriver graphicsdriver.Graphics) error { + if i.stale { + if err := i.readPixelsFromGPU(graphicsDriver); err != nil { + return err + } + } + return nil +} + func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) error { - if err := i.image.ReadPixels(graphicsDriver, []graphicsdriver.PixelsArgs{ - { - Pixels: pixels, - Region: region, - }, - }); err != nil { + if alwaysReadPixelsFromGPU() { + if err := i.image.ReadPixels(graphicsDriver, []graphicsdriver.PixelsArgs{ + { + Pixels: pixels, + Region: region, + }, + }); err != nil { + return err + } + return nil + } + + if err := i.readPixelsFromGPUIfNeeded(graphicsDriver); err != nil { return err } + if got, want := len(pixels), 4*region.Dx()*region.Dy(); got != want { + return fmt.Errorf("restorable: len(pixels) must be %d but %d at ReadPixels", want, got) + } + i.basePixels.ReadPixels(pixels, region, i.width, i.height) return nil } @@ -289,6 +333,56 @@ func (i *Image) makeStaleIfDependingOnShader(shader *Shader) { } } +// readPixelsFromGPU reads the pixels from GPU and resolves the image's 'stale' state. +func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error { + var rs []image.Rectangle + if i.stale { + rs = i.staleRegions + } else { + defer func() { + i.regionsCache = i.regionsCache[:0] + }() + rs = i.regionsCache + } + + args := make([]graphicsdriver.PixelsArgs, 0, len(rs)) + for _, r := range rs { + if r.Empty() { + continue + } + + if i.pixelsCache == nil { + i.pixelsCache = map[image.Rectangle][]byte{} + } + + pix, ok := i.pixelsCache[r] + if !ok { + pix = make([]byte, 4*r.Dx()*r.Dy()) + i.pixelsCache[r] = pix + } + + args = append(args, graphicsdriver.PixelsArgs{ + Pixels: pix, + Region: r, + }) + } + + if err := i.image.ReadPixels(graphicsDriver, args); err != nil { + return err + } + + for _, a := range args { + bs := graphics.NewManagedBytes(len(a.Pixels), func(bs []byte) { + copy(bs, a.Pixels) + }) + i.basePixels.AddOrReplace(bs, a.Region) + } + + i.stale = false + i.staleRegions = i.staleRegions[:0] + return nil +} + // dependsOn reports whether the image depends on target. func (i *Image) dependsOn(target *Image) bool { return false diff --git a/internal/restorable/images.go b/internal/restorable/images.go index aff81fdd7..3cf32dcbc 100644 --- a/internal/restorable/images.go +++ b/internal/restorable/images.go @@ -20,6 +20,10 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" ) +func alwaysReadPixelsFromGPU() bool { + return true +} + // images is a set of Image objects. type images struct { images map[*Image]struct{}