ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m25s
ci / release (push) Successful in 5m14s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
A busy frame (the profiler's 30 s graphs and a long agent transcript) passed the 65536 sprites of the editor's batch, and everything drawn after it, the right column and the menus, was dropped. The editor and the launcher now let the batch double up to the submission limit; games keep MaxSprites as their budget.
286 lines
6.1 KiB
Go
286 lines
6.1 KiB
Go
package sprite
|
|
|
|
import (
|
|
"unsafe"
|
|
|
|
"aego/core/bits"
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/core/mathx"
|
|
"aego/gpu"
|
|
)
|
|
|
|
const InstanceStride = 52
|
|
|
|
type Instance struct {
|
|
Pos mathx.Vec2
|
|
Size mathx.Vec2
|
|
SinCos mathx.Vec2
|
|
Origin mathx.Vec2
|
|
UVMin mathx.Vec2
|
|
UVMax mathx.Vec2
|
|
Color uint32
|
|
}
|
|
|
|
const _ = uint(InstanceStride - unsafe.Sizeof(Instance{}))
|
|
|
|
const _ = uint(unsafe.Sizeof(Instance{}) - InstanceStride)
|
|
|
|
type state struct {
|
|
texture gpu.TextureHandle
|
|
sampler gpu.SamplerHandle
|
|
blend BlendMode
|
|
clip mathx.Rect
|
|
scissor bool
|
|
}
|
|
|
|
type Batch struct {
|
|
First int
|
|
Count int
|
|
Texture gpu.TextureHandle
|
|
Sampler gpu.SamplerHandle
|
|
Blend BlendMode
|
|
Clip mathx.Rect
|
|
Scissor bool
|
|
}
|
|
|
|
type Batcher struct {
|
|
dev gpu.Device
|
|
max int
|
|
vb gpu.BufferHandle
|
|
inst []Instance
|
|
meta []state
|
|
keys []uint64
|
|
scratch []uint64
|
|
sorted []Instance
|
|
sortedMeta []state
|
|
sortedN int
|
|
batches []Batch
|
|
zMin, zMax float32
|
|
ppu float32
|
|
dropped uint32
|
|
clamped uint32
|
|
// limit is how far the batcher grows when a frame needs more than max sprites.
|
|
limit int
|
|
}
|
|
|
|
func NewBatcher(dev gpu.Device, maxSprites int) (*Batcher, error) {
|
|
if maxSprites < 1 {
|
|
maxSprites = 1
|
|
}
|
|
if maxSprites > MaxSubmission {
|
|
maxSprites = MaxSubmission
|
|
}
|
|
vb, err := dev.CreateBuffer(gpu.BufferDesc{
|
|
Usage: gpu.BufferVertex,
|
|
Size: maxSprites * InstanceStride,
|
|
Dynamic: true,
|
|
Label: "sprite.instances",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Batcher{
|
|
dev: dev,
|
|
max: maxSprites,
|
|
vb: vb,
|
|
inst: make([]Instance, 0, maxSprites),
|
|
meta: make([]state, 0, maxSprites),
|
|
keys: make([]uint64, 0, maxSprites),
|
|
scratch: make([]uint64, maxSprites),
|
|
sorted: make([]Instance, maxSprites),
|
|
sortedMeta: make([]state, maxSprites),
|
|
batches: make([]Batch, 0, 64),
|
|
zMax: 1,
|
|
ppu: 1,
|
|
limit: maxSprites,
|
|
}, nil
|
|
}
|
|
|
|
// SetLimit lets the batcher grow up to n sprites a frame instead of dropping the ones past its size.
|
|
func (b *Batcher) SetLimit(n int) {
|
|
b.limit = min(max(n, b.max), MaxSubmission)
|
|
}
|
|
|
|
// grow doubles the buffers; it allocates only when a frame needs more than any frame before it.
|
|
func (b *Batcher) grow() bool {
|
|
n := min(b.max*2, b.limit)
|
|
if n <= b.max {
|
|
return false
|
|
}
|
|
vb, err := b.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferVertex, Size: n * InstanceStride, Dynamic: true, Label: "sprite.instances"})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
b.dev.DestroyBuffer(b.vb)
|
|
b.vb, b.max = vb, n
|
|
b.scratch = make([]uint64, n)
|
|
b.sorted = make([]Instance, n)
|
|
b.sortedMeta = make([]state, n)
|
|
return true
|
|
}
|
|
|
|
func (b *Batcher) Buffer() gpu.BufferHandle {
|
|
return b.vb
|
|
}
|
|
|
|
func (b *Batcher) Reset() {
|
|
b.inst = b.inst[:0]
|
|
b.meta = b.meta[:0]
|
|
b.keys = b.keys[:0]
|
|
b.batches = b.batches[:0]
|
|
b.sortedN = 0
|
|
b.dropped = 0
|
|
b.clamped = 0
|
|
}
|
|
|
|
func (b *Batcher) SetView(zMin, zMax, pixelsPerUnit float32) {
|
|
b.zMin, b.zMax = zMin, zMax
|
|
b.ppu = pixelsPerUnit
|
|
}
|
|
|
|
func (b *Batcher) Len() int {
|
|
return len(b.inst)
|
|
}
|
|
|
|
func (b *Batcher) Dropped() uint32 {
|
|
return b.dropped
|
|
}
|
|
|
|
func (b *Batcher) Clamped() uint32 {
|
|
return b.clamped
|
|
}
|
|
|
|
func (b *Batcher) Push(d *Draw, defaultSampler gpu.SamplerHandle, clip mathx.Rect, scissor bool) bool {
|
|
if len(b.inst) >= b.max && !b.grow() {
|
|
b.dropped++
|
|
return false
|
|
}
|
|
if OutOfRange(d.Layer, d.Order, d.Z, b.zMin, b.zMax) {
|
|
b.clamped++
|
|
}
|
|
size := d.WorldSize(b.ppu)
|
|
sin, cos := mathx.Sin(d.Rotation), mathx.Cos(d.Rotation)
|
|
reg := d.Region
|
|
if d.FlipX {
|
|
reg = reg.FlippedX()
|
|
}
|
|
if d.FlipY {
|
|
reg = reg.FlippedY()
|
|
}
|
|
sampler := d.Sampler
|
|
if sampler.IsZero() {
|
|
sampler = defaultSampler
|
|
}
|
|
b.inst = append(b.inst, Instance{
|
|
Pos: d.Position,
|
|
Size: size,
|
|
SinCos: mathx.Vec2{X: sin, Y: cos},
|
|
Origin: d.Origin,
|
|
UVMin: reg.UVMin,
|
|
UVMax: reg.UVMax,
|
|
Color: d.tint().Premultiplied().PackRGBA8(),
|
|
})
|
|
b.meta = append(b.meta, state{
|
|
texture: reg.Texture,
|
|
sampler: sampler,
|
|
blend: d.Blend,
|
|
clip: clip,
|
|
scissor: scissor,
|
|
})
|
|
b.keys = append(b.keys, uint64(MakeKey(d.Layer, d.Order, d.Z, b.zMin, b.zMax, uint32(len(b.inst)-1))))
|
|
return true
|
|
}
|
|
|
|
func (b *Batcher) Sort() {
|
|
n := len(b.keys)
|
|
if n == 0 {
|
|
b.sortedN = 0
|
|
return
|
|
}
|
|
sorted := bits.RadixSort64(b.keys, b.scratch[:n])
|
|
for i, k := range sorted {
|
|
src := SortKey(k).Submission()
|
|
b.sorted[i] = b.inst[src]
|
|
b.sortedMeta[i] = b.meta[src]
|
|
}
|
|
b.sortedN = n
|
|
}
|
|
|
|
func (b *Batcher) Build() []Batch {
|
|
n := b.sortedN
|
|
b.batches = b.batches[:0]
|
|
if n == 0 {
|
|
return b.batches
|
|
}
|
|
cur := b.sortedMeta[0]
|
|
first := 0
|
|
for i := 1; i <= n; i++ {
|
|
if i < n && b.sortedMeta[i] == cur {
|
|
continue
|
|
}
|
|
b.batches = append(b.batches, Batch{
|
|
First: first,
|
|
Count: i - first,
|
|
Texture: cur.texture,
|
|
Sampler: cur.sampler,
|
|
Blend: cur.blend,
|
|
Clip: cur.clip,
|
|
Scissor: cur.scissor,
|
|
})
|
|
if i < n {
|
|
cur = b.sortedMeta[i]
|
|
first = i
|
|
}
|
|
}
|
|
return b.batches
|
|
}
|
|
|
|
func (b *Batcher) Upload() int {
|
|
n := len(b.inst)
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
raw := unsafe.Slice((*byte)(unsafe.Pointer(&b.sorted[0])), n*InstanceStride)
|
|
b.dev.UpdateBuffer(b.vb, 0, raw)
|
|
return n * InstanceStride
|
|
}
|
|
|
|
func (b *Batcher) Instances() []Instance {
|
|
return b.sorted[:len(b.inst)]
|
|
}
|
|
|
|
func (b *Batcher) Close() {
|
|
b.dev.DestroyBuffer(b.vb)
|
|
}
|
|
|
|
func Layout() gpu.VertexLayout {
|
|
return gpu.VertexLayout{
|
|
Attrs: []gpu.VertexAttr{
|
|
{Location: 0, Slot: 0, Format: gpu.VFFloat4, Offset: 0},
|
|
{Location: 1, Slot: 0, Format: gpu.VFFloat4, Offset: 16},
|
|
{Location: 2, Slot: 0, Format: gpu.VFFloat4, Offset: 32},
|
|
{Location: 3, Slot: 0, Format: gpu.VFUByte4Norm, Offset: 48},
|
|
},
|
|
Strides: [gpu.MaxVertexSlots]uint16{InstanceStride},
|
|
PerInstance: [gpu.MaxVertexSlots]bool{true},
|
|
}
|
|
}
|
|
|
|
func BlendState(m BlendMode) gpu.BlendState {
|
|
switch m {
|
|
case BlendOpaque:
|
|
return gpu.BlendState{}
|
|
case BlendAdditive:
|
|
return gpu.BlendState{Enabled: true, SrcRGB: gpu.BlendOne, DstRGB: gpu.BlendOne, SrcA: gpu.BlendOne, DstA: gpu.BlendOne}
|
|
}
|
|
return gpu.BlendPremultiplied()
|
|
}
|
|
|
|
func ValidateMode(m BlendMode) error {
|
|
if m >= blendCount {
|
|
return errs.New(errs.GPUResource, "unknown blend mode", log.F("mode", uint8(m)))
|
|
}
|
|
return nil
|
|
}
|