Compare commits

..

No commits in common. "393437b8be5232a39ad0e57e534155ada802709a" and "6eb0271f83cb94340d5789344a1bfa22b06e8c54" have entirely different histories.

22 changed files with 239 additions and 2937 deletions

View File

@ -25,10 +25,10 @@ import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
import {{.JavaPkg}}.ebitenmobileview.Renderer; import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView; import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView implements Renderer { class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
private class EbitenRenderer implements GLSurfaceView.Renderer { private class EbitenRenderer implements GLSurfaceView.Renderer {
@ -63,10 +63,6 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
onceSurfaceCreated_ = true; onceSurfaceCreated_ = true;
return; return;
} }
if (hasStrictContextRestoration()) {
Ebitenmobileview.onContextLost();
return;
}
contextLost_ = true; contextLost_ = true;
new Handler(Looper.getMainLooper()).post(new Runnable() { new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override @Override
@ -81,8 +77,6 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
} }
} }
private boolean strictContextRestoration_ = false;
public EbitenSurfaceView(Context context) { public EbitenSurfaceView(Context context) {
super(context); super(context);
initialize(); initialize();
@ -96,11 +90,9 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
private void initialize() { private void initialize() {
setEGLContextClientVersion(3); setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 0, 0); setEGLConfigChooser(8, 8, 8, 8, 0, 0);
// setRenderer must be called before setRenderRequester.
// Or, the application crashes.
setRenderer(new EbitenRenderer()); setRenderer(new EbitenRenderer());
setPreserveEGLContextOnPause(true);
Ebitenmobileview.setRenderer(this); Ebitenmobileview.setRenderRequester(this);
} }
private void onErrorOnGameUpdate(Exception e) { private void onErrorOnGameUpdate(Exception e) {
@ -122,16 +114,6 @@ class EbitenSurfaceView extends GLSurfaceView implements Renderer {
} }
} }
@Override
public synchronized void setStrictContextRestoration(boolean strictContextRestoration) {
strictContextRestoration_ = strictContextRestoration;
setPreserveEGLContextOnPause(!strictContextRestoration);
}
private synchronized boolean hasStrictContextRestoration() {
return strictContextRestoration_;
}
@Override @Override
public synchronized void requestRenderIfNeeded() { public synchronized void requestRenderIfNeeded() {
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) { if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {

View File

@ -20,7 +20,7 @@
#import "Ebitenmobileview.objc.h" #import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderer, EbitenmobileviewSetGameNotifier> @interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester, EbitenmobileviewSetGameNotifier>
@end @end
@implementation {{.PrefixUpper}}EbitenViewController { @implementation {{.PrefixUpper}}EbitenViewController {
@ -149,7 +149,7 @@
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)]; displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
EbitenmobileviewSetRenderer(self); EbitenmobileviewSetRenderRequester(self);
// Run the loop. This will never return. // Run the loop. This will never return.
[[NSRunLoop currentRunLoop] run]; [[NSRunLoop currentRunLoop] run];
@ -364,10 +364,6 @@
} }
} }
- (void)setStrictContextRestoration:(BOOL)strictContextRestoration {
// Do nothing.
}
- (void)setExplicitRenderingMode:(BOOL)explicitRendering { - (void)setExplicitRenderingMode:(BOOL)explicitRendering {
@synchronized(self) { @synchronized(self) {
explicitRendering_ = explicitRendering; explicitRendering_ = explicitRendering;

View File

@ -66,10 +66,6 @@ func (g *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
// An image on an atlas is surrounded by a transparent edge, // An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges. // and the shader program unexpectedly picks the pixel on the edges.
imageType := atlas.ImageTypeUnmanaged imageType := atlas.ImageTypeUnmanaged
if ui.Get().IsScreenClearedEveryFrame() {
// A volatile image is also always isolated.
imageType = atlas.ImageTypeVolatile
}
g.offscreen = newImage(image.Rect(0, 0, width, height), imageType) g.offscreen = newImage(image.Rect(0, 0, width, height), imageType)
return g.offscreen.image return g.offscreen.image
} }

View File

@ -22,10 +22,11 @@ import (
"runtime" "runtime"
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/packing" "github.com/hajimehoshi/ebiten/v2/internal/packing"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir" "github.com/hajimehoshi/ebiten/v2/internal/shaderir"
) )
@ -89,8 +90,11 @@ func putImagesOnSourceBackend() {
// backend is a big texture atlas that can have multiple images. // backend is a big texture atlas that can have multiple images.
// backend is a texture in GPU. // backend is a texture in GPU.
type backend struct { type backend struct {
// restorable is an atlas on which there might be multiple images. // image is an atlas on which there might be multiple images.
restorable *restorable.Image image *graphicscommand.Image
width int
height int
// page is an atlas map. Each part is called a node. // page is an atlas map. Each part is called a node.
// If page is nil, the backend's image is isolated and not on an atlas. // If page is nil, the backend's image is isolated and not on an atlas.
@ -117,11 +121,73 @@ func (b *backend) tryAlloc(width, height int) (*packing.Node, bool) {
return nil, false return nil, false
} }
b.restorable = b.restorable.Extend(b.page.Size()) b.extendIfNeeded(b.page.Size())
return n, true return n, true
} }
// extendIfNeeded extends the image by the given size if necessary.
// extendIfNeeded creates a new image with the given size and copies the pixels of the given source image.
// extendIfNeeded disposes an old image after its call when a new image is created.
func (b *backend) extendIfNeeded(width, height int) {
if b.width >= width && b.height >= height {
return
}
// Assume that the screen image is never extended.
newImg := newClearedImage(width, height, false)
srcs := [graphics.ShaderSrcImageCount]*graphicscommand.Image{b.image}
sw, sh := b.image.InternalSize()
vs := make([]float32, 4*graphics.VertexFloatCount)
graphics.QuadVerticesFromDstAndSrc(vs, 0, 0, float32(sw), float32(sh), 0, 0, float32(sw), float32(sh), 1, 1, 1, 1)
is := graphics.QuadIndices()
dr := image.Rect(0, 0, sw, sh)
newImg.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, NearestFilterShader.ensureShader(), nil, graphicsdriver.FillRuleFillAll)
b.image.Dispose()
b.image = newImg
b.width = width
b.height = height
}
// newClearedImage creates an emtpy image with the given size.
//
// Note that Dispose is not called automatically.
func newClearedImage(width, height int, screen bool) *graphicscommand.Image {
i := graphicscommand.NewImage(width, height, screen)
// This needs to use 'InternalSize' to render the whole region, or edges are unexpectedly cleared on some
// devices.
iw, ih := i.InternalSize()
clearImage(i, image.Rect(0, 0, iw, ih))
return i
}
func clearImage(i *graphicscommand.Image, region image.Rectangle) {
vs := make([]float32, 4*graphics.VertexFloatCount)
graphics.QuadVerticesFromDstAndSrc(vs, float32(region.Min.X), float32(region.Min.Y), float32(region.Max.X), float32(region.Max.Y), 0, 0, 0, 0, 0, 0, 0, 0)
is := graphics.QuadIndices()
i.DrawTriangles([graphics.ShaderSrcImageCount]*graphicscommand.Image{}, vs, is, graphicsdriver.BlendClear, region, [graphics.ShaderSrcImageCount]image.Rectangle{}, clearShader.ensureShader(), nil, graphicsdriver.FillRuleFillAll)
}
func (b *backend) clearPixels(region image.Rectangle) {
if region.Dx() <= 0 || region.Dy() <= 0 {
panic("atlas: width/height must be positive")
}
clearImage(b.image, region.Intersect(image.Rect(0, 0, b.width, b.height)))
}
func (b *backend) writePixels(pixels *graphics.ManagedBytes, region image.Rectangle) {
if region.Dx() <= 0 || region.Dy() <= 0 {
panic("atlas: width/height must be positive")
}
if !region.In(image.Rect(0, 0, b.width, b.height)) {
panic(fmt.Sprintf("atlas: out of range %v", region))
}
b.image.WritePixels(pixels, region)
}
var ( var (
// backendsM is a mutex for critical sections of the backend and packing.Node objects. // backendsM is a mutex for critical sections of the backend and packing.Node objects.
backendsM sync.Mutex backendsM sync.Mutex
@ -139,6 +205,8 @@ var (
imagesUsedAsDestination smallImageSet imagesUsedAsDestination smallImageSet
graphicsDriverInitialized bool
deferred []func() deferred []func()
// deferredM is a mutex for the slice operations. This must not be used for other usages. // deferredM is a mutex for the slice operations. This must not be used for other usages.
@ -156,8 +224,6 @@ const (
// A screen image is also unmanaged. // A screen image is also unmanaged.
ImageTypeScreen ImageTypeScreen
ImageTypeVolatile
// ImageTypeUnmanaged is an unmanaged image that is not on an atlas. // ImageTypeUnmanaged is an unmanaged image that is not on an atlas.
ImageTypeUnmanaged ImageTypeUnmanaged
) )
@ -176,7 +242,7 @@ type Image struct {
// usedAsSourceCount represents how long the image is used as a rendering source and kept not modified with // usedAsSourceCount represents how long the image is used as a rendering source and kept not modified with
// DrawTriangles. // DrawTriangles.
// In the current implementation, if an image is being modified by DrawTriangles, the image is separated from // In the current implementation, if an image is being modified by DrawTriangles, the image is separated from
// a restorable image on an atlas by ensureIsolatedFromSource. // a graphicscommand.Image on an atlas by ensureIsolatedFromSource.
// //
// The type is int64 instead of int to avoid overflow when comparing the limitation. // The type is int64 instead of int to avoid overflow when comparing the limitation.
// //
@ -367,6 +433,11 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
} }
func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule) { func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule) {
if len(vertices) == 0 {
return
}
// This slice is not escaped to the heap. This can be checked by `go build -gcflags=-m`.
backends := make([]*backend, 0, len(srcs)) backends := make([]*backend, 0, len(srcs))
for _, src := range srcs { for _, src := range srcs {
if src == nil { if src == nil {
@ -386,7 +457,7 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
for _, src := range srcs { for _, src := range srcs {
// Compare i and source images after ensuring i is not on an atlas, or // Compare i and source images after ensuring i is not on an atlas, or
// i and a source image might share the same atlas even though i != src. // i and a source image might share the same atlas even though i != src.
if src != nil && i.backend.restorable == src.backend.restorable { if src != nil && i.backend.image == src.backend.image {
panic("atlas: Image.DrawTriangles: source must be different from the receiver") panic("atlas: Image.DrawTriangles: source must be different from the receiver")
} }
} }
@ -408,8 +479,8 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
vertices[i+2] += oxf vertices[i+2] += oxf
vertices[i+3] += oyf vertices[i+3] += oyf
} }
if shader.ensureShader().Unit() == shaderir.Texels { if shader.ir.Unit == shaderir.Texels {
sw, sh := srcs[0].backend.restorable.InternalSize() sw, sh := srcs[0].backend.image.InternalSize()
swf, shf := float32(sw), float32(sh) swf, shf := float32(sw), float32(sh)
for i := 0; i < n; i += graphics.VertexFloatCount { for i := 0; i < n; i += graphics.VertexFloatCount {
vertices[i+2] /= swf vertices[i+2] /= swf
@ -439,15 +510,15 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
srcRegions[i] = srcRegions[i].Add(r.Min) srcRegions[i] = srcRegions[i].Add(r.Min)
} }
var imgs [graphics.ShaderSrcImageCount]*restorable.Image var imgs [graphics.ShaderSrcImageCount]*graphicscommand.Image
for i, src := range srcs { for i, src := range srcs {
if src == nil { if src == nil {
continue continue
} }
imgs[i] = src.backend.restorable imgs[i] = src.backend.image
} }
i.backend.restorable.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.ensureShader(), uniforms, fillRule) i.backend.image.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.ensureShader(), uniforms, fillRule)
for _, src := range srcs { for _, src := range srcs {
if src == nil { if src == nil {
@ -499,7 +570,7 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
region = region.Add(r.Min) region = region.Add(r.Min)
if pix == nil { if pix == nil {
i.backend.restorable.ClearPixels(region) i.backend.clearPixels(region)
return return
} }
@ -507,7 +578,7 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) { pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) {
copy(bs, pix) copy(bs, pix)
}) })
i.backend.restorable.WritePixels(pix2, region) i.backend.writePixels(pix2, region)
return return
} }
@ -536,7 +607,7 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
copy(bs[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()]) copy(bs[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()])
} }
}) })
i.backend.restorable.WritePixels(pixb, r) i.backend.writePixels(pixb, r)
} }
func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (ok bool, err error) { func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (ok bool, err error) {
@ -552,19 +623,31 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte
// To prevent memory leaks, flush the deferred functions here. // To prevent memory leaks, flush the deferred functions here.
flushDeferred() flushDeferred()
if i.backend == nil || i.backend.restorable == nil { if err := i.readPixels(graphicsDriver, pixels, region); err != nil {
for i := range pixels {
pixels[i] = 0
}
return true, nil
}
if err := i.backend.restorable.ReadPixels(graphicsDriver, pixels, region.Add(i.regionWithPadding().Min)); err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
func (i *Image) readPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) error {
if i.backend == nil {
for i := range pixels {
pixels[i] = 0
}
return nil
}
if err := i.backend.image.ReadPixels(graphicsDriver, []graphicsdriver.PixelsArgs{
{
Pixels: pixels,
Region: region.Add(i.regionWithPadding().Min),
},
}); err != nil {
return err
}
return nil
}
// Deallocate deallocates the internal state. // Deallocate deallocates the internal state.
// Even after this call, the image is still available as a new cleared image. // Even after this call, the image is still available as a new cleared image.
func (i *Image) Deallocate() { func (i *Image) Deallocate() {
@ -603,12 +686,13 @@ func (i *Image) deallocate() {
if !i.backend.page.IsEmpty() { if !i.backend.page.IsEmpty() {
// As this part can be reused, this should be cleared explicitly. // As this part can be reused, this should be cleared explicitly.
r := i.regionWithPadding() r := i.regionWithPadding()
i.backend.restorable.ClearPixels(r) i.backend.clearPixels(r)
return return
} }
} }
i.backend.restorable.Dispose() i.backend.image.Dispose()
i.backend.image = nil
for idx, sh := range theBackends { for idx, sh := range theBackends {
if sh == i.backend { if sh == i.backend {
@ -651,24 +735,16 @@ func (i *Image) finalize() {
} }
func (i *Image) allocate(forbiddenBackends []*backend, asSource bool) { func (i *Image) allocate(forbiddenBackends []*backend, asSource bool) {
if !graphicsDriverInitialized {
panic("atlas: graphics driver must be ready at allocate but not")
}
if i.backend != nil { if i.backend != nil {
panic("atlas: the image is already allocated") panic("atlas: the image is already allocated")
} }
runtime.SetFinalizer(i, (*Image).finalize) runtime.SetFinalizer(i, (*Image).finalize)
if i.imageType == ImageTypeScreen {
if asSource {
panic("atlas: a screen image cannot be created as a source")
}
// A screen image doesn't have a padding.
i.backend = &backend{
restorable: restorable.NewImage(i.width, i.height, restorable.ImageTypeScreen),
}
theBackends = append(theBackends, i.backend)
return
}
wp := i.width + i.paddingSize() wp := i.width + i.paddingSize()
hp := i.height + i.paddingSize() hp := i.height + i.paddingSize()
@ -677,13 +753,11 @@ func (i *Image) allocate(forbiddenBackends []*backend, asSource bool) {
panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height)) panic(fmt.Sprintf("atlas: the image being put on an atlas is too big: width: %d, height: %d", i.width, i.height))
} }
typ := restorable.ImageTypeRegular
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
i.backend = &backend{ i.backend = &backend{
restorable: restorable.NewImage(wp, hp, typ), image: newClearedImage(wp, hp, i.imageType == ImageTypeScreen),
source: asSource && typ == restorable.ImageTypeRegular, width: wp,
height: hp,
source: asSource && i.imageType == ImageTypeRegular,
} }
theBackends = append(theBackends, i.backend) theBackends = append(theBackends, i.backend)
return return
@ -727,14 +801,12 @@ loop:
height *= 2 height *= 2
} }
typ := restorable.ImageTypeRegular
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
b := &backend{ b := &backend{
restorable: restorable.NewImage(width, height, typ), image: newClearedImage(width, height, false),
page: packing.NewPage(width, height, maxSize), width: width,
source: asSource, height: height,
page: packing.NewPage(width, height, maxSize),
source: asSource,
} }
theBackends = append(theBackends, b) theBackends = append(theBackends, b)
@ -754,7 +826,7 @@ func (i *Image) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, path stri
panic("atlas: DumpScreenshots must be called in between BeginFrame and EndFrame") panic("atlas: DumpScreenshots must be called in between BeginFrame and EndFrame")
} }
return i.backend.restorable.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height)) return i.backend.image.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height))
} }
func EndFrame() error { func EndFrame() error {
@ -785,7 +857,15 @@ func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error {
} }
}() }()
if err := restorable.SwapBuffers(graphicsDriver); err != nil { if debug.IsDebug {
debug.FrameLogf("Internal image sizes:\n")
imgs := make([]*graphicscommand.Image, 0, len(theBackends))
for _, backend := range theBackends {
imgs = append(imgs, backend.image)
}
graphicscommand.LogImagesInfo(imgs)
}
if err := graphicscommand.FlushCommands(graphicsDriver, true); err != nil {
return err return err
} }
return nil return nil
@ -810,7 +890,7 @@ func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
var err error var err error
initOnce.Do(func() { initOnce.Do(func() {
err = restorable.InitializeGraphicsDriverState(graphicsDriver) err = graphicscommand.InitializeGraphicsDriverState(graphicsDriver)
if err != nil { if err != nil {
return return
} }
@ -826,18 +906,15 @@ func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
minDestinationSize = 16 minDestinationSize = 16
} }
if maxSize == 0 { if maxSize == 0 {
maxSize = floorPowerOf2(restorable.MaxImageSize(graphicsDriver)) maxSize = floorPowerOf2(graphicscommand.MaxImageSize(graphicsDriver))
} }
graphicsDriverInitialized = true
}) })
if err != nil { if err != nil {
return err return err
} }
// Restore images first before other image manipulations (#2075).
if err := restorable.RestoreIfNeeded(graphicsDriver); err != nil {
return err
}
flushDeferred() flushDeferred()
putImagesOnSourceBackend() putImagesOnSourceBackend()
@ -852,5 +929,9 @@ func DumpImages(graphicsDriver graphicsdriver.Graphics, dir string) (string, err
panic("atlas: DumpImages must be called in between BeginFrame and EndFrame") panic("atlas: DumpImages must be called in between BeginFrame and EndFrame")
} }
return restorable.DumpImages(graphicsDriver, dir) images := make([]*graphicscommand.Image, 0, len(theBackends))
for _, backend := range theBackends {
images = append(images, backend.image)
}
return graphicscommand.DumpImages(images, graphicsDriver, dir)
} }

View File

@ -174,8 +174,8 @@ func TestReputOnSourceBackend(t *testing.T) {
} }
img2.WritePixels(pix, image.Rect(0, 0, size, size)) img2.WritePixels(pix, image.Rect(0, 0, size, size))
// Create a volatile image. This should always be on a non-source backend. // Create an unmanaged image. This should always be on a non-source backend.
img3 := atlas.NewImage(size, size, atlas.ImageTypeVolatile) img3 := atlas.NewImage(size, size, atlas.ImageTypeUnmanaged)
defer img3.Deallocate() defer img3.Deallocate()
img3.WritePixels(make([]byte, 4*size*size), image.Rect(0, 0, size, size)) img3.WritePixels(make([]byte, 4*size*size), image.Rect(0, 0, size, size))
if got, want := img3.IsOnSourceBackendForTesting(), false; got != want { if got, want := img3.IsOnSourceBackendForTesting(), false; got != want {
@ -685,7 +685,7 @@ func TestImageIsNotReputOnSourceBackendWithoutUsingAsSource(t *testing.T) {
} }
func TestImageWritePixelsModify(t *testing.T) { func TestImageWritePixelsModify(t *testing.T) {
for _, typ := range []atlas.ImageType{atlas.ImageTypeRegular, atlas.ImageTypeVolatile, atlas.ImageTypeUnmanaged} { for _, typ := range []atlas.ImageType{atlas.ImageTypeRegular, atlas.ImageTypeRegular, atlas.ImageTypeUnmanaged} {
const size = 16 const size = 16
img := atlas.NewImage(size, size, typ) img := atlas.NewImage(size, size, typ)
defer img.Deallocate() defer img.Deallocate()

View File

@ -15,15 +15,20 @@
package atlas package atlas
import ( import (
"fmt"
"runtime" "runtime"
"github.com/hajimehoshi/ebiten/v2/internal/restorable" "golang.org/x/sync/errgroup"
"github.com/hajimehoshi/ebiten/v2/internal/builtinshader"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir" "github.com/hajimehoshi/ebiten/v2/internal/shaderir"
) )
type Shader struct { type Shader struct {
ir *shaderir.Program ir *shaderir.Program
shader *restorable.Shader shader *graphicscommand.Shader
} }
func NewShader(ir *shaderir.Program) *Shader { func NewShader(ir *shaderir.Program) *Shader {
@ -41,11 +46,11 @@ func (s *Shader) finalize() {
}) })
} }
func (s *Shader) ensureShader() *restorable.Shader { func (s *Shader) ensureShader() *graphicscommand.Shader {
if s.shader != nil { if s.shader != nil {
return s.shader return s.shader
} }
s.shader = restorable.NewShader(s.ir) s.shader = graphicscommand.NewShader(s.ir)
runtime.SetFinalizer(s, (*Shader).finalize) runtime.SetFinalizer(s, (*Shader).finalize)
return s.shader return s.shader
} }
@ -75,6 +80,38 @@ func (s *Shader) deallocate() {
} }
var ( var (
NearestFilterShader = &Shader{shader: restorable.NearestFilterShader} NearestFilterShader *Shader
LinearFilterShader = &Shader{shader: restorable.LinearFilterShader} LinearFilterShader *Shader
clearShader *Shader
) )
func init() {
var wg errgroup.Group
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ShaderSource(builtinshader.FilterNearest, builtinshader.AddressUnsafe, false)))
if err != nil {
return fmt.Errorf("atlas: compiling the nearest shader failed: %w", err)
}
NearestFilterShader = NewShader(ir)
return nil
})
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ShaderSource(builtinshader.FilterLinear, builtinshader.AddressUnsafe, false)))
if err != nil {
return fmt.Errorf("atlas: compiling the linear shader failed: %w", err)
}
LinearFilterShader = NewShader(ir)
return nil
})
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ClearShaderSource))
if err != nil {
return fmt.Errorf("atlas: compiling the clear shader failed: %w", err)
}
clearShader = NewShader(ir)
return nil
})
if err := wg.Wait(); err != nil {
panic(err)
}
}

View File

@ -21,7 +21,6 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
) )
var whiteImage *Image var whiteImage *Image
@ -47,8 +46,6 @@ type Image struct {
// pixels is cached pixels for ReadPixels. // pixels is cached pixels for ReadPixels.
// pixels might be out of sync with GPU. // pixels might be out of sync with GPU.
// The data of pixels is the secondary data of pixels for ReadPixels. // The data of pixels is the secondary data of pixels for ReadPixels.
//
// pixels is always nil when restorable.AlwaysReadPixelsFromGPU() returns false.
pixels []byte pixels []byte
// pixelsUnsynced represents whether the pixels in CPU and GPU are not synced. // pixelsUnsynced represents whether the pixels in CPU and GPU are not synced.
@ -71,6 +68,9 @@ func (i *Image) Deallocate() {
} }
func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (bool, error) { func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (bool, error) {
// Do not call flushDotsBufferIfNeeded here. This would slow (image/draw).Draw.
// See ebiten.TestImageDrawOver.
if region.Dx() == 1 && region.Dy() == 1 { if region.Dx() == 1 && region.Dy() == 1 {
if c, ok := i.dotsBuffer[region.Min]; ok { if c, ok := i.dotsBuffer[region.Min]; ok {
copy(pixels, c[:]) copy(pixels, c[:])
@ -78,19 +78,6 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte
} }
} }
// If restorable.AlwaysReadPixelsFromGPU() returns false, the pixel data is cached in the restorable package.
if !restorable.AlwaysReadPixelsFromGPU() {
i.syncPixelsIfNeeded()
ok, err := i.img.ReadPixels(graphicsDriver, pixels, region)
if err != nil {
return false, err
}
return ok, nil
}
// Do not call syncPixelsIfNeeded here. This would slow (image/draw).Draw.
// See ebiten.TestImageDrawOver.
if i.pixels == nil { if i.pixels == nil {
pix := make([]byte, 4*i.width*i.height) pix := make([]byte, 4*i.width*i.height)
ok, err := i.img.ReadPixels(graphicsDriver, pix, image.Rect(0, 0, i.width, i.height)) ok, err := i.img.ReadPixels(graphicsDriver, pix, image.Rect(0, 0, i.width, i.height))

View File

@ -72,23 +72,14 @@ type drawTrianglesCommand struct {
} }
func (c *drawTrianglesCommand) String() string { func (c *drawTrianglesCommand) String() string {
var blend string // TODO: Improve readability
switch c.blend { blend := fmt.Sprintf("{src-color: %d, src-alpha: %d, dst-color: %d, dst-alpha: %d, op-color: %d, op-alpha: %d}",
case graphicsdriver.BlendSourceOver: c.blend.BlendFactorSourceRGB,
blend = "(source-over)" c.blend.BlendFactorSourceAlpha,
case graphicsdriver.BlendClear: c.blend.BlendFactorDestinationRGB,
blend = "(clear)" c.blend.BlendFactorDestinationAlpha,
case graphicsdriver.BlendCopy: c.blend.BlendOperationRGB,
blend = "(copy)" c.blend.BlendOperationAlpha)
default:
blend = fmt.Sprintf("{src-rgb: %d, src-alpha: %d, dst-rgb: %d, dst-alpha: %d, op-rgb: %d, op-alpha: %d}",
c.blend.BlendFactorSourceRGB,
c.blend.BlendFactorSourceAlpha,
c.blend.BlendFactorDestinationRGB,
c.blend.BlendFactorDestinationAlpha,
c.blend.BlendOperationRGB,
c.blend.BlendOperationAlpha)
}
dst := fmt.Sprintf("%d", c.dst.id) dst := fmt.Sprintf("%d", c.dst.id)
if c.dst.screen { if c.dst.screen {

View File

@ -1,68 +0,0 @@
// Copyright 2017 The Ebiten 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 offers an Image struct that stores image commands
// and restores its pixel data from the commands when context lost happens.
//
// When a function like DrawImage or Fill is called, an Image tries to record
// the information for restoration.
//
// * Context lost
//
// Contest lost is a process that information on GPU memory are removed by OS
// to make more room on GPU memory.
// This can happen e.g. when GPU memory usage is high, or just switching applications
// might cause context lost on mobiles.
// As Ebitengine's image data is on GPU memory, the game can't continue when context lost happens
// without restoring image information.
// The package restorable is the package to record information for such restoration.
//
// * DrawImage
//
// DrawImage function tries to record an item of 'draw image history' in the target image.
// If a target image is stale or volatile, no item is created.
// If an item of the history is created,
// it can be said that the target image depends on the source image.
// In other words, If A.DrawImage(B, ...) is called,
// it can be said that the image A depends on the image B.
//
// * Fill, WritePixels and Dispose
//
// These functions are also drawing functions and the target image stores the pixel data
// instead of draw image history items. There is no dependency here.
//
// * Making images stale
//
// After any of the drawing functions is called, the target image can't be depended on by
// any other images. For example, if an image A depends on an image B, and B is changed
// by a Fill call after that, the image A can't depend on the image B anymore.
// In this case, as the image B can no longer be used to restore the image A,
// the image A becomes 'stale'.
// As all the stale images are resolved before context lost happens,
// draw image history items are kept as they are
// (even if an image C depends on the stale image A, it is still fine).
//
// * Stale image
//
// A stale image is an image that can't be restored from the recorded information.
// All stale images must be resolved by reading pixels from GPU before the frame ends.
// If a source image of DrawImage is a stale image, the target always becomes stale.
//
// * Volatile image
//
// A volatile image is a special image that is always cleared when a frame starts.
// For instance, the game screen passed via the update function is a volatile image.
// A volatile image doesn't have to record the drawing history.
// If a source image of DrawImage is a volatile image, the target always becomes stale.
package restorable

View File

@ -1,28 +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
import (
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
// EnableRestorationForTesting forces to enable restoration for testing.
func EnableRestorationForTesting() {
forceRestoration = true
}
func ResolveStaleImages(graphicsDriver graphicsdriver.Graphics) error {
return resolveStaleImages(graphicsDriver, false)
}

View File

@ -1,736 +0,0 @@
// Copyright 2016 The Ebiten 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
import (
"fmt"
"image"
"sort"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
type Pixels struct {
pixelsRecords *pixelsRecords
}
// Apply applies the Pixels state to the given image especially for restoration.
func (p *Pixels) Apply(img *graphicscommand.Image) {
// Pixels doesn't clear the image. This is a caller's responsibility.
if p.pixelsRecords == nil {
return
}
p.pixelsRecords.apply(img)
}
func (p *Pixels) AddOrReplace(pix *graphics.ManagedBytes, region image.Rectangle) {
if p.pixelsRecords == nil {
p.pixelsRecords = &pixelsRecords{}
}
p.pixelsRecords.addOrReplace(pix, region)
}
func (p *Pixels) Clear(region image.Rectangle) {
// Note that we don't care whether the region is actually removed or not here. There is an actual case that
// the region is allocated but nothing is rendered. See TestDisposeImmediately at shareable package.
if p.pixelsRecords == nil {
return
}
p.pixelsRecords.clear(region)
}
func (p *Pixels) ReadPixels(pixels []byte, region image.Rectangle, imageWidth, imageHeight int) {
if p.pixelsRecords == nil {
for i := range pixels {
pixels[i] = 0
}
return
}
p.pixelsRecords.readPixels(pixels, region, imageWidth, imageHeight)
}
func (p *Pixels) AppendRegion(regions []image.Rectangle) []image.Rectangle {
if p.pixelsRecords == nil {
return regions
}
return p.pixelsRecords.appendRegions(regions)
}
func (p *Pixels) Dispose() {
if p.pixelsRecords == nil {
return
}
p.pixelsRecords.dispose()
}
// drawTrianglesHistoryItem is an item for history of draw-image commands.
type drawTrianglesHistoryItem struct {
images [graphics.ShaderSrcImageCount]*Image
vertices []float32
indices []uint32
blend graphicsdriver.Blend
dstRegion image.Rectangle
srcRegions [graphics.ShaderSrcImageCount]image.Rectangle
shader *Shader
uniforms []uint32
fillRule graphicsdriver.FillRule
}
type ImageType int
const (
// ImageTypeRegular indicates the image is a regular image.
ImageTypeRegular ImageType = iota
// ImageTypeScreen indicates the image is used as an actual screen.
ImageTypeScreen
// ImageTypeVolatile indicates the image is cleared whenever a frame starts.
//
// Regular non-volatile images need to record drawing history or read its pixels from GPU if necessary so that all
// the images can be restored automatically from the context lost. However, such recording the drawing history or
// reading pixels from GPU are expensive operations. Volatile images can skip such operations, but the image content
// is cleared every frame instead.
ImageTypeVolatile
)
// Image represents an image that can be restored when GL context is lost.
type Image struct {
image *graphicscommand.Image
width int
height int
basePixels Pixels
// drawTrianglesHistory is a set of draw-image commands.
// TODO: This should be merged with the similar command queue in package graphics (#433).
drawTrianglesHistory []*drawTrianglesHistoryItem
// stale indicates whether the image needs to be synced with GPU as soon as possible.
stale bool
// staleRegions indicates the regions to restore.
// staleRegions is valid only when stale is true.
// staleRegions is not used when AlwaysReadPixelsFromGPU() returns true.
staleRegions []image.Rectangle
// pixelsCache is cached byte slices for pixels.
// pixelsCache is just a cache to avoid allocations (#2375).
//
// A key is the region and a value is a byte slice for the region.
//
// It is fine to reuse the same byte slice for the same region for basePixels,
// as old pixels for the same region will be invalidated at basePixel.AddOrReplace.
pixelsCache map[image.Rectangle][]byte
// regionsCache is cached regions.
// regionsCache is just a cache to avoid allocations (#2375).
regionsCache []image.Rectangle
imageType ImageType
}
// NewImage creates an emtpy image with the given size.
//
// The returned image is cleared.
//
// Note that Dispose is not called automatically.
func NewImage(width, height int, imageType ImageType) *Image {
if !graphicsDriverInitialized {
panic("restorable: graphics driver must be ready at NewImage but not")
}
i := &Image{
image: graphicscommand.NewImage(width, height, imageType == ImageTypeScreen),
width: width,
height: height,
imageType: imageType,
}
// This needs to use 'InternalSize' to render the whole region, or edges are unexpectedly cleared on some
// devices.
iw, ih := i.image.InternalSize()
clearImage(i.image, image.Rect(0, 0, iw, ih))
theImages.add(i)
return i
}
// Extend extends the image by the given size.
// Extend creates a new image with the given size and copies the pixels of the given source image.
// Extend disposes itself after its call.
func (i *Image) Extend(width, height int) *Image {
if i.width >= width && i.height >= height {
return i
}
newImg := NewImage(width, height, i.imageType)
// Use DrawTriangles instead of WritePixels because the image i might be stale and not have its pixels
// information.
srcs := [graphics.ShaderSrcImageCount]*Image{i}
sw, sh := i.image.InternalSize()
vs := make([]float32, 4*graphics.VertexFloatCount)
graphics.QuadVerticesFromDstAndSrc(vs, 0, 0, float32(sw), float32(sh), 0, 0, float32(sw), float32(sh), 1, 1, 1, 1)
is := graphics.QuadIndices()
dr := image.Rect(0, 0, sw, sh)
newImg.DrawTriangles(srcs, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, NearestFilterShader, nil, graphicsdriver.FillRuleFillAll)
i.Dispose()
return newImg
}
func clearImage(i *graphicscommand.Image, region image.Rectangle) {
vs := make([]float32, 4*graphics.VertexFloatCount)
graphics.QuadVerticesFromDstAndSrc(vs, float32(region.Min.X), float32(region.Min.Y), float32(region.Max.X), float32(region.Max.Y), 0, 0, 0, 0, 0, 0, 0, 0)
is := graphics.QuadIndices()
i.DrawTriangles([graphics.ShaderSrcImageCount]*graphicscommand.Image{}, vs, is, graphicsdriver.BlendClear, region, [graphics.ShaderSrcImageCount]image.Rectangle{}, clearShader.shader, nil, graphicsdriver.FillRuleFillAll)
}
// BasePixelsForTesting returns the image's basePixels for testing.
func (i *Image) BasePixelsForTesting() *Pixels {
return &i.basePixels
}
// 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
}
origSize := len(i.staleRegions)
i.staleRegions = i.appendRegionsForDrawTriangles(i.staleRegions)
if !rect.Empty() {
i.staleRegions = append(i.staleRegions, rect)
}
i.clearDrawTrianglesHistory()
// Clear pixels to save memory.
for _, r := range i.staleRegions[origSize:] {
i.basePixels.Clear(r)
}
// Don't have to call makeStale recursively here.
// Restoration 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.
func (i *Image) ClearPixels(region image.Rectangle) {
i.WritePixels(nil, region)
}
func (i *Image) needsRestoration() bool {
return i.imageType == ImageTypeRegular
}
// WritePixels replaces the image pixels with the given pixels slice.
//
// The specified region must not be overlapped with other regions by WritePixels.
func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) {
if region.Dx() <= 0 || region.Dy() <= 0 {
panic("restorable: width/height must be positive")
}
w, h := i.width, i.height
if !region.In(image.Rect(0, 0, w, h)) {
panic(fmt.Sprintf("restorable: out of range %v", region))
}
// TODO: Avoid making other images stale if possible. (#514)
// For this purpose, images should remember which part of that is used for DrawTriangles.
theImages.makeStaleIfDependingOn(i)
if pixels != nil {
i.image.WritePixels(pixels, region)
} else {
clearImage(i.image, region)
}
// Even if the image is already stale, call makeStale to extend the stale region.
if !needsRestoration() || !i.needsRestoration() || i.stale {
i.makeStale(region)
return
}
if region.Eq(image.Rect(0, 0, w, h)) {
if pixels != nil {
// Clone a ManagedBytes as the package graphicscommand has a different lifetime management.
i.basePixels.AddOrReplace(pixels.Clone(), image.Rect(0, 0, w, h))
} else {
i.basePixels.Clear(image.Rect(0, 0, w, h))
}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegions = i.staleRegions[:0]
return
}
// Records for DrawTriangles cannot come before records for WritePixels.
if len(i.drawTrianglesHistory) > 0 {
i.makeStale(region)
return
}
if pixels != nil {
// Clone a ManagedBytes as the package graphicscommand has a different lifetime management.
i.basePixels.AddOrReplace(pixels.Clone(), region)
} else {
i.basePixels.Clear(region)
}
}
// DrawTriangles draws triangles with the given image.
//
// The vertex floats are:
//
// 0: Destination X in pixels
// 1: Destination Y in pixels
// 2: Source X in texels
// 3: Source Y in texels
// 4: Color R [0.0-1.0]
// 5: Color G
// 6: Color B
// 7: Color Y
func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule) {
if len(vertices) == 0 {
return
}
theImages.makeStaleIfDependingOn(i)
// TODO: Add tests to confirm this logic.
var srcstale bool
for _, src := range srcs {
if src == nil {
continue
}
if src.stale || src.imageType == ImageTypeVolatile {
srcstale = true
break
}
}
// Even if the image is already stale, call makeStale to extend the stale region.
if srcstale || !needsRestoration() || !i.needsRestoration() || i.stale {
i.makeStale(dstRegion)
} else {
i.appendDrawTrianglesHistory(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule)
}
var imgs [graphics.ShaderSrcImageCount]*graphicscommand.Image
for i, src := range srcs {
if src == nil {
continue
}
imgs[i] = src.image
}
i.image.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.shader, uniforms, fillRule)
}
// appendDrawTrianglesHistory appends a draw-image history item to the image.
func (i *Image) appendDrawTrianglesHistory(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule) {
if i.stale || !i.needsRestoration() {
panic("restorable: an image must not be stale or need restoration at appendDrawTrianglesHistory")
}
if AlwaysReadPixelsFromGPU() {
panic("restorable: appendDrawTrianglesHistory must not be called when AlwaysReadPixelsFromGPU() returns true")
}
// TODO: Would it be possible to merge draw image history items?
const maxDrawTrianglesHistoryCount = 1024
if len(i.drawTrianglesHistory)+1 > maxDrawTrianglesHistoryCount {
i.makeStale(dstRegion)
return
}
// All images must be resolved and not stale each after frame.
// So we don't have to care if image is stale or not here.
vs := make([]float32, len(vertices))
copy(vs, vertices)
is := make([]uint32, len(indices))
copy(is, indices)
us := make([]uint32, len(uniforms))
copy(us, uniforms)
item := &drawTrianglesHistoryItem{
images: srcs,
vertices: vs,
indices: is,
blend: blend,
dstRegion: dstRegion,
srcRegions: srcRegions,
shader: shader,
uniforms: us,
fillRule: fillRule,
}
i.drawTrianglesHistory = append(i.drawTrianglesHistory, item)
}
func (i *Image) readPixelsFromGPUIfNeeded(graphicsDriver graphicsdriver.Graphics) error {
if len(i.drawTrianglesHistory) > 0 || 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 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
}
// makeStaleIfDependingOn makes the image stale if the image depends on target.
func (i *Image) makeStaleIfDependingOn(target *Image) {
if i.stale {
return
}
if i.dependsOn(target) {
// There is no new region to make stale.
i.makeStale(image.Rectangle{})
}
}
// makeStaleIfDependingOnShader makes the image stale if the image depends on shader.
func (i *Image) makeStaleIfDependingOnShader(shader *Shader) {
if i.stale {
return
}
if i.dependsOnShader(shader) {
// There is no new region to make stale.
i.makeStale(image.Rectangle{})
}
}
// 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 {
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() {
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.clearDrawTrianglesHistory()
i.stale = false
i.staleRegions = i.staleRegions[:0]
return nil
}
// resolveStale resolves the image's 'stale' state.
func (i *Image) resolveStale(graphicsDriver graphicsdriver.Graphics) error {
if !needsRestoration() {
return nil
}
if !i.needsRestoration() {
return nil
}
if !i.stale {
return nil
}
return i.readPixelsFromGPU(graphicsDriver)
}
// dependsOn reports whether the image depends on target.
func (i *Image) dependsOn(target *Image) bool {
for _, c := range i.drawTrianglesHistory {
for _, img := range c.images {
if img == nil {
continue
}
if img == target {
return true
}
}
}
return false
}
// dependsOnShader reports whether the image depends on shader.
func (i *Image) dependsOnShader(shader *Shader) bool {
for _, c := range i.drawTrianglesHistory {
if c.shader == shader {
return true
}
}
return false
}
// dependingImages returns all images that is depended on the image.
func (i *Image) dependingImages() map[*Image]struct{} {
r := map[*Image]struct{}{}
for _, c := range i.drawTrianglesHistory {
for _, img := range c.images {
if img == nil {
continue
}
r[img] = struct{}{}
}
}
return r
}
// hasDependency returns a boolean value indicating whether the image depends on another image.
func (i *Image) hasDependency() bool {
if i.stale {
return false
}
return len(i.drawTrianglesHistory) > 0
}
// Restore restores *graphicscommand.Image from the pixels using its state.
func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
w, h := i.width, i.height
// Do not dispose the image here. The image should be already disposed.
switch i.imageType {
case ImageTypeScreen:
// The screen image should also be recreated because framebuffer might
// be changed.
i.image = graphicscommand.NewImage(w, h, true)
i.basePixels.Dispose()
i.basePixels = Pixels{}
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegions = i.staleRegions[:0]
return nil
case ImageTypeVolatile:
i.image = graphicscommand.NewImage(w, h, false)
iw, ih := i.image.InternalSize()
clearImage(i.image, image.Rect(0, 0, iw, ih))
return nil
}
if i.stale {
panic("restorable: pixels must not be stale when restoring")
}
gimg := graphicscommand.NewImage(w, h, false)
// Clear the image explicitly.
iw, ih := gimg.InternalSize()
clearImage(gimg, image.Rect(0, 0, iw, ih))
i.basePixels.Apply(gimg)
for _, c := range i.drawTrianglesHistory {
var imgs [graphics.ShaderSrcImageCount]*graphicscommand.Image
for i, img := range c.images {
if img == nil {
continue
}
if img.hasDependency() {
panic("restorable: all dependencies must be already resolved but not")
}
imgs[i] = img.image
}
gimg.DrawTriangles(imgs, c.vertices, c.indices, c.blend, c.dstRegion, c.srcRegions, c.shader.shader, c.uniforms, c.fillRule)
}
// In order to clear the draw-triangles history, read pixels from GPU.
if len(i.drawTrianglesHistory) > 0 {
i.regionsCache = i.appendRegionsForDrawTriangles(i.regionsCache)
defer func() {
i.regionsCache = i.regionsCache[:0]
}()
args := make([]graphicsdriver.PixelsArgs, 0, len(i.regionsCache))
for _, r := range i.regionsCache {
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 := gimg.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.image = gimg
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegions = i.staleRegions[:0]
return nil
}
// Dispose disposes the image.
//
// After disposing, calling the function of the image causes unexpected results.
func (i *Image) Dispose() {
theImages.remove(i)
i.image.Dispose()
i.image = nil
i.basePixels.Dispose()
i.basePixels = Pixels{}
i.pixelsCache = nil
i.clearDrawTrianglesHistory()
i.stale = false
i.staleRegions = i.staleRegions[:0]
}
func (i *Image) Dump(graphicsDriver graphicsdriver.Graphics, path string, blackbg bool, rect image.Rectangle) (string, error) {
return i.image.Dump(graphicsDriver, path, blackbg, rect)
}
func (i *Image) clearDrawTrianglesHistory() {
// Clear the items explicitly, or the references might remain (#1803).
for idx := range i.drawTrianglesHistory {
i.drawTrianglesHistory[idx] = nil
}
i.drawTrianglesHistory = i.drawTrianglesHistory[:0]
}
func (i *Image) InternalSize() (int, int) {
return i.image.InternalSize()
}
func (i *Image) appendRegionsForDrawTriangles(regions []image.Rectangle) []image.Rectangle {
for _, d := range i.drawTrianglesHistory {
if d.dstRegion.Empty() {
continue
}
regions = append(regions, d.dstRegion)
}
return regions
}
// 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
}
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++
}
return n
}

View File

@ -1,291 +0,0 @@
// Copyright 2017 The Ebiten 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
import (
"image"
"runtime"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
// forceRestoration reports whether restoration forcibly happens or not.
// This is used only for testing.
var forceRestoration = false
// disabled indicates that restoration is disabled or not.
// Restoration is enabled by default for some platforms like Android for safety.
// Before SetGame, it is not possible to determine whether restoration is needed or not.
var disabled atomic.Bool
var disabledOnce sync.Once
// Disable disables restoration.
func Disable() {
disabled.Store(true)
}
// needsRestoration reports whether restoration process works or not.
func needsRestoration() bool {
if forceRestoration {
return true
}
// TODO: If Vulkan is introduced, restoration might not be needed.
if runtime.GOOS == "android" {
return !disabled.Load()
}
return false
}
// AlwaysReadPixelsFromGPU reports whether ReadPixels always reads pixels from GPU or not.
func AlwaysReadPixelsFromGPU() bool {
return !needsRestoration()
}
// images is a set of Image objects.
type images struct {
images map[*Image]struct{}
shaders map[*Shader]struct{}
lastTarget *Image
contextLost atomic.Bool
}
// theImages represents the images for the current process.
var theImages = &images{
images: map[*Image]struct{}{},
shaders: map[*Shader]struct{}{},
}
func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error {
if debug.IsDebug {
debug.FrameLogf("Internal image sizes:\n")
imgs := make([]*graphicscommand.Image, 0, len(theImages.images))
for i := range theImages.images {
imgs = append(imgs, i.image)
}
graphicscommand.LogImagesInfo(imgs)
}
return resolveStaleImages(graphicsDriver, true)
}
// resolveStaleImages flushes the queued draw commands and resolves all stale images.
// If endFrame is true, the current screen might be used to present when flushing the commands.
func resolveStaleImages(graphicsDriver graphicsdriver.Graphics, endFrame bool) error {
// When Disable is called, all the images data should be evicted once.
if disabled.Load() {
disabledOnce.Do(func() {
for img := range theImages.images {
img.makeStale(image.Rectangle{})
}
})
}
if err := graphicscommand.FlushCommands(graphicsDriver, endFrame); err != nil {
return err
}
if !needsRestoration() {
return nil
}
return theImages.resolveStaleImages(graphicsDriver)
}
// RestoreIfNeeded restores the images.
//
// Restoration means to make all *graphicscommand.Image objects have their textures and framebuffers.
func RestoreIfNeeded(graphicsDriver graphicsdriver.Graphics) error {
if !needsRestoration() {
return nil
}
if !forceRestoration && !theImages.contextLost.Load() {
return nil
}
if err := graphicscommand.ResetGraphicsDriverState(graphicsDriver); err != nil {
return err
}
return theImages.restore(graphicsDriver)
}
// DumpImages dumps all the current images to the specified directory.
//
// This is for testing usage.
func DumpImages(graphicsDriver graphicsdriver.Graphics, dir string) (string, error) {
images := make([]*graphicscommand.Image, 0, len(theImages.images))
for img := range theImages.images {
images = append(images, img.image)
}
return graphicscommand.DumpImages(images, graphicsDriver, dir)
}
// add adds img to the images.
func (i *images) add(img *Image) {
i.images[img] = struct{}{}
}
func (i *images) addShader(shader *Shader) {
i.shaders[shader] = struct{}{}
}
// remove removes img from the images.
func (i *images) remove(img *Image) {
i.makeStaleIfDependingOn(img)
delete(i.images, img)
}
func (i *images) removeShader(shader *Shader) {
i.makeStaleIfDependingOnShader(shader)
delete(i.shaders, shader)
}
// resolveStaleImages resolves stale images.
func (i *images) resolveStaleImages(graphicsDriver graphicsdriver.Graphics) error {
i.lastTarget = nil
for img := range i.images {
if err := img.resolveStale(graphicsDriver); err != nil {
return err
}
}
return nil
}
// makeStaleIfDependingOn makes all the images stale that depend on target.
//
// When target is modified, all images depending on target can't be restored with target.
// makeStaleIfDependingOn is called in such situation.
func (i *images) makeStaleIfDependingOn(target *Image) {
if target == nil {
panic("restorable: target must not be nil at makeStaleIfDependingOn")
}
if i.lastTarget == target {
return
}
i.lastTarget = target
for img := range i.images {
img.makeStaleIfDependingOn(target)
}
}
// makeStaleIfDependingOn makes all the images stale that depend on shader.
func (i *images) makeStaleIfDependingOnShader(shader *Shader) {
if shader == nil {
panic("restorable: shader must not be nil at makeStaleIfDependingOnShader")
}
for img := range i.images {
img.makeStaleIfDependingOnShader(shader)
}
}
// restore restores the images.
//
// Restoration means to make all *graphicscommand.Image objects have their textures and framebuffers.
func (i *images) restore(graphicsDriver graphicsdriver.Graphics) error {
if !needsRestoration() {
panic("restorable: restore cannot be called when restoration is disabled")
}
// Dispose all the shaders ahead of restoration. A current shader ID and a new shader ID can be duplicated.
for s := range i.shaders {
s.shader.Dispose()
s.shader = nil
}
for s := range i.shaders {
s.restore()
}
// Dispose all the images ahead of restoration. A current texture ID and a new texture ID can be duplicated.
// TODO: Write a test to confirm that ID duplication never happens.
for i := range i.images {
i.image.Dispose()
i.image = nil
}
// Let's do topological sort based on dependencies of drawing history.
// It is assured that there are not loops since cyclic drawing makes images stale.
type edge struct {
source *Image
target *Image
}
images := map[*Image]struct{}{}
for i := range i.images {
images[i] = struct{}{}
}
edges := map[edge]struct{}{}
for t := range images {
for s := range t.dependingImages() {
edges[edge{source: s, target: t}] = struct{}{}
}
}
var sorted []*Image
for len(images) > 0 {
// current represents images that have no incoming edges.
current := map[*Image]struct{}{}
for i := range images {
current[i] = struct{}{}
}
for e := range edges {
if _, ok := current[e.target]; ok {
delete(current, e.target)
}
}
for i := range current {
delete(images, i)
sorted = append(sorted, i)
}
removed := []edge{}
for e := range edges {
if _, ok := current[e.source]; ok {
removed = append(removed, e)
}
}
for _, e := range removed {
delete(edges, e)
}
}
for _, img := range sorted {
if err := img.restore(graphicsDriver); err != nil {
return err
}
}
i.contextLost.Store(false)
return nil
}
var graphicsDriverInitialized bool
// InitializeGraphicsDriverState initializes the graphics driver state.
func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) error {
graphicsDriverInitialized = true
return graphicscommand.InitializeGraphicsDriverState(graphicsDriver)
}
// MaxImageSize returns the maximum size of an image.
func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
return graphicscommand.MaxImageSize(graphicsDriver)
}
// OnContextLost is called when the context lost is detected in an explicit way.
func OnContextLost() {
theImages.contextLost.Store(true)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,159 +0,0 @@
// Copyright 2019 The Ebiten 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
import (
"fmt"
"image"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
)
type pixelsRecord struct {
rect image.Rectangle
pix *graphics.ManagedBytes
}
func (p *pixelsRecord) readPixels(pixels []byte, region image.Rectangle, imageWidth, imageHeight int) {
r := p.rect.Intersect(region.Intersect(image.Rect(0, 0, imageWidth, imageHeight)))
if r.Empty() {
return
}
dstBaseX := r.Min.X - region.Min.X
dstBaseY := r.Min.Y - region.Min.Y
lineWidth := 4 * r.Dx()
if p.pix != nil {
srcBaseX := r.Min.X - p.rect.Min.X
srcBaseY := r.Min.Y - p.rect.Min.Y
for j := 0; j < r.Dy(); j++ {
dstX := 4 * ((dstBaseY+j)*region.Dx() + dstBaseX)
srcX := 4 * ((srcBaseY+j)*p.rect.Dx() + srcBaseX)
p.pix.Read(pixels[dstX:dstX+lineWidth], srcX, srcX+lineWidth)
}
} else {
for j := 0; j < r.Dy(); j++ {
dstX := 4 * ((dstBaseY+j)*region.Dx() + dstBaseX)
for i := 0; i < lineWidth; i++ {
pixels[i+dstX] = 0
}
}
}
}
type pixelsRecords struct {
records []*pixelsRecord
}
func (pr *pixelsRecords) addOrReplace(pixels *graphics.ManagedBytes, region image.Rectangle) {
if pixels.Len() != 4*region.Dx()*region.Dy() {
msg := fmt.Sprintf("restorable: len(pixels) must be 4*%d*%d = %d but %d", region.Dx(), region.Dy(), 4*region.Dx()*region.Dy(), pixels.Len())
if pixels == nil {
msg += " (nil)"
}
panic(msg)
}
// Remove or update the duplicated records first.
var n int
for _, r := range pr.records {
if r.rect.In(region) {
continue
}
pr.records[n] = r
n++
}
for i := n; i < len(pr.records); i++ {
pr.records[i] = nil
}
pr.records = pr.records[:n]
// Add the new record.
pr.records = append(pr.records, &pixelsRecord{
rect: region,
pix: pixels,
})
}
func (pr *pixelsRecords) clear(region image.Rectangle) {
if region.Empty() {
return
}
var n int
var needsClear bool
for _, r := range pr.records {
if r.rect.In(region) {
continue
}
if r.rect.Overlaps(region) {
needsClear = true
}
pr.records[n] = r
n++
}
for i := n; i < len(pr.records); i++ {
pr.records[i] = nil
}
pr.records = pr.records[:n]
if needsClear {
pr.records = append(pr.records, &pixelsRecord{
rect: region,
})
}
}
func (pr *pixelsRecords) readPixels(pixels []byte, region image.Rectangle, imageWidth, imageHeight int) {
for i := range pixels {
pixels[i] = 0
}
for _, r := range pr.records {
r.readPixels(pixels, region, imageWidth, imageHeight)
}
}
func (pr *pixelsRecords) apply(img *graphicscommand.Image) {
// TODO: Isn't this too heavy? Can we merge the operations?
for _, r := range pr.records {
if r.pix != nil {
// Clone a ManagedBytes as the package graphicscommand has a different lifetime management.
img.WritePixels(r.pix.Clone(), r.rect)
} else {
clearImage(img, r.rect)
}
}
}
func (pr *pixelsRecords) appendRegions(regions []image.Rectangle) []image.Rectangle {
for _, r := range pr.records {
if r.rect.Empty() {
continue
}
regions = append(regions, r.rect)
}
return regions
}
func (pr *pixelsRecords) dispose() {
for _, r := range pr.records {
if r.pix == nil {
continue
}
// As the package graphicscommands already has cloned ManagedBytes objects, it is OK to release it.
_, f := r.pix.GetAndRelease()
f()
}
}

View File

@ -1,96 +0,0 @@
// Copyright 2020 The Ebiten 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
import (
"fmt"
"golang.org/x/sync/errgroup"
"github.com/hajimehoshi/ebiten/v2/internal/builtinshader"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
type Shader struct {
shader *graphicscommand.Shader
ir *shaderir.Program
}
func NewShader(ir *shaderir.Program) *Shader {
s := &Shader{
shader: graphicscommand.NewShader(ir),
ir: ir,
}
theImages.addShader(s)
return s
}
func (s *Shader) Dispose() {
theImages.removeShader(s)
s.shader.Dispose()
s.shader = nil
s.ir = nil
}
func (s *Shader) restore() {
s.shader = graphicscommand.NewShader(s.ir)
}
func (s *Shader) Unit() shaderir.Unit {
return s.ir.Unit
}
var (
NearestFilterShader *Shader
LinearFilterShader *Shader
clearShader *Shader
)
func init() {
var wg errgroup.Group
var nearestIR, linearIR, clearIR *shaderir.Program
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ShaderSource(builtinshader.FilterNearest, builtinshader.AddressUnsafe, false)))
if err != nil {
return fmt.Errorf("restorable: compiling the nearest shader failed: %w", err)
}
nearestIR = ir
return nil
})
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ShaderSource(builtinshader.FilterLinear, builtinshader.AddressUnsafe, false)))
if err != nil {
return fmt.Errorf("restorable: compiling the linear shader failed: %w", err)
}
linearIR = ir
return nil
})
wg.Go(func() error {
ir, err := graphics.CompileShader([]byte(builtinshader.ClearShaderSource))
if err != nil {
return fmt.Errorf("restorable: compiling the clear shader failed: %w", err)
}
clearIR = ir
return nil
})
if err := wg.Wait(); err != nil {
panic(err)
}
NearestFilterShader = NewShader(nearestIR)
LinearFilterShader = NewShader(linearIR)
clearShader = NewShader(clearIR)
}

View File

@ -1,200 +0,0 @@
// Copyright 2018 The Ebiten 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"
"image/color"
"testing"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
etesting "github.com/hajimehoshi/ebiten/v2/internal/testing"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
func clearImage(img *restorable.Image, w, h int) {
emptyImage := restorable.NewImage(3, 3, restorable.ImageTypeRegular)
defer emptyImage.Dispose()
dx0 := float32(0)
dy0 := float32(0)
dx1 := float32(w)
dy1 := float32(h)
sx0 := float32(1)
sy0 := float32(1)
sx1 := float32(2)
sy1 := float32(2)
vs := []float32{
dx0, dy0, sx0, sy0, 0, 0, 0, 0,
dx1, dy0, sx1, sy0, 0, 0, 0, 0,
dx0, dy1, sx0, sy1, 0, 0, 0, 0,
dx1, dy1, sx1, sy1, 0, 0, 0, 0,
}
is := graphics.QuadIndices()
dr := image.Rect(0, 0, w, h)
img.DrawTriangles([graphics.ShaderSrcImageCount]*restorable.Image{emptyImage}, vs, is, graphicsdriver.BlendClear, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, graphicsdriver.FillRuleFillAll)
}
func TestShader(t *testing.T) {
img := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
defer img.Dispose()
s := restorable.NewShader(etesting.ShaderProgramFill(0xff, 0, 0, 0xff))
dr := image.Rect(0, 0, 1, 1)
img.DrawTriangles([graphics.ShaderSrcImageCount]*restorable.Image{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s, nil, graphicsdriver.FillRuleFillAll)
if err := restorable.ResolveStaleImages(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
want := color.RGBA{R: 0xff, A: 0xff}
got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1)
if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestShaderChain(t *testing.T) {
const num = 10
imgs := []*restorable.Image{}
for i := 0; i < num; i++ {
img := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
defer img.Dispose()
imgs = append(imgs, img)
}
imgs[0].WritePixels(bytesToManagedBytes([]byte{0xff, 0, 0, 0xff}), image.Rect(0, 0, 1, 1))
s := restorable.NewShader(etesting.ShaderProgramImages(1))
for i := 0; i < num-1; i++ {
dr := image.Rect(0, 0, 1, 1)
imgs[i+1].DrawTriangles([graphics.ShaderSrcImageCount]*restorable.Image{imgs[i]}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s, nil, graphicsdriver.FillRuleFillAll)
}
if err := restorable.ResolveStaleImages(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
for i, img := range imgs {
want := color.RGBA{R: 0xff, A: 0xff}
got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1)
if !sameColors(got, want, 1) {
t.Errorf("%d: got %v, want %v", i, got, want)
}
}
}
func TestShaderMultipleSources(t *testing.T) {
var srcs [graphics.ShaderSrcImageCount]*restorable.Image
for i := range srcs {
srcs[i] = restorable.NewImage(1, 1, restorable.ImageTypeRegular)
}
srcs[0].WritePixels(bytesToManagedBytes([]byte{0x40, 0, 0, 0xff}), image.Rect(0, 0, 1, 1))
srcs[1].WritePixels(bytesToManagedBytes([]byte{0, 0x80, 0, 0xff}), image.Rect(0, 0, 1, 1))
srcs[2].WritePixels(bytesToManagedBytes([]byte{0, 0, 0xc0, 0xff}), image.Rect(0, 0, 1, 1))
dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
s := restorable.NewShader(etesting.ShaderProgramImages(3))
dr := image.Rect(0, 0, 1, 1)
dst.DrawTriangles(srcs, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s, nil, graphicsdriver.FillRuleFillAll)
// Clear one of the sources after DrawTriangles. dst should not be affected.
clearImage(srcs[0], 1, 1)
if err := restorable.ResolveStaleImages(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
want := color.RGBA{R: 0x40, G: 0x80, B: 0xc0, A: 0xff}
got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0, 1, 1)
if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
src := restorable.NewImage(3, 1, restorable.ImageTypeRegular)
src.WritePixels(bytesToManagedBytes([]byte{
0x40, 0, 0, 0xff,
0, 0x80, 0, 0xff,
0, 0, 0xc0, 0xff,
}), image.Rect(0, 0, 3, 1))
srcs := [graphics.ShaderSrcImageCount]*restorable.Image{src, src, src}
dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
s := restorable.NewShader(etesting.ShaderProgramImages(3))
dr := image.Rect(0, 0, 1, 1)
srcRegions := [graphics.ShaderSrcImageCount]image.Rectangle{
image.Rect(0, 0, 1, 1),
image.Rect(1, 0, 2, 1),
image.Rect(2, 0, 3, 1),
}
dst.DrawTriangles(srcs, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, srcRegions, s, nil, graphicsdriver.FillRuleFillAll)
// Clear one of the sources after DrawTriangles. dst should not be affected.
clearImage(srcs[0], 3, 1)
if err := restorable.ResolveStaleImages(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
want := color.RGBA{R: 0x40, G: 0x80, B: 0xc0, A: 0xff}
got := pixelsToColor(dst.BasePixelsForTesting(), 0, 0, 1, 1)
if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestShaderDispose(t *testing.T) {
img := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
defer img.Dispose()
s := restorable.NewShader(etesting.ShaderProgramFill(0xff, 0, 0, 0xff))
dr := image.Rect(0, 0, 1, 1)
img.DrawTriangles([graphics.ShaderSrcImageCount]*restorable.Image{}, quadVertices(1, 1, 0, 0), graphics.QuadIndices(), graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s, nil, graphicsdriver.FillRuleFillAll)
// Dispose the shader. This should invalidate all the images using this shader i.e., all the images become
// stale.
s.Dispose()
if err := restorable.ResolveStaleImages(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
if err := restorable.RestoreIfNeeded(ui.Get().GraphicsDriverForTesting()); err != nil {
t.Fatal(err)
}
want := color.RGBA{R: 0xff, A: 0xff}
got := pixelsToColor(img.BasePixelsForTesting(), 0, 0, 1, 1)
if !sameColors(got, want, 1) {
t.Errorf("got %v, want %v", got, want)
}
}

View File

@ -180,12 +180,6 @@ func (c *context) newOffscreenImage(w, h int) *Image {
} }
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInterface, forceDraw bool) error { func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInterface, forceDraw bool) error {
if (c.offscreen.imageType == atlas.ImageTypeVolatile) != ui.IsScreenClearedEveryFrame() {
w, h := c.offscreen.width, c.offscreen.height
c.offscreen.Deallocate()
c.offscreen = c.newOffscreenImage(w, h)
}
// isOffscreenModified is updated when an offscreen's modifyCallback. // isOffscreenModified is updated when an offscreen's modifyCallback.
c.isOffscreenModified = false c.isOffscreenModified = false

View File

@ -15,7 +15,6 @@
package ui package ui
import ( import (
"fmt"
"image" "image"
"math" "math"
@ -86,16 +85,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
if antialias { if antialias {
if i.bigOffscreenBuffer == nil { if i.bigOffscreenBuffer == nil {
var imageType atlas.ImageType i.bigOffscreenBuffer = i.ui.newBigOffscreenImage(i, atlas.ImageTypeUnmanaged)
switch i.imageType {
case atlas.ImageTypeRegular, atlas.ImageTypeUnmanaged:
imageType = atlas.ImageTypeUnmanaged
case atlas.ImageTypeScreen, atlas.ImageTypeVolatile:
imageType = atlas.ImageTypeVolatile
default:
panic(fmt.Sprintf("ui: unexpected image type: %d", imageType))
}
i.bigOffscreenBuffer = i.ui.newBigOffscreenImage(i, imageType)
} }
i.bigOffscreenBuffer.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap) i.bigOffscreenBuffer.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap)

View File

@ -172,16 +172,15 @@ func (u *UserInterface) dumpImages(dir string) (string, error) {
} }
type RunOptions struct { type RunOptions struct {
GraphicsLibrary GraphicsLibrary GraphicsLibrary GraphicsLibrary
InitUnfocused bool InitUnfocused bool
ScreenTransparent bool ScreenTransparent bool
SkipTaskbar bool SkipTaskbar bool
SingleThread bool SingleThread bool
DisableHiDPI bool DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace ColorSpace graphicsdriver.ColorSpace
X11ClassName string X11ClassName string
X11InstanceName string X11InstanceName string
StrictContextRestoration bool
} }
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair. // InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.

View File

@ -28,7 +28,6 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook" "github.com/hajimehoshi/ebiten/v2/internal/hook"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
) )
var ( var (
@ -98,11 +97,8 @@ type userInterfaceImpl struct {
inputState InputState inputState InputState
touches []TouchForInput touches []TouchForInput
fpsMode atomic.Int32 fpsMode atomic.Int32
renderer Renderer renderRequester RenderRequester
strictContextRestoration bool
strictContextRestorationOnce sync.Once
m sync.RWMutex m sync.RWMutex
} }
@ -156,11 +152,6 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.graphicsDriver = g u.graphicsDriver = g
u.setGraphicsLibrary(lib) u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh) close(u.graphicsLibraryInitCh)
u.strictContextRestoration = options.StrictContextRestoration
if !u.strictContextRestoration {
restorable.Disable()
}
u.renderer.SetStrictContextRestoration(u.strictContextRestoration)
for { for {
if err := u.update(); err != nil { if err := u.update(); err != nil {
@ -248,10 +239,10 @@ func (u *UserInterface) SetFPSMode(mode FPSModeType) {
} }
func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) { func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
if u.renderer == nil { if u.renderRequester == nil {
return return
} }
u.renderer.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum) u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
} }
func (u *UserInterface) readInputState(inputState *InputState) { func (u *UserInterface) readInputState(inputState *InputState) {
@ -306,24 +297,23 @@ func (u *UserInterface) Monitor() *Monitor {
func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) { func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keys, runes, touches) u.updateInputStateFromOutside(keys, runes, touches)
if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum { if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderer.RequestRenderIfNeeded() u.renderRequester.RequestRenderIfNeeded()
} }
} }
type Renderer interface { type RenderRequester interface {
SetExplicitRenderingMode(explicitRendering bool) SetExplicitRenderingMode(explicitRendering bool)
SetStrictContextRestoration(strictContextRestoration bool)
RequestRenderIfNeeded() RequestRenderIfNeeded()
} }
func (u *UserInterface) SetRenderer(renderer Renderer) { func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
u.renderer = renderer u.renderRequester = renderRequester
u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load())) u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load()))
} }
func (u *UserInterface) ScheduleFrame() { func (u *UserInterface) ScheduleFrame() {
if u.renderer != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum { if u.renderRequester != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderer.RequestRenderIfNeeded() u.renderRequester.RequestRenderIfNeeded()
} }
} }

View File

@ -30,7 +30,6 @@ import (
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/ui" "github.com/hajimehoshi/ebiten/v2/internal/ui"
) )
@ -113,20 +112,17 @@ func Resume() error {
return ui.Get().SetForeground(true) return ui.Get().SetForeground(true)
} }
func OnContextLost() {
restorable.OnContextLost()
}
func DeviceScale() float64 { func DeviceScale() float64 {
return ui.Get().Monitor().DeviceScaleFactor() return ui.Get().Monitor().DeviceScaleFactor()
} }
type Renderer interface { type RenderRequester interface {
ui.Renderer SetExplicitRenderingMode(explicitRendering bool)
RequestRenderIfNeeded()
} }
func SetRenderer(renderer Renderer) { func SetRenderRequester(renderRequester RenderRequester) {
ui.Get().SetRenderer(renderer) ui.Get().SetRenderRequester(renderRequester)
} }
func SetSetGameNotifier(setGameNotifier SetGameNotifier) { func SetSetGameNotifier(setGameNotifier SetGameNotifier) {

37
run.go
View File

@ -297,24 +297,6 @@ type RunGameOptions struct {
// X11InstanceName is an instance name in the ICCCM WM_CLASS window property. // X11InstanceName is an instance name in the ICCCM WM_CLASS window property.
X11InstanceName string X11InstanceName string
// StrictContextRestration indicates whether the context lost should be restored strictly by Ebitengine or not.
//
// StrictContextRestration is available only on Android. Otherwise, StrictContextRestration is ignored.
//
// When StrictContextRestration is false, Ebitengine tries to rely on the OS to restore the context.
// In Android, Ebitengien uses `GLSurfaceView`'s `setPreserveEGLContextOnPause(true)`.
// This works in most cases, but it is still possible that the context is lost in some minor cases.
// With StrictContextRestration false, the activity's launch mode should be singleInstance,
// or the activity no longer works correctly after the context is lost.
//
// When StrictContextRestration is true, Ebitengine tries to restore the context more strictly.
// This is useful when you want to restore the context in any case.
// However, this might cause a performance issue since Ebitengine tries to keep all the information
// to restore the context.
//
// The default (zero) value is false.
StrictContextRestration bool
} }
// RunGameWithOptions starts the main loop and runs the game with the specified options. // RunGameWithOptions starts the main loop and runs the game with the specified options.
@ -734,16 +716,15 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
options.X11InstanceName = defaultX11InstanceName options.X11InstanceName = defaultX11InstanceName
} }
return &ui.RunOptions{ return &ui.RunOptions{
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary), GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused, InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent, ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar, SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread, SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI, DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace), ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName, X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName, X11InstanceName: options.X11InstanceName,
StrictContextRestoration: options.StrictContextRestration,
} }
} }