Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions hal/metal/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ func (d *Device) CreateTexture(desc *hal.TextureDescriptor) (hal.Texture, error)
_ = MsgSend(texDesc, Sel("setWidth:"), uintptr(desc.Size.Width))
_ = MsgSend(texDesc, Sel("setHeight:"), uintptr(desc.Size.Height))

depth := desc.Size.DepthOrArrayLayers
if depth == 0 {
depth = 1
shape := metalTextureDescriptorShape(desc.Dimension, desc.Size.DepthOrArrayLayers)
_ = MsgSend(texDesc, Sel("setDepth:"), uintptr(shape.depth))
if texType == MTLTextureType1DArray || texType == MTLTextureType2DArray {
_ = MsgSend(texDesc, Sel("setArrayLength:"), uintptr(shape.arrayLength))
}
_ = MsgSend(texDesc, Sel("setDepth:"), uintptr(depth))

mipLevels := desc.MipLevelCount
if mipLevels == 0 {
Expand Down Expand Up @@ -241,7 +241,7 @@ func (d *Device) CreateTexture(desc *hal.TextureDescriptor) (hal.Texture, error)
format: desc.Format,
width: desc.Size.Width,
height: desc.Size.Height,
depth: depth,
depth: max(desc.Size.DepthOrArrayLayers, 1),
mipLevels: mipLevels,
samples: sampleCount,
dimension: desc.Dimension,
Expand Down
109 changes: 65 additions & 44 deletions hal/metal/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,28 +150,35 @@ func (e *CommandEncoder) CopyBufferToTexture(src hal.Buffer, dst hal.Texture, re
if !ok || dstTex == nil {
return
}
for _, region := range regions {
if _, _, ok := validateMetalBufferTextureCopyPlan(dstTex.format, dstTex.dimension, region.BufferLayout, region.TextureBase.Origin, region.Size); !ok {
return
}
}
pool := NewAutoreleasePool()
defer pool.Drain()
blitEncoder := MsgSend(e.cmdBuffer, Sel("blitCommandEncoder"))
if blitEncoder == 0 {
return
}
for _, region := range regions {
sourceSize := MTLSize{Width: NSUInteger(region.Size.Width), Height: NSUInteger(region.Size.Height), Depth: NSUInteger(region.Size.DepthOrArrayLayers)}
destOrigin := MTLOrigin{X: NSUInteger(region.TextureBase.Origin.X), Y: NSUInteger(region.TextureBase.Origin.Y), Z: NSUInteger(region.TextureBase.Origin.Z)}
bytesPerRow := region.BufferLayout.BytesPerRow
bytesPerImage := region.BufferLayout.RowsPerImage * bytesPerRow
msgSendVoid(blitEncoder, Sel("copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(srcBuf.raw)),
argUint64(region.BufferLayout.Offset),
argUint64(uint64(bytesPerRow)),
argUint64(uint64(bytesPerImage)),
argStruct(sourceSize, mtlSizeType),
argPointer(uintptr(dstTex.raw)),
argUint64(uint64(region.TextureBase.Origin.Z)),
argUint64(uint64(region.TextureBase.MipLevel)),
argStruct(destOrigin, mtlOriginType),
)
plan, bytesPerImage, _ := validateMetalBufferTextureCopyPlan(dstTex.format, dstTex.dimension, region.BufferLayout, region.TextureBase.Origin, region.Size)
strides := metalBlitStrides(dstTex.dimension, uint64(region.BufferLayout.BytesPerRow), bytesPerImage)
for operation := uint32(0); operation < plan.operationCount; operation++ {
destination, _ := plan.textureRegion(dstTex.dimension, region.TextureBase.Origin, region.Size, operation)
sourceOffset, _ := plan.bufferOffset(region.BufferLayout.Offset, bytesPerImage, operation)
msgSendVoid(blitEncoder, Sel("copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(srcBuf.raw)),
argUint64(sourceOffset),
argUint64(strides.bytesPerRow),
argUint64(strides.bytesPerImage),
argStruct(destination.size, mtlSizeType),
argPointer(uintptr(dstTex.raw)),
argUint64(uint64(destination.slice)),
argUint64(uint64(region.TextureBase.MipLevel)),
argStruct(destination.origin, mtlOriginType),
)
}
}
_ = MsgSend(blitEncoder, Sel("endEncoding"))
}
Expand All @@ -189,28 +196,35 @@ func (e *CommandEncoder) CopyTextureToBuffer(src hal.Texture, dst hal.Buffer, re
if !ok || dstBuf == nil {
return
}
for _, region := range regions {
if _, _, ok := validateMetalBufferTextureCopyPlan(srcTex.format, srcTex.dimension, region.BufferLayout, region.TextureBase.Origin, region.Size); !ok {
return
}
}
pool := NewAutoreleasePool()
defer pool.Drain()
blitEncoder := MsgSend(e.cmdBuffer, Sel("blitCommandEncoder"))
if blitEncoder == 0 {
return
}
for _, region := range regions {
sourceSize := MTLSize{Width: NSUInteger(region.Size.Width), Height: NSUInteger(region.Size.Height), Depth: NSUInteger(region.Size.DepthOrArrayLayers)}
sourceOrigin := MTLOrigin{X: NSUInteger(region.TextureBase.Origin.X), Y: NSUInteger(region.TextureBase.Origin.Y), Z: NSUInteger(region.TextureBase.Origin.Z)}
bytesPerRow := region.BufferLayout.BytesPerRow
bytesPerImage := region.BufferLayout.RowsPerImage * bytesPerRow
msgSendVoid(blitEncoder, Sel("copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:"),
argPointer(uintptr(srcTex.raw)),
argUint64(uint64(region.TextureBase.Origin.Z)),
argUint64(uint64(region.TextureBase.MipLevel)),
argStruct(sourceOrigin, mtlOriginType),
argStruct(sourceSize, mtlSizeType),
argPointer(uintptr(dstBuf.raw)),
argUint64(region.BufferLayout.Offset),
argUint64(uint64(bytesPerRow)),
argUint64(uint64(bytesPerImage)),
)
plan, bytesPerImage, _ := validateMetalBufferTextureCopyPlan(srcTex.format, srcTex.dimension, region.BufferLayout, region.TextureBase.Origin, region.Size)
strides := metalBlitStrides(srcTex.dimension, uint64(region.BufferLayout.BytesPerRow), bytesPerImage)
for operation := uint32(0); operation < plan.operationCount; operation++ {
source, _ := plan.textureRegion(srcTex.dimension, region.TextureBase.Origin, region.Size, operation)
destinationOffset, _ := plan.bufferOffset(region.BufferLayout.Offset, bytesPerImage, operation)
msgSendVoid(blitEncoder, Sel("copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toBuffer:destinationOffset:destinationBytesPerRow:destinationBytesPerImage:"),
argPointer(uintptr(srcTex.raw)),
argUint64(uint64(source.slice)),
argUint64(uint64(region.TextureBase.MipLevel)),
argStruct(source.origin, mtlOriginType),
argStruct(source.size, mtlSizeType),
argPointer(uintptr(dstBuf.raw)),
argUint64(destinationOffset),
argUint64(strides.bytesPerRow),
argUint64(strides.bytesPerImage),
)
}
}
_ = MsgSend(blitEncoder, Sel("endEncoding"))
}
Expand All @@ -228,27 +242,34 @@ func (e *CommandEncoder) CopyTextureToTexture(src, dst hal.Texture, regions []ha
if !ok || dstTex == nil {
return
}
for _, region := range regions {
if _, ok := validateMetalTextureCopyPlan(srcTex.dimension, dstTex.dimension, region.SrcBase.Origin, region.DstBase.Origin, region.Size); !ok {
return
}
}
pool := NewAutoreleasePool()
defer pool.Drain()
blitEncoder := MsgSend(e.cmdBuffer, Sel("blitCommandEncoder"))
if blitEncoder == 0 {
return
}
for _, region := range regions {
sourceSize := MTLSize{Width: NSUInteger(region.Size.Width), Height: NSUInteger(region.Size.Height), Depth: NSUInteger(region.Size.DepthOrArrayLayers)}
sourceOrigin := MTLOrigin{X: NSUInteger(region.SrcBase.Origin.X), Y: NSUInteger(region.SrcBase.Origin.Y), Z: NSUInteger(region.SrcBase.Origin.Z)}
destOrigin := MTLOrigin{X: NSUInteger(region.DstBase.Origin.X), Y: NSUInteger(region.DstBase.Origin.Y), Z: NSUInteger(region.DstBase.Origin.Z)}
msgSendVoid(blitEncoder, Sel("copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(srcTex.raw)),
argUint64(uint64(region.SrcBase.Origin.Z)),
argUint64(uint64(region.SrcBase.MipLevel)),
argStruct(sourceOrigin, mtlOriginType),
argStruct(sourceSize, mtlSizeType),
argPointer(uintptr(dstTex.raw)),
argUint64(uint64(region.DstBase.Origin.Z)),
argUint64(uint64(region.DstBase.MipLevel)),
argStruct(destOrigin, mtlOriginType),
)
plan, _ := validateMetalTextureCopyPlan(srcTex.dimension, dstTex.dimension, region.SrcBase.Origin, region.DstBase.Origin, region.Size)
for operation := uint32(0); operation < plan.operationCount; operation++ {
source, _ := plan.textureRegion(srcTex.dimension, region.SrcBase.Origin, region.Size, operation)
destination, _ := plan.textureRegion(dstTex.dimension, region.DstBase.Origin, region.Size, operation)
msgSendVoid(blitEncoder, Sel("copyFromTexture:sourceSlice:sourceLevel:sourceOrigin:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(srcTex.raw)),
argUint64(uint64(source.slice)),
argUint64(uint64(region.SrcBase.MipLevel)),
argStruct(source.origin, mtlOriginType),
argStruct(source.size, mtlSizeType),
argPointer(uintptr(dstTex.raw)),
argUint64(uint64(destination.slice)),
argUint64(uint64(region.DstBase.MipLevel)),
argStruct(destination.origin, mtlOriginType),
)
}
}
_ = MsgSend(blitEncoder, Sel("endEncoding"))
}
Expand Down
3 changes: 2 additions & 1 deletion hal/metal/metal.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,10 @@ func preRegisterSelectors() {
// MTLTexture
"width", "height", "depth",
"pixelFormat", "textureType",
"replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:",
"newTextureViewWithPixelFormat:",
// Descriptors
"setWidth:", "setHeight:", "setDepth:",
"setWidth:", "setHeight:", "setDepth:", "setArrayLength:",
"setPixelFormat:", "setTextureType:", "setUsage:",
"setStorageMode:", "setMipmapLevelCount:", "setSampleCount:",
// MTLRenderPipelineDescriptor
Expand Down
128 changes: 52 additions & 76 deletions hal/metal/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,29 @@ func (q *Queue) writeBufferStaged(buf *Buffer, offset uint64, data []byte) error
// the bytes into the staging buffer before this method returns, so the caller
// may reuse or free the data slice immediately.
func (q *Queue) WriteTexture(dst *hal.ImageCopyTexture, data []byte, layout *hal.ImageDataLayout, size *hal.Extent3D) error {
if dst == nil || layout == nil {
return fmt.Errorf("metal: WriteTexture: invalid arguments")
}
tex, ok := dst.Texture.(*Texture)
if !ok || tex == nil || len(data) == 0 || size == nil {
return fmt.Errorf("metal: WriteTexture: invalid arguments")
}
copyLayout, err := validateMetalTextureDataCopyLayout(uint64(len(data)), tex.format, *layout, *size)
if err != nil {
return fmt.Errorf("metal: WriteTexture: %w", err)
}
normalizedLayout := *layout
normalizedLayout.BytesPerRow = copyLayout.bytesPerRow
plan, bytesPerImage, ok := validateMetalBufferTextureCopyPlan(tex.format, tex.dimension, normalizedLayout, dst.Origin, *size)
if !ok || bytesPerImage != copyLayout.bytesPerImage {
return fmt.Errorf("metal: WriteTexture: copy address arithmetic overflows")
}

// Apple Silicon UMA fast path: Shared textures accept synchronous CPU writes
// via replaceRegion:. This avoids per-upload staging buffers and one-shot
// blit command buffers — the main source of resize-induced memory growth.
if tex.isShared {
return q.writeTextureShared(tex, dst, data, layout, size)
return q.writeTextureShared(tex, dst, data, layout, copyLayout, size, plan)
}

pool := NewAutoreleasePool()
Expand Down Expand Up @@ -295,44 +308,23 @@ func (q *Queue) WriteTexture(dst *hal.ImageCopyTexture, data []byte, layout *hal
return fmt.Errorf("metal: WriteTexture: blit encoder creation failed")
}

// Calculate layout parameters.
bytesPerRow := layout.BytesPerRow
if bytesPerRow == 0 {
// Estimate bytes per row from width and format (assume 4 bytes/pixel for RGBA8).
bytesPerRow = size.Width * 4
}
layers := size.DepthOrArrayLayers
if layers == 0 {
layers = 1
}
bytesPerImage := layout.RowsPerImage * bytesPerRow
if bytesPerImage == 0 {
bytesPerImage = size.Height * bytesPerRow
}

sourceOrigin := MTLOrigin{
X: NSUInteger(dst.Origin.X),
Y: NSUInteger(dst.Origin.Y),
Z: NSUInteger(dst.Origin.Z),
}
sourceSize := MTLSize{
Width: NSUInteger(size.Width),
Height: NSUInteger(size.Height),
Depth: NSUInteger(layers),
strides := metalBlitStrides(tex.dimension, uint64(copyLayout.bytesPerRow), copyLayout.bytesPerImage)
for operation := uint32(0); operation < plan.operationCount; operation++ {
destination, _ := plan.textureRegion(tex.dimension, dst.Origin, *size, operation)
sourceOffset, _ := plan.bufferOffset(layout.Offset, copyLayout.bytesPerImage, operation)
msgSendVoid(blitEncoder, Sel("copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(stagingBuffer)),
argUint64(sourceOffset),
argUint64(strides.bytesPerRow),
argUint64(strides.bytesPerImage),
argStruct(destination.size, mtlSizeType),
argPointer(uintptr(tex.raw)),
argUint64(uint64(destination.slice)),
argUint64(uint64(dst.MipLevel)),
argStruct(destination.origin, mtlOriginType),
)
}

msgSendVoid(blitEncoder, Sel("copyFromBuffer:sourceOffset:sourceBytesPerRow:sourceBytesPerImage:sourceSize:toTexture:destinationSlice:destinationLevel:destinationOrigin:"),
argPointer(uintptr(stagingBuffer)),
argUint64(layout.Offset),
argUint64(uint64(bytesPerRow)),
argUint64(uint64(bytesPerImage)),
argStruct(sourceSize, mtlSizeType),
argPointer(uintptr(tex.raw)),
argUint64(uint64(dst.Origin.Z)),
argUint64(uint64(dst.MipLevel)),
argStruct(sourceOrigin, mtlOriginType),
)

_ = MsgSend(blitEncoder, Sel("endEncoding"))

// Try async path: register a completion handler to release the staging
Expand Down Expand Up @@ -378,50 +370,34 @@ func (q *Queue) WriteTexture(dst *hal.ImageCopyTexture, data []byte, layout *hal
return nil
}

// writeTextureShared writes pixel data directly into a Shared-storage Metal texture
// using replaceRegion:mipmapLevel:withBytes:bytesPerRow:. This is a synchronous CPU
// write — no GPU command buffer or staging buffer is required. Valid only when
// tex.isShared is true (Apple Silicon UMA).
func (q *Queue) writeTextureShared(tex *Texture, dst *hal.ImageCopyTexture, data []byte, layout *hal.ImageDataLayout, size *hal.Extent3D) error {
bytesPerRow := layout.BytesPerRow
if bytesPerRow == 0 {
bytesPerRow = size.Width * 4
}
depth := size.DepthOrArrayLayers
if depth == 0 {
depth = 1
}

region := MTLRegion{
Origin: MTLOrigin{
X: NSUInteger(dst.Origin.X),
Y: NSUInteger(dst.Origin.Y),
Z: NSUInteger(dst.Origin.Z),
},
Size: MTLSize{
Width: NSUInteger(size.Width),
Height: NSUInteger(size.Height),
Depth: NSUInteger(depth),
},
}

src := data[layout.Offset:]
if len(src) == 0 {
return nil
// writeTextureShared writes pixel data directly into a Shared-storage Metal texture.
// It uses the slice-aware replaceRegion form so 2D arrays and true 3D textures
// preserve their distinct Metal layouts. The write is synchronous; no GPU command
// buffer or staging buffer is required. Valid only when tex.isShared is true.
func (q *Queue) writeTextureShared(tex *Texture, dst *hal.ImageCopyTexture, data []byte, layout *hal.ImageDataLayout, copyLayout metalTextureDataCopyLayout, size *hal.Extent3D, plan metalCopyPlan) error {
strides := metalReplaceRegionStrides(tex.dimension, uint64(copyLayout.bytesPerRow), copyLayout.bytesPerImage)
for operation := uint32(0); operation < plan.operationCount; operation++ {
offset, _ := plan.bufferOffset(layout.Offset, copyLayout.bytesPerImage, operation)
src := data[offset:]
destination, _ := plan.textureRegion(tex.dimension, dst.Origin, *size, operation)
region := MTLRegion{Origin: destination.origin, Size: destination.size}

// The slice-aware form carries an image stride for true 3D writes. Array
// writes target one physical slice and therefore pass a zero image stride.
msgSendVoid(tex.raw, Sel("replaceRegion:mipmapLevel:slice:withBytes:bytesPerRow:bytesPerImage:"),
argStruct(region, mtlRegionType),
argUint64(uint64(dst.MipLevel)),
argUint64(uint64(destination.slice)),
argPointer(uintptr(unsafe.Pointer(&src[0]))),
argUint64(strides.bytesPerRow),
argUint64(strides.bytesPerImage),
)
}

// replaceRegion:mipmapLevel:withBytes:bytesPerRow: — synchronous CPU→Shared write.
msgSendVoid(tex.raw, Sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
argStruct(region, mtlRegionType),
argUint64(uint64(dst.MipLevel)),
argPointer(uintptr(unsafe.Pointer(&src[0]))),
argUint64(uint64(bytesPerRow)),
)

hal.Logger().Debug("metal: WriteTexture (shared direct)",
"width", size.Width,
"height", size.Height,
"bytesPerRow", bytesPerRow,
"bytesPerRow", copyLayout.bytesPerRow,
)
return nil
}
Expand Down
Loading