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:
Hajime Hoshi 2023-10-08 22:59:35 +09:00
parent 34d577a5ff
commit f269b61903
10 changed files with 296 additions and 87 deletions

View File

@ -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
View 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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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,
})
}

View File

@ -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.
}

View File

@ -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()

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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)