updated ebiten version from 2.7.9 to 2.9.9

This commit is contained in:
2026-06-15 19:06:55 +02:00
parent 21edbc41c4
commit db1b625069
405 changed files with 31913 additions and 12595 deletions
+513 -75
View File
@@ -18,6 +18,11 @@ import (
"fmt"
"image"
"image/color"
"math"
"slices"
"sync"
"sync/atomic"
"unsafe"
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
@@ -25,6 +30,7 @@ import (
"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/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
@@ -43,19 +49,56 @@ type Image struct {
// tmpVertices must not be reused until ui.Image.Draw* is called.
tmpVertices []float32
// tmpIndices must not be reused until ui.Image.Draw* is called.
tmpIndices []uint32
// tmpUniforms must not be reused until ui.Image.Draw* is called.
tmpUniforms []uint32
// subImageCache is a cache for sub-images.
// subImageCache is valid only when the image is not a sub-image.
subImageCache map[image.Rectangle]*Image
// subImageGCLastTick is the last tick when old sub images are removed from the cache.
subImageGCLastTick int64
// subImageCacheM is a mutex for subImageCache.
// subImageCache can be accessed from the image and its sub-images at the same time,
// so the map must be protected by a mutex.
subImageCacheM sync.Mutex
// atime is the last access time.
// atime needs to be an atomic value since a sub-image atime can be accessed from its original image.
atime atomic.Int64
// usageCallbacks are callbacks that are invoked when the image is used.
// usageCallbacks is valid only when the image is not a sub-image.
usageCallbacks map[int64]usageCallback
// inUsageCallbacks reports whether the image is in usageCallbacks.
inUsageCallbacks atomic.Bool
// usageCallbacksM is a mutex for usageCallbacks.
usageCallbacksM sync.Mutex
// Do not add a 'buffering' member that are resolved lazily.
// This tends to forget resolving the buffer easily (#2362).
}
type usageCallback struct {
fn func(image *Image)
}
func (i *Image) copyCheck() {
if i.addr != i {
panic("ebiten: illegal use of non-zero Image copied by value")
}
}
func (i *Image) updateAccessTime() {
i.atime.Store(Tick())
}
// Size returns the size of the image.
//
// Deprecated: as of v2.5. Use Bounds().Dx() and Bounds().Dy() or Bounds().Size() instead.
@@ -88,6 +131,10 @@ func (i *Image) Fill(clr color.Color) {
return
}
i.invokeUsageCallbacks()
i.updateAccessTime()
var crf, cgf, cbf, caf float32
cr, cg, cb, ca := clr.RGBA()
crf = float32(cr) / 0xffff
@@ -97,11 +144,11 @@ func (i *Image) Fill(clr color.Color) {
i.image.Fill(crf, cgf, cbf, caf, i.adjustedBounds())
}
func canSkipMipmap(geom GeoM, filter builtinshader.Filter) bool {
func canSkipMipmap(det float32, filter builtinshader.Filter) bool {
if filter != builtinshader.FilterLinear {
return true
}
return geom.det2x2() >= 0.999
return math.Abs(float64(det)) >= 0.999
}
// DrawImageOptions represents options for DrawImage.
@@ -139,6 +186,16 @@ type DrawImageOptions struct {
// Filter is a type of texture filter.
// The default (zero) value is FilterNearest.
Filter Filter
// DisableMipmaps disables mipmaps.
// When Filter is FilterLinear and GeoM shrinks the image, mipmaps are used by default.
// Mipmap is useful to render a shrunk image with high quality.
// However, mipmaps can be expensive, especially on mobiles.
// When DisableMipmaps is true, mipmap is not used.
// When Filter is not FilterLinear, DisableMipmaps is ignored.
//
// The default (zero) value is false.
DisableMipmaps bool
}
// adjustPosition converts the position in the *ebiten.Image coordinate to the *ui.Image coordinate.
@@ -220,6 +277,12 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
return
}
i.invokeUsageCallbacks()
img.invokeUsageCallbacks()
i.updateAccessTime()
img.updateAccessTime()
if options == nil {
options = &DrawImageOptions{}
}
@@ -237,6 +300,10 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
geoM.Translate(float64(offsetX), float64(offsetY))
}
a, b, c, d, tx, ty := geoM.elements32()
det := a*d - b*c
if det == 0 {
return
}
bounds := img.Bounds()
sx0, sy0 := img.adjustPosition(bounds.Min.X, bounds.Min.Y)
@@ -244,10 +311,10 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
colorm, cr, cg, cb, ca := colorMToScale(options.ColorM.affineColorM())
cr, cg, cb, ca = options.ColorScale.apply(cr, cg, cb, ca)
vs := i.ensureTmpVertices(4 * graphics.VertexFloatCount)
graphics.QuadVertices(vs, float32(sx0), float32(sy0), float32(sx1), float32(sy1), a, b, c, d, tx, ty, cr, cg, cb, ca)
graphics.QuadVerticesFromSrcAndMatrix(vs, float32(sx0), float32(sy0), float32(sx1), float32(sy1), a, b, c, d, tx, ty, cr, cg, cb, ca)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageCount]*ui.Image{img.image}
srcs := [graphics.ShaderSrcImageCount]*ui.Image{img.image}
useColorM := !colorm.IsIdentity()
shader := builtinShader(filter, builtinshader.AddressUnsafe, useColorM)
@@ -262,7 +329,36 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) {
})
}
i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedBounds(), [graphics.ShaderImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillAll, canSkipMipmap(geoM, filter), false)
dr := i.adjustedBounds()
hint := restorable.HintNone
if overwritesDstRegion(options.Blend, dr, geoM, sx0, sy0, sx1, sy1) {
hint = restorable.HintOverwriteDstRegion
}
skipMipmap := options.DisableMipmaps
if !skipMipmap {
skipMipmap = canSkipMipmap(det, filter)
}
i.image.DrawTriangles(srcs, vs, is, blend, dr, [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRuleFillAll, skipMipmap, false, hint)
}
// overwritesDstRegion reports whether the given parameters overwrite the destination region completely.
func overwritesDstRegion(blend Blend, dstRegion image.Rectangle, geoM GeoM, sx0, sy0, sx1, sy1 int) bool {
// TODO: More precisely, BlendFactorDestinationRGB, BlendFactorDestinationAlpha, and operations should be checked.
if blend != BlendCopy && blend != BlendClear {
return false
}
// Check the result vertices is not a rotated rectangle.
if geoM.b != 0 || geoM.c != 0 {
return false
}
// Check the result vertices completely covers dstRegion.
x0, y0 := geoM.Apply(float64(sx0), float64(sy0))
x1, y1 := geoM.Apply(float64(sx1), float64(sy1))
if float64(dstRegion.Min.X) < x0 || float64(dstRegion.Min.Y) < y0 || float64(dstRegion.Max.X) > x1 || float64(dstRegion.Max.Y) > y1 {
return false
}
return true
}
// Vertex represents a vertex passed to DrawTriangles.
@@ -274,6 +370,10 @@ type Vertex struct {
// SrcX and SrcY represents a point on a source image.
// Be careful that SrcX/SrcY coordinates are on the image's bounds.
// This means that an upper-left point of a sub-image might not be (0, 0).
//
// Before passing vertices to a Kage shader, SrcX/SrcY are converted to texture coordinates of the first image,
// which is DrawRectShaderOptions.Image[0] or DrawTrianglesShaderOptions.Images[0].
// If the image is nil, SrcX/SrcY are not converted and used as-is.
SrcX float32
SrcY float32
@@ -291,8 +391,20 @@ type Vertex struct {
ColorG float32
ColorB float32
ColorA float32
// Custom0/Custom1/Custom2/Custom3 represents general-purpose values passed to the shader.
// In order to use them, Fragment must have an additional vec4 argument.
//
// These values are valid only when DrawTrianglesShader is used.
// In other cases, these values are ignored.
Custom0 float32
Custom1 float32
Custom2 float32
Custom3 float32
}
var _ [0]byte = [unsafe.Sizeof(Vertex{}) - unsafe.Sizeof(float32(0))*graphics.VertexFloatCount]byte{}
// Address represents a sampler address mode.
type Address int
@@ -308,19 +420,46 @@ const (
)
// FillRule is the rule whether an overlapped region is rendered with DrawTriangles(Shader).
//
// Deprecated: as of v2.9.
type FillRule int
const (
// FillRuleFillAll indicates all the triangles are rendered regardless of overlaps.
//
// Deprecated: as of v2.9.
FillRuleFillAll FillRule = FillRule(graphicsdriver.FillRuleFillAll)
// FillRuleNonZero means that triangles are rendered based on the non-zero rule.
// If and only if the number of overlaps is not 0, the region is rendered.
//
// Deprecated: as of v2.9.
FillRuleNonZero FillRule = FillRule(graphicsdriver.FillRuleNonZero)
// FillRuleEvenOdd means that triangles are rendered based on the even-odd rule.
// If and only if the number of overlaps is odd, the region is rendered.
//
// Deprecated: as of v2.9.
FillRuleEvenOdd FillRule = FillRule(graphicsdriver.FillRuleEvenOdd)
)
const (
// FillAll indicates all the triangles are rendered regardless of overlaps.
FillAll FillRule = FillRule(graphicsdriver.FillAll)
//
// Deprecated: as of v2.8. Use FillRuleFillAll instead.
FillAll = FillRuleFillAll
// NonZero means that triangles are rendered based on the non-zero rule.
// If and only if the number of overlaps is not 0, the region is rendered.
NonZero FillRule = FillRule(graphicsdriver.NonZero)
//
// Deprecated: as of v2.8. Use FillRuleNonZero instead.
NonZero = FillRuleNonZero
// EvenOdd means that triangles are rendered based on the even-odd rule.
// If and only if the number of overlaps is odd, the region is rendered.
EvenOdd FillRule = FillRule(graphicsdriver.EvenOdd)
//
// Deprecated: as of v2.8. Use FillRuleEvenOdd instead.
EvenOdd = FillRuleEvenOdd
)
// ColorScaleMode is the mode of color scales in vertices.
@@ -371,11 +510,13 @@ type DrawTrianglesOptions struct {
// FillRule indicates the rule how an overlapped region is rendered.
//
// The rules NonZero and EvenOdd are useful when you want to render a complex polygon.
// The rules FillRuleNonZero and FillRuleEvenOdd are useful when you want to render a complex polygon.
// A complex polygon is a non-convex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon.
// See examples/vector for actual usages.
//
// The default (zero) value is FillAll.
// The default (zero) value is FillRuleFillAll.
//
// Deprecated: as of v2.9. Use [github.com/hajimehoshi/ebiten/v2/vector.FillPath] instead.
FillRule FillRule
// AntiAlias indicates whether the rendering uses anti-alias or not.
@@ -384,8 +525,20 @@ type DrawTrianglesOptions struct {
// AntiAlias increases internal draw calls and might affect performance.
// Use the build tag `ebitenginedebug` to check the number of draw calls if you care.
//
// The default (zero) value is false.
// The default (zero) value is false.//
//
// Deprecated: as of v2.9. Use [github.com/hajimehoshi/ebiten/v2/vector.FillPath] instead.
AntiAlias bool
// DisableMipmaps disables mipmaps.
// When Filter is FilterLinear and GeoM shrinks the image, mipmaps are used by default.
// Mipmap is useful to render a shrunk image with high quality.
// However, mipmaps can be expensive, especially on mobiles.
// When DisableMipmaps is true, mipmap is not used.
// When Filter is not FilterLinear, DisableMipmaps is ignored.
//
// The default (zero) value is false.
DisableMipmaps bool
}
// MaxIndicesCount is the maximum number of indices for DrawTriangles and DrawTrianglesShader.
@@ -428,6 +581,36 @@ const MaxVertexCount = graphicscommand.MaxVertexCount
//
// When the image i is disposed, DrawTriangles does nothing.
func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, options *DrawTrianglesOptions) {
is := i.ensureTmpIndices(len(indices))
for i := range is {
is[i] = uint32(indices[i])
}
i.DrawTriangles32(vertices, is, img, options)
}
// DrawTriangles32 draws triangles with the specified vertices and their indices.
// DrawTriangles32 is the version of DrawTriangles with uint32 indices.
//
// img is used as a source image. img cannot be nil.
// If you want to draw triangles with a solid color, use a small white image
// and adjust the color elements in the vertices. For an actual implementation,
// see the example 'vector'.
//
// Vertex contains color values, which are interpreted as straight-alpha colors by default.
// This depends on the option's ColorScaleMode.
//
// If len(vertices) is more than MaxVertexCount, the exceeding part is ignored.
//
// If len(indices) is not multiple of 3, DrawTriangles32 panics.
//
// If a value in indices is out of range of vertices, or not less than MaxVertexCount, DrawTriangles32 panics.
//
// The rule in which DrawTriangles32 works effectively is same as DrawImage's.
//
// When the given image is disposed, DrawTriangles32 panics.
//
// When the image i is disposed, DrawTriangles32 does nothing.
func (i *Image) DrawTriangles32(vertices []Vertex, indices []uint32, img *Image, options *DrawTrianglesOptions) {
i.copyCheck()
if img != nil && img.isDisposed() {
@@ -437,6 +620,16 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
return
}
if len(indices) == 0 {
return
}
i.invokeUsageCallbacks()
img.invokeUsageCallbacks()
img.updateAccessTime()
i.updateAccessTime()
if len(vertices) > graphicscommand.MaxVertexCount {
// The last part cannot be specified by indices. Just omit them.
vertices = vertices[:graphicscommand.MaxVertexCount]
@@ -469,38 +662,40 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
vs := i.ensureTmpVertices(len(vertices) * graphics.VertexFloatCount)
dst := i
if options.ColorScaleMode == ColorScaleModeStraightAlpha {
for i, v := range vertices {
dx, dy := dst.adjustPositionF32(v.DstX, v.DstY)
vs[i*graphics.VertexFloatCount] = dx
vs[i*graphics.VertexFloatCount+1] = dy
sx, sy := img.adjustPositionF32(v.SrcX, v.SrcY)
vs[i*graphics.VertexFloatCount+2] = sx
vs[i*graphics.VertexFloatCount+3] = sy
vs[i*graphics.VertexFloatCount+4] = v.ColorR * v.ColorA * cr
vs[i*graphics.VertexFloatCount+5] = v.ColorG * v.ColorA * cg
vs[i*graphics.VertexFloatCount+6] = v.ColorB * v.ColorA * cb
vs[i*graphics.VertexFloatCount+7] = v.ColorA * ca
// Avoid using `for i, v := range vertices` as adding `v` creates a copy from `vertices` unnecessarily on each loop (#3103).
for i := range vertices {
// Create a temporary slice to reduce boundary checks.
vs := vs[i*graphics.VertexFloatCount : i*graphics.VertexFloatCount+8]
dx, dy := dst.adjustPositionF32(vertices[i].DstX, vertices[i].DstY)
vs[0] = dx
vs[1] = dy
sx, sy := img.adjustPositionF32(vertices[i].SrcX, vertices[i].SrcY)
vs[2] = sx
vs[3] = sy
vs[4] = vertices[i].ColorR * vertices[i].ColorA * cr
vs[5] = vertices[i].ColorG * vertices[i].ColorA * cg
vs[6] = vertices[i].ColorB * vertices[i].ColorA * cb
vs[7] = vertices[i].ColorA * ca
}
} else {
for i, v := range vertices {
dx, dy := dst.adjustPositionF32(v.DstX, v.DstY)
vs[i*graphics.VertexFloatCount] = dx
vs[i*graphics.VertexFloatCount+1] = dy
sx, sy := img.adjustPositionF32(v.SrcX, v.SrcY)
vs[i*graphics.VertexFloatCount+2] = sx
vs[i*graphics.VertexFloatCount+3] = sy
vs[i*graphics.VertexFloatCount+4] = v.ColorR * cr
vs[i*graphics.VertexFloatCount+5] = v.ColorG * cg
vs[i*graphics.VertexFloatCount+6] = v.ColorB * cb
vs[i*graphics.VertexFloatCount+7] = v.ColorA * ca
// See comment above (#3103).
for i := range vertices {
// Create a temporary slice to reduce boundary checks.
vs := vs[i*graphics.VertexFloatCount : i*graphics.VertexFloatCount+8]
dx, dy := dst.adjustPositionF32(vertices[i].DstX, vertices[i].DstY)
vs[0] = dx
vs[1] = dy
sx, sy := img.adjustPositionF32(vertices[i].SrcX, vertices[i].SrcY)
vs[2] = sx
vs[3] = sy
vs[4] = vertices[i].ColorR * cr
vs[5] = vertices[i].ColorG * cg
vs[6] = vertices[i].ColorB * cb
vs[7] = vertices[i].ColorA * ca
}
}
is := make([]uint32, len(indices))
for i := range is {
is[i] = uint32(indices[i])
}
srcs := [graphics.ShaderImageCount]*ui.Image{img.image}
srcs := [graphics.ShaderSrcImageCount]*ui.Image{img.image}
useColorM := !colorm.IsIdentity()
shader := builtinShader(filter, address, useColorM)
@@ -515,7 +710,11 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
})
}
i.image.DrawTriangles(srcs, vs, is, blend, i.adjustedBounds(), [graphics.ShaderImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), filter != builtinshader.FilterLinear, options.AntiAlias)
skipMipmap := options.DisableMipmaps
if !skipMipmap {
skipMipmap = filter != builtinshader.FilterLinear
}
i.image.DrawTriangles(srcs, vs, indices, blend, i.adjustedBounds(), [graphics.ShaderSrcImageCount]image.Rectangle{img.adjustedBounds()}, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), skipMipmap, options.AntiAlias, restorable.HintNone)
}
// DrawTrianglesShaderOptions represents options for DrawTrianglesShader.
@@ -533,7 +732,7 @@ type DrawTrianglesShaderOptions struct {
// Uniforms is a set of uniform variables for the shader.
// The keys are the names of the uniform variables.
// The values must be a numeric type, or a slice or an array of a numeric type.
// The values must be a numeric/boolean type, or a slice or an array of a numeric/boolean type.
// If the uniform variable type is an array, a vector or a matrix,
// you have to specify linearly flattened values as a slice or an array.
// For example, if the uniform variable type is [4]vec4, the length will be 16.
@@ -542,16 +741,19 @@ type DrawTrianglesShaderOptions struct {
Uniforms map[string]any
// Images is a set of the source images.
// All the images' sizes must be the same.
// In the texel mode, all the image sizes must be the same.
// The pixel mode allows images of different sizes.
Images [4]*Image
// FillRule indicates the rule how an overlapped region is rendered.
//
// The rules NonZero and EvenOdd are useful when you want to render a complex polygon.
// The rules FillRuleNonZero and FillRuleEvenOdd are useful when you want to render a complex polygon.
// A complex polygon is a non-convex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon.
// See examples/vector for actual usages.
//
// The default (zero) value is FillAll.
// The default (zero) value is FillRuleFillAll.
//
// Deprecated: as of v2.9. Use [github.com/hajimehoshi/ebiten/v2/vector.FillPath] instead.
FillRule FillRule
// AntiAlias indicates whether the rendering uses anti-alias or not.
@@ -561,11 +763,13 @@ type DrawTrianglesShaderOptions struct {
// Use the build tag `ebitenginedebug` to check the number of draw calls if you care.
//
// The default (zero) value is false.
//
// Deprecated: as of v2.9. Use [github.com/hajimehoshi/ebiten/v2/vector.FillPath] instead.
AntiAlias bool
}
// Check the number of images.
var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]struct{} = [0]struct{}{}
var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderSrcImageCount]struct{} = [0]struct{}{}
// DrawTrianglesShader draws triangles with the specified vertices and their indices with the specified shader.
//
@@ -592,6 +796,39 @@ var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]stru
//
// When the image i is disposed, DrawTrianglesShader does nothing.
func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) {
is := i.ensureTmpIndices(len(indices))
for i := range is {
is[i] = uint32(indices[i])
}
i.DrawTrianglesShader32(vertices, is, shader, options)
}
// DrawTrianglesShader32 draws triangles with the specified vertices and their indices with the specified shader.
// DrawTrianglesShader32 is the version of DrawTrianglesShader with uint32 indices.
//
// Vertex contains color values, which can be interpreted for any purpose by the shader.
//
// For the details about the shader, see https://ebitengine.org/en/documents/shader.html.
//
// If the shader unit is texels, one of the specified image is non-nil and its size is different from (width, height),
// DrawTrianglesShader32 panics.
// If one of the specified image is non-nil and is disposed, DrawTrianglesShader32 panics.
//
// If len(vertices) is more than MaxVertexCount, the exceeding part is ignored.
//
// If len(indices) is not multiple of 3, DrawTrianglesShader32 panics.
//
// If a value in indices is out of range of vertices, or not less than MaxVertexCount, DrawTrianglesShader32 panics.
//
// When a specified image is non-nil and is disposed, DrawTrianglesShader32 panics.
//
// If a specified uniform variable's length or type doesn't match with an expected one, DrawTrianglesShader32 panics.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image i is disposed, DrawTrianglesShader32 does nothing.
func (i *Image) DrawTrianglesShader32(vertices []Vertex, indices []uint32, shader *Shader, options *DrawTrianglesShaderOptions) {
i.copyCheck()
if i.isDisposed() {
@@ -602,6 +839,30 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
panic("ebiten: the given shader to DrawTrianglesShader must not be disposed")
}
if len(indices) == 0 {
return
}
i.invokeUsageCallbacks()
if options != nil {
for _, img := range options.Images {
if img == nil {
continue
}
img.invokeUsageCallbacks()
}
}
if options != nil {
for _, img := range options.Images {
if img == nil {
continue
}
img.updateAccessTime()
}
}
i.updateAccessTime()
if len(vertices) > graphicscommand.MaxVertexCount {
// The last part cannot be specified by indices. Just omit them.
vertices = vertices[:graphicscommand.MaxVertexCount]
@@ -629,28 +890,30 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
vs := i.ensureTmpVertices(len(vertices) * graphics.VertexFloatCount)
dst := i
src := options.Images[0]
for i, v := range vertices {
dx, dy := dst.adjustPositionF32(v.DstX, v.DstY)
vs[i*graphics.VertexFloatCount] = dx
vs[i*graphics.VertexFloatCount+1] = dy
sx, sy := v.SrcX, v.SrcY
// Avoid using `for i, v := range vertices` as adding `v` creates a copy from `vertices` unnecessarily on each loop (#3103).
for i := range vertices {
// Create a temporary slice to reduce boundary checks.
vs := vs[i*graphics.VertexFloatCount : i*graphics.VertexFloatCount+12]
dx, dy := dst.adjustPositionF32(vertices[i].DstX, vertices[i].DstY)
vs[0] = dx
vs[1] = dy
sx, sy := vertices[i].SrcX, vertices[i].SrcY
if src != nil {
sx, sy = src.adjustPositionF32(sx, sy)
}
vs[i*graphics.VertexFloatCount+2] = sx
vs[i*graphics.VertexFloatCount+3] = sy
vs[i*graphics.VertexFloatCount+4] = v.ColorR
vs[i*graphics.VertexFloatCount+5] = v.ColorG
vs[i*graphics.VertexFloatCount+6] = v.ColorB
vs[i*graphics.VertexFloatCount+7] = v.ColorA
vs[2] = sx
vs[3] = sy
vs[4] = vertices[i].ColorR
vs[5] = vertices[i].ColorG
vs[6] = vertices[i].ColorB
vs[7] = vertices[i].ColorA
vs[8] = vertices[i].Custom0
vs[9] = vertices[i].Custom1
vs[10] = vertices[i].Custom2
vs[11] = vertices[i].Custom3
}
is := make([]uint32, len(indices))
for i := range is {
is[i] = uint32(indices[i])
}
var imgs [graphics.ShaderImageCount]*ui.Image
var imgs [graphics.ShaderSrcImageCount]*ui.Image
var imgSize image.Point
for i, img := range options.Images {
if img == nil {
@@ -672,7 +935,7 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
imgs[i] = img.image
}
var srcRegions [graphics.ShaderImageCount]image.Rectangle
var srcRegions [graphics.ShaderSrcImageCount]image.Rectangle
for i, img := range options.Images {
if img == nil {
continue
@@ -683,7 +946,7 @@ func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader
i.tmpUniforms = i.tmpUniforms[:0]
i.tmpUniforms = shader.appendUniforms(i.tmpUniforms, options.Uniforms)
i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedBounds(), srcRegions, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), true, options.AntiAlias)
i.image.DrawTriangles(imgs, vs, indices, blend, i.adjustedBounds(), srcRegions, shader.shader, i.tmpUniforms, graphicsdriver.FillRule(options.FillRule), true, options.AntiAlias, restorable.HintNone)
}
// DrawRectShaderOptions represents options for DrawRectShader.
@@ -710,7 +973,7 @@ type DrawRectShaderOptions struct {
// Uniforms is a set of uniform variables for the shader.
// The keys are the names of the uniform variables.
// The values must be a numeric type, or a slice or an array of a numeric type.
// The values must be a numeric/boolean type, or a slice or an array of a numeric/boolean type.
// If the uniform variable type is an array, a vector or a matrix,
// you have to specify linearly flattened values as a slice or an array.
// For example, if the uniform variable type is [4]vec4, the length will be 16.
@@ -724,7 +987,7 @@ type DrawRectShaderOptions struct {
}
// Check the number of images.
var _ [len(DrawRectShaderOptions{}.Images)]struct{} = [graphics.ShaderImageCount]struct{}{}
var _ [len(DrawRectShaderOptions{}.Images)]struct{} = [graphics.ShaderSrcImageCount]struct{}{}
// DrawRectShader draws a rectangle with the specified width and height with the specified shader.
//
@@ -760,6 +1023,26 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
panic("ebiten: the given shader to DrawRectShader must not be disposed")
}
if options != nil {
for _, img := range options.Images {
if img == nil {
continue
}
img.invokeUsageCallbacks()
}
}
i.invokeUsageCallbacks()
if options != nil {
for _, img := range options.Images {
if img == nil {
continue
}
img.updateAccessTime()
}
}
i.updateAccessTime()
if options == nil {
options = &DrawRectShaderOptions{}
}
@@ -771,7 +1054,7 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
blend = options.CompositeMode.blend().internalBlend()
}
var imgs [graphics.ShaderImageCount]*ui.Image
var imgs [graphics.ShaderSrcImageCount]*ui.Image
for i, img := range options.Images {
if img == nil {
continue
@@ -785,7 +1068,7 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
imgs[i] = img.image
}
var srcRegions [graphics.ShaderImageCount]image.Rectangle
var srcRegions [graphics.ShaderSrcImageCount]image.Rectangle
for i, img := range options.Images {
if img == nil {
if shader.unit == shaderir.Pixels && i == 0 {
@@ -803,11 +1086,14 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
geoM.Translate(float64(offsetX), float64(offsetY))
}
a, b, c, d, tx, ty := geoM.elements32()
if det := a*d - b*c; det == 0 {
return
}
cr, cg, cb, ca := options.ColorScale.elements()
vs := i.ensureTmpVertices(4 * graphics.VertexFloatCount)
// Do not use srcRegions[0].Dx() and srcRegions[0].Dy() as these might be empty.
graphics.QuadVertices(vs,
graphics.QuadVerticesFromSrcAndMatrix(vs,
float32(srcRegions[0].Min.X), float32(srcRegions[0].Min.Y),
float32(srcRegions[0].Min.X+width), float32(srcRegions[0].Min.Y+height),
a, b, c, d, tx, ty, cr, cg, cb, ca)
@@ -816,7 +1102,14 @@ func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawR
i.tmpUniforms = i.tmpUniforms[:0]
i.tmpUniforms = shader.appendUniforms(i.tmpUniforms, options.Uniforms)
i.image.DrawTriangles(imgs, vs, is, blend, i.adjustedBounds(), srcRegions, shader.shader, i.tmpUniforms, graphicsdriver.FillAll, true, false)
dr := i.adjustedBounds()
hint := restorable.HintNone
// Do not use srcRegions[0].Dx() and srcRegions[0].Dy() as these might be empty.
if overwritesDstRegion(options.Blend, dr, geoM, srcRegions[0].Min.X, srcRegions[0].Min.Y, srcRegions[0].Min.X+width, srcRegions[0].Min.Y+height) {
hint = restorable.HintOverwriteDstRegion
}
i.image.DrawTriangles(imgs, vs, is, blend, dr, srcRegions, shader.shader, i.tmpUniforms, graphicsdriver.FillRuleFillAll, true, false, hint)
}
// SubImage returns an image representing the portion of the image p visible through r.
@@ -838,24 +1131,53 @@ func (i *Image) SubImage(r image.Rectangle) image.Image {
return nil
}
if i.isSubImage() {
return i.original.SubImage(r.Intersect(i.Bounds()))
}
r = r.Intersect(i.Bounds())
// Need to check Empty explicitly. See the standard image package implementations.
if r.Empty() {
r = image.ZR
r = image.Rectangle{}
}
var orig = i
if i.isSubImage() {
orig = i.original
i.subImageCacheM.Lock()
defer i.subImageCacheM.Unlock()
// The image might already be disposed in another goroutine.
// Recheck this.
if i.isDisposed() {
return nil
}
if img, ok := i.subImageCache[r]; ok {
img.updateAccessTime()
return img
}
if tick := Tick(); i.subImageGCLastTick < tick {
i.subImageGCLastTick = tick
for _, img := range i.subImageCache {
if img.atime.Load()+60 < tick {
delete(i.subImageCache, img.bounds)
}
}
}
img := &Image{
image: i.image,
bounds: r,
original: orig,
original: i,
}
img.addr = img
if i.subImageCache == nil {
i.subImageCache = map[image.Rectangle]*Image{}
}
i.subImageCache[r] = img
img.updateAccessTime()
return img
}
@@ -906,6 +1228,8 @@ func (i *Image) ReadPixels(pixels []byte) {
return
}
i.invokeUsageCallbacks()
i.image.ReadPixels(pixels, i.adjustedBounds())
}
@@ -950,6 +1274,8 @@ func (i *Image) at(x, y int) (r, g, b, a byte) {
return 0, 0, 0, 0
}
i.invokeUsageCallbacks()
x, y = i.adjustPosition(x, y)
var pix [4]byte
i.image.ReadPixels(pix[:], image.Rect(x, y, x+1, y+1))
@@ -960,15 +1286,24 @@ func (i *Image) at(x, y int) (r, g, b, a byte) {
//
// Set implements the standard draw.Image's Set.
//
// If (x, y) is outside the image bounds, Set does nothing.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// If the image is disposed, Set does nothing.
//
// For performance, it is recommended to use WritePixels instead of Set whenever possible.
func (i *Image) Set(x, y int, clr color.Color) {
i.copyCheck()
if i.isDisposed() {
return
}
i.invokeUsageCallbacks()
i.updateAccessTime()
if !image.Pt(x, y).In(i.Bounds()) {
return
}
@@ -1003,6 +1338,10 @@ func (i *Image) Dispose() {
}
i.image.Deallocate()
i.image = nil
i.subImageCacheM.Lock()
i.subImageCache = nil
i.subImageCacheM.Unlock()
i.usageCallbacks = nil
}
// Deallocate clears the image and deallocates the internal state of the image.
@@ -1026,6 +1365,7 @@ func (i *Image) Deallocate() {
return
}
i.image.Deallocate()
i.usageCallbacks = nil
}
// WritePixels replaces the pixels of the image.
@@ -1048,6 +1388,8 @@ func (i *Image) WritePixels(pixels []byte) {
return
}
i.invokeUsageCallbacks()
// Do not need to copy pixels here.
// * In internal/mipmap, pixels are copied when necessary.
// * In internal/atlas, pixels are copied to make its paddings.
@@ -1110,7 +1452,7 @@ func NewImageWithOptions(bounds image.Rectangle, options *NewImageOptions) *Imag
func newImage(bounds image.Rectangle, imageType atlas.ImageType) *Image {
if isRunGameEnded() {
panic(fmt.Sprintf("ebiten: NewImage cannot be called after RunGame finishes"))
panic("ebiten: NewImage cannot be called after RunGame finishes")
}
width, height := bounds.Dx(), bounds.Dy()
@@ -1246,6 +1588,102 @@ func (i *Image) ensureTmpVertices(n int) []float32 {
return i.tmpVertices[:n]
}
func (i *Image) ensureTmpIndices(n int) []uint32 {
if cap(i.tmpIndices) < n {
i.tmpIndices = make([]uint32, n)
}
return i.tmpIndices[:n]
}
// private implements FinalScreen.
func (*Image) private() {
}
// Do not use usage callbacks except for Ebitengine packages.
// There is no guarantee for compatibility of this function.
var currentCallbackToken atomic.Int64
//go:linkname originalImage
func originalImage(img *Image) *Image {
if img.isSubImage() {
return img.original
}
return img
}
//go:linkname addUsageCallback
func addUsageCallback(img *Image, callback func(image *Image)) int64 {
return img.addUsageCallback(callback)
}
func (i *Image) addUsageCallback(callback func(image *Image)) int64 {
if i.isSubImage() {
return i.original.addUsageCallback(callback)
}
token := currentCallbackToken.Add(1)
i.usageCallbacksM.Lock()
defer i.usageCallbacksM.Unlock()
if i.usageCallbacks == nil {
i.usageCallbacks = map[int64]usageCallback{}
}
i.usageCallbacks[token] = usageCallback{
fn: callback,
}
return token
}
//go:linkname removeUsageCallback
func removeUsageCallback(img *Image, token int64) {
img.removeUsageCallback(token)
}
func (i *Image) removeUsageCallback(token int64) {
if i.isSubImage() {
i.original.removeUsageCallback(token)
return
}
i.usageCallbacksM.Lock()
defer i.usageCallbacksM.Unlock()
delete(i.usageCallbacks, token)
}
var theTmpUsageCallbackSlicePool = sync.Pool{
New: func() any {
slice := make([]usageCallback, 0, 16)
return &slice
},
}
func (i *Image) invokeUsageCallbacks() {
if i.isSubImage() {
i.original.invokeUsageCallbacks()
return
}
// Do not allow recursive calls.
if !i.inUsageCallbacks.CompareAndSwap(false, true) {
return
}
defer i.inUsageCallbacks.Store(false)
tmpUsageCallbackSlice := theTmpUsageCallbackSlicePool.Get().(*[]usageCallback)
func() {
i.usageCallbacksM.Lock()
defer i.usageCallbacksM.Unlock()
for _, cb := range i.usageCallbacks {
*tmpUsageCallbackSlice = append(*tmpUsageCallbackSlice, cb)
}
}()
for _, cb := range *tmpUsageCallbackSlice {
cb.fn(i)
}
*tmpUsageCallbackSlice = slices.Delete(*tmpUsageCallbackSlice, 0, len(*tmpUsageCallbackSlice))
theTmpUsageCallbackSlicePool.Put(tmpUsageCallbackSlice)
}