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:
Hajime Hoshi
2025-10-21 00:24:29 +09:00
parent f0f0bb339d
commit a8a21ba687
5 changed files with 214 additions and 91 deletions
+24 -61
View File
@@ -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)
+88
View File
@@ -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
View File
@@ -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)
+30
View File
@@ -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()
}