在MacOS上Metal计算管线无法工作,但在iOS上可以。

4
我正在尝试使用Metal进行一些GPGPU计算。我有一个基本的Metal管道,其中:
- 创建所需的MTLComputePipelineState管道和所有相关对象(MTLComputeCommandEncoder、命令队列等); - 创建一个目标纹理以进行写入(使用desc.usage = MTLTextureUsageShaderWrite;); - 启动一个基本着色器来用一些值填充这个纹理(在我的实验中,要么将颜色分量之一设置为1,要么根据线程坐标创建一个灰度梯度); - 从GPU读回此纹理的内容。
我在两个环境中测试这个代码:
- 在OSX 10.11上,使用MacBook Pro early 2013; - 在iOS 9上,使用iPhone 6。
iOS版本可以正常运行,并且我得到了我要求着色器执行的结果。但是,在OSX上,我得到了一个有效的(非nil,大小正确)输出纹理。然而,当获取数据时,我得到的只是0。
我是否错过了与OS X实现特定的步骤?这似乎发生在NVIDIA GT650M和Intel HD4000上,或者可能是运行时中的错误?
由于我目前不知道如何进一步调查此问题,因此在此方面的任何帮助也将不胜感激:-)
编辑 - 我当前的实现
这是我的实现的初始(失败)状态。这是一个有点冗长,但主要是用于创建管道的样板代码:
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
id<MTLLibrary> library = [device newDefaultLibrary];
id<MTLCommandQueue> commandQueue = [device newCommandQueue];

NSError *error = nil;
id<MTLComputePipelineState> pipeline = [device newComputePipelineStateWithFunction:[library
                                                                                    newFunctionWithName:@"dummy"]
                                                                             error:&error];
if (error)
{
    NSLog(@"%@", [error localizedDescription]);
}
MTLTextureDescriptor *desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
                                                                                width:16
                                                                               height:1
                                                                            mipmapped:NO];
desc.usage = MTLTextureUsageShaderWrite;

id<MTLTexture> texture = [device newTextureWithDescriptor:desc];

MTLSize threadGroupCounts = MTLSizeMake(8, 1, 1);
MTLSize threadGroups = MTLSizeMake([texture width]  / threadGroupCounts.width,
                                   [texture height] / threadGroupCounts.height,
                                   1);

id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];

id<MTLComputeCommandEncoder> commandEncoder = [commandBuffer computeCommandEncoder];
[commandEncoder setComputePipelineState:pipeline];

[commandEncoder setTexture:texture atIndex:0];
[commandEncoder dispatchThreadgroups:threadGroups threadsPerThreadgroup:threadGroupCounts];
[commandEncoder endEncoding];

[commandBuffer commit];
[commandBuffer waitUntilCompleted];

用于获取数据的代码如下(我将文件分成两部分以获得更小的代码块):

// Get the data back
uint8_t* imageBytes = malloc([texture width] * [texture height] * 4);
assert(imageBytes);
MTLRegion region = MTLRegionMake2D(0, 0, [texture width], [texture height]);
[texture getBytes:imageBytes bytesPerRow:[texture width]*4 fromRegion:region mipmapLevel:0];
for (int i = 0; i < 16; ++i)
{
    NSLog(@"Pix = %d %d %d %d",
          *((uint8_t*)imageBytes + 4 * i),
          *((uint8_t*)imageBytes + 4 * i + 1),
          *((uint8_t*)imageBytes + 4 * i + 2),
          *((uint8_t*)imageBytes + 4 * i + 3));
}

这是着色器代码(将1写入红色和alpha通道,在主机上读取时应该变成0xff):

#include <metal_stdlib>

using namespace metal;

kernel void dummy(texture2d<float, access::write> outTexture [[ texture(0) ]],
                  uint2 gid [[ thread_position_in_grid ]])
{
    outTexture.write(float4(1.0, 0.0, 0.0, 1.0), gid);
}
3个回答

9

同步代码:

[commandEncoder setTexture:texture atIndex:0];
[commandEncoder dispatchThreadgroups:threadGroups threadsPerThreadgroup:threadGroupCounts];
[commandEncoder endEncoding];

//
// synchronize texture from gpu to host mem
//
id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];
[blitEncoder synchronizeTexture:texture slice:0 level:0];
[blitEncoder endEncoding];

[commandBuffer commit];
[commandBuffer waitUntilCompleted];

这是在Mac Book 2012年中使用与您相同的GPU以及2015年中使用AMD Radeon R9 M370X 2048 МБ进行测试的。

有时我使用以下技巧获取纹理数据而无需同步:

id<MTLComputeCommandEncoder> commandEncoder = [commandBuffer computeCommandEncoder];
[commandEncoder setComputePipelineState:pipeline];

[commandEncoder setTexture:texture atIndex:0];
[commandEncoder dispatchThreadgroups:threadGroups threadsPerThreadgroup:threadGroupCounts];
[commandEncoder endEncoding];

//
// one trick: copy texture from GPU mem to shared
//
id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];

[blitEncoder copyFromTexture:texture
                 sourceSlice: 0
                 sourceLevel: 0
                sourceOrigin: MTLOriginMake(0, 0, 0)
                  sourceSize: MTLSizeMake([texture width], [texture height], 1)
                    toBuffer: texturebuffer
           destinationOffset: 0
      destinationBytesPerRow: [texture width] * 4
    destinationBytesPerImage: 0];

[blitEncoder endEncoding];

[commandBuffer commit];
[commandBuffer waitUntilCompleted];


// Get the data back
uint8_t* imageBytes = [texturebuffer contents];

for (int i = 0; i < 16; ++i)
{
    NSLog(@"Pix = %d %d %d %d",
          *((uint8_t*)imageBytes + 4 * i),
          *((uint8_t*)imageBytes + 4 * i + 1),
          *((uint8_t*)imageBytes + 4 * i + 2),
          *((uint8_t*)imageBytes + 4 * i + 3));
}

两种方法都能正常工作。

输入图像描述


谢谢!我今晚会在家测试,然后让你知道结果。因为添加同步代码似乎很不自然(甚至在编程指南中都没有提到),所以我可能会稍后提交问题报告。 - sansuiso
哦,非常酷,它确实起作用了!感谢您的帮助。我会向苹果报告这个问题(至少,希望他们会更新开发者指南)。 - sansuiso
2
只是出于好奇:原始代码(不包括 blit 调用)在搭载 AMD 显卡的 Mac 上是否能正常工作?我想检查一下这是否与驱动程序有关,或者更普遍地说,与 Mac OS Metal 实现有关。 - sansuiso
1
不,它没有起作用。我想这是OSX Metal实现的问题。 - Denn Nevera
在搭配 AMD 的 macOS 11 上运行得非常好。谢谢! - VoodooBoot

1
我想你没有调用- synchronizeTexture:slice:level:。也许下面的例子(jpeg-turbo写入类实现的一部分)可以解决你的问题:
    row_stride = (int)cinfo.image_width  * cinfo.input_components; /* JSAMPLEs per row in image_buffer */

uint   counts        = cinfo.image_width * 4;
uint   componentSize = sizeof(uint8);
uint8 *tmp = NULL;
if (texture.pixelFormat == MTLPixelFormatRGBA16Unorm) {
    tmp  = malloc(row_stride);
    row_stride *= 2;
    componentSize = sizeof(uint16);
}

//
// Synchronize texture with host memory 
//
id<MTLCommandQueue> queue             = [texture.device newCommandQueue];
id<MTLCommandBuffer> commandBuffer    = [queue commandBuffer];
id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];

[blitEncoder synchronizeTexture:texture slice:0 level:0];
[blitEncoder endEncoding];

[commandBuffer commit];
[commandBuffer waitUntilCompleted];

void       *image_buffer  = malloc(row_stride);

int j=0;
while (cinfo.next_scanline < cinfo.image_height) {

    MTLRegion region = MTLRegionMake2D(0, cinfo.next_scanline, cinfo.image_width, 1);

    [texture getBytes:image_buffer
                   bytesPerRow:cinfo.image_width * 4 * componentSize
                    fromRegion:region
                   mipmapLevel:0];

    if (texture.pixelFormat == MTLPixelFormatRGBA16Unorm) {
        uint16 *s = image_buffer;
        for (int i=0; i<counts; i++) {
            tmp[i] = (s[i]>>8) & 0xff;
            j++;
        }
        row_pointer[0] = tmp;
    }
    else{
        row_pointer[0] = image_buffer;
    }
    (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
}

free(image_buffer);
if (tmp != NULL) free(tmp);

它在搭载NVIDIA GeForce GT 650M 1024MB的2012年中期MacBook Pro上进行了测试。 Apple开发者论坛上的讨论。

谢谢您的回答,但在我的情况下它无法工作。我使用的是MTLComputeCommandEncoder而不是普通的命令编码器,并且-synchronizeTexture:消息不可用。它被-dispatchThreadgroups:所取代,我确实调用了它。 - sansuiso
您可以独立地并行使用blit编码器和任何其他MTL编码器。 Blit编码器只加速一些有用的操作,如数据复制和内存同步。在上面的示例中,我只展示了其中一种方法。 - Denn Nevera

0

这是一个例子,当我们可以同时使用blit命令编码器和计算命令编码器。在任何计算后,您可以应用blit操作,或者在任何上下文中想要的操作。

        self.context.execute { (commandBuffer) -> Void in
        //
        // clear buffer
        //
        let blitEncoder = commandBuffer.blitCommandEncoder()
        blitEncoder.fillBuffer(buffer, range: NSMakeRange(0, buffer.length), value: 0)

        let commandEncoder = commandBuffer.computeCommandEncoder()

        //
        // create compute pipe
        //
        commandEncoder.setComputePipelineState(self.kernel.pipeline!);
        commandEncoder.setTexture(texture, atIndex:0)
        commandEncoder.setBuffer(buffer, offset:0, atIndex:0)
        commandEncoder.setBuffer(self.channelsToComputeBuffer,offset:0, atIndex:1)
        commandEncoder.setBuffer(self.regionUniformBuffer,    offset:0, atIndex:2)
        commandEncoder.setBuffer(self.scaleUniformBuffer,     offset:0, atIndex:3)

        self.configure(self.kernel, command: commandEncoder)

        //
        // compute 
        //
        commandEncoder.dispatchThreadgroups(self.threadgroups, threadsPerThreadgroup:threadgroupCounts);
        commandEncoder.endEncoding()

        //
        // synchronize texture state
        // 
        [blitEncoder synchronizeTexture:texture slice:0 level:0];
        [blitEncoder endEncoding];

    }

我尝试添加blit编码器来强制同步,但没有帮助(我仍然得到相同的空结果)。 - sansuiso
你能分享一下你的示例代码吗?我想在我苹果设备上的GPU芯片组上尝试测试一个。 - Denn Nevera
谢谢您的关注,我非常感激。我已经添加了管线和着色器的代码。 - sansuiso
不介意的话:) 我已经基于Metal开发了两个月的图像处理框架,并对可能在不同平台上出现的问题感兴趣。我刚刚测试了你的示例,并添加了一些额外的代码行,你可以在下一个回答中看到它们。 - Denn Nevera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接