graphics: Delay draw commands and execute them only when needed

This change introduces a queue for delayed graphics commands.
When an image's pixels are retrieved or the screen is rendered,
Ebiten calculates the set of the necessary draw commands and
execute them.

This reduces the number of draw calls especially for the launching
phase.

Fixes #921
This commit is contained in:
Hajime Hoshi 2019-09-19 00:51:55 +09:00
parent 2621681a2a
commit 0c70823f27
3 changed files with 122 additions and 6 deletions

View File

@ -488,7 +488,9 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
float32(r.Min.X), float32(r.Min.Y), float32(r.Max.X), float32(r.Max.Y), float32(r.Min.X), float32(r.Min.Y), float32(r.Max.X), float32(r.Max.Y),
v.ColorR, v.ColorG, v.ColorB, v.ColorA) v.ColorR, v.ColorG, v.ColorB, v.ColorA)
} }
i.mipmap.original().DrawTriangles(src, vs, indices, options.ColorM.impl, mode, filter, driver.Address(options.Address)) is := make([]uint16, len(indices))
copy(is, indices)
i.mipmap.original().DrawTriangles(src, vs, is, options.ColorM.impl, mode, filter, driver.Address(options.Address))
i.disposeMipmaps() i.disposeMipmaps()
} }

View File

@ -53,6 +53,7 @@ type command interface {
AddNumVertices(n int) AddNumVertices(n int)
AddNumIndices(n int) AddNumIndices(n int)
CanMerge(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool CanMerge(dst, src *Image, color *affine.ColorM, mode driver.CompositeMode, filter driver.Filter, address driver.Address) bool
Dst() *Image
} }
type size struct { type size struct {
@ -412,6 +413,10 @@ func (c *drawTrianglesCommand) CanMerge(dst, src *Image, color *affine.ColorM, m
return true return true
} }
func (c *drawTrianglesCommand) Dst() *Image {
return c.dst
}
// replacePixelsCommand represents a command to replace pixels of an image. // replacePixelsCommand represents a command to replace pixels of an image.
type replacePixelsCommand struct { type replacePixelsCommand struct {
dst *Image dst *Image
@ -450,6 +455,10 @@ func (c *replacePixelsCommand) CanMerge(dst, src *Image, color *affine.ColorM, m
return false return false
} }
func (c *replacePixelsCommand) Dst() *Image {
return c.dst
}
type pixelsCommand struct { type pixelsCommand struct {
result []byte result []byte
img *Image img *Image
@ -487,6 +496,10 @@ func (c *pixelsCommand) CanMerge(dst, src *Image, color *affine.ColorM, mode dri
return false return false
} }
func (c *pixelsCommand) Dst() *Image {
return c.img
}
// disposeCommand represents a command to dispose an image. // disposeCommand represents a command to dispose an image.
type disposeCommand struct { type disposeCommand struct {
target *Image target *Image
@ -520,6 +533,10 @@ func (c *disposeCommand) CanMerge(dst, src *Image, color *affine.ColorM, mode dr
return false return false
} }
func (c *disposeCommand) Dst() *Image {
return c.target
}
// newImageCommand represents a command to create an empty image with given width and height. // newImageCommand represents a command to create an empty image with given width and height.
type newImageCommand struct { type newImageCommand struct {
result *Image result *Image
@ -559,6 +576,10 @@ func (c *newImageCommand) CanMerge(dst, src *Image, color *affine.ColorM, mode d
return false return false
} }
func (c *newImageCommand) Dst() *Image {
return c.result
}
// newScreenFramebufferImageCommand is a command to create a special image for the screen. // newScreenFramebufferImageCommand is a command to create a special image for the screen.
type newScreenFramebufferImageCommand struct { type newScreenFramebufferImageCommand struct {
result *Image result *Image
@ -595,6 +616,10 @@ func (c *newScreenFramebufferImageCommand) CanMerge(dst, src *Image, color *affi
return false return false
} }
func (c *newScreenFramebufferImageCommand) Dst() *Image {
return c.result
}
// ResetGraphicsDriverState resets or initializes the current graphics driver state. // ResetGraphicsDriverState resets or initializes the current graphics driver state.
func ResetGraphicsDriverState() error { func ResetGraphicsDriverState() error {
return theGraphicsDriver.Reset() return theGraphicsDriver.Reset()

View File

@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"image" "image"
"os" "os"
"sort"
"strings" "strings"
"github.com/hajimehoshi/ebiten/internal/affine" "github.com/hajimehoshi/ebiten/internal/affine"
@ -56,6 +57,79 @@ func genNextID() int {
return id return id
} }
var delayedCommands []interface{}
type drawTrianglesCommandArgs struct {
dst *Image
src *Image
vertices []float32
indices []uint16
color *affine.ColorM
mode driver.CompositeMode
filter driver.Filter
address driver.Address
}
func enqueueDelayedCommand(c interface{}) {
delayedCommands = append(delayedCommands, c)
}
func flushDelayedCommandsFor(dstID int) {
// Choose commands that are needed to solve an image that ID is dstID.
indices := map[int]struct{}{}
ids := map[int]struct{}{dstID: {}}
for len(ids) > 0 {
newIDs := map[int]struct{}{}
for i, c := range delayedCommands {
if _, ok := indices[i]; ok {
continue
}
switch c := c.(type) {
case *drawTrianglesCommandArgs:
if _, ok := ids[c.dst.id]; ok {
indices[i] = struct{}{}
newIDs[c.src.id] = struct{}{}
}
case command:
if _, ok := ids[c.Dst().id]; ok {
indices[i] = struct{}{}
}
default:
panic(fmt.Sprintf("graphicscommands: unexpected command: %v", c))
}
}
ids = newIDs
}
// Enqueue the chosen commands. Replace the used command with nil.
var sortedIndices []int
for i := range indices {
sortedIndices = append(sortedIndices, i)
}
sort.Ints(sortedIndices)
for _, i := range sortedIndices {
switch c := delayedCommands[i].(type) {
case *drawTrianglesCommandArgs:
theCommandQueue.EnqueueDrawTrianglesCommand(c.dst, c.src, c.vertices, c.indices, c.color, c.mode, c.filter, c.address)
case command:
theCommandQueue.Enqueue(c)
default:
panic(fmt.Sprintf("graphicscommands: unexpected command: %v", c))
}
delayedCommands[i] = nil
}
// Remove the used commands from delayedCommands.
var newDelayedCommands []interface{}
for _, c := range delayedCommands {
if c == nil {
continue
}
newDelayedCommands = append(newDelayedCommands, c)
}
delayedCommands = newDelayedCommands
}
// NewImage returns a new image. // NewImage returns a new image.
// //
// Note that the image is not initialized yet. // Note that the image is not initialized yet.
@ -72,7 +146,7 @@ func NewImage(width, height int) *Image {
width: width, width: width,
height: height, height: height,
} }
theCommandQueue.Enqueue(c) enqueueDelayedCommand(c)
return i return i
} }
@ -90,15 +164,16 @@ func NewScreenFramebufferImage(width, height int) *Image {
width: width, width: width,
height: height, height: height,
} }
theCommandQueue.Enqueue(c) enqueueDelayedCommand(c)
return i return i
} }
func (i *Image) Dispose() { func (i *Image) Dispose() {
// TODO: We can remove unnecessary commands from delayedCommand.
c := &disposeCommand{ c := &disposeCommand{
target: i, target: i,
} }
theCommandQueue.Enqueue(c) enqueueDelayedCommand(c)
} }
func (i *Image) InternalSize() (int, int) { func (i *Image) InternalSize() (int, int) {
@ -116,7 +191,20 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
} }
} }
theCommandQueue.EnqueueDrawTrianglesCommand(i, src, vertices, indices, clr, mode, filter, address) // vertices and indices are created internally and it is fine to assume they are immutable.
enqueueDelayedCommand(&drawTrianglesCommandArgs{
dst: i,
src: src,
vertices: vertices,
indices: indices,
color: clr,
mode: mode,
filter: filter,
address: address,
})
if i.screen {
flushDelayedCommandsFor(i.id)
}
if i.lastCommand == lastCommandNone && !i.screen { if i.lastCommand == lastCommandNone && !i.screen {
i.lastCommand = lastCommandClear i.lastCommand = lastCommandClear
@ -128,6 +216,7 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
// Pixels returns the image's pixels. // Pixels returns the image's pixels.
// Pixels might return nil when OpenGL error happens. // Pixels might return nil when OpenGL error happens.
func (i *Image) Pixels() []byte { func (i *Image) Pixels() []byte {
flushDelayedCommandsFor(i.id)
c := &pixelsCommand{ c := &pixelsCommand{
result: nil, result: nil,
img: i, img: i,
@ -154,7 +243,7 @@ func (i *Image) ReplacePixels(p []byte, x, y, width, height int) {
width: width, width: width,
height: height, height: height,
} }
theCommandQueue.Enqueue(c) enqueueDelayedCommand(c)
i.lastCommand = lastCommandReplacePixels i.lastCommand = lastCommandReplacePixels
} }