Bindless Image Experimental Extension

API

Bindless Images

A bindless image is defined as one which provides access to the underlying data via image reference handles. At the application level, this allows the user to implement programs where the number of images is not known at compile-time, and store all handles to images irrespective of varying formats and layouts in some container such as a dynamic array.

Currently, in Level Zero, zeImageCreate performs the image memory allocation and image handle generation. This function only allows for the allocation of image memory in an implementation-specific layout.

In this extension, we propose the following additions:
  • Provide a new image descriptor and flags for Bindless images.

  • Support for creation of images on linearly allocated memory backed by USM.

  • Extension API to create an image handle from pitched memory

A “Bindless image” can be created by passing ze_image_bindless_exp_desc_t to pNext member of ze_image_desc_t and set the flags value as ZE_IMAGE_BINDLESS_EXP_FLAG_BINDLESS

This extension is complimentary to and may be used in conjunction with the ZE_extension_image_view extension

Programming example with Bindless images

// Assumed image data on host
std::vector<float> imageDataHost;

// 2D image dimensions
size_t imageWidth = 1024;
size_t imageHeight = 1024;

// Single-precision float image format with one channel
ze_image_format_t imageFormat = {
    ZE_IMAGE_FORMAT_LAYOUT_32, ZE_IMAGE_FORMAT_TYPE_FLOAT,
    ZE_IMAGE_FORMAT_SWIZZLE_R, ZE_IMAGE_FORMAT_SWIZZLE_X,
    ZE_IMAGE_FORMAT_SWIZZLE_R, ZE_IMAGE_FORMAT_SWIZZLE_X
}

// Create an image descriptor for bindless image
ze_image_desc_t imageDesc = {
    ZE_STRUCTURE_TYPE_IMAGE_DESC,
    nullptr,
    0,
    ZE_IMAGE_TYPE_2D,
    imageFormat,
    128, 128, 0, 0, 0
};

ze_image_bindless_exp_desc_t bindlessImageDesc = {ZE_STRUCTURE_TYPE_BINDLESS_IMAGE_EXP_DESC};
bindlessImageDesc.flags = ZE_IMAGE_BINDLESS_EXP_FLAG_BINDLESS;
imageDesc.pNext = &bindlessImageDesc;

// A bindless image is valid on both host and device and can be passed into kernels
// When passing ZE_IMAGE_BINDLESS_EXP_FLAG_BINDLESS to zeImageCreate, only the backing memory is allocated for Image
ze_image_handle_t hImage;
zeImageCreate(hContext, hDevice, &imageDesc, &hImage);

//Copy To new bindless image memory
zeCommandListAppendImageCopyFromMemory(hCommandlist, hImage, imageDataHost.data(), nullptr, nullptr, 0, nullptr);

// Launch kernel and perform appropriate synchronizations

// Copy back
zeCommandListAppendImageCopyToMemory(hCommandlist, imageDataHost.data(), hImage, nullptr, nullptr, 0, nullptr);

// Further image views can be created from the existing memory allocated using bindless flags
ze_image_handle_t hImageView;
zeImageViewCreateExt(hContext, hDevice, &imageDesc, hImage, &hImageView);

// New image view can be separately used by users and destroyed
// ...

// Once all operations are complete we need destroy bindless image handle(s)
zeImageDestroy(hImageView);
zeImageDestroy(hImage);

Programming example with pitched memory usage

// Retrieve pitched alloc properties specific to device
ze_device_image_properties_t deviceImageProperties = {};
ze_device_pitched_alloc_exp_properties_t pitchedAllocProperties = {};
pitchedAllocProperties.stype  = ZE_STRUCTURE_TYPE_PITCHED_ALLOC_DEVICE_EXP_PROPERTIES

deviceImageProperties.pNext = &pitchedAllocProperties;
zeDeviceGetImageProperties(hDevice, &deviceImageProperties);

// Assumed image data on host
std::vector<float> imageDataHost;

// 2D image dimensions
size_t imageWidth = 1024;
size_t imageHeight = 1024;

//Pitched memory in linear layout
size_t rowPitch;
unsigned int elementSize = 128;
zeMemGetPitchFor2dImage(hContext, hDevice, imageWidth, imageHeight, elementSize, &rowPitch);
size_t allocSize = rowPitch * imageHeight;
ze_device_mem_alloc_desc_t allocDesc = {ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC};
zeMemAllocDevice(hContext, &allocDesc, allocSize, allocSize, hDevice, &pitchedPtr);

// Declare the copy region for copying
ze_copy_region_t copyRegion = {0, 0, 0, imageWidth * sizeof(float), imageHeight, 0};

// Copy from host to device
zeCommandListAppendMemoryCopyRegion(hCommandList, pitchedPtr, &copyRegion, rowPitch, 0, imageDataHost.data(), &copyRegion, imageWidth * sizeof(float), 0, nullptr, 0, nullptr);

// Single-precision float image format with one channel
ze_image_format_t imageFormat = {
    ZE_IMAGE_FORMAT_LAYOUT_32, ZE_IMAGE_FORMAT_TYPE_FLOAT,
    ZE_IMAGE_FORMAT_SWIZZLE_R, ZE_IMAGE_FORMAT_SWIZZLE_X,
    ZE_IMAGE_FORMAT_SWIZZLE_R, ZE_IMAGE_FORMAT_SWIZZLE_X
}

// Create an image descriptor for bindless image
ze_image_desc_t imageDesc = {
    ZE_STRUCTURE_TYPE_IMAGE_DESC,
    nullptr,
    0,
    ZE_IMAGE_TYPE_2D,
    imageFormat,
    128, 128, 0, 0, 0
};

ze_image_pitched_exp_desc_t pitchedImageDesc = {ZE_STRUCTURE_TYPE_PITCHED_IMAGE_EXP_DESC};
pitchedImageDesc.ptr = pitchedPtr;
imageDesc.pNext = &pitchedImageDesc;

// A image created out of pitched memory is valid on both host and device and can be passed into kernels
ze_image_handle_t hImage;
zeImageCreate(hContext, hDevice, &imageDesc, &hImage);

// Launch kernel and perform appropriate synchronizations

// ...

// Copy from device to host
zeCommandListAppendMemoryCopyRegion(hCommandList, imageDataHost.data(), &copyRegion, imageWidth * sizeof(float), 0, pitchedPtr, &copyRegion, rowPitch, 0, nullptr, 0, nullptr);

// Once all operations on the image are complete we need destroy image handle and free memory
zeImageDestroy(hImage);
zeMemFree(hContext, pitchedPtr);