Crate screen_13

source ·
Expand description

Provides a high performance Vulkan 1.2 driver using smart pointers. Supports windowed and headless rendering with a fully-featured render graph and resource pooling types.

This crate allows graphics programmers to focus on acceleration structure, buffer, and image resources and the shader pipelines they are accessed from. There are no restrictions or opinions placed on the types of graphics algorithms you might create. Some implementations of common graphics patterns are provided in the contrib directory.

§Getting Sarted

Typical usage involves creating an operating system window event loop, and rendering frames using the provided FrameContext closure. The EventLoop builder handles creating an instance of the Device driver, however you may construct one manually for headless rendering.

use screen_13::prelude::*;

fn main() -> Result<(), DisplayError> {
    let event_loop = EventLoop::new().build()?;

    // Use the device to create resources and pipelines before running
    let device = &event_loop.device;

    event_loop.run(|frame| {
        // You may also create resources and pipelines while running
        let device = &frame.device;
    })
}

§Resources and Pipelines

All resources and pipelines, as well as the driver itself, use shared reference tracking to keep pointers alive. Screen 13 uses std::sync::Arc to track references.

§Information

All driver types have associated information structures which describe their properties. Each object provides a create function which uses the information to return an instance.

For example, a typical host-mappable buffer:

let info = BufferInfo::host_mem(1024, vk::BufferUsageFlags::STORAGE_BUFFER);
let my_buf = Buffer::create(&device, info)?;

For example, a graphics pipeline:

// shader code is raw SPIR-V code as bytes
let vert = Shader::new_vertex(my_vert_code.as_slice());
let frag = Shader::new_fragment(my_frag_code.as_slice());
let info = GraphicPipelineInfo::default();
let my_pipeline = GraphicPipeline::create(&device, info, [vert, frag])?;

§Pooling

Multiple pool types are available to reduce the impact of frequently creating and dropping resources. Leased resources behave identically to owned resources and can be used in a render graph.

Resource aliasing is also availble as an optional way to reduce the number of concurrent resources that may be required.

For example, leasing an image:

let mut pool = LazyPool::new(&device);

let info = ImageInfo::image_2d(8, 8, vk::Format::R8G8B8A8_UNORM, vk::ImageUsageFlags::STORAGE);
let my_image = pool.lease(info)?;

§Render Graph Operations

All rendering in Screen 13 is performed using a RenderGraph composed of user-specified passes, which may include pipelines and read/write access to resources. Recorded passes are automatically optimized before submission to the graphics hardware.

Some notes about the awesome render pass optimization which was totally stolen from Granite:

  • Scheduling: passes are submitted to the Vulkan API using batches designed for low-latency
  • Re-ordering: passes are shuffled using a heuristic which gives the GPU more time to complete work
  • Merging: compatible passes are merged into dynamic subpasses when it is more efficient (on-tile rendering)
  • Aliasing: resources and pipelines are optimized to emit minimal barriers per unit of work (max one, typically zero)

§Nodes

Resources may be directly bound to a render graph. During the time a resource is bound we refer to it as a node. Bound nodes may only be used with the graphs they were bound to. Nodes implement Copy to make using them easier.

println!("{:?}", buffer); // Buffer
println!("{:?}", image); // Image

// Bind our resources into opaque "usize" nodes
let buffer = graph.bind_node(buffer);
let image = graph.bind_node(image);

// The results have unique types!
println!("{:?}", buffer); // BufferNode
println!("{:?}", image); // ImageNode

// Unbind nodes back into resources (Optional!)
let buffer = graph.unbind_node(buffer);
let image = graph.unbind_node(image);

// Magically, they return to the correct types! (the graph wrapped them in Arc for us)
println!("{:?}", buffer); // Arc<Buffer>
println!("{:?}", image); // Arc<Image>

Note: See this code for all the things that can be bound or unbound from a graph.

Note: Once unbound, the node is invalid and should be dropped.

§Access and synchronization

Render graphs and their passes contain a set of functions used to handle Vulkan synchronization with prefixes of access, read, or write. For each resource used in a computing, graphics subpass, ray tracing, or general command buffer you must call an access function. Generally choose a read or write function unless you want to be most efficient.

Example:

let mut graph = RenderGraph::new();
let buffer_node = graph.bind_node(buffer);
let image_node = graph.bind_node(image);
graph
    .begin_pass("Do some raw Vulkan or interop with another Vulkan library")
    .record_cmd_buf(move |device, cmd_buf, bindings| unsafe {
        // I always run first!
    })
    .read_node(buffer_node) // <-- These two functions, read_node/write_node, completely
    .write_node(image_node) //     handle vulkan synchronization.
    .record_cmd_buf(move |device, cmd_buf, bindings| unsafe {
        // device is &ash::Device
        // cmd_buf is vk::CommandBuffer
        // bindings is a magical object you can retrieve the Vulkan resource from
        let vk_buffer: vk::Buffer = *bindings[buffer_node];
        let vk_image: vk::Image = *bindings[image_node];

        // You are free to READ vk_buffer and WRITE vk_image!
    });

§Shader pipelines

Pipeline instances may be bound to a PassRef in order to execute the associated shader code:

graph
    .begin_pass("My compute pass")
    .bind_pipeline(&my_compute_pipeline)
    .record_compute(|compute, _| {
        compute.push_constants(&42u32.to_ne_bytes())
               .dispatch(128, 1, 1);
    });

§Image samplers

By default, Screen 13 will use “linear repeat-mode” samplers unless a special suffix appears as part of the name within GLSL or HLSL shader code. The _sampler_123 suffix should be used where 1, 2, and 3 are replaced with:

  1. l for LINEAR texel filtering (default) or n for NEAREST
  2. l (default) or n, as above, but for mipmap filtering
  3. Addressing mode where:
    • b is CLAMP_TO_BORDER
    • e is CLAMP_TO_EDGE
    • m is MIRRORED_REPEAT
    • r is REPEAT

For example, the following sampler named pages_sampler_nnr specifies nearest texel/mipmap modes and repeat addressing:

layout(set = 0, binding = 0) uniform sampler2D pages_sampler_nnr[NUM_PAGES];

For more complex image sampling, use ShaderBuilder::image_sampler to specify the exact image sampling mode.

§Vertex input

Optional name suffixes are used in the same way with vertex input as with image samplers. The additional attribution of your shader code is optional but may help in a few scenarios:

  • Per-instance vertex rate data
  • Multiple vertex buffer binding indexes

The data for vertex input is assumed to be per-vertex and bound to vertex buffer binding index zero. Add _ibindX for per-instance data, or the matching _vbindX for per-vertex data where X is replaced with the vertex buffer binding index in each case.

For more complex vertex layouts, use the ShaderBuilder::vertex_input to specify the exact layout.

Modules§

  • Vulkan 1.2 interface based on smart pointers.
  • Rendering operations and command submission.
  • Resource leasing and pooling types.
  • Things which are used in almost every single Screen 13 program.

Structs§

  • A physical display interface.
  • Pumps an operating system event loop in order to handle input and other events while drawing to the screen, continuously.
  • Builder for EventLoop.
  • A request to render a single frame to the provided render graph.

Enums§

Traits§