graphicscommand: Optimize replace-image calls

Now ReplacePixels command is called only when necessary.

This also ensures that DrawImage must be called after ReplacePixels
is called since there is a potential problem that rendering images
on a texture without initializing by replacing pixels might cause
problems (escpecially on Metal. Perhaps #593 might be related).
This commit is contained in:
Hajime Hoshi 2018-11-30 00:59:31 +01:00
parent e799a0c6a8
commit c6dd0a75d9
4 changed files with 91 additions and 4 deletions

View File

@ -38,17 +38,37 @@ func MaxImageSize() int {
return maxImageSize return maxImageSize
} }
// imageState is a state of an image.
type imageState int
const (
// imageStateInit represents that the image is just allocated and not ready for DrawImages.
imageStateInit imageState = iota
// imageStateReplacePixelsOnly represents that only ReplacePixels is acceptable.
imageStateReplacePixelsOnly
// imageStateDrawable represents that the image is ready to draw with any commands.
imageStateDrawable
// imageStateScreen is the special state for screen framebuffer.
// Only copying image on the screen image is allowed.
imageStateScreen
)
// Image represents an image that is implemented with OpenGL. // Image represents an image that is implemented with OpenGL.
type Image struct { type Image struct {
image graphicsdriver.Image image graphicsdriver.Image
width int width int
height int height int
state imageState
} }
func NewImage(width, height int) *Image { func NewImage(width, height int) *Image {
i := &Image{ i := &Image{
width: width, width: width,
height: height, height: height,
state: imageStateInit, // The screen image must be inited with ReplacePixels first.
} }
c := &newImageCommand{ c := &newImageCommand{
result: i, result: i,
@ -63,6 +83,7 @@ func NewScreenFramebufferImage(width, height int) *Image {
i := &Image{ i := &Image{
width: width, width: width,
height: height, height: height,
state: imageStateScreen,
} }
c := &newScreenFramebufferImageCommand{ c := &newScreenFramebufferImageCommand{
result: i, result: i,
@ -73,6 +94,26 @@ func NewScreenFramebufferImage(width, height int) *Image {
return i return i
} }
// clearByReplacingPixels clears the image by replacing pixels.
//
// The implementation must use replacing-pixels way instead of drawing polygons, since
// some environments (e.g. Metal) require replacing-pixels way as initialization.
func (i *Image) clearByReplacingPixels() {
if i.state != imageStateInit {
panic("not reached")
}
c := &replacePixelsCommand{
dst: i,
pixels: make([]byte, 4*i.width*i.height),
x: 0,
y: 0,
width: i.width,
height: i.height,
}
theCommandQueue.Enqueue(c)
i.state = imageStateDrawable
}
func (i *Image) Dispose() { func (i *Image) Dispose() {
c := &disposeCommand{ c := &disposeCommand{
target: i, target: i,
@ -86,12 +127,41 @@ func (i *Image) Size() (int, int) {
} }
func (i *Image) DrawImage(src *Image, vertices []float32, indices []uint16, clr *affine.ColorM, mode graphics.CompositeMode, filter graphics.Filter) { func (i *Image) DrawImage(src *Image, vertices []float32, indices []uint16, clr *affine.ColorM, mode graphics.CompositeMode, filter graphics.Filter) {
switch i.state {
case imageStateInit:
// Before DrawImage, the image must be initialized with ReplacePixels.
// Especially on Metal, the image might be broken when drawing without initializing.
i.clearByReplacingPixels()
case imageStateReplacePixelsOnly:
panic("not reached")
case imageStateDrawable:
// Do nothing
case imageStateScreen:
if mode != graphics.CompositeModeCopy {
panic("not reached")
}
default:
panic("not reached")
}
theCommandQueue.EnqueueDrawImageCommand(i, src, vertices, indices, clr, mode, filter) theCommandQueue.EnqueueDrawImageCommand(i, src, vertices, indices, clr, mode, filter)
} }
// 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 {
switch i.state {
case imageStateInit:
i.clearByReplacingPixels()
case imageStateReplacePixelsOnly:
// Do nothing
// TODO: Check the region?
case imageStateDrawable:
// Do nothing
case imageStateScreen:
panic("not reached")
default:
panic("not reached")
}
c := &pixelsCommand{ c := &pixelsCommand{
result: nil, result: nil,
img: i, img: i,
@ -102,6 +172,22 @@ func (i *Image) Pixels() []byte {
} }
func (i *Image) ReplacePixels(p []byte, x, y, width, height int) { func (i *Image) ReplacePixels(p []byte, x, y, width, height int) {
switch i.state {
case imageStateInit:
if x == 0 && y == 0 && width == i.width && height == i.height {
i.state = imageStateDrawable
} else {
i.state = imageStateReplacePixelsOnly
}
case imageStateReplacePixelsOnly:
// Do nothing
case imageStateDrawable:
// Do nothing
case imageStateScreen:
panic("not reached")
default:
panic("not reached")
}
pixels := make([]byte, len(p)) pixels := make([]byte, len(p))
copy(pixels, p) copy(pixels, p)
c := &replacePixelsCommand{ c := &replacePixelsCommand{

View File

@ -146,6 +146,8 @@ func (c *context) newTexture(width, height int) (textureNative, error) {
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
// If data is nil, this just allocates memory and the content is undefined.
// https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, nil) gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, int32(width), int32(height), 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
return nil return nil
}) })

View File

@ -78,7 +78,6 @@ func newImageWithoutInit(width, height int, volatile bool) *Image {
// Note that Dispose is not called automatically. // Note that Dispose is not called automatically.
func NewImage(width, height int, volatile bool) *Image { func NewImage(width, height int, volatile bool) *Image {
i := newImageWithoutInit(width, height, volatile) i := newImageWithoutInit(width, height, volatile)
i.ReplacePixels(nil, 0, 0, width, height)
return i return i
} }
@ -94,7 +93,6 @@ func NewScreenFramebufferImage(width, height int) *Image {
screen: true, screen: true,
} }
theImages.add(i) theImages.add(i)
// The screen image doesn't have to be cleared.
return i return i
} }
@ -163,7 +161,7 @@ func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) {
vs := graphics.QuadVertices(w2, h2, 0, 0, dw, dh, vs := graphics.QuadVertices(w2, h2, 0, 0, dw, dh,
float32(width)/float32(dw), 0, 0, float32(height)/float32(dh), float32(width)/float32(dw), 0, 0, float32(height)/float32(dh),
float32(x), float32(y), float32(x), float32(y),
0, 0, 0, 0) 1, 1, 1, 1)
is := graphics.QuadIndices() is := graphics.QuadIndices()
i.image.DrawImage(dummyImage.image, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest) i.image.DrawImage(dummyImage.image, vs, is, nil, graphics.CompositeModeCopy, graphics.FilterNearest)
} }

View File

@ -319,7 +319,8 @@ func (i *Image) dispose(markDisposed bool) {
if !i.backend.page.IsEmpty() { if !i.backend.page.IsEmpty() {
// As this part can be reused, this should be cleared explicitly. // As this part can be reused, this should be cleared explicitly.
x, y, w, h := i.region() x, y, w, h := i.region()
i.backend.restorable.ReplacePixels(nil, x, y, w, h) // TODO: Now nil cannot be used here (see the test result). Fix this.
i.backend.restorable.ReplacePixels(make([]byte, 4*w*h), x, y, w, h)
return return
} }