diff --git a/internal/atlas/image.go b/internal/atlas/image.go index 78af38203..c93f9831f 100644 --- a/internal/atlas/image.go +++ b/internal/atlas/image.go @@ -21,6 +21,7 @@ import ( "sync" "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" @@ -33,68 +34,6 @@ var ( maxSize = 0 ) -type temporaryBytes struct { - pixels []byte - pos int - notFullyUsedTime int -} - -var ( - theTemporaryBytesSet [2]temporaryBytes - temporaryBytesIndex int -) - -func currentTemporaryBytes() *temporaryBytes { - return &theTemporaryBytesSet[temporaryBytesIndex] -} - -func switchTemporaryBytes() { - temporaryBytesIndex++ - temporaryBytesIndex %= len(theTemporaryBytesSet) -} - -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) resetAtFrameEnd() { - 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 -} - func max(a, b int) int { if a > b { return a @@ -563,13 +502,13 @@ func (i *Image) writePixels(pix []byte, region image.Rectangle) { } // Copy pixels in the case when pix is modified before the graphics command is executed. - pix2 := currentTemporaryBytes().alloc(len(pix)) + pix2 := graphicscommand.AllocBytes(len(pix)) copy(pix2, pix) i.backend.restorable.WritePixels(pix2, region) return } - pixb := currentTemporaryBytes().alloc(4 * r.Dx() * r.Dy()) + pixb := graphicscommand.AllocBytes(4 * r.Dx() * r.Dy()) // Clear the edges. pixb might not be zero-cleared. // TODO: These loops assume that paddingSize is 1. @@ -807,9 +746,6 @@ func EndFrame(graphicsDriver graphicsdriver.Graphics, swapBuffersForGL func()) e return err } - currentTemporaryBytes().resetAtFrameEnd() - switchTemporaryBytes() - for b := range theSourceBackendsForOneFrame { delete(theSourceBackendsForOneFrame, b) } diff --git a/internal/graphicscommand/bytes.go b/internal/graphicscommand/bytes.go new file mode 100644 index 000000000..1571f29d1 --- /dev/null +++ b/internal/graphicscommand/bytes.go @@ -0,0 +1,71 @@ +// 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 +} + +func AllocBytes(size int) []byte { + return currentCommandQueue().temporaryBytes.alloc(size) +} diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 7d4d757ab..aa77f1102 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -75,6 +75,8 @@ type commandQueue struct { uint32sBuffer uint32sBuffer + temporaryBytes temporaryBytes + err atomic.Value } @@ -255,6 +257,7 @@ func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bo if endFrame { q.uint32sBuffer.reset() + q.temporaryBytes.reset() } }()