internal/restorable: use clearImage to avoid allocations

Bytes from a pool in a command queue is now pretty hard to use correctly
as the lifetime of a queue is not clear.

Remove the byte pools once. Let's reconsider pool usages later.

This change also removes imagesWithBuffers as this is no longer needed.
imagesWithBuffers was necessary to ensure all the bytes from the pool
of the command queue was used before the queue flushes the commands,
as the command queue cleared the pool after flushing. The lifetimes
were pretty ticky.
This commit is contained in:
Hajime Hoshi 2023-10-09 00:42:58 +09:00
parent cc8cf688f4
commit 6e5361c328
4 changed files with 4 additions and 119 deletions

View File

@ -22,7 +22,6 @@ import (
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/packing" "github.com/hajimehoshi/ebiten/v2/internal/packing"
"github.com/hajimehoshi/ebiten/v2/internal/restorable" "github.com/hajimehoshi/ebiten/v2/internal/restorable"
@ -483,13 +482,15 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) {
} }
// Copy pixels in the case when pix is modified before the graphics command is executed. // Copy pixels in the case when pix is modified before the graphics command is executed.
pix2 := graphicscommand.AllocBytes(len(pix)) // TODO: Create byte slices from a pool.
pix2 := make([]byte, len(pix))
copy(pix2, pix) copy(pix2, pix)
i.backend.restorable.WritePixels(pix2, region) i.backend.restorable.WritePixels(pix2, region)
return return
} }
pixb := graphicscommand.AllocBytes(4 * r.Dx() * r.Dy()) // TODO: Create byte slices from a pool.
pixb := make([]byte, 4*r.Dx()*r.Dy())
// Clear the edges. pixb might not be zero-cleared. // Clear the edges. pixb might not be zero-cleared.
// TODO: These loops assume that paddingSize is 1. // TODO: These loops assume that paddingSize is 1.

View File

@ -1,75 +0,0 @@
// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package graphicscommand
type temporaryBytes struct {
pixels []byte
pos int
notFullyUsedTime int
}
func temporaryBytesSize(size int) int {
l := 16
for l < size {
l *= 2
}
return l
}
// alloc allocates the pixels and returns it.
//
// Be careful that the returned pixels might not be zero-cleared.
func (t *temporaryBytes) alloc(size int) []byte {
if len(t.pixels) < t.pos+size {
t.pixels = make([]byte, max(len(t.pixels)*2, temporaryBytesSize(size)))
t.pos = 0
}
pix := t.pixels[t.pos : t.pos+size]
t.pos += size
return pix
}
func (t *temporaryBytes) reset() {
// reset is called in a render thread.
// When reset is called, a queue is being flushed in a render thread, and the queue is never used in the game thread.
// Thus, a mutex lock is not needed in alloc and reset.
const maxNotFullyUsedTime = 60
if temporaryBytesSize(t.pos) < len(t.pixels) {
if t.notFullyUsedTime < maxNotFullyUsedTime {
t.notFullyUsedTime++
}
} else {
t.notFullyUsedTime = 0
}
// Let the pixels GCed if this is not used for a while.
if t.notFullyUsedTime == maxNotFullyUsedTime && len(t.pixels) > 0 {
t.pixels = nil
t.notFullyUsedTime = 0
}
// Reset the position and reuse the allocated bytes.
// t.pixels should already be sent to GPU, then this can be reused.
t.pos = 0
}
// AllocBytes allocates bytes from the cache.
//
// Be careful that the returned pixels might not be zero-cleared.
func AllocBytes(size int) []byte {
return theCommandQueueManager.allocBytes(size)
}

View File

@ -30,7 +30,6 @@ import (
// FlushCommands flushes the command queue and present the screen if needed. // FlushCommands flushes the command queue and present the screen if needed.
// If endFrame is true, the current screen might be used to present. // If endFrame is true, the current screen might be used to present.
func FlushCommands(graphicsDriver graphicsdriver.Graphics, endFrame bool, swapBuffersForGL func()) error { func FlushCommands(graphicsDriver graphicsdriver.Graphics, endFrame bool, swapBuffersForGL func()) error {
flushImageBuffers()
if err := theCommandQueueManager.flush(graphicsDriver, endFrame, swapBuffersForGL); err != nil { if err := theCommandQueueManager.flush(graphicsDriver, endFrame, swapBuffersForGL); err != nil {
return err return err
} }
@ -52,8 +51,6 @@ type commandQueue struct {
uint32sBuffer uint32sBuffer uint32sBuffer uint32sBuffer
temporaryBytes temporaryBytes
err atomic.Value err atomic.Value
} }
@ -223,7 +220,6 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
if endFrame { if endFrame {
q.uint32sBuffer.reset() q.uint32sBuffer.reset()
q.temporaryBytes.reset()
} }
}() }()
@ -441,13 +437,6 @@ type commandQueueManager struct {
var theCommandQueueManager commandQueueManager var theCommandQueueManager commandQueueManager
func (c *commandQueueManager) allocBytes(size int) []byte {
if c.current == nil {
c.current, _ = c.pool.get()
}
return c.current.temporaryBytes.alloc(size)
}
func (c *commandQueueManager) enqueueCommand(command command) { func (c *commandQueueManager) enqueueCommand(command command) {
if c.current == nil { if c.current == nil {
c.current, _ = c.pool.get() c.current, _ = c.pool.get()

View File

@ -54,31 +54,6 @@ func genNextID() int {
return id return id
} }
// imagesWithBuffers is the set of an image with buffers.
var imagesWithBuffers = map[*Image]struct{}{}
// addImageWithBuffer adds an image to the list of images with unflushed buffers.
func addImageWithBuffer(img *Image) {
imagesWithBuffers[img] = struct{}{}
}
// removeImageWithBuffer removes an image from the list of images with unflushed buffers.
func removeImageWithBuffer(img *Image) {
delete(imagesWithBuffers, img)
}
// flushImageBuffers flushes all the image buffers and send to the command queue.
// flushImageBuffers should be called before flushing commands.
func flushImageBuffers() {
for img := range imagesWithBuffers {
img.flushBufferedWritePixels()
}
if len(imagesWithBuffers) != 0 {
panic("graphicscommand: len(imagesWithBuffers) must be empty after flushing")
}
}
// NewImage returns a new image. // NewImage returns a new image.
// //
// Note that the image is not initialized yet. // Note that the image is not initialized yet.
@ -110,8 +85,6 @@ func (i *Image) flushBufferedWritePixels() {
theCommandQueueManager.enqueueCommand(c) theCommandQueueManager.enqueueCommand(c)
i.bufferedWritePixelsArgs = nil i.bufferedWritePixelsArgs = nil
removeImageWithBuffer(i)
} }
func (i *Image) Dispose() { func (i *Image) Dispose() {
@ -120,8 +93,6 @@ func (i *Image) Dispose() {
target: i, target: i,
} }
theCommandQueueManager.enqueueCommand(c) theCommandQueueManager.enqueueCommand(c)
removeImageWithBuffer(i)
} }
func (i *Image) InternalSize() (int, int) { func (i *Image) InternalSize() (int, int) {
@ -193,7 +164,6 @@ func (i *Image) WritePixels(pixels []byte, region image.Rectangle) {
Pixels: pixels, Pixels: pixels,
Region: region, Region: region,
}) })
addImageWithBuffer(i)
} }
func (i *Image) IsInvalidated(graphicsDriver graphicsdriver.Graphics) (bool, error) { func (i *Image) IsInvalidated(graphicsDriver graphicsdriver.Graphics) (bool, error) {