Starting with Vulkan

Vulkan is a new (2016) API from Khronos group (the maintainers of OpenGL and many more OpenWhatevers).

It is low-level than OpenGL and alternatives and feels more like “GPU programming” than “graphics programming”. A deep dive into technicalities may be well worth it, since you’re supposedly writing your own engine.

Vulkan trades some boilerplate for a more refined and thought-out paradigm where you don’t have to track whatever you think your driver is tracking in its deep pockets of mutable state.

While the amount of boilerplate code may appear unruly it is very straight-forward and can be easily stowed away under some combinators and utility packages.

FAQ

  • Q: Why not OpenGL?
    A: Reasons.
  • Q: Do I need to learn OpenGL first?
    A: No.
  • Q: Do I need new hardware to run Vulkan?
    A: Maybe. It’s more of a driver thing.

Typical flow

  • Initial boilerplate:
    • Create window with SDL or GLFW.
      • Pick either, but you’ll have to use its event processing.
    • Get a list of extensions to create Instance.
    • Create Instance and window Surface.
    • Pick physical device.
    • Create logical Device.
    • Select rendering and presentation Queues.
    • Create CommandBuffer and Descriptor pools.
    • Create synchronization primitives.
    • Create memory allocator (optional).
  • Load static resources:
    • Vertex and Index buffers for meshes.
    • Textures, texture arrays and cube maps.
    • Samplers.
  • Set up rendering:
    • Create one or more RenderPass.
    • Create Framebuffer for each Swapchain and its resources to render into.
      • Should be updated each time window changes size. It’s a major PITA.
    • Create your shaders (Pipelines) that will run in RenderPasses.
      • You can compile their bytecode right in, or load on startup and update.
  • Render stuff:
    • Acquire image index to use its resources (synchronization, framebuffer).
    • Allocate and fill CommandBuffer.
      • Bind RenderPass.
        • Bind one of its Pipeline.
          • Bind DescriptorSet, if any.
          • Bind vertex and index buffers.
          • Set Push Constants.
          • Draw!
    • Submit CommandBuffer to rendering.
    • Submit Swapchain to presentation.
    • Wait on synchronization.
    • Free transient resources (buffers, memory)

Resource management

While not imposing any particular way to deal with lifetimes, every beginXxx/endXxx pair of functions has a matching withXxx form that accepts bracketing function. This allows to use ordinary bracket, unflited bracket or managed/resourcet combinators to control the scope and lifetime.

The main problem is keeping transient resources long enough for the frame that uses them to be rendered and/or presented. When you receive a freshly-acquired image index that means that all resources for the same index could be safely disposed or reused. Beyond that, the usual concurrency practices are to be observed.

It is advised to retain data for that aren’t changing. This is in stark contrast with Immediate Mode rendering where everything is transient and CPU is hamster-busy telling GPU to draw exactly the same scene in all the details again and again.

For example, camera projection value does not change every frame, so you’d better not recalculate and reupload it. Furthermore, camera view may not change every frame, and that allows to squash projection and view together to free vertex shader from doing extra work for each vertex in the world.

There is supplemental VulkanMemoryAllocator package to manage GPU memory on a higher level than raw Vulkan bindings allow.

Ecosystem

TBD

Official guides

Those are usually quite basic and in C (C++ even), but that doesn’t matter too much. It’s the data flow and entity relations that matter.

vulkan