graphicsdriver/metal: Avoid synching texture data at ReplacePixels

Instead, use a temporary texture, transfer texture data to it, and
copy the data on GPU side.
This commit is contained in:
Hajime Hoshi 2020-06-28 18:15:25 +09:00
parent 383ff1e6a7
commit ebd0e11c0d
6 changed files with 124 additions and 57 deletions

View File

@ -326,7 +326,7 @@ type Graphics struct {
transparent bool
maxImageSize int
drawCalled bool
tmpTexture mtl.Texture
t *thread.Thread
@ -353,7 +353,7 @@ func (g *Graphics) Begin() {
}
func (g *Graphics) End() {
g.flush(false, true)
g.flushIfNeeded(false, true)
g.t.Call(func() error {
g.screenDrawable = ca.MetalDrawable{}
C.releaseAutoreleasePool(g.pool)
@ -390,7 +390,7 @@ func (g *Graphics) SetVertices(vertices []float32, indices []uint16) {
})
}
func (g *Graphics) flush(wait bool, present bool) {
func (g *Graphics) flushIfNeeded(wait bool, present bool) {
g.t.Call(func() error {
if g.cb == (mtl.CommandBuffer{}) {
return nil
@ -639,8 +639,6 @@ func (g *Graphics) Draw(dstID, srcID driver.ImageID, indexLen int, indexOffset i
dst := g.images[dstID]
src := g.images[srcID]
g.drawCalled = true
if err := g.t.Call(func() error {
g.view.update()
@ -841,7 +839,7 @@ func (i *Image) syncTexture() {
i.graphics.t.Call(func() error {
if i.graphics.cb != (mtl.CommandBuffer{}) {
panic("metal: command buffer must be empty at syncTexture: flush is not called yet?")
panic("metal: command buffer must be empty at syncTexture: flushIfNeeded is not called yet?")
}
cb := i.graphics.cq.MakeCommandBuffer()
@ -855,7 +853,7 @@ func (i *Image) syncTexture() {
}
func (i *Image) Pixels() ([]byte, error) {
i.graphics.flush(true, false)
i.graphics.flushIfNeeded(true, false)
i.syncTexture()
b := make([]byte, 4*i.width*i.height)
@ -868,33 +866,12 @@ func (i *Image) Pixels() ([]byte, error) {
return b, nil
}
// needsToSyncBeforeReplaceRegion reports whether syncTexture is needed before calling ReplaceRegion.
//
// Before a part region is updated, the texture data needs to be loaded to CPU. Otherwise, the remaining region will
// be uninitialized (#1213).
func (i *Image) needsToSyncBeforeReplaceRegion(args []*driver.ReplacePixelsArgs) bool {
if len(args) == 0 {
return false
}
if len(args) > 1 {
return true
}
if a := args[0]; a.X == 0 && a.Y == 0 && a.Width == i.width && a.Height == i.height {
return false
}
return true
}
func (i *Image) ReplacePixels(args []*driver.ReplacePixelsArgs) {
g := i.graphics
if g.drawCalled {
g.flush(true, false)
g.drawCalled = false
if i.needsToSyncBeforeReplaceRegion(args) {
i.syncTexture()
}
}
g.flushIfNeeded(true, false)
// If the memory is shared (e.g., iOS), texture data doen't have to be synced. Send the data directly.
if storageMode == mtl.StorageModeShared {
g.t.Call(func() error {
for _, a := range args {
i.texture.ReplaceRegion(mtl.Region{
@ -904,4 +881,47 @@ func (i *Image) ReplacePixels(args []*driver.ReplacePixelsArgs) {
}
return nil
})
return
}
// If the memory is managed (e.g., macOS), texture data cannot be sent to the destination directly because
// this requires synchronizing data between CPU and GPU. As synchronizing is inefficient, let's send the
// data to a temporary texture once, and then copy it in GPU.
g.t.Call(func() error {
w, h := i.texture.Width(), i.texture.Height()
if g.tmpTexture == (mtl.Texture{}) || w > g.tmpTexture.Width() || h > g.tmpTexture.Height() {
if g.tmpTexture != (mtl.Texture{}) {
g.tmpTexture.Release()
}
td := mtl.TextureDescriptor{
TextureType: mtl.TextureType2D,
PixelFormat: mtl.PixelFormatRGBA8UNorm,
Width: w,
Height: h,
StorageMode: storageMode,
Usage: mtl.TextureUsageShaderRead | mtl.TextureUsageRenderTarget,
}
g.tmpTexture = g.view.getMTLDevice().MakeTexture(td)
}
for _, a := range args {
g.tmpTexture.ReplaceRegion(mtl.Region{
Origin: mtl.Origin{X: a.X, Y: a.Y, Z: 0},
Size: mtl.Size{Width: a.Width, Height: a.Height, Depth: 1},
}, 0, unsafe.Pointer(&a.Pixels[0]), 4*a.Width)
}
if g.cb == (mtl.CommandBuffer{}) {
g.cb = i.graphics.cq.MakeCommandBuffer()
}
bce := g.cb.MakeBlitCommandEncoder()
for _, a := range args {
o := mtl.Origin{X: a.X, Y: a.Y, Z: 0}
s := mtl.Size{Width: a.Width, Height: a.Height, Depth: 1}
bce.CopyFromTexture(g.tmpTexture, 0, 0, o, s, i.texture, 0, 0, o)
}
bce.EndEncoding()
return nil
})
}

View File

@ -182,9 +182,9 @@ fragment float4 FragmentShader(Vertex in [[stage_in]]) {
cb.WaitUntilCompleted()
// Read pixels from output texture into an image.
img := image.NewNRGBA(image.Rect(0, 0, texture.Width, texture.Height))
bytesPerRow := 4 * texture.Width
region := mtl.RegionMake2D(0, 0, texture.Width, texture.Height)
img := image.NewNRGBA(image.Rect(0, 0, texture.Width(), texture.Height()))
bytesPerRow := 4 * texture.Width()
region := mtl.RegionMake2D(0, 0, texture.Width(), texture.Height())
texture.GetBytes(&img.Pix[0], uintptr(bytesPerRow), region, 0)
// Output image to stdout as grayscale ASCII art.

View File

@ -501,8 +501,6 @@ func (d Device) MakeTexture(td TextureDescriptor) Texture {
}
return Texture{
texture: C.Device_MakeTexture(d.device, descriptor),
Width: td.Width, // TODO: Fetch dimensions of actually created texture.
Height: td.Height, // TODO: Fetch dimensions of actually created texture.
}
}
@ -700,6 +698,10 @@ func (bce BlitCommandEncoder) SynchronizeTexture(texture Texture, slice int, lev
C.BlitCommandEncoder_SynchronizeTexture(bce.commandEncoder, texture.texture, C.uint_t(slice), C.uint_t(level))
}
func (bce BlitCommandEncoder) CopyFromTexture(sourceTexture Texture, sourceSlice int, sourceLevel int, sourceOrigin Origin, sourceSize Size, destinationTexture Texture, destinationSlice int, destinationLevel int, destinationOrigin Origin) {
C.BlitCommandEncoder_CopyFromTexture(bce.commandEncoder, sourceTexture.texture, C.uint_t(sourceSlice), C.uint_t(sourceLevel), sourceOrigin.c(), sourceSize.c(), destinationTexture.texture, C.uint_t(destinationSlice), C.uint_t(destinationLevel), destinationOrigin.c())
}
// Library is a collection of compiled graphics or compute functions.
//
// Reference: https://developer.apple.com/documentation/metal/mtllibrary.
@ -725,14 +727,6 @@ func (l Library) MakeFunction(name string) (Function, error) {
// Reference: https://developer.apple.com/documentation/metal/mtltexture.
type Texture struct {
texture unsafe.Pointer
// TODO: Change these fields into methods.
// Width is the width of the texture image for the base level mipmap, in pixels.
Width int
// Height is the height of the texture image for the base level mipmap, in pixels.
Height int
}
// NewTexture returns a Texture that wraps an existing id<MTLTexture> pointer.
@ -764,6 +758,20 @@ func (t Texture) ReplaceRegion(region Region, level int, pixelBytes unsafe.Point
C.Texture_ReplaceRegion(t.texture, r, C.uint_t(level), pixelBytes, C.uint_t(bytesPerRow))
}
// Width is the width of the texture image for the base level mipmap, in pixels.
//
// Reference: https://developer.apple.com/documentation/metal/mtltexture/1515339-width
func (t Texture) Width() int {
return int(C.Texture_Width(t.texture))
}
// Height is the height of the texture image for the base level mipmap, in pixels.
//
// Reference: https://developer.apple.com/documentation/metal/mtltexture/1515938-height
func (t Texture) Height() int {
return int(C.Texture_Height(t.texture))
}
// Buffer is a memory allocation for storing unformatted data
// that is accessible to the GPU.
//
@ -814,16 +822,8 @@ type Region struct {
func (r *Region) c() C.struct_Region {
return C.struct_Region{
Origin: C.struct_Origin{
X: C.uint_t(r.Origin.X),
Y: C.uint_t(r.Origin.Y),
Z: C.uint_t(r.Origin.Z),
},
Size: C.struct_Size{
Width: C.uint_t(r.Size.Width),
Height: C.uint_t(r.Size.Height),
Depth: C.uint_t(r.Size.Depth),
},
Origin: r.Origin.c(),
Size: r.Size.c(),
}
}
@ -833,12 +833,28 @@ func (r *Region) c() C.struct_Region {
// Reference: https://developer.apple.com/documentation/metal/mtlorigin.
type Origin struct{ X, Y, Z int }
func (o *Origin) c() C.struct_Origin {
return C.struct_Origin{
X: C.uint_t(o.X),
Y: C.uint_t(o.Y),
Z: C.uint_t(o.Z),
}
}
// Size represents the set of dimensions that declare the size of an object,
// such as an image, texture, threadgroup, or grid.
//
// Reference: https://developer.apple.com/documentation/metal/mtlsize.
type Size struct{ Width, Height, Depth int }
func (s *Size) c() C.struct_Size {
return C.struct_Size{
Width: C.uint_t(s.Width),
Height: C.uint_t(s.Height),
Depth: C.uint_t(s.Depth),
}
}
// RegionMake2D returns a 2D, rectangular region for image or texture data.
//
// Reference: https://developer.apple.com/documentation/metal/1515675-mtlregionmake2d.

View File

@ -163,6 +163,11 @@ void BlitCommandEncoder_Synchronize(void *blitCommandEncoder, void *resource);
void BlitCommandEncoder_SynchronizeTexture(void *blitCommandEncoder,
void *texture, uint_t slice,
uint_t level);
void BlitCommandEncoder_CopyFromTexture(
void *blitCommandEncoder, void *sourceTexture, uint_t sourceSlice,
uint_t sourceLevel, struct Origin sourceOrigin, struct Size sourceSize,
void *destinationTexture, uint_t destinationSlice, uint_t destinationLevel,
struct Origin destinationOrigin);
void *Library_MakeFunction(void *library, const char *name);
@ -171,6 +176,8 @@ void Texture_GetBytes(void *texture, void *pixelBytes, size_t bytesPerRow,
struct Region region, uint_t level);
void Texture_ReplaceRegion(void *texture, struct Region region, uint_t level,
void *pixelBytes, uint_t bytesPerRow);
int Texture_Width(void *texture);
int Texture_Height(void *texture);
void Buffer_CopyToContents(void *buffer, void *data, size_t lengthInBytes);
void Buffer_Retain(void *buffer);

View File

@ -280,6 +280,26 @@ void BlitCommandEncoder_SynchronizeTexture(void *blitCommandEncoder,
#endif
}
void BlitCommandEncoder_CopyFromTexture(
void *blitCommandEncoder, void *sourceTexture, uint_t sourceSlice,
uint_t sourceLevel, struct Origin sourceOrigin, struct Size sourceSize,
void *destinationTexture, uint_t destinationSlice, uint_t destinationLevel,
struct Origin destinationOrigin) {
[(id<MTLBlitCommandEncoder>)blitCommandEncoder
copyFromTexture:(id<MTLTexture>)sourceTexture
sourceSlice:(NSUInteger)sourceSlice
sourceLevel:(NSUInteger)sourceLevel
sourceOrigin:(MTLOrigin){sourceOrigin.X, sourceOrigin.Y,
sourceOrigin.Z}
sourceSize:(MTLSize){sourceSize.Width, sourceSize.Height,
sourceSize.Depth}
toTexture:(id<MTLTexture>)destinationTexture
destinationSlice:(NSUInteger)destinationSlice
destinationLevel:(NSUInteger)destinationLevel
destinationOrigin:(MTLOrigin){destinationOrigin.X, destinationOrigin.Y,
destinationOrigin.Z}];
}
void *Library_MakeFunction(void *library, const char *name) {
return [(id<MTLLibrary>)library
newFunctionWithName:[NSString stringWithUTF8String:name]];
@ -312,6 +332,10 @@ void Texture_ReplaceRegion(void *texture, struct Region region, uint_t level,
bytesPerRow:(NSUInteger)bytesPerRow];
}
int Texture_Width(void *texture) { return [(id<MTLTexture>)texture width]; }
int Texture_Height(void *texture) { return [(id<MTLTexture>)texture height]; }
void Buffer_CopyToContents(void *buffer, void *data, size_t lengthInBytes) {
memcpy(((id<MTLBuffer>)buffer).contents, data, lengthInBytes);
}

View File

@ -125,9 +125,9 @@ fragment float4 FragmentShader(Vertex in [[stage_in]]) {
cb.WaitUntilCompleted()
// Read pixels from output texture into an image.
got := image.NewNRGBA(image.Rect(0, 0, texture.Width, texture.Height))
bytesPerRow := 4 * texture.Width
region := mtl.RegionMake2D(0, 0, texture.Width, texture.Height)
got := image.NewNRGBA(image.Rect(0, 0, texture.Width(), texture.Height()))
bytesPerRow := 4 * texture.Width()
region := mtl.RegionMake2D(0, 0, texture.Width(), texture.Height())
texture.GetBytes(&got.Pix[0], uintptr(bytesPerRow), region, 0)
// TODO: Embed this file?