Search code examples
metalmetalkit

Crash with passing about a MTLTexture


Here's my scenario:

In an MTKView (and a single command buffer therein), I have a MTLTexture that is the result of a MTLComputeEncoder operation.

My draw method on the MTKView is called as a high priority instance of a dispatch_get_global_queue. At the tail end of drawRect in that MTKView, I "scoop" that texture and set it as a property on a different MTKView. I then dispatch a low priority thread to call that view's draw method to copy a region of the original texture into the second MTKView, along with an eventual presentDrawable call -- all on a different command buffer. Meanwhile, the original MTKView ends the draw method with its own call to presentDrawable.

I should mention that both these MTKViews are configured with isPaused=YES and enableSetNeedsDisplay=NO.

This is all working reasonably well, but I occasionally get a crash with either a "[CAMetalLayerDrawable texture] should not be called after presenting the drawable" warning or the occasional crash in some unidentifiable thread:

QuartzCore`-[CAMetalDrawable present]:
0x7fff881cf471 <+0>:  pushq  %rbp
0x7fff881cf472 <+1>:  movq   %rsp, %rbp
0x7fff881cf475 <+4>:  movq   %rdi, %rax
0x7fff881cf478 <+7>:  movq   0x15f25ce1(%rip), %rcx    ; CAMetalDrawable._priv
0x7fff881cf47f <+14>: movq   (%rax,%rcx), %rcx
0x7fff881cf483 <+18>: movq   0x20(%rcx), %rdi   <---- crashes here
0x7fff881cf487 <+22>: xorps  %xmm0, %xmm0
0x7fff881cf48a <+25>: movq   %rax, %rsi
0x7fff881cf48d <+28>: popq   %rbp
0x7fff881cf48e <+29>: jmp    0x7fff881cf493            ; layer_private_present(_CAMetalLayerPrivate*, CAMetalDrawable*, double, unsigned int)

If I remove/comment out the ancillary "scoop" and redraw of the original texture, there is no crash.

Is there a more acceptable way to accomplish this? Possibly using an MTLBlitCommandEncoder to explicitly copy the contents of the supplied texture?

UPDATE #1

A little more clarity with the code is a wise idea, possibly:

This is the drawRect method of the primary MTKView, which is driven by a dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) :

- (void)drawRect:(NSRect)dirtyRect {
    dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER);
    [super drawRect:dirtyRect];
    …
    id<MTLTexture> theTexture = …
    [renderedQueue addObject:theTexture];

    // populate secondary views
    for (BoardMonitorMTKView *v in self.downstreamOutputs)
        [v enqueueTexture:(hasValidContent) ? [renderedQueue firstObject] : nil];

    __block dispatch_semaphore_t fSemaphore = inflightSemaphore;
    __weak __block NSMutableArray *rQueue = renderedQueue;
    [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
        if ([rQueue count]==2)
            [rQueue removeObjectAtIndex:0];
        dispatch_semaphore_signal(fSemaphore);
    }];

    [commandBuffer presentDrawable:self.currentDrawable];
    [commandBuffer commit];
}

In the secondary view:

-(void)enqueueTexture:(id<MTLTexture>)inputTexture {
    dispatch_semaphore_wait(inflightSemaphore2, DISPATCH_TIME_FOREVER);
    if (inputTexture) {
        [textureQueue addObject:inputTexture];
        if ([textureQueue count]>kTextureQueueCapacity)
            [textureQueue removeObject:[textureQueue firstObject]];
    }
    dispatch_semaphore_signal(inflightSemaphore2);

    dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    dispatch_async(aQueue, ^{
        [self draw];
    });
}

- (void)drawRect:(NSRect)dirtyRect {
    dispatch_semaphore_wait(inflightSemaphore2, DISPATCH_TIME_FOREVER);
    [super drawRect:dirtyRect];
    …
    id<MTLTexture>inputTexture = [textureQueue firstObject];

    MTLRenderPassDescriptor *renderPassDescriptor = self.currentRenderPassDescriptor;
    id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
    MTLViewport viewPort = {0.0, 0.0, (double)self.drawableSize.width, (double)self.drawableSize.height, -1.0, 1.0};
    [encoder setViewport:viewPort];
    [encoder setRenderPipelineState:metalVertexPipelineState];

    // draw main content
    NSUInteger vSize = _vertexInfo.metalVertexCount*sizeof(AAPLVertex);
    id<MTLBuffer> mBuff = [self.device newBufferWithBytes:_vertexInfo.metalVertices
                                                   length:vSize
                                                  options:MTLResourceStorageModeShared];
    [encoder setVertexBuffer:mBuff offset:0 atIndex:0];
    [encoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:1];
    [encoder setFragmentTexture:inputTexture atIndex:0];
    [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_vertexInfo.metalVertexCount];
    [encoder endEncoding];

    __block dispatch_semaphore_t fSemaphore = inflightSemaphore2;
    [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
        [textureQueue removeObject:[textureQueue firstObject]];
        dispatch_semaphore_signal(fSemaphore);
    }];

    [commandBuffer presentDrawable:self.currentDrawable];
    [commandBuffer commit];
}

Solution

  • I'm not sure you're aware that it's both acceptable and common to create your own texture and render to that. You don't have to render to the texture of a drawable of a view or layer.

    Create your own texture(s) to render to and then, for just the present step, render from your texture to a drawable's texture.

    Mind you, depending on exactly what you're doing, you may want a pool of three or so textures that you rotate through. The issue you need to be concerned with is whether Metal is still reading from a texture, so you don't write to it before it's done being read.