mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 03:08:54 +01:00
internal/atlas: introduce a managed byte slice pool
A managed byte slice from the new byte slice pool has a function to release and put it back to the pool explicitly, and this doesn't rely on GCs. Updates #1681 Closes #2804
This commit is contained in:
parent
34d577a5ff
commit
f269b61903
@ -482,39 +482,38 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
|
||||
}
|
||||
|
||||
// Copy pixels in the case when pix is modified before the graphics command is executed.
|
||||
// TODO: Create byte slices from a pool.
|
||||
pix2 := make([]byte, len(pix))
|
||||
copy(pix2, pix)
|
||||
pix2 := graphics.NewManagedBytes(len(pix), func(bs []byte) {
|
||||
copy(bs, pix)
|
||||
})
|
||||
i.backend.restorable.WritePixels(pix2, region)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Create byte slices from a pool.
|
||||
pixb := make([]byte, 4*r.Dx()*r.Dy())
|
||||
|
||||
// Clear the edges. pixb might not be zero-cleared.
|
||||
// TODO: These loops assume that paddingSize is 1.
|
||||
// TODO: Is clearing edges explicitly really needed?
|
||||
const paddingSize = 1
|
||||
if paddingSize != i.paddingSize() {
|
||||
panic(fmt.Sprintf("atlas: writePixels assumes the padding is always 1 but the actual padding was %d", i.paddingSize()))
|
||||
}
|
||||
rowPixels := 4 * r.Dx()
|
||||
for i := 0; i < rowPixels; i++ {
|
||||
pixb[rowPixels*(r.Dy()-1)+i] = 0
|
||||
}
|
||||
for j := 1; j < r.Dy(); j++ {
|
||||
pixb[rowPixels*j-4] = 0
|
||||
pixb[rowPixels*j-3] = 0
|
||||
pixb[rowPixels*j-2] = 0
|
||||
pixb[rowPixels*j-1] = 0
|
||||
}
|
||||
|
||||
// Copy the content.
|
||||
for j := 0; j < region.Dy(); j++ {
|
||||
copy(pixb[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()])
|
||||
}
|
||||
pixb := graphics.NewManagedBytes(4*r.Dx()*r.Dy(), func(bs []byte) {
|
||||
// Clear the edges. bs might not be zero-cleared.
|
||||
rowPixels := 4 * r.Dx()
|
||||
for i := 0; i < rowPixels; i++ {
|
||||
bs[rowPixels*(r.Dy()-1)+i] = 0
|
||||
}
|
||||
for j := 1; j < r.Dy(); j++ {
|
||||
bs[rowPixels*j-4] = 0
|
||||
bs[rowPixels*j-3] = 0
|
||||
bs[rowPixels*j-2] = 0
|
||||
bs[rowPixels*j-1] = 0
|
||||
}
|
||||
|
||||
// Copy the content.
|
||||
for j := 0; j < region.Dy(); j++ {
|
||||
copy(bs[4*j*r.Dx():], pix[4*j*region.Dx():4*(j+1)*region.Dx()])
|
||||
}
|
||||
})
|
||||
i.backend.restorable.WritePixels(pixb, r)
|
||||
}
|
||||
|
||||
|
134
internal/graphics/bytes.go
Normal file
134
internal/graphics/bytes.go
Normal file
@ -0,0 +1,134 @@
|
||||
// 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 graphics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ManagedBytes is a managed byte slice.
|
||||
// The internal byte alice are managed in a pool.
|
||||
// ManagedBytes is useful when its lifetime is explicit, as the underlying byte slice can be reused for another ManagedBytes later.
|
||||
// This can redduce allocations and GCs.
|
||||
type ManagedBytes struct {
|
||||
bytes []byte
|
||||
pool *bytesPool
|
||||
}
|
||||
|
||||
// Len returns the length of the slice.
|
||||
func (m *ManagedBytes) Len() int {
|
||||
return len(m.bytes)
|
||||
}
|
||||
|
||||
// Read reads the byte slice's content to dst.
|
||||
func (m *ManagedBytes) Read(dst []byte, from, to int) {
|
||||
copy(dst, m.bytes[from:to])
|
||||
}
|
||||
|
||||
// Clone creates a new ManagedBytes with the same content.
|
||||
func (m *ManagedBytes) Clone() *ManagedBytes {
|
||||
return NewManagedBytes(len(m.bytes), func(bs []byte) {
|
||||
copy(bs, m.bytes)
|
||||
})
|
||||
}
|
||||
|
||||
// GetAndRelease returns the raw byte slice and a finalizer.
|
||||
// A finalizer should be called when you can ensure that the slice is no longer used,
|
||||
// e.g. when a graphics command using this slice is sent and executed.
|
||||
//
|
||||
// After GetAndRelease is called, the underlying byte slice is no longer available.
|
||||
func (m *ManagedBytes) GetAndRelease() ([]byte, func()) {
|
||||
bs := m.bytes
|
||||
m.bytes = nil
|
||||
return bs, func() {
|
||||
m.pool.put(bs)
|
||||
runtime.SetFinalizer(m, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// NewManagedBytes returns a managed byte slice initialized by the given constructor f.
|
||||
//
|
||||
// The byte slice is not zero-cleared at the constructor.
|
||||
func NewManagedBytes(size int, f func([]byte)) *ManagedBytes {
|
||||
bs := theBytesPool.get(size)
|
||||
f(bs.bytes)
|
||||
return bs
|
||||
}
|
||||
|
||||
type bytesPool struct {
|
||||
pool [][]byte
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
var theBytesPool bytesPool
|
||||
|
||||
func (b *bytesPool) get(size int) *ManagedBytes {
|
||||
bs := b.getFromCache(size)
|
||||
if bs == nil {
|
||||
bs = make([]byte, size)
|
||||
}
|
||||
m := &ManagedBytes{
|
||||
bytes: bs,
|
||||
pool: b,
|
||||
}
|
||||
runtime.SetFinalizer(m, func(m *ManagedBytes) {
|
||||
b.put(m.bytes)
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func (b *bytesPool) getFromCache(size int) []byte {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
|
||||
for i, bs := range b.pool {
|
||||
if cap(bs) < size {
|
||||
continue
|
||||
}
|
||||
|
||||
copy(b.pool[i:], b.pool[i+1:])
|
||||
b.pool[len(b.pool)-1] = nil
|
||||
b.pool = b.pool[:len(b.pool)-1]
|
||||
return bs[:size]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bytesPool) put(bs []byte) {
|
||||
if len(bs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
|
||||
b.pool = append(b.pool, bs)
|
||||
|
||||
// GC the pool. The size limitation is arbitrary.
|
||||
for len(b.pool) >= 32 || b.totalSize() >= 1024*1024*1024 {
|
||||
b.pool = b.pool[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bytesPool) totalSize() int {
|
||||
var s int
|
||||
for _, bs := range b.pool {
|
||||
s += len(bs)
|
||||
}
|
||||
return s
|
||||
}
|
@ -16,6 +16,7 @@ package graphicscommand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
@ -33,7 +34,7 @@ import (
|
||||
type command interface {
|
||||
fmt.Stringer
|
||||
|
||||
Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error
|
||||
Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error
|
||||
NeedsSync() bool
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ func (c *drawTrianglesCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes the drawTrianglesCommand.
|
||||
func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *drawTrianglesCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
// TODO: Is it ok not to bind any framebuffer here?
|
||||
if len(c.dstRegions) == 0 {
|
||||
return nil
|
||||
@ -211,7 +212,12 @@ func mightOverlapDstRegions(vertices1, vertices2 []float32) bool {
|
||||
// writePixelsCommand represents a command to replace pixels of an image.
|
||||
type writePixelsCommand struct {
|
||||
dst *Image
|
||||
args []graphicsdriver.PixelsArgs
|
||||
args []writePixelsCommandArgs
|
||||
}
|
||||
|
||||
type writePixelsCommandArgs struct {
|
||||
pixels *graphics.ManagedBytes
|
||||
region image.Rectangle
|
||||
}
|
||||
|
||||
func (c *writePixelsCommand) String() string {
|
||||
@ -219,11 +225,24 @@ func (c *writePixelsCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes the writePixelsCommand.
|
||||
func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *writePixelsCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
if len(c.args) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := c.dst.image.WritePixels(c.args); err != nil {
|
||||
args := make([]graphicsdriver.PixelsArgs, 0, len(c.args))
|
||||
for _, a := range c.args {
|
||||
pix, f := a.pixels.GetAndRelease()
|
||||
// A finalizer is executed when flushing the queue at the end of the frame.
|
||||
// At the end of the frame, the last command is rendering triangles onto the screen,
|
||||
// so the bytes are already sent to GPU and synced.
|
||||
// TODO: This might be fragile. When is the better time to call finalizers by a command queue?
|
||||
commandQueue.addFinalizer(f)
|
||||
args = append(args, graphicsdriver.PixelsArgs{
|
||||
Pixels: pix,
|
||||
Region: a.region,
|
||||
})
|
||||
}
|
||||
if err := c.dst.image.WritePixels(args); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -239,7 +258,7 @@ type readPixelsCommand struct {
|
||||
}
|
||||
|
||||
// Exec executes a readPixelsCommand.
|
||||
func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *readPixelsCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
if err := c.img.image.ReadPixels(c.args); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -264,7 +283,7 @@ func (c *disposeImageCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes the disposeImageCommand.
|
||||
func (c *disposeImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *disposeImageCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
c.target.image.Dispose()
|
||||
return nil
|
||||
}
|
||||
@ -283,7 +302,7 @@ func (c *disposeShaderCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes the disposeShaderCommand.
|
||||
func (c *disposeShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *disposeShaderCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
c.target.shader.Dispose()
|
||||
return nil
|
||||
}
|
||||
@ -305,7 +324,7 @@ func (c *newImageCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes a newImageCommand.
|
||||
func (c *newImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *newImageCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
var err error
|
||||
if c.screen {
|
||||
c.result.image, err = graphicsDriver.NewScreenFramebufferImage(c.width, c.height)
|
||||
@ -330,7 +349,7 @@ func (c *newShaderCommand) String() string {
|
||||
}
|
||||
|
||||
// Exec executes a newShaderCommand.
|
||||
func (c *newShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *newShaderCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
s, err := graphicsDriver.NewShader(c.ir)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -352,7 +371,7 @@ func (c *isInvalidatedCommand) String() string {
|
||||
return fmt.Sprintf("is-invalidated: image: %d", c.image.id)
|
||||
}
|
||||
|
||||
func (c *isInvalidatedCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
func (c *isInvalidatedCommand) Exec(commandQueue *commandQueue, graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
|
||||
c.result = c.image.image.IsInvalidated()
|
||||
return nil
|
||||
}
|
||||
|
@ -50,10 +50,17 @@ type commandQueue struct {
|
||||
drawTrianglesCommandPool drawTrianglesCommandPool
|
||||
|
||||
uint32sBuffer uint32sBuffer
|
||||
finalizers []func()
|
||||
|
||||
err atomic.Value
|
||||
}
|
||||
|
||||
// addFinalizer adds a finalizer function to this queue.
|
||||
// A finalizer is executed when the command queue is flushed at the end of the frame.
|
||||
func (q *commandQueue) addFinalizer(f func()) {
|
||||
q.finalizers = append(q.finalizers, f)
|
||||
}
|
||||
|
||||
func (q *commandQueue) appendIndices(indices []uint16, offset uint16) {
|
||||
n := len(q.indices)
|
||||
q.indices = append(q.indices, indices...)
|
||||
@ -220,6 +227,11 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
|
||||
|
||||
if endFrame {
|
||||
q.uint32sBuffer.reset()
|
||||
for i, f := range q.finalizers {
|
||||
f()
|
||||
q.finalizers[i] = nil
|
||||
}
|
||||
q.finalizers = q.finalizers[:0]
|
||||
}
|
||||
}()
|
||||
|
||||
@ -247,7 +259,7 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
|
||||
}
|
||||
indexOffset := 0
|
||||
for _, c := range cs[:nc] {
|
||||
if err := c.Exec(graphicsDriver, indexOffset); err != nil {
|
||||
if err := c.Exec(q, graphicsDriver, indexOffset); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Logf(" %s\n", c)
|
||||
|
@ -43,7 +43,7 @@ type Image struct {
|
||||
// have its graphicsdriver.Image.
|
||||
id int
|
||||
|
||||
bufferedWritePixelsArgs []graphicsdriver.PixelsArgs
|
||||
bufferedWritePixelsArgs []writePixelsCommandArgs
|
||||
}
|
||||
|
||||
var nextID = 1
|
||||
@ -78,6 +78,7 @@ func (i *Image) flushBufferedWritePixels() {
|
||||
if len(i.bufferedWritePixelsArgs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
c := &writePixelsCommand{
|
||||
dst: i,
|
||||
args: i.bufferedWritePixelsArgs,
|
||||
@ -159,10 +160,10 @@ func (i *Image) ReadPixels(graphicsDriver graphicsdriver.Graphics, args []graphi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Image) WritePixels(pixels []byte, region image.Rectangle) {
|
||||
i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs, graphicsdriver.PixelsArgs{
|
||||
Pixels: pixels,
|
||||
Region: region,
|
||||
func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) {
|
||||
i.bufferedWritePixelsArgs = append(i.bufferedWritePixelsArgs, writePixelsCommandArgs{
|
||||
pixels: pixels,
|
||||
region: region,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,12 @@ func TestWritePixelsPartAfterDrawTriangles(t *testing.T) {
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{clr}, vs, is, graphicsdriver.BlendClear, dr, [graphics.ShaderImageCount]image.Rectangle{}, nearestFilterShader, nil, false)
|
||||
dst.DrawTriangles([graphics.ShaderImageCount]*graphicscommand.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, nearestFilterShader, nil, false)
|
||||
dst.WritePixels(make([]byte, 4), image.Rect(0, 0, 1, 1))
|
||||
bs := graphics.NewManagedBytes(4, func(bs []byte) {
|
||||
for i := range bs {
|
||||
bs[i] = 0
|
||||
}
|
||||
})
|
||||
dst.WritePixels(bs, image.Rect(0, 0, 1, 1))
|
||||
|
||||
// TODO: Check the result.
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ func (p *Pixels) Apply(img *graphicscommand.Image) {
|
||||
p.pixelsRecords.apply(img)
|
||||
}
|
||||
|
||||
func (p *Pixels) AddOrReplace(pix []byte, region image.Rectangle) {
|
||||
func (p *Pixels) AddOrReplace(pix *graphics.ManagedBytes, region image.Rectangle) {
|
||||
if p.pixelsRecords == nil {
|
||||
p.pixelsRecords = &pixelsRecords{}
|
||||
}
|
||||
@ -70,6 +70,13 @@ func (p *Pixels) AppendRegion(regions []image.Rectangle) []image.Rectangle {
|
||||
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.ShaderImageCount]*Image
|
||||
@ -251,7 +258,7 @@ func (i *Image) needsRestoring() bool {
|
||||
// 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 []byte, region image.Rectangle) {
|
||||
func (i *Image) WritePixels(pixels *graphics.ManagedBytes, region image.Rectangle) {
|
||||
if region.Dx() <= 0 || region.Dy() <= 0 {
|
||||
panic("restorable: width/height must be positive")
|
||||
}
|
||||
@ -278,7 +285,8 @@ func (i *Image) WritePixels(pixels []byte, region image.Rectangle) {
|
||||
|
||||
if region.Eq(image.Rect(0, 0, w, h)) {
|
||||
if pixels != nil {
|
||||
i.basePixels.AddOrReplace(pixels, image.Rect(0, 0, w, h))
|
||||
// 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))
|
||||
}
|
||||
@ -295,7 +303,8 @@ func (i *Image) WritePixels(pixels []byte, region image.Rectangle) {
|
||||
}
|
||||
|
||||
if pixels != nil {
|
||||
i.basePixels.AddOrReplace(pixels, region)
|
||||
// Clone a ManagedBytes as the package graphicscommand has a different lifetime management.
|
||||
i.basePixels.AddOrReplace(pixels.Clone(), region)
|
||||
} else {
|
||||
i.basePixels.Clear(region)
|
||||
}
|
||||
@ -483,7 +492,10 @@ func (i *Image) readPixelsFromGPU(graphicsDriver graphicsdriver.Graphics) error
|
||||
}
|
||||
|
||||
for _, a := range args {
|
||||
i.basePixels.AddOrReplace(a.Pixels, a.Region)
|
||||
bs := graphics.NewManagedBytes(len(a.Pixels), func(bs []byte) {
|
||||
copy(bs, a.Pixels)
|
||||
})
|
||||
i.basePixels.AddOrReplace(bs, a.Region)
|
||||
}
|
||||
|
||||
i.clearDrawTrianglesHistory()
|
||||
@ -563,6 +575,7 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
|
||||
// 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
|
||||
@ -633,7 +646,10 @@ func (i *Image) restore(graphicsDriver graphicsdriver.Graphics) error {
|
||||
}
|
||||
|
||||
for _, a := range args {
|
||||
i.basePixels.AddOrReplace(a.Pixels, a.Region)
|
||||
bs := graphics.NewManagedBytes(len(a.Pixels), func(bs []byte) {
|
||||
copy(bs, a.Pixels)
|
||||
})
|
||||
i.basePixels.AddOrReplace(bs, a.Region)
|
||||
}
|
||||
}
|
||||
|
||||
@ -651,6 +667,7 @@ func (i *Image) Dispose() {
|
||||
theImages.remove(i)
|
||||
i.image.Dispose()
|
||||
i.image = nil
|
||||
i.basePixels.Dispose()
|
||||
i.basePixels = Pixels{}
|
||||
i.pixelsCache = nil
|
||||
i.clearDrawTrianglesHistory()
|
||||
|
@ -37,6 +37,15 @@ func pixelsToColor(p *restorable.Pixels, i, j, imageWidth, imageHeight int) colo
|
||||
return color.RGBA{R: pix[0], G: pix[1], B: pix[2], A: pix[3]}
|
||||
}
|
||||
|
||||
func bytesToManagedBytes(src []byte) *graphics.ManagedBytes {
|
||||
if len(src) == 0 {
|
||||
panic("restorable: len(src) must be > 0")
|
||||
}
|
||||
return graphics.NewManagedBytes(len(src), func(dst []byte) {
|
||||
copy(dst, src)
|
||||
})
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
@ -61,7 +70,7 @@ func TestRestore(t *testing.T) {
|
||||
defer img0.Dispose()
|
||||
|
||||
clr0 := color.RGBA{A: 0xff}
|
||||
img0.WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, 1, 1))
|
||||
img0.WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, 1, 1))
|
||||
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -129,7 +138,7 @@ func TestRestoreChain(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
clr := color.RGBA{A: 0xff}
|
||||
imgs[0].WritePixels([]byte{clr.R, clr.G, clr.B, clr.A}, image.Rect(0, 0, 1, 1))
|
||||
imgs[0].WritePixels(bytesToManagedBytes([]byte{clr.R, clr.G, clr.B, clr.A}), image.Rect(0, 0, 1, 1))
|
||||
for i := 0; i < num-1; i++ {
|
||||
vs := quadVertices(1, 1, 0, 0)
|
||||
is := graphics.QuadIndices()
|
||||
@ -169,11 +178,11 @@ func TestRestoreChain2(t *testing.T) {
|
||||
}()
|
||||
|
||||
clr0 := color.RGBA{R: 0xff, A: 0xff}
|
||||
imgs[0].WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, w, h))
|
||||
imgs[0].WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, w, h))
|
||||
clr7 := color.RGBA{G: 0xff, A: 0xff}
|
||||
imgs[7].WritePixels([]byte{clr7.R, clr7.G, clr7.B, clr7.A}, image.Rect(0, 0, w, h))
|
||||
imgs[7].WritePixels(bytesToManagedBytes([]byte{clr7.R, clr7.G, clr7.B, clr7.A}), image.Rect(0, 0, w, h))
|
||||
clr8 := color.RGBA{B: 0xff, A: 0xff}
|
||||
imgs[8].WritePixels([]byte{clr8.R, clr8.G, clr8.B, clr8.A}, image.Rect(0, 0, w, h))
|
||||
imgs[8].WritePixels(bytesToManagedBytes([]byte{clr8.R, clr8.G, clr8.B, clr8.A}), image.Rect(0, 0, w, h))
|
||||
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
@ -218,12 +227,12 @@ func TestRestoreOverrideSource(t *testing.T) {
|
||||
}()
|
||||
clr0 := color.RGBA{A: 0xff}
|
||||
clr1 := color.RGBA{B: 0x01, A: 0xff}
|
||||
img1.WritePixels([]byte{clr0.R, clr0.G, clr0.B, clr0.A}, image.Rect(0, 0, w, h))
|
||||
img1.WritePixels(bytesToManagedBytes([]byte{clr0.R, clr0.G, clr0.B, clr0.A}), image.Rect(0, 0, w, h))
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
img2.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img1}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
img3.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img2}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
img0.WritePixels([]byte{clr1.R, clr1.G, clr1.B, clr1.A}, image.Rect(0, 0, w, h))
|
||||
img0.WritePixels(bytesToManagedBytes([]byte{clr1.R, clr1.G, clr1.B, clr1.A}), image.Rect(0, 0, w, h))
|
||||
img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, quadVertices(w, h, 0, 0), is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -390,7 +399,7 @@ func TestRestoreComplexGraph(t *testing.T) {
|
||||
func newImageFromImage(rgba *image.RGBA) *restorable.Image {
|
||||
s := rgba.Bounds().Size()
|
||||
img := restorable.NewImage(s.X, s.Y, restorable.ImageTypeRegular)
|
||||
img.WritePixels(rgba.Pix, image.Rect(0, 0, s.X, s.Y))
|
||||
img.WritePixels(bytesToManagedBytes(rgba.Pix), image.Rect(0, 0, s.X, s.Y))
|
||||
return img
|
||||
}
|
||||
|
||||
@ -459,7 +468,7 @@ func TestWritePixels(t *testing.T) {
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
img.WritePixels(pix, image.Rect(5, 7, 9, 11))
|
||||
img.WritePixels(bytesToManagedBytes(pix), image.Rect(5, 7, 9, 11))
|
||||
// Check the region (5, 7)-(9, 11). Outside state is indeterminate.
|
||||
pix = make([]byte, 4*4*4)
|
||||
for i := range pix {
|
||||
@ -514,7 +523,7 @@ func TestDrawTrianglesAndWritePixels(t *testing.T) {
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, 2, 1)
|
||||
img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
img1.WritePixels([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, image.Rect(0, 0, 2, 1))
|
||||
img1.WritePixels(bytesToManagedBytes([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}), image.Rect(0, 0, 2, 1))
|
||||
|
||||
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -580,7 +589,7 @@ func TestWritePixelsPart(t *testing.T) {
|
||||
|
||||
img := restorable.NewImage(4, 4, restorable.ImageTypeRegular)
|
||||
// This doesn't make the image stale. Its base pixels are available.
|
||||
img.WritePixels(pix, image.Rect(1, 1, 3, 3))
|
||||
img.WritePixels(bytesToManagedBytes(pix), image.Rect(1, 1, 3, 3))
|
||||
|
||||
cases := []struct {
|
||||
i int
|
||||
@ -655,14 +664,14 @@ func TestWritePixelsOnly(t *testing.T) {
|
||||
defer img1.Dispose()
|
||||
|
||||
for i := 0; i < w*h; i += 5 {
|
||||
img0.WritePixels([]byte{1, 2, 3, 4}, image.Rect(i%w, i/w, i%w+1, i/w+1))
|
||||
img0.WritePixels(bytesToManagedBytes([]byte{1, 2, 3, 4}), image.Rect(i%w, i/w, i%w+1, i/w+1))
|
||||
}
|
||||
|
||||
vs := quadVertices(1, 1, 0, 0)
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, 1, 1)
|
||||
img1.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{img0}, vs, is, graphicsdriver.BlendCopy, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
img0.WritePixels([]byte{5, 6, 7, 8}, image.Rect(0, 0, 1, 1))
|
||||
img0.WritePixels(bytesToManagedBytes([]byte{5, 6, 7, 8}), image.Rect(0, 0, 1, 1))
|
||||
|
||||
// BasePixelsForTesting is available without GPU accessing.
|
||||
for j := 0; j < h; j++ {
|
||||
@ -704,14 +713,14 @@ func TestReadPixelsFromVolatileImage(t *testing.T) {
|
||||
src := restorable.NewImage(w, h, restorable.ImageTypeRegular)
|
||||
|
||||
// First, make sure that dst has pixels
|
||||
dst.WritePixels(make([]byte, 4*w*h), image.Rect(0, 0, w, h))
|
||||
dst.WritePixels(bytesToManagedBytes(make([]byte, 4*w*h)), image.Rect(0, 0, w, h))
|
||||
|
||||
// Second, draw src to dst. If the implementation is correct, dst becomes stale.
|
||||
pix := make([]byte, 4*w*h)
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
src.WritePixels(pix, image.Rect(0, 0, w, h))
|
||||
src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h))
|
||||
vs := quadVertices(1, 1, 0, 0)
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
@ -740,7 +749,7 @@ func TestAllowWritePixelsAfterDrawTriangles(t *testing.T) {
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
dst.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
dst.WritePixels(make([]byte, 4*w*h), image.Rect(0, 0, w, h))
|
||||
dst.WritePixels(bytesToManagedBytes(make([]byte, 4*w*h)), image.Rect(0, 0, w, h))
|
||||
// WritePixels for a whole image doesn't panic.
|
||||
}
|
||||
|
||||
@ -753,13 +762,13 @@ func TestAllowWritePixelsForPartAfterDrawTriangles(t *testing.T) {
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
src.WritePixels(pix, image.Rect(0, 0, w, h))
|
||||
src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h))
|
||||
|
||||
vs := quadVertices(w, h, 0, 0)
|
||||
is := graphics.QuadIndices()
|
||||
dr := image.Rect(0, 0, w, h)
|
||||
dst.DrawTriangles([graphics.ShaderImageCount]*restorable.Image{src}, vs, is, graphicsdriver.BlendSourceOver, dr, [graphics.ShaderImageCount]image.Rectangle{}, restorable.NearestFilterShader, nil, false)
|
||||
dst.WritePixels(make([]byte, 4*2*2), image.Rect(0, 0, 2, 2))
|
||||
dst.WritePixels(bytesToManagedBytes(make([]byte, 4*2*2)), image.Rect(0, 0, 2, 2))
|
||||
// WritePixels for a part of image doesn't panic.
|
||||
|
||||
if err := restorable.ResolveStaleImages(ui.GraphicsDriverForTesting()); err != nil {
|
||||
@ -806,7 +815,7 @@ func TestExtend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
orig.WritePixels(pix, image.Rect(0, 0, w, h))
|
||||
orig.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h))
|
||||
extended := orig.Extend(w*2, h*2) // After this, orig is already disposed.
|
||||
|
||||
result := make([]byte, 4*(w*2)*(h*2))
|
||||
@ -846,7 +855,7 @@ func TestDrawTrianglesAndExtend(t *testing.T) {
|
||||
pix[4*idx+3] = v
|
||||
}
|
||||
}
|
||||
src.WritePixels(pix, image.Rect(0, 0, w, h))
|
||||
src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h))
|
||||
|
||||
orig := restorable.NewImage(w, h, restorable.ImageTypeRegular)
|
||||
vs := quadVertices(w, h, 0, 0)
|
||||
@ -876,13 +885,13 @@ func TestDrawTrianglesAndExtend(t *testing.T) {
|
||||
func TestClearPixels(t *testing.T) {
|
||||
const w, h = 16, 16
|
||||
img := restorable.NewImage(w, h, restorable.ImageTypeRegular)
|
||||
img.WritePixels(make([]byte, 4*4*4), image.Rect(0, 0, 4, 4))
|
||||
img.WritePixels(make([]byte, 4*4*4), image.Rect(4, 0, 8, 4))
|
||||
img.WritePixels(bytesToManagedBytes(make([]byte, 4*4*4)), image.Rect(0, 0, 4, 4))
|
||||
img.WritePixels(bytesToManagedBytes(make([]byte, 4*4*4)), image.Rect(4, 0, 8, 4))
|
||||
img.ClearPixels(image.Rect(0, 0, 4, 4))
|
||||
img.ClearPixels(image.Rect(4, 0, 8, 4))
|
||||
|
||||
// After clearing, the regions will be available again.
|
||||
img.WritePixels(make([]byte, 4*8*4), image.Rect(0, 0, 8, 4))
|
||||
img.WritePixels(bytesToManagedBytes(make([]byte, 4*8*4)), image.Rect(0, 0, 8, 4))
|
||||
}
|
||||
|
||||
func TestMutateSlices(t *testing.T) {
|
||||
@ -896,7 +905,7 @@ func TestMutateSlices(t *testing.T) {
|
||||
pix[4*i+2] = byte(i)
|
||||
pix[4*i+3] = 0xff
|
||||
}
|
||||
src.WritePixels(pix, image.Rect(0, 0, w, h))
|
||||
src.WritePixels(bytesToManagedBytes(pix), image.Rect(0, 0, w, h))
|
||||
|
||||
vs := quadVertices(w, h, 0, 0)
|
||||
is := make([]uint16, len(graphics.QuadIndices()))
|
||||
@ -950,7 +959,7 @@ func TestOverlappedPixels(t *testing.T) {
|
||||
pix0[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
dst.WritePixels(pix0, image.Rect(0, 0, 2, 2))
|
||||
dst.WritePixels(bytesToManagedBytes(pix0), image.Rect(0, 0, 2, 2))
|
||||
|
||||
pix1 := make([]byte, 4*2*2)
|
||||
for j := 0; j < 2; j++ {
|
||||
@ -962,7 +971,7 @@ func TestOverlappedPixels(t *testing.T) {
|
||||
pix1[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
dst.WritePixels(pix1, image.Rect(1, 1, 3, 3))
|
||||
dst.WritePixels(bytesToManagedBytes(pix1), image.Rect(1, 1, 3, 3))
|
||||
|
||||
wantColors := []color.RGBA{
|
||||
{0xff, 0, 0, 0xff},
|
||||
@ -1032,7 +1041,7 @@ func TestOverlappedPixels(t *testing.T) {
|
||||
pix2[idx+3] = 0xff
|
||||
}
|
||||
}
|
||||
dst.WritePixels(pix2, image.Rect(1, 1, 3, 3))
|
||||
dst.WritePixels(bytesToManagedBytes(pix2), image.Rect(1, 1, 3, 3))
|
||||
|
||||
wantColors = []color.RGBA{
|
||||
{0xff, 0, 0, 0xff},
|
||||
@ -1089,7 +1098,7 @@ func TestDrawTrianglesAndReadPixels(t *testing.T) {
|
||||
src := restorable.NewImage(w, h, restorable.ImageTypeRegular)
|
||||
dst := restorable.NewImage(w, h, restorable.ImageTypeRegular)
|
||||
|
||||
src.WritePixels([]byte{0x80, 0x80, 0x80, 0x80}, image.Rect(0, 0, 1, 1))
|
||||
src.WritePixels(bytesToManagedBytes([]byte{0x80, 0x80, 0x80, 0x80}), image.Rect(0, 0, 1, 1))
|
||||
|
||||
vs := quadVertices(w, h, 0, 0)
|
||||
is := graphics.QuadIndices()
|
||||
@ -1109,10 +1118,10 @@ func TestWritePixelsAndDrawTriangles(t *testing.T) {
|
||||
src := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
|
||||
dst := restorable.NewImage(2, 1, restorable.ImageTypeRegular)
|
||||
|
||||
src.WritePixels([]byte{0x80, 0x80, 0x80, 0x80}, image.Rect(0, 0, 1, 1))
|
||||
src.WritePixels(bytesToManagedBytes([]byte{0x80, 0x80, 0x80, 0x80}), image.Rect(0, 0, 1, 1))
|
||||
|
||||
// Call WritePixels first.
|
||||
dst.WritePixels([]byte{0x40, 0x40, 0x40, 0x40}, image.Rect(0, 0, 1, 1))
|
||||
dst.WritePixels(bytesToManagedBytes([]byte{0x40, 0x40, 0x40, 0x40}), image.Rect(0, 0, 1, 1))
|
||||
|
||||
// Call DrawTriangles at a different region second.
|
||||
vs := quadVertices(1, 1, 1, 0)
|
||||
|
@ -18,12 +18,13 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
|
||||
)
|
||||
|
||||
type pixelsRecord struct {
|
||||
rect image.Rectangle
|
||||
pix []byte
|
||||
pix *graphics.ManagedBytes
|
||||
}
|
||||
|
||||
func (p *pixelsRecord) readPixels(pixels []byte, region image.Rectangle, imageWidth, imageHeight int) {
|
||||
@ -41,7 +42,7 @@ func (p *pixelsRecord) readPixels(pixels []byte, region image.Rectangle, imageWi
|
||||
for j := 0; j < r.Dy(); j++ {
|
||||
dstX := 4 * ((dstBaseY+j)*region.Dx() + dstBaseX)
|
||||
srcX := 4 * ((srcBaseY+j)*p.rect.Dx() + srcBaseX)
|
||||
copy(pixels[dstX:dstX+lineWidth], p.pix[srcX:srcX+lineWidth])
|
||||
p.pix.Read(pixels[dstX:dstX+lineWidth], srcX, srcX+lineWidth)
|
||||
}
|
||||
} else {
|
||||
for j := 0; j < r.Dy(); j++ {
|
||||
@ -57,9 +58,9 @@ type pixelsRecords struct {
|
||||
records []*pixelsRecord
|
||||
}
|
||||
|
||||
func (pr *pixelsRecords) addOrReplace(pixels []byte, region image.Rectangle) {
|
||||
if len(pixels) != 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(), len(pixels))
|
||||
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)"
|
||||
}
|
||||
@ -128,7 +129,8 @@ 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 {
|
||||
img.WritePixels(r.pix, r.rect)
|
||||
// Clone a ManagedBytes as the package graphicscommand has a different lifetime management.
|
||||
img.WritePixels(r.pix.Clone(), r.rect)
|
||||
} else {
|
||||
clearImage(img, r.rect)
|
||||
}
|
||||
@ -144,3 +146,14 @@ func (pr *pixelsRecords) appendRegions(regions []image.Rectangle) []image.Rectan
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func TestShaderChain(t *testing.T) {
|
||||
imgs = append(imgs, img)
|
||||
}
|
||||
|
||||
imgs[0].WritePixels([]byte{0xff, 0, 0, 0xff}, image.Rect(0, 0, 1, 1))
|
||||
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++ {
|
||||
@ -109,9 +109,9 @@ func TestShaderMultipleSources(t *testing.T) {
|
||||
for i := range srcs {
|
||||
srcs[i] = restorable.NewImage(1, 1, restorable.ImageTypeRegular)
|
||||
}
|
||||
srcs[0].WritePixels([]byte{0x40, 0, 0, 0xff}, image.Rect(0, 0, 1, 1))
|
||||
srcs[1].WritePixels([]byte{0, 0x80, 0, 0xff}, image.Rect(0, 0, 1, 1))
|
||||
srcs[2].WritePixels([]byte{0, 0, 0xc0, 0xff}, image.Rect(0, 0, 1, 1))
|
||||
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)
|
||||
|
||||
@ -138,11 +138,11 @@ func TestShaderMultipleSources(t *testing.T) {
|
||||
|
||||
func TestShaderMultipleSourcesOnOneTexture(t *testing.T) {
|
||||
src := restorable.NewImage(3, 1, restorable.ImageTypeRegular)
|
||||
src.WritePixels([]byte{
|
||||
src.WritePixels(bytesToManagedBytes([]byte{
|
||||
0x40, 0, 0, 0xff,
|
||||
0, 0x80, 0, 0xff,
|
||||
0, 0, 0xc0, 0xff,
|
||||
}, image.Rect(0, 0, 3, 1))
|
||||
}), image.Rect(0, 0, 3, 1))
|
||||
srcs := [graphics.ShaderImageCount]*restorable.Image{src, src, src}
|
||||
|
||||
dst := restorable.NewImage(1, 1, restorable.ImageTypeRegular)
|
||||
|
Loading…
Reference in New Issue
Block a user