mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2026-04-22 15:57:15 +08:00
ebiten, vector: bug fix: race conditions
This change fixes these race conditions in * (*ebiten.Image).invokeUsageCallbacks concurrent invocations * (*ebiten.Image).usageCallbacks usages * vector.theCallbackTokens usages * vector's global shader initializations Closes #3333
This commit is contained in:
+24
-61
@@ -15,6 +15,7 @@
|
||||
package vector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
@@ -121,6 +122,8 @@ var (
|
||||
|
||||
// theAtlas manages the atlas for stencil buffer images.
|
||||
// theAtlas is a singleton to avoid unnecessary texture allocations.
|
||||
//
|
||||
// theAtlas methods are used only at fillPathsState.fillPaths, and should be protected by theFillPathM.
|
||||
var theAtlas atlas
|
||||
|
||||
type fillPathsState struct {
|
||||
@@ -147,6 +150,7 @@ func (f *fillPathsState) addPath(path *Path, clr ebiten.ColorScale) {
|
||||
if path == nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.paths = slices.Grow(f.paths, 1)[:len(f.paths)+1]
|
||||
if f.paths[len(f.paths)-1] == nil {
|
||||
f.paths[len(f.paths)-1] = &Path{}
|
||||
@@ -163,62 +167,13 @@ func (f *fillPathsState) addPath(path *Path, clr ebiten.ColorScale) {
|
||||
}
|
||||
|
||||
// fillPaths fills the specified path with the specified color.
|
||||
//
|
||||
// fillPaths callers must be protected by theFillPathM.
|
||||
func (f *fillPathsState) fillPaths(dst *ebiten.Image) {
|
||||
if len(f.paths) != len(f.colors) {
|
||||
panic("vector: the number of paths and colors must be the same")
|
||||
}
|
||||
|
||||
if stencilBufferFillShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferFillShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferFillShader = s
|
||||
}
|
||||
if stencilBufferBezierShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferBezierShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferBezierShader = s
|
||||
}
|
||||
if !f.antialias && f.fillRule == FillRuleNonZero {
|
||||
if stencilBufferNonZeroShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferNonZeroShader = s
|
||||
}
|
||||
}
|
||||
if f.antialias && f.fillRule == FillRuleNonZero {
|
||||
if stencilBufferNonZeroAAShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroAAShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferNonZeroAAShader = s
|
||||
}
|
||||
}
|
||||
if !f.antialias && f.fillRule == FillRuleEvenOdd {
|
||||
if stencilBufferEvenOddShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferEvenOddShader = s
|
||||
}
|
||||
}
|
||||
if f.antialias && f.fillRule == FillRuleEvenOdd {
|
||||
if stencilBufferEvenOddAAShader == nil {
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddAAShaderSrc))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
stencilBufferEvenOddAAShader = s
|
||||
}
|
||||
}
|
||||
|
||||
vs := f.vertices[:0]
|
||||
is := f.indices[:0]
|
||||
defer func() {
|
||||
@@ -343,7 +298,11 @@ func (f *fillPathsState) fillPaths(dst *ebiten.Image) {
|
||||
}
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Blend = ebiten.BlendLighter
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, stencilBufferFillShader, op)
|
||||
shader, err := ensureStencilBufferShaders()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer shader: %v", err))
|
||||
}
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, shader, op)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +374,11 @@ func (f *fillPathsState) fillPaths(dst *ebiten.Image) {
|
||||
}
|
||||
op := &ebiten.DrawTrianglesShaderOptions{}
|
||||
op.Blend = ebiten.BlendLighter
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, stencilBufferBezierShader, op)
|
||||
shader, err := ensureStencilBufferBezierShader()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer bezier shader: %v", err))
|
||||
}
|
||||
stencilBufferImage.DrawTrianglesShader32(vs, is, shader, op)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,16 +469,16 @@ func (f *fillPathsState) fillPaths(dst *ebiten.Image) {
|
||||
var shader *ebiten.Shader
|
||||
switch f.fillRule {
|
||||
case FillRuleNonZero:
|
||||
if f.antialias {
|
||||
shader = stencilBufferNonZeroAAShader
|
||||
} else {
|
||||
shader = stencilBufferNonZeroShader
|
||||
var err error
|
||||
shader, err = ensureStencilBufferNonZeroShader(f.antialias)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer non-zero shader: %v", err))
|
||||
}
|
||||
case FillRuleEvenOdd:
|
||||
if f.antialias {
|
||||
shader = stencilBufferEvenOddAAShader
|
||||
} else {
|
||||
shader = stencilBufferEvenOddShader
|
||||
var err error
|
||||
shader, err = ensureStencilBufferEvenOddShader(f.antialias)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vector: failed to create stencil buffer even-odd shader: %v", err))
|
||||
}
|
||||
}
|
||||
dst.DrawTrianglesShader32(vs, is, shader, op)
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package vector
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
@@ -30,8 +32,94 @@ var (
|
||||
stencilBufferNonZeroAAShader *ebiten.Shader
|
||||
stencilBufferEvenOddShader *ebiten.Shader
|
||||
stencilBufferEvenOddAAShader *ebiten.Shader
|
||||
|
||||
stencilBufferM sync.Mutex
|
||||
)
|
||||
|
||||
func ensureStencilBufferShaders() (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if stencilBufferFillShader != nil {
|
||||
return stencilBufferFillShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferFillShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferFillShader = s
|
||||
return stencilBufferFillShader, err
|
||||
}
|
||||
|
||||
func ensureStencilBufferBezierShader() (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if stencilBufferBezierShader != nil {
|
||||
return stencilBufferBezierShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferBezierShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferBezierShader = s
|
||||
return stencilBufferBezierShader, nil
|
||||
}
|
||||
|
||||
func ensureStencilBufferNonZeroShader(antialias bool) (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if antialias {
|
||||
if stencilBufferNonZeroAAShader != nil {
|
||||
return stencilBufferNonZeroAAShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroAAShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferNonZeroAAShader = s
|
||||
return stencilBufferNonZeroAAShader, nil
|
||||
}
|
||||
|
||||
if stencilBufferNonZeroShader != nil {
|
||||
return stencilBufferNonZeroShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferNonZeroShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferNonZeroShader = s
|
||||
return stencilBufferNonZeroShader, nil
|
||||
}
|
||||
|
||||
func ensureStencilBufferEvenOddShader(antialias bool) (*ebiten.Shader, error) {
|
||||
stencilBufferM.Lock()
|
||||
defer stencilBufferM.Unlock()
|
||||
|
||||
if antialias {
|
||||
if stencilBufferEvenOddAAShader != nil {
|
||||
return stencilBufferEvenOddAAShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddAAShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferEvenOddAAShader = s
|
||||
return stencilBufferEvenOddAAShader, nil
|
||||
}
|
||||
|
||||
if stencilBufferEvenOddShader != nil {
|
||||
return stencilBufferEvenOddShader, nil
|
||||
}
|
||||
s, err := ebiten.NewShader([]byte(stencilBufferEvenOddShaderSrc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stencilBufferEvenOddShader = s
|
||||
return stencilBufferEvenOddShader, nil
|
||||
}
|
||||
|
||||
//ebitengine:shadersource
|
||||
const stencilBufferFillShaderSrc = `//kage:unit pixels
|
||||
|
||||
|
||||
+20
-14
@@ -371,22 +371,28 @@ func FillPath(dst *ebiten.Image, path *Path, fillOptions *FillOptions, drawPathO
|
||||
s.fillRule = fillOptions.FillRule
|
||||
s.addPath(path, drawPathOptions.ColorScale)
|
||||
|
||||
token := addUsageCallback(dst, func() {
|
||||
// Remove the callback not to call this twice.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
// Use an independent callback function to avoid unexpected captures.
|
||||
theCallbackTokens[dst] = addUsageCallback(dst, fillPathCallback)
|
||||
}
|
||||
|
||||
s := theFillPathsStates[dst]
|
||||
s.fillPaths(dst)
|
||||
func fillPathCallback(dst *ebiten.Image) {
|
||||
theFillPathM.Lock()
|
||||
defer theFillPathM.Unlock()
|
||||
|
||||
delete(theFillPathsStates, dst)
|
||||
s.reset()
|
||||
theFillPathsStatesPool.Put(s)
|
||||
})
|
||||
theCallbackTokens[dst] = token
|
||||
// Remove the callback not to call this twice.
|
||||
if token, ok := theCallbackTokens[dst]; ok {
|
||||
removeUsageCallback(dst, token)
|
||||
}
|
||||
delete(theCallbackTokens, dst)
|
||||
|
||||
s, ok := theFillPathsStates[dst]
|
||||
if !ok {
|
||||
panic("vector: fillPathsState must exist here")
|
||||
}
|
||||
s.fillPaths(dst)
|
||||
|
||||
s.reset()
|
||||
theFillPathsStatesPool.Put(s)
|
||||
}
|
||||
|
||||
// StrokePath strokes the specified path with the specified options.
|
||||
@@ -399,7 +405,7 @@ func StrokePath(dst *ebiten.Image, path *Path, strokeOptions *StrokeOptions, dra
|
||||
}
|
||||
|
||||
//go:linkname addUsageCallback github.com/hajimehoshi/ebiten/v2.addUsageCallback
|
||||
func addUsageCallback(img *ebiten.Image, fn func()) int64
|
||||
func addUsageCallback(img *ebiten.Image, fn func(img *ebiten.Image)) int64
|
||||
|
||||
//go:linkname removeUsageCallback github.com/hajimehoshi/ebiten/v2.removeUsageCallback
|
||||
func removeUsageCallback(img *ebiten.Image, token int64)
|
||||
|
||||
@@ -17,6 +17,7 @@ package vector_test
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
@@ -140,3 +141,32 @@ func TestFillPathSubImage(t *testing.T) {
|
||||
t.Errorf("got: %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceConditionWithSubImage(t *testing.T) {
|
||||
const w, h = 16, 16
|
||||
src := ebiten.NewImage(w, h)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range h {
|
||||
for j := range w {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
subImg := src.SubImage(image.Rect(i, j, i+1, j+1)).(*ebiten.Image)
|
||||
var p vector.Path
|
||||
p.MoveTo(0, 0)
|
||||
p.LineTo(w, 0)
|
||||
p.LineTo(w, h)
|
||||
p.LineTo(0, h)
|
||||
p.Close()
|
||||
op := &vector.DrawPathOptions{}
|
||||
op.ColorScale.ScaleWithColor(color.White)
|
||||
op.AntiAlias = true
|
||||
vector.FillPath(subImg, &p, nil, op)
|
||||
dst := ebiten.NewImage(w, h)
|
||||
dst.DrawImage(subImg, nil)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user