Crate rafx_api

source ·
Expand description

Rafx API is an unsafe graphics API abstraction layer designed specifically for games and tools for games. The goal is to achieve near-native performance with reduced complexity. It may be used directly, or indirectly through other crates in rafx (such as [rafx-resources] and [rafx-assets]).

rafx-api is an opinionated API. It does not expose every possible operation a graphics API might provide. However, the wrapped API-specific objects are exposed in an easily accessible manner.

The API does not track resource lifetimes or states (such as vulkan image layouts) or try to enforce safe usage at compile time or runtime. Safer abstractions are available in rafx-framework and rafx-assets.

Every API call is potentially unsafe. However, the unsafe keyword is only placed on APIs that are particularly likely to cause undefined behavior if used incorrectly.

The general shape of the API is inspired by The Forge. It was chosen for its modern design, multiple working backends, open development model, and track record of shipped games. However, there are some changes in API design, feature set, and implementation details.

Additional high-level documentation is available in the github repo

§Main API Objects

  • RafxApi - Primary entry point to using the API. Use the new_* functions to initialize the desired backend.
  • RafxBuffer - Memory that can be accessed by the rendering API. It may reside in CPU or GPU memory.
  • RafxCommandBuffer - A list of commands recorded by the CPU and submitted to the GPU.
  • RafxCommandPool - A pool of command buffers. A command pool is necessary to create a command buffer.
  • RafxDescriptorSetArray - An array of descriptor sets. These are expected to be pooled and reused.
  • RafxDeviceContext - A cloneable, thread-safe handle used to create graphics resources.
  • RafxFence - A GPU -> CPU synchronization mechanism.
  • RafxPipeline - Represents a complete GPU configuration for executing work.
  • RafxQueue - A queue allows work to be submitted to the GPU
  • RafxRootSignature - Represents the full “layout” or “interface” of a shader (or set of shaders.)
  • RafxSampler - Configures how images will be sampled by the GPU
  • RafxSemaphore - A GPU -> GPU synchronization mechanism.
  • RafxShader - Represents one or more shader stages, producing an entire “program” to execute on the GPU
  • RafxShaderModule - Rrepresents loaded shader code that can be used to create a pipeline.
  • RafxSwapchain - A set of images that act as a “backbuffer” of a window.
  • RafxTexture - An image that can be used by the GPU.

§Usage Summary

In order to interact with a graphics API, construct a RafxApi. A different new_* function exists for each backend.

let api = RafxApi::new_vulkan(...);

After initialization, most interaction will be via RafxDeviceContext Call RafxApi::device_context() on the the api object to obtain a cloneable handle that can be used from multiple threads.

let device_context = api.device_context();

Most objects are created via RafxDeviceContext. For example:

// (See examples for more detail here!)
let texture = device_context.create_texture(...)?;
let buffer = device_context.create_buffer(...)?;
let shader_module = device_context.create_shader_module(...)?;

In order to submit work to the GPU, a RafxCommandBuffer must be submitted to a RafxQueue. Most commonly, this needs to be a “Graphics” queue.

Obtaining a RafxQueue is straightforward. Here we will get a “Graphics” queue. This queue type supports ALL operations (including compute) and is usually the correct one to use if you aren’t sure.

let queue = device_context.create_queue(RafxQueueType::Graphics)?;

A command buffer cannot be created directly. It must be allocated out of a pool.

The command pool and all command buffers allocated from it share memory. The standard rust rules about mutability apply but are not enforced at compile time or runtime.

  • Do not modify two command buffers from the same pool concurrently
  • Do not allocate from a command pool while modifying one of its command buffers
  • Once a command buffer is submitted to the GPU, do not modify its pool, or any command buffers created from it, until the GPU completes its work.

In general, do not modify textures, buffers, command buffers, or other GPU resources while a command buffer referencing them is submitted. Additionally, these resources must persist for the entire duration of the submitted workload.

let command_pool = queue.create_command_pool(&RafxCommandPoolDef {
    transient: true
})?;

let command_buffer = command_pool.create_command_buffer(&RafxCommandBufferDef {
    is_secondary: false,
})?;

Once a command buffer is obtained, write to it by calling “cmd” functions on it, For example, drawing primitives looks like this. Call begin() before writing to it, and end() after finished writing to it.

command_buffer.begin()?;
// other setup...
command_buffer.cmd_draw(3, 0)?;
command_buffer.end()?;

For the most part, no actual work is performed when calling these functions. We are just “scheduling” work to happen later when we give the command buffer to the GPU.

After writing the command buffer, it must be submitted to the queue. The “scheduled” work described in the command buffer will happen asynchronously from the rest of the program.

queue.submit(
    &[&command_buffer],
    &[], // No semaphores or fences in this example
    &[],
    None
)?;
queue.wait_for_queue_idle()?;

The command buffer, the command pool it was allocated from, all other command buffers allocated from that pool, and any other resources referenced by this command buffer cannot be dropped until the queued work is complete, and generally speaking must remain immutable.

More fine-grained synchronization is available via RafxFence and RafxSemaphore but that will not be covered here.

§Resource Barriers

CPUs generally provide a single “coherent” view of memory, but this is not the case for GPUs. Resources can also be stored in many forms depending on how they are used. (The details of this are device-specific and outside the scope of these docs). Resources must be placed into an appropriate state to use them.

Additionally modifying a resource (or transitioning its state) can result in memory hazards. A memory hazard is when reading/writing to memory occurs in an undefined order, resulting in undefined behavior.

Barriers are used to transition resources into the correct state and to avoid these hazards. Here is an example where we take an image from the swapchain and prepare it for use. (We will also need a barrier after we modify it to transition it back to PRESENT!)

command_buffer.cmd_resource_barrier(
    &[], // no buffers to transition
    &[
        // Transition `texture` from PRESENT state to RENDER_TARGET state
        RafxTextureBarrier::state_transition(
            &texture,
            RafxResourceState::PRESENT,
            RafxResourceState::RENDER_TARGET,
        )
    ],
)?;

§“Definition” structs

Many functions take a “def” parameter. For example, RafxDeviceContext::create_texture() takes a single RafxTextureDef parameter. Here is an example call:

    let texture = device_context.create_texture(&RafxTextureDef {
        extents: RafxExtents3D {
            width: 512,
            height: 512,
            depth: 1,
        },
        array_length: 1,
        mip_count: 1,
        sample_count: RafxSampleCount::SampleCount1,
        format: RafxFormat::R8G8B8A8_UNORM,
        resource_type: RafxResourceType::TEXTURE,
        dimensions: RafxTextureDimensions::Dim2D,
    })?;

There are advantages to this approach:

  • The code is easier to read - parameters are clearly labeled
  • Default values can be used
  • When new “parameters” are added, if Default is used, the code will still compile. This avoids boilerplate to implement the builder pattern
    let texture = device_context.create_texture(&RafxTextureDef {
        extents: RafxExtents3D {
            width: 512,
            height: 512,
            depth: 1,
        },
        format: RafxFormat::R8G8B8A8_UNORM,
        ..Default::default()
    })?;

Re-exports§

Modules§

Structs§

Enums§

  • Affects image sampling, particularly for UV coordinates outside the [0, 1] range. Similar to VkSamplerAddressMode
  • Primary entry point to using the API. Use the new_* functions to initialize the desired backend.
  • Determines if a barrier is transferring a resource from one queue to another.
  • Affects blending. Similar to VkBlendFactor
  • Affects blending. Similar to VkBlendOp
  • Memory that can be accessed by the rendering API. It may reside in CPU or GPU memory.
  • The color space an image data is in. The correct color space often varies between texture types (like normal maps vs. albedo maps).
  • A list of commands recorded by the CPU and submitted to the GPU.
  • A pool of command buffers. A command pool is necessary to create a command buffer.
  • Affects depth testing and sampling. Similar to VkCompareOp
  • Determines if we cull polygons that are front-facing or back-facing. Facing direction is determined by RafxFrontFace, sometimes called “winding order”. Similar to VkCullModeFlags
  • Selects a particular descriptor in a descriptor set
  • An array of descriptor sets. These are expected to be pooled and reused.
  • A lightweight handle to a specific descriptor set in a RafxDescriptorSetArray.
  • A cloneable, thread-safe handle used to create graphics resources.
  • Generic error that contains all the different kinds of errors that may occur when using the API
  • A GPU -> CPU synchronization mechanism.
  • Indicates the current state of a fence.
  • Whether to fill in polygons or not. Similar to VkPolygonMode
  • Filtering method when sampling. Similar to VkFilter
  • Describes the encoding of an image or buffer.
  • Determines what winding order is considerered the front face of a polygon. Similar to VkFrontFace
  • Indicates which immutable sampler is being set
  • The size of index buffer elements
  • Determines if the contents of an image attachment in a renderpass begins with its previous contents, a clear value, or undefined data. Similar to VkAttachmentLoadOp
  • Indicates how the memory will be accessed and affects where in memory it needs to be allocated.
  • Similar to VkSamplerMipmapMode
  • Represents a complete GPU configuration for executing work.
  • Indicates the type of pipeline, roughly corresponds with RafxQueueType
  • Indicates the result of presenting a swapchain image
  • How to intepret vertex data into a form of geometry. Similar to VkPrimitiveTopology
  • A queue allows work to be submitted to the GPU
  • Used to indicate which type of queue to use. Some operations require certain types of queues.
  • Represents the full “layout” or “interface” of a shader (or set of shaders.)
  • Number of MSAA samples to use. 1xMSAA and 4xMSAA are most broadly supported
  • Configures how images will be sampled by the GPU
  • A GPU -> GPU synchronization mechanism.
  • Represents one or more shader stages, producing an entire “program” to execute on the GPU
  • Rrepresents loaded shader code that can be used to create a pipeline.
  • Metal-specific shader package. Can be used to create a RafxShaderModuleDef, which in turn is used to initialize a shader module GPU object
  • GL ES 2.0-specific shader package. Can be used to create a RafxShaderModuleDef, which in turn is used to initialize a shader module GPU object
  • GL ES 3.0-specific shader package. Can be used to create a RafxShaderModuleDef, which in turn is used to initialize a shader module GPU object
  • Metal-specific shader package. Can be used to create a RafxShaderModuleDef, which in turn is used to initialize a shader module GPU object
  • Vulkan-specific shader package. Can be used to create a RafxShaderModuleDef, which in turn is used to initialize a shader module GPU object
  • Similar to VkStencilOp
  • Determines if the contents of an image attachment in a rander pass will store the resulting state for use after the render pass
  • A set of images that act as a “backbuffer” of a window.
  • An image that can be used by the GPU.
  • Used when binding a texture to select between different ways to bind the texture
  • Determines how many dimensions the texture will have.
  • Controls if validation is enabled or not. The requirements/behaviors of validation is API-specific.
  • Affects how quickly vertex attributes are consumed from buffers, similar to VkVertexInputRate

Constants§

Type Aliases§