Vireo  0.0
Vireo 3D Rendering Hardware Interface
Images

The Image class represent multidimensional arrays of data which can be used for various purposes (e.g. attachments, textures), by binding them to a graphics or compute pipeline via descriptor sets, or by directly specifying them as parameters to certain commands.

An image have a pixel format. Vireo supports images formats that are commons to both Vulkan and DirectX, so it's safe to use any of theses formats without compromising portability.

They are created with vireo::Vireo::createImage. You need to provide the format, the width and height in number of pixels, the number of mips level (e.g. for mipmaps) and the size of the image array (e.g. 6 for a cubemap) :

texture = vireo->createImage(vireo::ImageFormat::R8G8B8A8_SRGB, 512, 512, 1, 1, L"CheckerBoardTexture");

The last parameter is the resource name used when you debug the rendering process with tools like RenderDoc.

Uploading an image

To be accessible by the GPU and the shader an image needs to be uploaded into video memory (VRAM).

Like for memory buffers there is two methods to upload an image into the GPU memory :

  • Using upload() : the easy way
  • Using copy(): the flexible way

using upload()

By using the vireo::CommandList::upload or vireo::CommandList::uploadArray methods you can easily upload data into a buffer. The upload methods will take care of the staging buffer and temporary copy of the data for you, which can be tricky if you have multiple mip levels or multiples image layers.

The GPU commands recorded with a CommandList are executed asynchronously by the GPU. Once you have uploaded all of the data using oneCommandList you need to call vireo::CommandList::cleanup on this CommandList to clear the temporary (staging) buffers used for the host-to-device transfers. The asynchronous nature of the operation means that we have to wait for the end of the operation to free the host-visible allocated memory, which mean you have to call cleanup only after the submit queue have finished the commands execution by blocking the CPU-side with vireo::SubmitQueue::waitIdle.

Note
Since the call to cleanup is done automatically in the command list destructor, you only have to call it explicitly on non-temporary command lists.
// Example of a texture upload using upload()
// In a local scope for temporary objects destruction
{
// Load texture data from the disk
auto* pixels = .... // insert the call to your preferred image loading library
// Allocate the texture in device-only memory
texture = vireo->createImage(vireo::ImageFormat::R8G8B8A8_SRGB);
// Create a command list
const auto uploadCommandAllocator = vireo->createCommandAllocator(vireo::CommandType::TRANSFER);
const auto uploadCommandList = uploadCommandAllocator->createCommandList();
uploadCommandList->begin();
// insert a memory barrier to prepare the image to be written
uploadCommandList->barrier(texture, vireo::ResourceState::UNDEFINED, vireo::ResourceState::COPY_DST);
// record copy command
uploadCommandList->upload(stagingBuffer, texture);
uploadCommandList->end();
// Execute the copy command
const auto transferQueue = vireo->createSubmitQueue(vireo::CommandType::TRANSFER);
transferQueue->submit({uploadCommandList});
// Wait for the command to executed by the GPU
transferQueue->waitIdle();
} // cleanup() is automatically called
Note
It's more efficient to load the image data from the disk directly into the staging buffer by using the mapped address, but you need to use the copy() method below.

using copy()

This method can be useful if you want, for example, to write data directly into the staging buffer to avoid a CPU-to-CPU transfer or if you need to copy one mip level independently from the others.

When using vireo::CommandList::copy you need to copy the image data into a temporary, host-accessible (staging) buffer, then you ask the GPU to copy the data to the image in device-only memory (memory only accessible by the GPU). Remember that you have to keep the staging buffer alive until the copy command have been executed (you may need to wait on the CPU side for the completion of the commands on the submit queue).

// Example of a texture upload using copy()
// In a local scope for temporary objects destruction
{
// Load texture data from the disk
auto* pixels = .... // insert the call to your preferred image loading library
// Allocate the texture in device-only memory
texture = vireo->createImage(vireo::ImageFormat::R8G8B8A8_SRGB);
// Allocate the staging buffer in host-accessible memory
const auto stagingBuffer = vireo->createBuffer(vireo::BufferType::IMAGE_UPLOAD, texture->getImageSize());
// Copy the image data into the staging buffer
stagingBuffer->map();
stagingBuffer->write(pixels);
// Create a command list
const auto uploadCommandAllocator = vireo->createCommandAllocator(vireo::CommandType::TRANSFER);
const auto uploadCommandList = uploadCommandAllocator->createCommandList();
uploadCommandList->begin();
// insert a memory barrier to prepare the image to be written
uploadCommandList->barrier(texture, vireo::ResourceState::UNDEFINED, vireo::ResourceState::COPY_DST);
// record copy command
uploadCommandList->copy(stagingBuffer, texture);
uploadCommandList->end();
// Execute the copy command
const auto transferQueue = vireo->createSubmitQueue(vireo::CommandType::TRANSFER);
transferQueue->submit({uploadCommandList});
// Wait for the command to executed by the GPU
transferQueue->waitIdle();
} // unmap() is automatically called on the staging buffer and the staging buffer is destroyed
Note
It's more efficient to load the image data from the disk directly into the staging buffer by using the mapped address.

Downloading an image

Downloading an image to save the result of a rendering or the result of a compute shader is similar to uploading with copy :

  • Allocate a staging buffer
  • Copy the image to the buffer
  • Use map() to access the memory

The main difference is that you have to take care of the memory alignment of the rows of the image imposed by the graphic API :

// Start recording commands
commandList->begin();
// Create a temporary, host-visible memory buffer
const auto buffer = vireo->createBuffer(vireo::BufferType::IMAGE_DOWNLOAD, image->getAlignedImageSize());
// Copy image to buffer
// The image must be in the COPY_SRC state; insert a barrier if needed
commandList->copy(image, buffer);
// End recording commands
commandList->end();
// Submit work to the GPU and wait for the command execution
graphicSubmitQueue->submit({commandList});
graphicSubmitQueue->waitIdle();
// Map the buffer to the CPU virtual address space
buffer->map();
const auto* source = static_cast<uint8_t*>(buffer->getMappedAddress());
// Copy the image row by row in CPU memory
const auto rowPitch = image->getRowPitch();
const auto alignedRowPitch = image->getAlignedRowPitch();
std::vector<uint8_t> imageData(image->getImageSize());
for (int y = 0; y < image->getHeight(); ++y) {
memcpy(&imageData[y * rowPitch], &source[y * alignedRowPitch], rowPitch);
}
// Write data to a file
stbi_write_png("output.png",
image->getWidth(),
image->getHeight(),
image->getPixelSize(image->getFormat()),
imageData.data(),
rowPitch);