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),
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()
}

View File

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

View File

@ -18,6 +18,7 @@ import (
"fmt"
"image"
"os"
"sort"
"strings"
"github.com/hajimehoshi/ebiten/internal/affine"
@ -56,6 +57,79 @@ func genNextID() int {
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.
//
// Note that the image is not initialized yet.
@ -72,7 +146,7 @@ func NewImage(width, height int) *Image {
width: width,
height: height,
}
theCommandQueue.Enqueue(c)
enqueueDelayedCommand(c)
return i
}
@ -90,15 +164,16 @@ func NewScreenFramebufferImage(width, height int) *Image {
width: width,
height: height,
}
theCommandQueue.Enqueue(c)
enqueueDelayedCommand(c)
return i
}
func (i *Image) Dispose() {
// TODO: We can remove unnecessary commands from delayedCommand.
c := &disposeCommand{
target: i,
}
theCommandQueue.Enqueue(c)
enqueueDelayedCommand(c)
}
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 {
i.lastCommand = lastCommandClear
@ -128,6 +216,7 @@ func (i *Image) DrawTriangles(src *Image, vertices []float32, indices []uint16,
// Pixels returns the image's pixels.
// Pixels might return nil when OpenGL error happens.
func (i *Image) Pixels() []byte {
flushDelayedCommandsFor(i.id)
c := &pixelsCommand{
result: nil,
img: i,
@ -154,7 +243,7 @@ func (i *Image) ReplacePixels(p []byte, x, y, width, height int) {
width: width,
height: height,
}
theCommandQueue.Enqueue(c)
enqueueDelayedCommand(c)
i.lastCommand = lastCommandReplacePixels
}