ebiten/internal/graphicscommand/command.go
Hajime Hoshi 49582519c1 all: add a compiler directive kage:unit
This change adds a new compiler directive 'kage:unit' to Kage. This
takes one of these two values: 'pixel' and 'texel'. The default value
is 'texel'.

With the pixel-unit mode, all the built-in functions treats pixels
instead of texels, and the texCoord argument of Fragment is in pixels.
This simplifies shader programs as programs no longer have the notion
of texels.

With the texel-unit mode, the behavior is the same as the current
behavior.

Closes #1431
2023-04-23 22:11:57 +09:00

691 lines
19 KiB
Go

// Copyright 2016 Hajime Hoshi
//
// 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
import (
"fmt"
"math"
"strings"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
// command represents a drawing command.
//
// A command for drawing that is created when Image functions are called like DrawTriangles,
// or Fill.
// A command is not immediately executed after created. Instead, it is queued after created,
// and executed only when necessary.
type command interface {
fmt.Stringer
Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error
}
type drawTrianglesCommandPool struct {
pool []*drawTrianglesCommand
}
func (p *drawTrianglesCommandPool) get() *drawTrianglesCommand {
if len(p.pool) == 0 {
return &drawTrianglesCommand{}
}
v := p.pool[len(p.pool)-1]
p.pool = p.pool[:len(p.pool)-1]
return v
}
func (p *drawTrianglesCommandPool) put(v *drawTrianglesCommand) {
if len(p.pool) >= 1024 {
return
}
p.pool = append(p.pool, v)
}
// commandQueue is a command queue for drawing commands.
type commandQueue struct {
// commands is a queue of drawing commands.
commands []command
// vertices represents a vertices data in OpenGL's array buffer.
vertices []float32
indices []uint16
tmpNumVertexFloats int
drawTrianglesCommandPool drawTrianglesCommandPool
uint32sBuffer uint32sBuffer
}
// theCommandQueue is the command queue for the current process.
var theCommandQueue = &commandQueue{}
func (q *commandQueue) appendIndices(indices []uint16, offset uint16) {
n := len(q.indices)
q.indices = append(q.indices, indices...)
for i := range indices {
q.indices[n+i] += offset
}
}
// mustUseDifferentVertexBuffer reports whether a different vertex buffer must be used.
func mustUseDifferentVertexBuffer(nextNumVertexFloats int) bool {
return nextNumVertexFloats > graphics.MaxVertexFloatsCount
}
// EnqueueDrawTrianglesCommand enqueues a drawing-image command.
func (q *commandQueue) EnqueueDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageCount]*Image, offsets [graphics.ShaderImageCount - 1][2]float32, vertices []float32, indices []uint16, blend graphicsdriver.Blend, dstRegion, srcRegion graphicsdriver.Region, shader *Shader, uniforms []uint32, evenOdd bool) {
if len(vertices) > graphics.MaxVertexFloatsCount {
panic(fmt.Sprintf("graphicscommand: len(vertices) must equal to or less than %d but was %d", graphics.MaxVertexFloatsCount, len(vertices)))
}
split := false
if mustUseDifferentVertexBuffer(q.tmpNumVertexFloats + len(vertices)) {
q.tmpNumVertexFloats = 0
split = true
}
// Assume that all the image sizes are same.
// Assume that the images are packed from the front in the slice srcs.
q.vertices = append(q.vertices, vertices...)
q.appendIndices(indices, uint16(q.tmpNumVertexFloats/graphics.VertexFloatCount))
q.tmpNumVertexFloats += len(vertices)
// prependPreservedUniforms not only prepends values to the given slice but also creates a new slice.
// Allocating a new slice is necessary to make EnqueueDrawTrianglesCommand safe so far.
// TODO: This might cause a performance issue (#2601).
uniforms = q.prependPreservedUniforms(uniforms, shader, dst, srcs, offsets, dstRegion, srcRegion)
// Remove unused uniform variables so that more commands can be merged.
shader.ir.FilterUniformVariables(uniforms)
// TODO: If dst is the screen, reorder the command to be the last.
if !split && 0 < len(q.commands) {
if last, ok := q.commands[len(q.commands)-1].(*drawTrianglesCommand); ok {
if last.CanMergeWithDrawTrianglesCommand(dst, srcs, vertices, blend, shader, uniforms, evenOdd) {
last.setVertices(q.lastVertices(len(vertices) + last.numVertices()))
if last.dstRegions[len(last.dstRegions)-1].Region == dstRegion {
last.dstRegions[len(last.dstRegions)-1].IndexCount += len(indices)
} else {
last.dstRegions = append(last.dstRegions, graphicsdriver.DstRegion{
Region: dstRegion,
IndexCount: len(indices),
})
}
return
}
}
}
c := q.drawTrianglesCommandPool.get()
c.dst = dst
c.srcs = srcs
c.vertices = q.lastVertices(len(vertices))
c.blend = blend
c.dstRegions = []graphicsdriver.DstRegion{
{
Region: dstRegion,
IndexCount: len(indices),
},
}
c.shader = shader
c.uniforms = uniforms
c.evenOdd = evenOdd
q.commands = append(q.commands, c)
}
func (q *commandQueue) lastVertices(n int) []float32 {
return q.vertices[len(q.vertices)-n : len(q.vertices)]
}
// Enqueue enqueues a drawing command other than a draw-triangles command.
//
// For a draw-triangles command, use EnqueueDrawTrianglesCommand.
func (q *commandQueue) Enqueue(command command) {
// TODO: If dst is the screen, reorder the command to be the last.
q.commands = append(q.commands, command)
}
// Flush flushes the command queue.
func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bool) (err error) {
runOnRenderThread(func() {
err = q.flush(graphicsDriver, endFrame)
})
if endFrame {
q.uint32sBuffer.reset()
}
return
}
// flush must be called the main thread.
func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bool) (err error) {
// If endFrame is true, Begin/End should be called to ensure the framebuffer is swapped.
if len(q.commands) == 0 && !endFrame {
return nil
}
es := q.indices
vs := q.vertices
debug.Logf("Graphics commands:\n")
if err := graphicsDriver.Begin(); err != nil {
return err
}
defer func() {
// Call End even if an error causes, or the graphics driver's state might be stale (#2388).
if err1 := graphicsDriver.End(endFrame); err1 != nil && err == nil {
err = err1
}
// Release the commands explicitly (#1803).
// Apparently, the part of a slice between len and cap-1 still holds references.
// Then, resetting the length by [:0] doesn't release the references.
for i, c := range q.commands {
if c, ok := c.(*drawTrianglesCommand); ok {
q.drawTrianglesCommandPool.put(c)
}
q.commands[i] = nil
}
q.commands = q.commands[:0]
q.vertices = q.vertices[:0]
q.indices = q.indices[:0]
q.tmpNumVertexFloats = 0
}()
cs := q.commands
for len(cs) > 0 {
nv := 0
ne := 0
nc := 0
for _, c := range cs {
if dtc, ok := c.(*drawTrianglesCommand); ok {
if nc > 0 && mustUseDifferentVertexBuffer(nv+dtc.numVertices()) {
break
}
nv += dtc.numVertices()
ne += dtc.numIndices()
}
nc++
}
if 0 < ne {
if err := graphicsDriver.SetVertices(vs[:nv], es[:ne]); err != nil {
return err
}
es = es[ne:]
vs = vs[nv:]
}
indexOffset := 0
for _, c := range cs[:nc] {
if err := c.Exec(graphicsDriver, indexOffset); err != nil {
return err
}
debug.Logf(" %s\n", c)
// TODO: indexOffset should be reset if the command type is different
// from the previous one. This fix is needed when another drawing command is
// introduced than drawTrianglesCommand.
if dtc, ok := c.(*drawTrianglesCommand); ok {
indexOffset += dtc.numIndices()
}
}
cs = cs[nc:]
}
return nil
}
// FlushCommands flushes the command queue and present the screen if needed.
// If endFrame is true, the current screen might be used to present.
func FlushCommands(graphicsDriver graphicsdriver.Graphics, endFrame bool) error {
flushImageBuffers()
return theCommandQueue.Flush(graphicsDriver, endFrame)
}
// drawTrianglesCommand represents a drawing command to draw an image on another image.
type drawTrianglesCommand struct {
dst *Image
srcs [graphics.ShaderImageCount]*Image
vertices []float32
blend graphicsdriver.Blend
dstRegions []graphicsdriver.DstRegion
shader *Shader
uniforms []uint32
evenOdd bool
}
func (c *drawTrianglesCommand) String() string {
// TODO: Improve readability
blend := fmt.Sprintf("{src-color: %d, src-alpha: %d, dst-color: %d, dst-alpha: %d, op-color: %d, op-alpha: %d}",
c.blend.BlendFactorSourceRGB,
c.blend.BlendFactorSourceAlpha,
c.blend.BlendFactorDestinationRGB,
c.blend.BlendFactorDestinationAlpha,
c.blend.BlendOperationRGB,
c.blend.BlendOperationAlpha)
dst := fmt.Sprintf("%d", c.dst.id)
if c.dst.screen {
dst += " (screen)"
}
var srcstrs [graphics.ShaderImageCount]string
for i, src := range c.srcs {
if src == nil {
srcstrs[i] = "(nil)"
continue
}
srcstrs[i] = fmt.Sprintf("%d", src.id)
if src.screen {
srcstrs[i] += " (screen)"
}
}
return fmt.Sprintf("draw-triangles: dst: %s <- src: [%s], num of dst regions: %d, num of indices: %d, blend: %s, even-odd: %t", dst, strings.Join(srcstrs[:], ", "), len(c.dstRegions), c.numIndices(), blend, c.evenOdd)
}
// Exec executes the drawTrianglesCommand.
func (c *drawTrianglesCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
// TODO: Is it ok not to bind any framebuffer here?
if len(c.dstRegions) == 0 {
return nil
}
var imgs [graphics.ShaderImageCount]graphicsdriver.ImageID
for i, src := range c.srcs {
if src == nil {
imgs[i] = graphicsdriver.InvalidImageID
continue
}
imgs[i] = src.image.ID()
}
return graphicsDriver.DrawTriangles(c.dst.image.ID(), imgs, c.shader.shader.ID(), c.dstRegions, indexOffset, c.blend, c.uniforms, c.evenOdd)
}
func (c *drawTrianglesCommand) numVertices() int {
return len(c.vertices)
}
func (c *drawTrianglesCommand) numIndices() int {
var nindices int
for _, dstRegion := range c.dstRegions {
nindices += dstRegion.IndexCount
}
return nindices
}
func (c *drawTrianglesCommand) setVertices(vertices []float32) {
c.vertices = vertices
}
// CanMergeWithDrawTrianglesCommand returns a boolean value indicating whether the other drawTrianglesCommand can be merged
// with the drawTrianglesCommand c.
func (c *drawTrianglesCommand) CanMergeWithDrawTrianglesCommand(dst *Image, srcs [graphics.ShaderImageCount]*Image, vertices []float32, blend graphicsdriver.Blend, shader *Shader, uniforms []uint32, evenOdd bool) bool {
if c.shader != shader {
return false
}
if len(c.uniforms) != len(uniforms) {
return false
}
for i := range c.uniforms {
if c.uniforms[i] != uniforms[i] {
return false
}
}
if c.dst != dst {
return false
}
if c.srcs != srcs {
return false
}
if c.blend != blend {
return false
}
if c.evenOdd != evenOdd {
return false
}
if c.evenOdd && mightOverlapDstRegions(c.vertices, vertices) {
return false
}
return true
}
var (
posInf32 = float32(math.Inf(1))
negInf32 = float32(math.Inf(-1))
)
func dstRegionFromVertices(vertices []float32) (minX, minY, maxX, maxY float32) {
minX = posInf32
minY = posInf32
maxX = negInf32
maxY = negInf32
for i := 0; i < len(vertices)/graphics.VertexFloatCount; i++ {
x := vertices[graphics.VertexFloatCount*i]
y := vertices[graphics.VertexFloatCount*i+1]
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if maxX < x {
maxX = x
}
if maxY < y {
maxY = y
}
}
return
}
func mightOverlapDstRegions(vertices1, vertices2 []float32) bool {
minX1, minY1, maxX1, maxY1 := dstRegionFromVertices(vertices1)
minX2, minY2, maxX2, maxY2 := dstRegionFromVertices(vertices2)
const mergin = 1
return minX1 < maxX2+mergin && minX2 < maxX1+mergin && minY1 < maxY2+mergin && minY2 < maxY1+mergin
}
// writePixelsCommand represents a command to replace pixels of an image.
type writePixelsCommand struct {
dst *Image
args []*graphicsdriver.WritePixelsArgs
}
func (c *writePixelsCommand) String() string {
return fmt.Sprintf("write-pixels: dst: %d, len(args): %d", c.dst.id, len(c.args))
}
// Exec executes the writePixelsCommand.
func (c *writePixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
if len(c.args) == 0 {
return nil
}
if err := c.dst.image.WritePixels(c.args); err != nil {
return err
}
return nil
}
type readPixelsCommand struct {
result []byte
img *Image
x int
y int
width int
height int
}
// Exec executes a readPixelsCommand.
func (c *readPixelsCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
if err := c.img.image.ReadPixels(c.result, c.x, c.y, c.width, c.height); err != nil {
return err
}
return nil
}
func (c *readPixelsCommand) String() string {
return fmt.Sprintf("read-pixels: image: %d", c.img.id)
}
// disposeImageCommand represents a command to dispose an image.
type disposeImageCommand struct {
target *Image
}
func (c *disposeImageCommand) String() string {
return fmt.Sprintf("dispose-image: target: %d", c.target.id)
}
// Exec executes the disposeImageCommand.
func (c *disposeImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
c.target.image.Dispose()
return nil
}
// disposeShaderCommand represents a command to dispose a shader.
type disposeShaderCommand struct {
target *Shader
}
func (c *disposeShaderCommand) String() string {
return fmt.Sprintf("dispose-shader: target")
}
// Exec executes the disposeShaderCommand.
func (c *disposeShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
c.target.shader.Dispose()
return nil
}
// newImageCommand represents a command to create an empty image with given width and height.
type newImageCommand struct {
result *Image
width int
height int
screen bool
}
func (c *newImageCommand) String() string {
return fmt.Sprintf("new-image: result: %d, width: %d, height: %d, screen: %t", c.result.id, c.width, c.height, c.screen)
}
// Exec executes a newImageCommand.
func (c *newImageCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
var err error
if c.screen {
c.result.image, err = graphicsDriver.NewScreenFramebufferImage(c.width, c.height)
} else {
c.result.image, err = graphicsDriver.NewImage(c.width, c.height)
}
return err
}
// newShaderCommand is a command to create a shader.
type newShaderCommand struct {
result *Shader
ir *shaderir.Program
}
func (c *newShaderCommand) String() string {
return fmt.Sprintf("new-shader")
}
// Exec executes a newShaderCommand.
func (c *newShaderCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
s, err := graphicsDriver.NewShader(c.ir)
if err != nil {
return err
}
c.result.shader = s
return nil
}
type isInvalidatedCommand struct {
result bool
image *Image
}
func (c *isInvalidatedCommand) String() string {
return fmt.Sprintf("is-invalidated: image: %d", c.image.id)
}
func (c *isInvalidatedCommand) Exec(graphicsDriver graphicsdriver.Graphics, indexOffset int) error {
c.result = c.image.image.IsInvalidated()
return nil
}
// InitializeGraphicsDriverState initialize the current graphics driver state.
func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error) {
runOnRenderThread(func() {
err = graphicsDriver.Initialize()
})
return
}
// ResetGraphicsDriverState resets the current graphics driver state.
// If the graphics driver doesn't have an API to reset, ResetGraphicsDriverState does nothing.
func ResetGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) (err error) {
if r, ok := graphicsDriver.(graphicsdriver.Resetter); ok {
runOnRenderThread(func() {
err = r.Reset()
})
}
return nil
}
// MaxImageSize returns the maximum size of an image.
func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
var size int
runOnRenderThread(func() {
size = graphicsDriver.MaxImageSize()
})
return size
}
func max(a, b int) int {
if a < b {
return b
}
return a
}
func roundUpPower2(x int) int {
p2 := 1
for p2 < x {
p2 *= 2
}
return p2
}
func (q *commandQueue) prependPreservedUniforms(uniforms []uint32, shader *Shader, dst *Image, srcs [graphics.ShaderImageCount]*Image, offsets [graphics.ShaderImageCount - 1][2]float32, dstRegion, srcRegion graphicsdriver.Region) []uint32 {
origUniforms := uniforms
uniforms = q.uint32sBuffer.alloc(len(origUniforms) + graphics.PreservedUniformUint32Count)
copy(uniforms[graphics.PreservedUniformUint32Count:], origUniforms)
var idx int
// Set the destination texture size.
dw, dh := dst.InternalSize()
uniforms[idx+0] = math.Float32bits(float32(dw))
uniforms[idx+1] = math.Float32bits(float32(dh))
idx += 2
// Set the source texture sizes.
for i, src := range srcs {
if src == nil {
continue
}
w, h := src.InternalSize()
uniforms[idx+2*i] = math.Float32bits(float32(w))
uniforms[idx+2*i+1] = math.Float32bits(float32(h))
}
idx += len(srcs) * 2
if shader.unit() == shaderir.Texel {
dstRegion.X /= float32(dw)
dstRegion.Y /= float32(dh)
dstRegion.Width /= float32(dw)
dstRegion.Height /= float32(dh)
}
// Set the destination region.
uniforms[idx+0] = math.Float32bits(dstRegion.X)
uniforms[idx+1] = math.Float32bits(dstRegion.Y)
idx += 2
uniforms[idx+0] = math.Float32bits(dstRegion.Width)
uniforms[idx+1] = math.Float32bits(dstRegion.Height)
idx += 2
if shader.unit() == shaderir.Texel && srcs[0] != nil {
w, h := srcs[0].InternalSize()
srcRegion.X /= float32(w)
srcRegion.Y /= float32(h)
srcRegion.Width /= float32(w)
srcRegion.Height /= float32(h)
for i := range offsets {
offsets[i][0] /= float32(w)
offsets[i][1] /= float32(h)
}
}
// Set the source offsets.
for i, offset := range offsets {
uniforms[idx+2*i] = math.Float32bits(offset[0])
uniforms[idx+2*i+1] = math.Float32bits(offset[1])
}
idx += len(offsets) * 2
// Set the source region of texture0.
uniforms[idx+0] = math.Float32bits(srcRegion.X)
uniforms[idx+1] = math.Float32bits(srcRegion.Y)
idx += 2
uniforms[idx+0] = math.Float32bits(srcRegion.Width)
uniforms[idx+1] = math.Float32bits(srcRegion.Height)
idx += 2
uniforms[idx+0] = math.Float32bits(2 / float32(dw))
uniforms[idx+1] = 0
uniforms[idx+2] = 0
uniforms[idx+3] = 0
uniforms[idx+4] = 0
uniforms[idx+5] = math.Float32bits(2 / float32(dh))
uniforms[idx+6] = 0
uniforms[idx+7] = 0
uniforms[idx+8] = 0
uniforms[idx+9] = 0
uniforms[idx+10] = math.Float32bits(1)
uniforms[idx+11] = 0
uniforms[idx+12] = math.Float32bits(-1)
uniforms[idx+13] = math.Float32bits(-1)
uniforms[idx+14] = 0
uniforms[idx+15] = math.Float32bits(1)
idx += 16
return uniforms
}
// uint32sBuffer is a reusable buffer to allocate []uint32.
type uint32sBuffer struct {
buf [2][]uint32
// index is switched at the end of the frame,
// and the buffer of the original index is kept until the next frame ends.
index int
}
func (b *uint32sBuffer) alloc(n int) []uint32 {
buf := b.buf[b.index]
if len(buf)+n > cap(buf) {
buf = make([]uint32, 0, max(roundUpPower2(len(buf)+n), 16))
}
s := buf[len(buf) : len(buf)+n]
b.buf[b.index] = buf[:len(buf)+n]
return s
}
func (b *uint32sBuffer) reset() {
b.buf[b.index] = b.buf[b.index][:0]
b.index++
b.index %= len(b.buf)
}