Search code examples
gpuposixmetalmmapmtlbuffer

How to correctly use mmap() and newBufferWithBytesNoCopy together?


While generating a texture from an MTLBuffer created from mmap() via newBufferWithBytesNoCopy, if the number of pages requested by the len argument to mmap is larger than the number of pages of size of the file, the mmap call succeeds, and the newBufferWithBytesNoCopy message does not result in a nil return or error, but when I pass the buffer to the GPU to copy the data to an MTLTexture, the following is printed to the console, and all GPU commands fail to perform any action:

Execution of the command buffer was aborted due to an error during execution. Internal Error (IOAF code -536870211)

Here is code to demonstrate the problem:

static id<MTLDevice> Device;
static id<MTLCommandQueue> Queue;
static id<MTLTexture> BlockTexture[3];
#define TEX_LEN_1 1 // These are all made 1 in this question for simplicity
#define TEX_LEN_2 1
#define TEX_LEN_4 1
#define TEX_SIZE ((TEX_LEN_1<<10)+(TEX_LEN_2<<11)+(TEX_LEN_4<<12))
#define PAGE_ALIGN(S) ((S)+PAGE_SIZE-1&~(PAGE_SIZE-1))
int main(void) {
    if (!(Queue = [Device = MTLCreateSystemDefaultDevice() newCommandQueue]))
        return EXIT_FAILURE;
    @autoreleasepool {
        const id<MTLBuffer> data = ({
            void *const map = ({
                NSFileHandle *const file = [NSFileHandle fileHandleForReadingAtPath:[NSBundle.mainBundle pathForResource:@"Content" ofType:nil]];
                if (!file)
                    return EXIT_FAILURE;
                mmap(NULL, TEX_SIZE, PROT_READ, MAP_SHARED, file.fileDescriptor, 0);
            });
            if (map == MAP_FAILED)
                return errno;
            [Device newBufferWithBytesNoCopy:map length:PAGE_ALIGN(TEX_SIZE) options:MTLResourceStorageModeShared deallocator:^(void *const ptr, const NSUInteger len){
                munmap(ptr, len);
            }];
        });
        if (!data)
            return EXIT_FAILURE;
        const id<MTLCommandBuffer> buffer = [Queue commandBuffer];
        const id<MTLBlitCommandEncoder> encoder = [buffer blitCommandEncoder];
        if (!encoder)
            return EXIT_FAILURE;
        {
            MTLTextureDescriptor *const descriptor = [MTLTextureDescriptor new];
            descriptor.width = descriptor.height = 32;
            descriptor.mipmapLevelCount = 6;
            descriptor.textureType = MTLTextureType2DArray;
            descriptor.storageMode = MTLStorageModePrivate;
            const enum MTLPixelFormat format[] = {MTLPixelFormatR8Unorm, MTLPixelFormatRG8Unorm, MTLPixelFormatRGBA8Unorm};
            const NSUInteger len[] = {TEX_LEN_1, TEX_LEN_2, TEX_LEN_4};
            for (NSUInteger i = 3, off = 0; i--;) {
                descriptor.pixelFormat = format[i];
                const NSUInteger l = descriptor.arrayLength = len[i];
                const id<MTLTexture> texture = [Device newTextureWithDescriptor:descriptor];
                if (!texture)
                    return EXIT_FAILURE;
                const NSUInteger br = 32<<i, bi = 1024<<i;
                for (NSUInteger j = 0; j < l; off += bi)
                    [encoder copyFromBuffer:data sourceOffset:off sourceBytesPerRow:br sourceBytesPerImage:bi sourceSize:(const MTLSize){32, 32, 1} toTexture:texture destinationSlice:j++ destinationLevel:0 destinationOrigin:(const MTLOrigin){0}];
                [encoder generateMipmapsForTexture:BlockTexture[i] = texture];
            }
        }
        [encoder endEncoding];
        [buffer commit];
    }
    // Rest of code to initialize application (omitted)
}

In this case, the command will fail if the size of the actual Content file is less than 4097 bytes, assuming a 4096 page size. What is the most strange is that neither the mmap() nor the newBufferWithBytesNoCopy fails in this case, and any/all subsequent GPU calls also fail.

I thought mmap() space beyond the file was just valid 0 memory. Why is this apparently not the case here? At the very least, how can I detect GPU execution errors or invalid buffers like this to handle them gracefully, besides manually checking if the file is too small? Is this a bug in the OS or are the functions being utilized incorrectly?


Solution

  • There is nothing wrong with the code or the usage of mmap() here. The problem is that by design, the pages of a mapped file that lie beyond the end of the file will result in a SIGBUS if accessed.

    The GPU errors are the result of the GPU performing the access violation that would result in a SIGBUS.

    The solution is to use fstat() to ensure the texture file is large enough before trying to copy the data to the textures.