diff --git a/internal/graphicsdriver/metal/graphics.go b/internal/graphicsdriver/metal/graphics.go index 3e97b4eba..bbf921360 100644 --- a/internal/graphicsdriver/metal/graphics.go +++ b/internal/graphicsdriver/metal/graphics.go @@ -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,40 +866,62 @@ 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{ + 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) + } + 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 { - i.texture.ReplaceRegion(mtl.Region{ + 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 }) } diff --git a/internal/graphicsdriver/metal/mtl/example_test.go b/internal/graphicsdriver/metal/mtl/example_test.go index 035e2a169..0d4901831 100644 --- a/internal/graphicsdriver/metal/mtl/example_test.go +++ b/internal/graphicsdriver/metal/mtl/example_test.go @@ -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. diff --git a/internal/graphicsdriver/metal/mtl/mtl.go b/internal/graphicsdriver/metal/mtl/mtl.go index 8f45e6f9e..4879c083b 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.go +++ b/internal/graphicsdriver/metal/mtl/mtl.go @@ -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 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. diff --git a/internal/graphicsdriver/metal/mtl/mtl.h b/internal/graphicsdriver/metal/mtl/mtl.h index 96a1bf075..215f440d5 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.h +++ b/internal/graphicsdriver/metal/mtl/mtl.h @@ -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); diff --git a/internal/graphicsdriver/metal/mtl/mtl.m b/internal/graphicsdriver/metal/mtl/mtl.m index 3aca8443d..ed143b474 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.m +++ b/internal/graphicsdriver/metal/mtl/mtl.m @@ -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)blitCommandEncoder + copyFromTexture:(id)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)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)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)texture width]; } + +int Texture_Height(void *texture) { return [(id)texture height]; } + void Buffer_CopyToContents(void *buffer, void *data, size_t lengthInBytes) { memcpy(((id)buffer).contents, data, lengthInBytes); } diff --git a/internal/graphicsdriver/metal/mtl/mtl_test.go b/internal/graphicsdriver/metal/mtl/mtl_test.go index bf58f14bb..0a4ee7593 100644 --- a/internal/graphicsdriver/metal/mtl/mtl_test.go +++ b/internal/graphicsdriver/metal/mtl/mtl_test.go @@ -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?