Compare commits

...

38 Commits

Author SHA1 Message Date
Bertrand Jung
59e45a523d
Merge 1dd96726c4 into 4e6fdc6db5 2024-09-08 00:30:47 +09:00
Hajime Hoshi
4e6fdc6db5 all: update dependencies 2024-09-08 00:11:18 +09:00
Hajime Hoshi
1488e5e685 internal/graphicscommand: add attributes to images 2024-09-07 23:38:16 +09:00
Hajime Hoshi
4fa8265c58 internal: add comments 2024-09-07 23:24:55 +09:00
Hajime Hoshi
46cf09197b internal/graphicscommand: enable to show shader names 2024-09-07 22:06:41 +09:00
Hajime Hoshi
393437b8be internal/graphicscommand: imporve blend log 2024-09-07 21:29:50 +09:00
Hajime Hoshi
1b2a2afbfb internal/restorable: rename restoring -> restoration 2024-09-07 19:09:00 +09:00
Hajime Hoshi
d50a438c07 internal/restorable: optimize removeDuplicatedRegions
Updates #2375
Updates #2626
Updates #3083
2024-09-07 19:01:05 +09:00
Hajime Hoshi
35f4884a74 ebiten: add RunGameOptions.StrictContextRestration
This reverts commit a30f075896.

This change adds a new option StrictContextRestration to make the
restoration optional.

Closes #3083
2024-09-07 18:36:04 +09:00
Hajime Hoshi
935e7a6d5d Revert "internal/restorable: remove the case when the restoring is needed"
This reverts commit c08a2193a9.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
5e18f191c1 Revert "internal/restorable: remove drawTrianglesHistoryItem"
This reverts commit afe3f7a8ff.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
16d2052836 Revert "internal/restorable: remove alwaysReadPixelsFromGPU"
This reverts commit f3206721a3.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
81d35df33b Revert "internal/restorable: remove unused functions and variables"
This reverts commit 8169253a57.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
6453e552f3 Revert "internal/restorable: remove unnecessary functions and variables around shaders"
This reverts commit 4f3e00ec3a.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
34639d0028 Revert "internal/restorable: remove ImageTypeRestorable"
This reverts commit 8c2d4e1408.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
169b9fe51e Revert "internal/restorable: move DumpImages to internal/atlas"
This reverts commit 7c9266d8b6.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
a9d8f374c8 Revert "internal/restorable: move SwapBuffers to internal/atlas"
This reverts commit f610cb5724.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
c2c5480878 Revert "internal/atlas: move some functions in internal/restorable to internal/atlas"
This reverts commit 3651d73e0b.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
a324cfd3b6 Revert "internal/restorable: remove ImageType"
This reverts commit 21ef462c37.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
62ed5bed4b Revert "internal/restorable: integrate some functions into internal/atlas"
This reverts commit c5d0ec3de7.

Updates #3803
2024-09-07 16:54:20 +09:00
Hajime Hoshi
d533461936 Revert "internal/restorable: integrate ReadPixels into internal/atlas"
This reverts commit e804f9e58c.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
becada4afc Revert "internal/restorable: refactoring"
This reverts commit 81e1104613.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
19d2009a5f Revert "internal/restorable: integrate Shader.Dispose into internal/atlas"
This reverts commit bfa2c460a2.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
17a5488c49 Revert "internal/restorable: integrate Image.DrawTriangles into internal/atlas"
This reverts commit 3ee905bc4d.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
d733308eb1 Revert "internal/restorable: integrate Image.WritePixels into internal/atlas"
This reverts commit 6cc8150185.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
104cc18477 Revert "internal/restorable: integrate Image.Extend into internal/atlas"
This reverts commit 6151fd313f.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
54c117b0de Revert "internal/restorable: integrate Image functions into internal/atlas"
This reverts commit 59896e4447.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
5a5feb0401 Revert "internal/restorable: remove Image"
This reverts commit 812cd494de.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
0b01aeea16 Revert "internal/restorable: integrate the implementation into internal/atlas"
This reverts commit c404b448aa.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
81c75e1b0a Revert "internal/atlas: refactoring: remove ImageTypeVolatile"
This reverts commit def82fd5d3.

Updates #3083
2024-09-07 16:54:20 +09:00
Hajime Hoshi
cb63f1e56b mobile/ebitenmobileview: refactoring 2024-09-07 16:53:01 +09:00
Zyko
1dd96726c4 Add a benchmark + fix sub image allocations 2024-08-15 19:48:36 +02:00
Zyko
30157b5dea Add license header 2024-08-05 20:41:04 +02:00
Zyko
b20692f523 Fixed colorscale mode 2024-08-05 20:33:53 +02:00
Zyko
2eebe55b90 Restore go1.19 2024-08-05 20:27:36 +02:00
Zyko
ec06c68fa3 Re-use internal/packing logic and remove external dep 2024-08-05 20:25:54 +02:00
Zyko
4601cffaba Cleanup 2024-07-27 18:01:06 +02:00
Zyko
5e8d969034 PoC text/v2 glyph atlas 2024-07-27 17:41:53 +02:00
39 changed files with 3402 additions and 307 deletions

View File

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

View File

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

View File

@ -46,7 +46,7 @@ func newGameForUI(game Game, transparent bool) *gameForUI {
transparent: transparent,
}
s, err := NewShader(builtinshader.ScreenShaderSource)
s, err := newShader(builtinshader.ScreenShaderSource, "screen")
if err != nil {
panic(fmt.Sprintf("ebiten: compiling the screen shader failed: %v", err))
}
@ -66,6 +66,10 @@ func (g *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
// An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges.
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)
return g.offscreen.image
}

4
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/ebitengine/purego v0.8.0-alpha.5
github.com/gen2brain/mpeg v0.3.2-0.20240412154320-a2ac4fc8a46f
github.com/go-text/typesetting v0.1.1
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.4.0.20240905090502-03ded50a2328
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.5
github.com/hajimehoshi/go-mp3 v0.3.4
github.com/jakecoffman/cp v1.2.1
github.com/jezek/xgb v1.1.1
@ -17,7 +17,7 @@ require (
github.com/kisielk/errcheck v1.7.0
golang.org/x/image v0.19.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.24.0
golang.org/x/sys v0.25.0
golang.org/x/text v0.17.0
golang.org/x/tools v0.24.0
)

8
go.sum
View File

@ -13,8 +13,8 @@ github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZ
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.4.0.20240905090502-03ded50a2328 h1:AYqx/yoXuPMmNJMXEblljVeqFDQnKFu5QOjHZMhGfMY=
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.4.0.20240905090502-03ded50a2328/go.mod h1:/GmYyEKgzzM7dzJBsL7aS5iR83Dr666E5bhQLVVPYsw=
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.5 h1:gtIcN2INlD2qlfUiECuvbI0moNIoANgIY7MwgW4cFGE=
github.com/hajimehoshi/bitmapfont/v3 v3.2.0-alpha.5/go.mod h1:/GmYyEKgzzM7dzJBsL7aS5iR83Dr666E5bhQLVVPYsw=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
@ -74,8 +74,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@ -22,11 +22,10 @@ import (
"runtime"
"sync"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"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/packing"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
@ -90,11 +89,8 @@ func putImagesOnSourceBackend() {
// backend is a big texture atlas that can have multiple images.
// backend is a texture in GPU.
type backend struct {
// image is an atlas on which there might be multiple images.
image *graphicscommand.Image
width int
height int
// restorable is an atlas on which there might be multiple images.
restorable *restorable.Image
// 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.
@ -121,73 +117,11 @@ func (b *backend) tryAlloc(width, height int) (*packing.Node, bool) {
return nil, false
}
b.extendIfNeeded(b.page.Size())
b.restorable = b.restorable.Extend(b.page.Size())
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 (
// backendsM is a mutex for critical sections of the backend and packing.Node objects.
backendsM sync.Mutex
@ -205,8 +139,6 @@ var (
imagesUsedAsDestination smallImageSet
graphicsDriverInitialized bool
deferred []func()
// deferredM is a mutex for the slice operations. This must not be used for other usages.
@ -224,6 +156,9 @@ const (
// A screen image is also unmanaged.
ImageTypeScreen
// ImageTypeVolatile is a volatile image that is cleared every frame.
ImageTypeVolatile
// ImageTypeUnmanaged is an unmanaged image that is not on an atlas.
ImageTypeUnmanaged
)
@ -242,7 +177,7 @@ type Image struct {
// usedAsSourceCount represents how long the image is used as a rendering source and kept not modified with
// DrawTriangles.
// In the current implementation, if an image is being modified by DrawTriangles, the image is separated from
// a graphicscommand.Image on an atlas by ensureIsolatedFromSource.
// a restorable image on an atlas by ensureIsolatedFromSource.
//
// The type is int64 instead of int to avoid overflow when comparing the limitation.
//
@ -433,11 +368,6 @@ 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) {
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))
for _, src := range srcs {
if src == nil {
@ -457,7 +387,7 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
for _, src := range srcs {
// 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.
if src != nil && i.backend.image == src.backend.image {
if src != nil && i.backend.restorable == src.backend.restorable {
panic("atlas: Image.DrawTriangles: source must be different from the receiver")
}
}
@ -479,8 +409,8 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
vertices[i+2] += oxf
vertices[i+3] += oyf
}
if shader.ir.Unit == shaderir.Texels {
sw, sh := srcs[0].backend.image.InternalSize()
if shader.ensureShader().Unit() == shaderir.Texels {
sw, sh := srcs[0].backend.restorable.InternalSize()
swf, shf := float32(sw), float32(sh)
for i := 0; i < n; i += graphics.VertexFloatCount {
vertices[i+2] /= swf
@ -510,15 +440,15 @@ func (i *Image) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
srcRegions[i] = srcRegions[i].Add(r.Min)
}
var imgs [graphics.ShaderSrcImageCount]*graphicscommand.Image
var imgs [graphics.ShaderSrcImageCount]*restorable.Image
for i, src := range srcs {
if src == nil {
continue
}
imgs[i] = src.backend.image
imgs[i] = src.backend.restorable
}
i.backend.image.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.ensureShader(), uniforms, fillRule)
i.backend.restorable.DrawTriangles(imgs, vertices, indices, blend, dstRegion, srcRegions, shader.ensureShader(), uniforms, fillRule)
for _, src := range srcs {
if src == nil {
@ -570,7 +500,7 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
region = region.Add(r.Min)
if pix == nil {
i.backend.clearPixels(region)
i.backend.restorable.ClearPixels(region)
return
}
@ -578,7 +508,7 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) {
copy(bs, pix)
})
i.backend.writePixels(pix2, region)
i.backend.restorable.WritePixels(pix2, region)
return
}
@ -607,7 +537,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()])
}
})
i.backend.writePixels(pixb, r)
i.backend.restorable.WritePixels(pixb, r)
}
func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) (ok bool, err error) {
@ -623,29 +553,17 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte
// To prevent memory leaks, flush the deferred functions here.
flushDeferred()
if err := i.readPixels(graphicsDriver, pixels, region); err != nil {
return false, err
}
return true, nil
}
func (i *Image) readPixels(graphicsDriver graphicsdriver.Graphics, pixels []byte, region image.Rectangle) error {
if i.backend == nil {
if i.backend == nil || i.backend.restorable == nil {
for i := range pixels {
pixels[i] = 0
}
return nil
return true, nil
}
if err := i.backend.image.ReadPixels(graphicsDriver, []graphicsdriver.PixelsArgs{
{
Pixels: pixels,
Region: region.Add(i.regionWithPadding().Min),
},
}); err != nil {
return err
if err := i.backend.restorable.ReadPixels(graphicsDriver, pixels, region.Add(i.regionWithPadding().Min)); err != nil {
return false, err
}
return nil
return true, nil
}
// Deallocate deallocates the internal state.
@ -686,13 +604,12 @@ func (i *Image) deallocate() {
if !i.backend.page.IsEmpty() {
// As this part can be reused, this should be cleared explicitly.
r := i.regionWithPadding()
i.backend.clearPixels(r)
i.backend.restorable.ClearPixels(r)
return
}
}
i.backend.image.Dispose()
i.backend.image = nil
i.backend.restorable.Dispose()
for idx, sh := range theBackends {
if sh == i.backend {
@ -735,16 +652,24 @@ func (i *Image) finalize() {
}
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 {
panic("atlas: the image is already allocated")
}
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()
hp := i.height + i.paddingSize()
@ -753,11 +678,13 @@ 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))
}
typ := restorable.ImageTypeRegular
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
i.backend = &backend{
image: newClearedImage(wp, hp, i.imageType == ImageTypeScreen),
width: wp,
height: hp,
source: asSource && i.imageType == ImageTypeRegular,
restorable: restorable.NewImage(wp, hp, typ),
source: asSource && typ == restorable.ImageTypeRegular,
}
theBackends = append(theBackends, i.backend)
return
@ -801,12 +728,14 @@ loop:
height *= 2
}
typ := restorable.ImageTypeRegular
if i.imageType == ImageTypeVolatile {
typ = restorable.ImageTypeVolatile
}
b := &backend{
image: newClearedImage(width, height, false),
width: width,
height: height,
page: packing.NewPage(width, height, maxSize),
source: asSource,
restorable: restorable.NewImage(width, height, typ),
page: packing.NewPage(width, height, maxSize),
source: asSource,
}
theBackends = append(theBackends, b)
@ -826,7 +755,7 @@ func (i *Image) DumpScreenshot(graphicsDriver graphicsdriver.Graphics, path stri
panic("atlas: DumpScreenshots must be called in between BeginFrame and EndFrame")
}
return i.backend.image.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height))
return i.backend.restorable.Dump(graphicsDriver, path, blackbg, image.Rect(0, 0, i.width, i.height))
}
func EndFrame() error {
@ -857,15 +786,7 @@ func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error {
}
}()
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 {
if err := restorable.SwapBuffers(graphicsDriver); err != nil {
return err
}
return nil
@ -890,7 +811,7 @@ func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
var err error
initOnce.Do(func() {
err = graphicscommand.InitializeGraphicsDriverState(graphicsDriver)
err = restorable.InitializeGraphicsDriverState(graphicsDriver)
if err != nil {
return
}
@ -906,15 +827,18 @@ func BeginFrame(graphicsDriver graphicsdriver.Graphics) error {
minDestinationSize = 16
}
if maxSize == 0 {
maxSize = floorPowerOf2(graphicscommand.MaxImageSize(graphicsDriver))
maxSize = floorPowerOf2(restorable.MaxImageSize(graphicsDriver))
}
graphicsDriverInitialized = true
})
if err != nil {
return err
}
// Restore images first before other image manipulations (#2075).
if err := restorable.RestoreIfNeeded(graphicsDriver); err != nil {
return err
}
flushDeferred()
putImagesOnSourceBackend()
@ -929,9 +853,5 @@ func DumpImages(graphicsDriver graphicsdriver.Graphics, dir string) (string, err
panic("atlas: DumpImages must be called in between BeginFrame and EndFrame")
}
images := make([]*graphicscommand.Image, 0, len(theBackends))
for _, backend := range theBackends {
images = append(images, backend.image)
}
return graphicscommand.DumpImages(images, graphicsDriver, dir)
return restorable.DumpImages(graphicsDriver, dir)
}

View File

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

View File

@ -15,26 +15,23 @@
package atlas
import (
"fmt"
"runtime"
"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/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
type Shader struct {
ir *shaderir.Program
shader *graphicscommand.Shader
shader *restorable.Shader
name string
}
func NewShader(ir *shaderir.Program) *Shader {
func NewShader(ir *shaderir.Program, name string) *Shader {
// A shader is initialized lazily, and the lock is not needed.
return &Shader{
ir: ir,
ir: ir,
name: name,
}
}
@ -46,11 +43,11 @@ func (s *Shader) finalize() {
})
}
func (s *Shader) ensureShader() *graphicscommand.Shader {
func (s *Shader) ensureShader() *restorable.Shader {
if s.shader != nil {
return s.shader
}
s.shader = graphicscommand.NewShader(s.ir)
s.shader = restorable.NewShader(s.ir, s.name)
runtime.SetFinalizer(s, (*Shader).finalize)
return s.shader
}
@ -80,38 +77,6 @@ func (s *Shader) deallocate() {
}
var (
NearestFilterShader *Shader
LinearFilterShader *Shader
clearShader *Shader
NearestFilterShader = &Shader{shader: restorable.NearestFilterShader}
LinearFilterShader = &Shader{shader: restorable.LinearFilterShader}
)
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

@ -36,12 +36,12 @@ func TestShaderFillTwice(t *testing.T) {
is := graphics.QuadIndices()
dr := image.Rect(0, 0, w, h)
g := ui.Get().GraphicsDriverForTesting()
s0 := atlas.NewShader(etesting.ShaderProgramFill(0xff, 0xff, 0xff, 0xff))
s0 := atlas.NewShader(etesting.ShaderProgramFill(0xff, 0xff, 0xff, 0xff), "")
dst.DrawTriangles([graphics.ShaderSrcImageCount]*atlas.Image{}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s0, nil, graphicsdriver.FillRuleFillAll)
// Vertices must be recreated (#1755)
vs = quadVertices(w, h, 0, 0, 1)
s1 := atlas.NewShader(etesting.ShaderProgramFill(0x80, 0x80, 0x80, 0xff))
s1 := atlas.NewShader(etesting.ShaderProgramFill(0x80, 0x80, 0x80, 0xff), "")
dst.DrawTriangles([graphics.ShaderSrcImageCount]*atlas.Image{}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s1, nil, graphicsdriver.FillRuleFillAll)
pix := make([]byte, 4*w*h)
@ -89,7 +89,7 @@ func TestImageDrawTwice(t *testing.T) {
}
func TestGCShader(t *testing.T) {
s := atlas.NewShader(etesting.ShaderProgramFill(0xff, 0xff, 0xff, 0xff))
s := atlas.NewShader(etesting.ShaderProgramFill(0xff, 0xff, 0xff, 0xff), "")
// Use the shader to initialize it.
const w, h = 1, 1

View File

@ -21,6 +21,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)
var whiteImage *Image
@ -46,6 +47,8 @@ type Image struct {
// pixels is cached pixels for ReadPixels.
// pixels might be out of sync with GPU.
// The data of pixels is the secondary data of pixels for ReadPixels.
//
// pixels is always nil when restorable.AlwaysReadPixelsFromGPU() returns false.
pixels []byte
// pixelsUnsynced represents whether the pixels in CPU and GPU are not synced.
@ -68,9 +71,6 @@ func (i *Image) Deallocate() {
}
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 c, ok := i.dotsBuffer[region.Min]; ok {
copy(pixels, c[:])
@ -78,6 +78,19 @@ 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 {
pix := make([]byte, 4*i.width*i.height)
ok, err := i.img.ReadPixels(graphicsDriver, pix, image.Rect(0, 0, i.width, i.height))

View File

@ -72,18 +72,29 @@ type drawTrianglesCommand struct {
}
func (c *drawTrianglesCommand) String() string {
// TODO: Improve readability
blend := fmt.Sprintf("{src-color: %d, src-alpha: %d, dst-color: %d, dst-alpha: %d, op-color: %d, op-alpha: %d}",
c.blend.BlendFactorSourceRGB,
c.blend.BlendFactorSourceAlpha,
c.blend.BlendFactorDestinationRGB,
c.blend.BlendFactorDestinationAlpha,
c.blend.BlendOperationRGB,
c.blend.BlendOperationAlpha)
var blend string
switch c.blend {
case graphicsdriver.BlendSourceOver:
blend = "(source-over)"
case graphicsdriver.BlendClear:
blend = "(clear)"
case graphicsdriver.BlendCopy:
blend = "(copy)"
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)
if c.dst.screen {
dst += " (screen)"
} else if c.dst.attribute != "" {
dst += " (" + c.dst.attribute + ")"
}
var srcstrs [graphics.ShaderSrcImageCount]string
@ -95,10 +106,17 @@ func (c *drawTrianglesCommand) String() string {
srcstrs[i] = fmt.Sprintf("%d", src.id)
if src.screen {
srcstrs[i] += " (screen)"
} else if src.attribute != "" {
srcstrs[i] += " (" + src.attribute + ")"
}
}
return fmt.Sprintf("draw-triangles: dst: %s <- src: [%s], num of dst regions: %d, num of indices: %d, blend: %s, fill rule: %s, shader id: %d", dst, strings.Join(srcstrs[:], ", "), len(c.dstRegions), c.numIndices(), blend, c.fillRule, c.shader.id)
shader := fmt.Sprintf("%d", c.shader.id)
if c.shader.name != "" {
shader += " (" + c.shader.name + ")"
}
return fmt.Sprintf("draw-triangles: dst: %s <- src: [%s], num of dst regions: %d, num of indices: %d, blend: %s, fill rule: %s, shader: %s", dst, strings.Join(srcstrs[:], ", "), len(c.dstRegions), c.numIndices(), blend, c.fillRule, shader)
}
// Exec executes the drawTrianglesCommand.
@ -321,14 +339,19 @@ func (c *disposeShaderCommand) NeedsSync() bool {
// newImageCommand represents a command to create an empty image with given width and height.
type newImageCommand struct {
result *Image
width int
height int
screen bool
result *Image
width int
height int
screen bool
attribute string
}
func (c *newImageCommand) String() string {
return fmt.Sprintf("new-image: result: %d, width: %d, height: %d, screen: %t", c.result.id, c.width, c.height, c.screen)
str := fmt.Sprintf("new-image: result: %d, width: %d, height: %d, screen: %t", c.result.id, c.width, c.height, c.screen)
if c.attribute != "" {
str += ", attribute: " + c.attribute
}
return str
}
// Exec executes a newImageCommand.

View File

@ -37,6 +37,9 @@ type Image struct {
internalHeight int
screen bool
// attribute is used only for logs.
attribute string
// id is an identifier for the image. This is used only when dumping the information.
//
// This is duplicated with graphicsdriver.Image's ID, but this id is still necessary because this image might not
@ -57,18 +60,20 @@ func genNextImageID() int {
// NewImage returns a new image.
//
// Note that the image is not initialized yet.
func NewImage(width, height int, screenFramebuffer bool) *Image {
func NewImage(width, height int, screenFramebuffer bool, attribute string) *Image {
i := &Image{
width: width,
height: height,
screen: screenFramebuffer,
id: genNextImageID(),
width: width,
height: height,
screen: screenFramebuffer,
id: genNextImageID(),
attribute: attribute,
}
c := &newImageCommand{
result: i,
width: width,
height: height,
screen: screenFramebuffer,
result: i,
width: width,
height: height,
screen: screenFramebuffer,
attribute: attribute,
}
theCommandQueueManager.enqueueCommand(c)
return i

View File

@ -35,7 +35,7 @@ func init() {
if err != nil {
panic(fmt.Sprintf("graphicscommand: compiling the nearest shader failed: %v", err))
}
nearestFilterShader = graphicscommand.NewShader(ir)
nearestFilterShader = graphicscommand.NewShader(ir, "")
}
func TestMain(m *testing.M) {
@ -50,8 +50,8 @@ func quadVertices(w, h float32) []float32 {
func TestClear(t *testing.T) {
const w, h = 1024, 1024
src := graphicscommand.NewImage(w/2, h/2, false)
dst := graphicscommand.NewImage(w, h, false)
src := graphicscommand.NewImage(w/2, h/2, false, "")
dst := graphicscommand.NewImage(w, h, false, "")
vs := quadVertices(w/2, h/2)
is := graphics.QuadIndices()
@ -81,9 +81,9 @@ func TestClear(t *testing.T) {
func TestWritePixelsPartAfterDrawTriangles(t *testing.T) {
const w, h = 32, 32
clr := graphicscommand.NewImage(w, h, false)
src := graphicscommand.NewImage(w/2, h/2, false)
dst := graphicscommand.NewImage(w, h, false)
clr := graphicscommand.NewImage(w, h, false, "")
src := graphicscommand.NewImage(w/2, h/2, false, "")
dst := graphicscommand.NewImage(w, h, false, "")
vs := quadVertices(w/2, h/2)
is := graphics.QuadIndices()
dr := image.Rect(0, 0, w, h)
@ -101,15 +101,15 @@ func TestWritePixelsPartAfterDrawTriangles(t *testing.T) {
func TestShader(t *testing.T) {
const w, h = 16, 16
clr := graphicscommand.NewImage(w, h, false)
dst := graphicscommand.NewImage(w, h, false)
clr := graphicscommand.NewImage(w, h, false, "")
dst := graphicscommand.NewImage(w, h, false, "")
vs := quadVertices(w, h)
is := graphics.QuadIndices()
dr := image.Rect(0, 0, w, h)
dst.DrawTriangles([graphics.ShaderSrcImageCount]*graphicscommand.Image{clr}, vs, is, graphicsdriver.BlendClear, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, nearestFilterShader, nil, graphicsdriver.FillRuleFillAll)
g := ui.Get().GraphicsDriverForTesting()
s := graphicscommand.NewShader(etesting.ShaderProgramFill(0xff, 0, 0, 0xff))
s := graphicscommand.NewShader(etesting.ShaderProgramFill(0xff, 0, 0, 0xff), "")
dst.DrawTriangles([graphics.ShaderSrcImageCount]*graphicscommand.Image{}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderSrcImageCount]image.Rectangle{}, s, nil, graphicsdriver.FillRuleFillAll)
pix := make([]byte, 4*w*h)
@ -136,7 +136,7 @@ func TestShader(t *testing.T) {
// Issue #3036
func TestSuccessiveWritePixels(t *testing.T) {
const w, h = 32, 32
dst := graphicscommand.NewImage(w, h, false)
dst := graphicscommand.NewImage(w, h, false, "")
dst.WritePixels(graphics.NewManagedBytes(4, func(bs []byte) {
for i := range bs {

View File

@ -31,12 +31,16 @@ type Shader struct {
shader graphicsdriver.Shader
ir *shaderir.Program
id int
// name is used only for logging.
name string
}
func NewShader(ir *shaderir.Program) *Shader {
func NewShader(ir *shaderir.Program, name string) *Shader {
s := &Shader{
ir: ir,
id: genNextShaderID(),
ir: ir,
id: genNextShaderID(),
name: name,
}
c := &newShaderCommand{
result: s,

View File

@ -0,0 +1,68 @@
// 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

@ -0,0 +1,28 @@
// 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

@ -0,0 +1,741 @@
// 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")
}
var attribute string
switch imageType {
case ImageTypeVolatile:
attribute = "volatile"
}
i := &Image{
image: graphicscommand.NewImage(width, height, imageType == ImageTypeScreen, attribute),
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, "volatile")
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

@ -0,0 +1,291 @@
// 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

@ -0,0 +1,159 @@
// 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

@ -0,0 +1,98 @@
// 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
name string
}
func NewShader(ir *shaderir.Program, name string) *Shader {
s := &Shader{
shader: graphicscommand.NewShader(ir, name),
ir: ir,
name: name,
}
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, s.name)
}
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, "nearest")
LinearFilterShader = NewShader(linearIR, "linear")
clearShader = NewShader(clearIR, "clear")
}

View File

@ -0,0 +1,200 @@
// 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,6 +180,12 @@ func (c *context) newOffscreenImage(w, h int) *Image {
}
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.
c.isOffscreenModified = false

View File

@ -15,6 +15,7 @@
package ui
import (
"fmt"
"image"
"math"
@ -85,7 +86,16 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertice
if antialias {
if i.bigOffscreenBuffer == nil {
i.bigOffscreenBuffer = i.ui.newBigOffscreenImage(i, atlas.ImageTypeUnmanaged)
var imageType atlas.ImageType
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)

View File

@ -32,9 +32,9 @@ type Shader struct {
uniformUint32Count int
}
func NewShader(ir *shaderir.Program) *Shader {
func NewShader(ir *shaderir.Program, name string) *Shader {
return &Shader{
shader: atlas.NewShader(ir),
shader: atlas.NewShader(ir, name),
uniformNames: ir.UniformNames[graphics.PreservedUniformVariablesCount:],
uniformTypes: ir.Uniforms[graphics.PreservedUniformVariablesCount:],
}

View File

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

View File

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

View File

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

37
run.go
View File

@ -297,6 +297,24 @@ type RunGameOptions struct {
// X11InstanceName is an instance name in the ICCCM WM_CLASS window property.
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.
@ -716,15 +734,16 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
options.X11InstanceName = defaultX11InstanceName
}
return &ui.RunOptions{
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName,
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName,
StrictContextRestoration: options.StrictContextRestration,
}
}

View File

@ -38,12 +38,16 @@ type Shader struct {
//
// For the details about the shader, see https://ebitengine.org/en/documents/shader.html.
func NewShader(src []byte) (*Shader, error) {
return newShader(src, "")
}
func newShader(src []byte, name string) (*Shader, error) {
ir, err := graphics.CompileShader(src)
if err != nil {
return nil, err
}
return &Shader{
shader: ui.NewShader(ir),
shader: ui.NewShader(ir, name),
unit: ir.Unit,
}, nil
}
@ -108,7 +112,23 @@ func builtinShader(filter builtinshader.Filter, address builtinshader.Address, u
}
} else {
src := builtinshader.ShaderSource(filter, address, useColorM)
s, err := NewShader(src)
var name string
switch filter {
case builtinshader.FilterNearest:
name = "nearest"
case builtinshader.FilterLinear:
name = "linear"
}
switch address {
case builtinshader.AddressClampToZero:
name += "-clamptozero"
case builtinshader.AddressRepeat:
name += "-repeat"
}
if useColorM {
name += "-colorm"
}
s, err := newShader(src, name)
if err != nil {
panic(fmt.Sprintf("ebiten: NewShader for a built-in shader failed: %v", err))
}

297
text/v2/atlas.go Normal file
View File

@ -0,0 +1,297 @@
// Copyright 2024 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 text
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/packing"
)
type glyphAtlas struct {
page *packing.Page
image *ebiten.Image
}
type glyphImage struct {
atlas *glyphAtlas
node *packing.Node
img *ebiten.Image
}
func (i *glyphImage) Image() *ebiten.Image {
return i.img
}
func newGlyphAtlas() *glyphAtlas {
return &glyphAtlas{
// Note: 128x128 is arbitrary, maybe a better value can be inferred
// from the font size or something
page: packing.NewPage(128, 128, 1024), // TODO: not 1024
image: ebiten.NewImage(128, 128),
}
}
func (g *glyphAtlas) NewImage(w, h int) *glyphImage {
n := g.page.Alloc(w, h)
pw, ph := g.page.Size()
if pw > g.image.Bounds().Dx() || ph > g.image.Bounds().Dy() {
newImage := ebiten.NewImage(pw, ph)
newImage.DrawImage(g.image, nil)
g.image = newImage
}
return &glyphImage{
atlas: g,
node: n,
img: g.image.SubImage(n.Region()).(*ebiten.Image),
}
}
func (g *glyphAtlas) Free(img *glyphImage) {
g.page.Free(img.node)
}
type drawRange struct {
atlas *glyphAtlas
end int
}
// drawList stores triangle versions of DrawImage calls when
// all images are sub-images of an atlas.
// Temporary vertices and indices can be re-used after calling
// Flush, so it is more efficient to keep a reference to a drawList
// instead of creating a new one every frame.
type drawList struct {
ranges []drawRange
vx []ebiten.Vertex
ix []uint16
}
// drawCommand is the equivalent of the regular DrawImageOptions
// but only including options that will not break batching.
// Filter, Address, Blend and AntiAlias are determined at Flush()
type drawCommand struct {
Image *glyphImage
ColorScale ebiten.ColorScale
GeoM ebiten.GeoM
}
var rectIndices = [6]uint16{0, 1, 2, 1, 2, 3}
type point struct {
X, Y float32
}
func pt(x, y float64) point {
return point{
X: float32(x),
Y: float32(y),
}
}
type rectOpts struct {
Dsts [4]point
SrcX0, SrcY0 float32
SrcX1, SrcY1 float32
R, G, B, A float32
}
// adjustDestinationPixel is the original ebitengine implementation found here:
// https://github.com/hajimehoshi/ebiten/blob/v2.8.0-alpha.1/internal/graphics/vertex.go#L102-L126
func adjustDestinationPixel(x float32) float32 {
// Avoid the center of the pixel, which is problematic (#929, #1171).
// Instead, align the vertices with about 1/3 pixels.
//
// The intention here is roughly this code:
//
// float32(math.Floor((float64(x)+1.0/6.0)*3) / 3)
//
// The actual implementation is more optimized than the above implementation.
ix := float32(int(x))
if x < 0 && x != ix {
ix -= 1
}
frac := x - ix
switch {
case frac < 3.0/16.0:
return ix
case frac < 8.0/16.0:
return ix + 5.0/16.0
case frac < 13.0/16.0:
return ix + 11.0/16.0
default:
return ix + 16.0/16.0
}
}
func appendRectVerticesIndices(vertices []ebiten.Vertex, indices []uint16, index int, opts *rectOpts) ([]ebiten.Vertex, []uint16) {
sx0, sy0, sx1, sy1 := opts.SrcX0, opts.SrcY0, opts.SrcX1, opts.SrcY1
r, g, b, a := opts.R, opts.G, opts.B, opts.A
vertices = append(vertices,
ebiten.Vertex{
DstX: adjustDestinationPixel(opts.Dsts[0].X),
DstY: adjustDestinationPixel(opts.Dsts[0].Y),
SrcX: sx0,
SrcY: sy0,
ColorR: r,
ColorG: g,
ColorB: b,
ColorA: a,
},
ebiten.Vertex{
DstX: adjustDestinationPixel(opts.Dsts[1].X),
DstY: adjustDestinationPixel(opts.Dsts[1].Y),
SrcX: sx1,
SrcY: sy0,
ColorR: r,
ColorG: g,
ColorB: b,
ColorA: a,
},
ebiten.Vertex{
DstX: adjustDestinationPixel(opts.Dsts[2].X),
DstY: adjustDestinationPixel(opts.Dsts[2].Y),
SrcX: sx0,
SrcY: sy1,
ColorR: r,
ColorG: g,
ColorB: b,
ColorA: a,
},
ebiten.Vertex{
DstX: adjustDestinationPixel(opts.Dsts[3].X),
DstY: adjustDestinationPixel(opts.Dsts[3].Y),
SrcX: sx1,
SrcY: sy1,
ColorR: r,
ColorG: g,
ColorB: b,
ColorA: a,
},
)
indiceCursor := uint16(index * 4)
indices = append(indices,
rectIndices[0]+indiceCursor,
rectIndices[1]+indiceCursor,
rectIndices[2]+indiceCursor,
rectIndices[3]+indiceCursor,
rectIndices[4]+indiceCursor,
rectIndices[5]+indiceCursor,
)
return vertices, indices
}
// Add adds DrawImage commands to the DrawList, images from multiple
// atlases can be added but they will break the previous batch bound to
// a different atlas, requiring an additional draw call internally.
// So, it is better to have the maximum of consecutive DrawCommand images
// sharing the same atlas.
func (dl *drawList) Add(commands ...*drawCommand) {
if len(commands) == 0 {
return
}
var batch *drawRange
if len(dl.ranges) > 0 {
batch = &dl.ranges[len(dl.ranges)-1]
} else {
dl.ranges = append(dl.ranges, drawRange{
atlas: commands[0].Image.atlas,
})
batch = &dl.ranges[0]
}
// Add vertices and indices
opts := &rectOpts{}
for _, cmd := range commands {
if cmd.Image.atlas != batch.atlas {
dl.ranges = append(dl.ranges, drawRange{
atlas: cmd.Image.atlas,
})
batch = &dl.ranges[len(dl.ranges)-1]
}
// Dst attributes
bounds := cmd.Image.node.Region()
opts.Dsts[0] = pt(cmd.GeoM.Apply(0, 0))
opts.Dsts[1] = pt(cmd.GeoM.Apply(
float64(bounds.Dx()), 0,
))
opts.Dsts[2] = pt(cmd.GeoM.Apply(
0, float64(bounds.Dy()),
))
opts.Dsts[3] = pt(cmd.GeoM.Apply(
float64(bounds.Dx()), float64(bounds.Dy()),
))
// Color and source attributes
opts.R = cmd.ColorScale.R()
opts.G = cmd.ColorScale.G()
opts.B = cmd.ColorScale.B()
opts.A = cmd.ColorScale.A()
opts.SrcX0 = float32(bounds.Min.X)
opts.SrcY0 = float32(bounds.Min.Y)
opts.SrcX1 = float32(bounds.Max.X)
opts.SrcY1 = float32(bounds.Max.Y)
dl.vx, dl.ix = appendRectVerticesIndices(
dl.vx, dl.ix, batch.end, opts,
)
batch.end++
}
}
// DrawOptions are additional options that will be applied to
// all draw commands from the draw list when calling Flush().
type drawOptions struct {
ColorScaleMode ebiten.ColorScaleMode
Blend ebiten.Blend
Filter ebiten.Filter
Address ebiten.Address
AntiAlias bool
}
// Flush executes all the draw commands as the smallest possible
// amount of draw calls, and then clears the list for next uses.
func (dl *drawList) Flush(dst *ebiten.Image, opts *drawOptions) {
var topts *ebiten.DrawTrianglesOptions
if opts != nil {
topts = &ebiten.DrawTrianglesOptions{
ColorScaleMode: opts.ColorScaleMode,
Blend: opts.Blend,
Filter: opts.Filter,
Address: opts.Address,
AntiAlias: opts.AntiAlias,
}
}
index := 0
for _, r := range dl.ranges {
dst.DrawTriangles(
dl.vx[index*4:(index+r.end)*4],
dl.ix[index*6:(index+r.end)*6],
r.atlas.image,
topts,
)
index += r.end
}
// Clear buffers
dl.ranges = dl.ranges[:0]
dl.vx = dl.vx[:0]
dl.ix = dl.ix[:0]
}

View File

@ -18,7 +18,6 @@ import (
"math"
"sync"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
@ -38,17 +37,18 @@ func init() {
}
type glyphImageCacheEntry struct {
image *ebiten.Image
image *glyphImage
atime int64
}
type glyphImageCache[Key comparable] struct {
atlas *glyphAtlas
cache map[Key]*glyphImageCacheEntry
atime int64
m sync.Mutex
}
func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *ebiten.Image) *ebiten.Image {
func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func(a *glyphAtlas) *glyphImage) *glyphImage {
g.m.Lock()
defer g.m.Unlock()
@ -61,10 +61,11 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *eb
}
if g.cache == nil {
g.atlas = newGlyphAtlas()
g.cache = map[Key]*glyphImageCacheEntry{}
}
img := create()
img := create(g.atlas)
e = &glyphImageCacheEntry{
image: img,
}
@ -91,6 +92,7 @@ func (g *glyphImageCache[Key]) getOrCreate(face Face, key Key, create func() *eb
continue
}
delete(g.cache, key)
g.atlas.Free(e.image)
}
}
}

View File

@ -311,11 +311,16 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
img, imgX, imgY := g.glyphImage(glyph, o)
// Append a glyph even if img is nil.
// This is necessary to return index information for control characters.
var ebitenImage *ebiten.Image
if img != nil {
ebitenImage = img.Image()
}
glyphs = append(glyphs, Glyph{
img: img,
StartIndexInBytes: indexOffset + glyph.startIndex,
EndIndexInBytes: indexOffset + glyph.endIndex,
GID: uint32(glyph.shapingGlyph.GlyphID),
Image: img,
Image: ebitenImage,
X: float64(imgX),
Y: float64(imgY),
OriginX: fixed26_6ToFloat64(origin.X),
@ -332,7 +337,7 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
return glyphs
}
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) {
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*glyphImage, int, int) {
if g.direction().isHorizontal() {
origin.X = adjustGranularity(origin.X, g)
origin.Y &^= ((1 << 6) - 1)
@ -352,8 +357,8 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
yoffset: subpixelOffset.Y,
variations: g.ensureVariationsString(),
}
img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image {
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
img := g.Source.getOrCreateGlyphImage(g, key, func(a *glyphAtlas) *glyphImage {
return segmentsToImage(a, glyph.scaledSegments, subpixelOffset, b)
})
imgX := (origin.X + b.Min.X).Floor()

View File

@ -26,8 +26,6 @@ import (
"github.com/go-text/typesetting/opentype/loader"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
)
type goTextOutputCacheKey struct {
@ -282,7 +280,7 @@ func (g *GoTextFaceSource) scale(size float64) float64 {
return size / float64(g.f.Upem())
}
func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func(a *glyphAtlas) *glyphImage) *glyphImage {
if g.glyphImageCache == nil {
g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{}
}

View File

@ -19,11 +19,11 @@ import (
"image/draw"
"math"
"github.com/go-text/typesetting/opentype/api"
"golang.org/x/image/math/fixed"
gvector "golang.org/x/image/vector"
"github.com/hajimehoshi/ebiten/v2"
"github.com/go-text/typesetting/opentype/api"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2/vector"
)
@ -75,7 +75,7 @@ func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 {
}
}
func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
func segmentsToImage(a *glyphAtlas, segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage {
if len(segs) == 0 {
return nil
}
@ -122,7 +122,10 @@ func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBo
dst := image.NewRGBA(image.Rect(0, 0, w, h))
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
return ebiten.NewImageFromImage(dst)
img := a.NewImage(w, h)
img.Image().WritePixels(dst.Pix)
return img
}
func appendVectorPathFromSegments(path *vector.Path, segs []api.Segment, x, y float32) {

View File

@ -21,7 +21,6 @@ import (
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
@ -119,9 +118,10 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
// Append a glyph even if img is nil.
// This is necessary to return index information for control characters.
glyphs = append(glyphs, Glyph{
img: img,
StartIndexInBytes: indexOffset + i,
EndIndexInBytes: indexOffset + i + size,
Image: img,
Image: img.Image(),
X: float64(imgX),
Y: float64(imgY),
OriginX: fixed26_6ToFloat64(origin.X),
@ -136,7 +136,7 @@ func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
return glyphs
}
func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) {
func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*glyphImage, int, int, fixed.Int26_6) {
// Assume that GoXFace's direction is always horizontal.
origin.X = adjustGranularity(origin.X, s)
origin.Y &^= ((1 << 6) - 1)
@ -150,15 +150,15 @@ func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
rune: r,
xoffset: subpixelOffset.X,
}
img := s.glyphImageCache.getOrCreate(s, key, func() *ebiten.Image {
return s.glyphImageImpl(r, subpixelOffset, b)
img := s.glyphImageCache.getOrCreate(s, key, func(a *glyphAtlas) *glyphImage {
return s.glyphImageImpl(a, r, subpixelOffset, b)
})
imgX := (origin.X + b.Min.X).Floor()
imgY := (origin.Y + b.Min.Y).Floor()
return img, imgX, imgY, a
}
func (s *GoXFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
func (s *GoXFace) glyphImageImpl(a *glyphAtlas, r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *glyphImage {
w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
if w == 0 || h == 0 {
return nil
@ -182,7 +182,10 @@ func (s *GoXFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBo
}
d.DrawString(string(r))
return ebiten.NewImageFromImage(rgba)
img := a.NewImage(w, h)
img.Image().WritePixels(rgba.Pix)
return img
}
// direction implements Face.

View File

@ -111,15 +111,24 @@ func Draw(dst *ebiten.Image, text string, face Face, options *DrawOptions) {
geoM := drawOp.GeoM
dl := &drawList{}
dc := &drawCommand{}
for _, g := range AppendGlyphs(nil, text, face, &layoutOp) {
if g.Image == nil {
continue
}
drawOp.GeoM.Reset()
drawOp.GeoM.Translate(g.X, g.Y)
drawOp.GeoM.Concat(geoM)
dst.DrawImage(g.Image, &drawOp)
dc.GeoM.Reset()
dc.GeoM.Translate(g.X, g.Y)
dc.GeoM.Concat(geoM)
dc.ColorScale = drawOp.ColorScale
dc.Image = g.img
dl.Add(dc)
}
dl.Flush(dst, &drawOptions{
Blend: drawOp.Blend,
Filter: drawOp.Filter,
ColorScaleMode: ebiten.ColorScaleModePremultipliedAlpha,
})
}
// AppendGlyphs appends glyphs to the given slice and returns a slice.

View File

@ -115,6 +115,11 @@ func adjustGranularity(x fixed.Int26_6, face Face) fixed.Int26_6 {
// Glyph represents one glyph to render.
type Glyph struct {
// Image is a rasterized glyph image.
// Image is a grayscale image i.e. RGBA values are the same.
// Image should be used as a render source and should not be modified.
img *glyphImage
// StartIndexInBytes is the start index in bytes for the given string at AppendGlyphs.
StartIndexInBytes int

View File

@ -15,6 +15,7 @@
package text_test
import (
"bytes"
"image"
"image/color"
"regexp"
@ -23,6 +24,7 @@ import (
"github.com/hajimehoshi/bitmapfont/v3"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
@ -371,3 +373,23 @@ func TestDrawOptionsNotModified(t *testing.T) {
t.Errorf("got: %v, want: %v", got, want)
}
}
func BenchmarkDrawText(b *testing.B) {
var txt string
for i := 0; i < 32; i++ {
txt += "The quick brown fox jumps over the lazy dog.\n"
}
screen := ebiten.NewImage(16, 16)
source, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
if err != nil {
b.Fatal(err)
}
f := &text.GoTextFace{
Source: source,
Size: 10,
}
op := &text.DrawOptions{}
for i := 0; i < b.N; i++ {
text.Draw(screen, txt, f, op)
}
}