更改纹理的一小部分会导致其余纹理变黑

Changing a small region of my texture causes the rest of the texture to go black

我有一个应用程序在多个线程上对 CPU 进行一些计算。每个线程计算输出图像的 64x64 像素区域。当线程完成计算后,它会将计算出的像素数据通过 -[MTLTexture replaceRegion:] 上传到 MTLTexture。在任何 CPU 线程开始之前,我已经将完整结果的低分辨率版本放入纹理中,我希望 CPU 上的线程在它们运行时覆盖图像数据以更高分辨率计算它。

大部分情况下我都能正常工作,但是当第一次调用 -replaceRegion: 时,它似乎将纹理清除为黑色,然后根据要求替换 64x64 区域。

下面是我创建纹理的方式:

MTLTextureDescriptor*   outputTextureDesc   = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
                                                                                             width:viewportSize.width
                                                                                            height:viewportSize.height
                                                                                         mipmapped:NO];
outputTextureDesc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
_outputTexture = [device newTextureWithDescriptor:outputTextureDesc];

然后,当我想将输出的低分辨率版本放入纹理时,我从另一个纹理复制它,如下所示:

const Vertex2D quadVertices[] =
{
    //Pixel Positions, Texture Coordinates
    { { viewportSize.x / 2.0,  viewportSize.y / -2.0 }, { right, bottom } },
    { { viewportSize.x / -2.0, viewportSize.y / -2.0 }, { left, bottom } },
    { { viewportSize.x / -2.0, viewportSize.y / 2.0 }, { left, top } },

    { { viewportSize.x / 2.0,  viewportSize.y / -2.0 }, { right, bottom } },
    { { viewportSize.x / -2.0, viewportSize.y / 2.0 }, { left, top } },
    { { viewportSize.x / 2.0, viewportSize.y / 2.0 }, { right, top } },
};

MTLRenderPassDescriptor *renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments [ 0 ].texture = _outputTexture;
renderPassDescriptor.colorAttachments [ 0 ].loadAction = MTLLoadActionDontCare;
renderPassDescriptor.colorAttachments [ 0 ].storeAction = MTLStoreActionStore;

if(renderPassDescriptor != nil)
{
    id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
    commandBuffer.label = @"Copy Selection Command Buffer";

    // Create a render command encoder so we can render into something
    id<MTLRenderCommandEncoder> renderEncoder =
        [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
    renderEncoder.label = @"Copy Selection Command Encoder";

    [renderEncoder setViewport:(MTLViewport){0.0, 0.0, viewportSize.x, viewportSize.y, -1.0, 1.0 }];

    [renderEncoder setRenderPipelineState:renderPipelineState];

    [renderEncoder setVertexBytes:quadVertices
                           length:sizeof(quadVertices)
                          atIndex:MBV_VertexIndex];

    [renderEncoder setVertexBytes:&viewportSize
                           length:sizeof(viewportSize)
                          atIndex:MBV_ViewportSize];

    [renderEncoder setFragmentTexture:oldRender
                              atIndex:MBF_Texture];

    // Draw the vertices of our triangles
    [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                      vertexStart:0
                      vertexCount:6];

    [renderEncoder endEncoding];
    [commandBuffer commit];
    [commandBuffer waitUntilCompleted];
}

我还尝试将 renderPassDescriptor.colorAttachment [ 0 ].loadAction 更改为 MTLLoadActionLoad 而不是 MTLLoadActionDontCare,如文档所述:

The existing contents of the texture are preserved.

但没有任何区别。

最后,当一个线程完成时,它通过执行以下操作上传其 64x64 像素结果:

MTLRegion currentRegion = { { colStart, rowStart, 0}, { colEnd - colStart, rowEnd - rowStart, 1 } };
[_outputTexture replaceRegion:currentRegion
                  mipmapLevel:0
                    withBytes:_outputBitmap + (rowStart * (int)viewportSize.width) + colStart
                  bytesPerRow:viewportSize.width * sizeof(*_outputBitmap)];

如果我使用 Metal 调试工具查看低分辨率数据初始副本后的纹理,它包含正确的像素。但是在第一次调用 -replaceRegion: 之后,除了被替换的区域之外的所有内容都是黑色的。对 -replaceRegion: 的后续调用工作正常,不会覆盖以前写入的结果。

我应该提一下,这个纹理也在被 MTKView 更新时显示。我有时会在高分辨率图块开始填充之前看到一两帧的低分辨率副本。知道为什么调用 -replaceRegion: 会清除纹理吗? (或者如果不是调用 -replaceRegion:,还有什么可能会清除纹理?)

假设这是在 macOS 而不是 iOS 上,纹理的存储模式默认为 MTLStorageModeManaged。这意味着您必须显式同步纹理以获得 CPU 到 "see" GPU 所做的任何修改。由于您的初始 low-resolution 图像被绘制到纹理中,因此仅在您同步的 GPU unless/until 上。由于您未能做到这一点,因此 CPU 具有未初始化的数据。当您使用 CPU 替换区域时,它正在修改其(未初始化的)副本,然后将其推送到 GPU。替换绘制的内容。

要同步,您应该创建一个 blit 命令编码器 (MTLBlitCommandEncoder) 并使用它来使用 -synchronizeResource: 或(如果您想要更具选择性)[=13= 对同步命令进行编码].

最后,我不确定,但我会担心各种 -replaceRegion:... 调用的线程安全。所以,你应该使用串行调度队列或其他东西来序列化它们。