I have an application that does some calculations on the CPU on multiple threads. Each thread calculates a 64x64 pixel region of an output image. When a thread has completed its calculations, it uploads the calculated pixel data to a MTLTexture
via -[MTLTexture replaceRegion:]
. Before any of the CPU threads start, I have placed a low-res version of the full result into the texture, and I'd like for the threads on the CPU to overwrite the image data as they calculate it at a higher resolution.
I have this working for the most part, but when the first call to -replaceRegion:
happens, it appears to clear the texture to black, and then replaces the 64x64 region as requested.
Here's how I'm creating the texture:
MTLTextureDescriptor* outputTextureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:viewportSize.width
height:viewportSize.height
mipmapped:NO];
outputTextureDesc.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead | MTLTextureUsageRenderTarget;
_outputTexture = [device newTextureWithDescriptor:outputTextureDesc];
Then, when I want to put the low-res version of the output into the texture, I copy it from another texture like this:
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];
}
I also tried changing the renderPassDescriptor.colorAttachment [ 0 ].loadAction
to MTLLoadActionLoad
instead of MTLLoadActionDontCare
, as the docs says:
The existing contents of the texture are preserved.
but it didn't make any difference.
Finally, when a thread finishes, it uploads its 64x64 pixel result by doing this:
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)];
If I use the Metal debugging tools to look at the texture after the initial copy of the low-res data, it contains the correct pixels. But after the first call to -replaceRegion:
, everything other than the region replaced is black. Subsequent calls to -replaceRegion:
work correctly and do not overwrite the previously written results.
I should mention that this texture is also being displayed as it's updated by an MTKView
. I do sometimes see the low-res copy for a frame or two before the high-res tiles start filling in. Any idea why the call to -replaceRegion:
would clear the texture? (Or what else might be clearing the texture if it's not the call to -replaceRegion:
?)
Assuming this is on macOS and not iOS, the texture's storage mode defaults to MTLStorageModeManaged
. That means that you must explicitly synchronize the texture to get the CPU to "see" any modifications made by the GPU. Since your initial low-resolution image is drawn into the texture, that's only on the GPU unless/until you synchronize. Since you're failing to do that, the CPU has uninitialized data. When you use the CPU to replace a region, it's modifying its (uninitialized) copy and then pushing that to the GPU. That replaces the drawn content.
To synchronize, you should create a blit command encoder (MTLBlitCommandEncoder
) and use that to encode a synchronization command using -synchronizeResource:
or (if you want to be more selective) -synchronizeTexture:slice:level:
.
Finally, I'm not sure, but I'd be concerned about the thread safety of the various -replaceRegion:...
calls. So, you should use a serial dispatch queue or something to serialize those.