Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/LWJGL/lwjgl3/llms.txt

Use this file to discover all available pages before exploring further.

Vulkan gives applications direct control over the GPU — synchronisation, memory allocation, render passes, and command recording are all explicit. This power comes with significant verbosity: a working triangle in Vulkan requires hundreds of lines of setup that OpenGL handles automatically. LWJGL 3 provides a thin, zero-overhead Java binding over the Vulkan C API, using MemoryStack for short-lived struct allocation and long handles for all Vulkan objects. This guide explains the initialization sequence and the key patterns you will encounter.
Vulkan is substantially more complex than OpenGL. This guide explains the concepts and shows the LWJGL API patterns, but a full working renderer is beyond what fits in a single page. The HelloVulkan.java sample in the LWJGL repository is a complete reference, and vulkan-tutorial.com provides an in-depth walkthrough that you can adapt to LWJGL’s Java API.

Overview of the initialization sequence

glfwInit  →  VkInstance  →  VkSurfaceKHR  →  VkPhysicalDevice
→  VkDevice + VkQueue  →  VkSwapchainKHR  →  VkRenderPass
→  VkFramebuffer[]  →  VkCommandPool + VkCommandBuffer[]
→  render loop
Each arrow represents a non-trivial amount of code. The sections below cover each stage.

Stack allocation pattern

Vulkan structs are allocated on a thread-local MemoryStack for short-lived objects and on the heap (via malloc/calloc) for objects that outlive the current scope. Always use try-with-resources so the stack frame is popped automatically:
import static org.lwjgl.system.MemoryStack.*;

try (MemoryStack stack = stackPush()) {
    VkApplicationInfo appInfo = VkApplicationInfo.malloc(stack)
        .sType$Default()        // sets sType to VK_STRUCTURE_TYPE_APPLICATION_INFO
        .pNext(NULL)
        .pApplicationName(stack.UTF8("My App"))
        .applicationVersion(0)
        .pEngineName(stack.UTF8("No Engine"))
        .engineVersion(0)
        .apiVersion(VK.getInstanceVersionSupported());
    // appInfo is valid inside this block; freed automatically on exit
}
sType$Default() is an LWJGL convenience method that sets sType to the correct enum value for the struct type, saving you from looking it up manually.
1

Create a VkInstance

The instance is the root Vulkan object. It requires a list of instance-level extensions (at minimum the GLFW surface extensions) and, during development, the validation layer.
import static org.lwjgl.glfw.GLFWVulkan.*;
import static org.lwjgl.vulkan.VK10.*;
import static org.lwjgl.vulkan.EXTDebugUtils.*;

try (MemoryStack stack = stackPush()) {
    // GLFW tells us which extensions the platform surface needs
    PointerBuffer requiredExtensions = glfwGetRequiredInstanceExtensions();
    if (requiredExtensions == null) {
        throw new IllegalStateException("No Vulkan surface extensions found");
    }

    VkApplicationInfo appInfo = VkApplicationInfo.malloc(stack)
        .sType$Default()
        .pNext(NULL)
        .pApplicationName(stack.UTF8("My App"))
        .applicationVersion(0)
        .pEngineName(stack.UTF8("No Engine"))
        .engineVersion(0)
        .apiVersion(VK.getInstanceVersionSupported());

    VkInstanceCreateInfo createInfo = VkInstanceCreateInfo.malloc(stack)
        .sType$Default()
        .pNext(NULL)
        .flags(0)
        .pApplicationInfo(appInfo)
        .ppEnabledLayerNames(null)       // set validation layers here
        .ppEnabledExtensionNames(requiredExtensions);

    PointerBuffer pp = stack.mallocPointer(1);
    int err = vkCreateInstance(createInfo, null, pp);
    if (err != VK_SUCCESS) {
        throw new IllegalStateException(
            String.format("vkCreateInstance failed [0x%X]", err)
        );
    }
    VkInstance instance = new VkInstance(pp.get(0), createInfo);
}
2

Enable validation layers during development

Validation layers intercept every Vulkan call and check for incorrect usage, undefined behaviour, and performance warnings. Always enable them during development.
// Before creating the instance, enumerate available layers
IntBuffer ip = stack.mallocInt(1);
vkEnumerateInstanceLayerProperties(ip, null);

VkLayerProperties.Buffer availableLayers = VkLayerProperties.malloc(ip.get(0), stack);
vkEnumerateInstanceLayerProperties(ip, availableLayers);

// Check VK_LAYER_KHRONOS_validation is present
PointerBuffer requiredLayers = stack.mallocPointer(1);
requiredLayers.put(0, stack.ASCII("VK_LAYER_KHRONOS_validation"));

// Then pass requiredLayers to VkInstanceCreateInfo.ppEnabledLayerNames(...)
Pair the validation layer with the VK_EXT_debug_utils extension and a VkDebugUtilsMessengerEXT callback to receive the messages. HelloVulkan.java shows the full setup: it registers a VkDebugUtilsMessengerCallbackEXT that prints severity, type, and message text to System.err.
Use VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT as the severity filter during early development to keep the output manageable.
3

Select a physical device

Enumerate the GPUs visible to the instance and pick one. Most applications simply take the first discrete GPU or fall back to the first available device:
// Query count
vkEnumeratePhysicalDevices(instance, ip, null);
if (ip.get(0) == 0) {
    throw new IllegalStateException("No Vulkan-capable GPUs found");
}

// Fetch handles
PointerBuffer physicalDevices = stack.mallocPointer(ip.get(0));
vkEnumeratePhysicalDevices(instance, ip, physicalDevices);

// For simplicity, use the first device
VkPhysicalDevice physicalDevice =
    new VkPhysicalDevice(physicalDevices.get(0), instance);
Call vkGetPhysicalDeviceProperties to read the device name and type, and vkGetPhysicalDeviceFeatures to check for optional capabilities (geometry shaders, clip distance, etc.) before committing to a device.
4

Create a logical device and retrieve queues

A VkDevice is the logical representation of the physical device. You specify which queue families you need — for a basic renderer you need at least one graphics queue that also supports presentation to the surface created in the next step.
// Find a queue family that supports both graphics and present
// (see HelloVulkan.demo_init_vk_swapchain for the full search logic)
int graphicsQueueIndex = /* result of queue family search */;

VkDeviceQueueCreateInfo.Buffer queueCreateInfo =
    VkDeviceQueueCreateInfo.malloc(1, stack)
        .sType$Default()
        .pNext(NULL)
        .flags(0)
        .queueFamilyIndex(graphicsQueueIndex)
        .pQueuePriorities(stack.floats(1.0f));

VkDeviceCreateInfo deviceCreateInfo = VkDeviceCreateInfo.malloc(stack)
    .sType$Default()
    .pNext(NULL)
    .flags(0)
    .pQueueCreateInfos(queueCreateInfo)
    .ppEnabledLayerNames(null)
    .ppEnabledExtensionNames(/* KHR_swapchain extension name buffer */)
    .pEnabledFeatures(null);

vkCreateDevice(physicalDevice, deviceCreateInfo, null, pp);
VkDevice device = new VkDevice(pp.get(0), physicalDevice, deviceCreateInfo);

// Retrieve the queue handle
vkGetDeviceQueue(device, graphicsQueueIndex, 0, pp);
VkQueue graphicsQueue = new VkQueue(pp.get(0), device);
5

Create a GLFW surface and swapchain

LWJGL provides glfwCreateWindowSurface to bridge GLFW windows and Vulkan surfaces. The swapchain manages the images that are presented to the display.
import static org.lwjgl.glfw.GLFWVulkan.*;
import static org.lwjgl.vulkan.KHRSurface.*;
import static org.lwjgl.vulkan.KHRSwapchain.*;

// Surface
LongBuffer lp = stack.mallocLong(1);
glfwCreateWindowSurface(instance, window, null, lp);
long surface = lp.get(0);

// Swapchain (abbreviated — see HelloVulkan.demo_prepare_buffers for full code)
VkSwapchainCreateInfoKHR swapchainInfo = VkSwapchainCreateInfoKHR.calloc(stack)
    .sType$Default()
    .surface(surface)
    .minImageCount(desiredImageCount)
    .imageFormat(surfaceFormat)
    .imageColorSpace(colorSpace)
    .imageExtent(it -> it.width(width).height(height))
    .imageArrayLayers(1)
    .imageUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
    .imageSharingMode(VK_SHARING_MODE_EXCLUSIVE)
    .preTransform(VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
    .compositeAlpha(VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
    .presentMode(VK_PRESENT_MODE_FIFO_KHR)
    .clipped(true)
    .oldSwapchain(VK_NULL_HANDLE);

vkCreateSwapchainKHR(device, swapchainInfo, null, lp);
long swapchain = lp.get(0);
VK_PRESENT_MODE_FIFO_KHR is the only mode guaranteed to be available and implements vsync. Prefer it unless you have a specific reason to use mailbox or immediate mode.
6

Create render pass, framebuffers, and command buffers

These three objects make up the per-frame rendering infrastructure:
  • Render pass — describes the colour and depth attachments and how their contents are loaded/stored each frame.
  • Framebuffer — binds the swapchain image views to the render pass.
  • Command buffer — records the sequence of draw calls and barriers to submit to the GPU.
// Render pass (single colour attachment, no depth in this sketch)
VkAttachmentDescription.Buffer attachment = VkAttachmentDescription.calloc(1, stack)
    .format(surfaceFormat)
    .samples(VK_SAMPLE_COUNT_1_BIT)
    .loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR)
    .storeOp(VK_ATTACHMENT_STORE_OP_STORE)
    .initialLayout(VK_IMAGE_LAYOUT_UNDEFINED)
    .finalLayout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);

VkAttachmentReference.Buffer colorRef = VkAttachmentReference.calloc(1, stack)
    .attachment(0)
    .layout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);

VkSubpassDescription.Buffer subpass = VkSubpassDescription.calloc(1, stack)
    .pipelineBindPoint(VK_PIPELINE_BIND_POINT_GRAPHICS)
    .pColorAttachments(colorRef);

VkRenderPassCreateInfo rpInfo = VkRenderPassCreateInfo.calloc(stack)
    .sType$Default()
    .pAttachments(attachment)
    .pSubpasses(subpass);

vkCreateRenderPass(device, rpInfo, null, lp);
long renderPass = lp.get(0);

// Command pool
VkCommandPoolCreateInfo poolInfo = VkCommandPoolCreateInfo.calloc(stack)
    .sType$Default()
    .queueFamilyIndex(graphicsQueueIndex);

vkCreateCommandPool(device, poolInfo, null, lp);
long commandPool = lp.get(0);
7

The render loop

Each frame acquires a swapchain image, records commands into the command buffer, submits them, and presents the result:
// Acquire next image
vkAcquireNextImageKHR(device, swapchain, Long.MAX_VALUE,
    imageAvailableSemaphore, VK_NULL_HANDLE, ip);
int imageIndex = ip.get(0);

// Submit command buffer
VkSubmitInfo submitInfo = VkSubmitInfo.calloc(stack)
    .sType$Default()
    .pWaitSemaphores(stack.longs(imageAvailableSemaphore))
    .pWaitDstStageMask(stack.ints(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT))
    .pCommandBuffers(stack.pointers(commandBuffers[imageIndex]))
    .pSignalSemaphores(stack.longs(renderFinishedSemaphore));

vkQueueSubmit(graphicsQueue, submitInfo, VK_NULL_HANDLE);

// Present
VkPresentInfoKHR presentInfo = VkPresentInfoKHR.calloc(stack)
    .sType$Default()
    .pWaitSemaphores(stack.longs(renderFinishedSemaphore))
    .pSwapchains(stack.longs(swapchain))
    .pImageIndices(ip.put(0, imageIndex));

vkQueuePresentKHR(graphicsQueue, presentInfo);
vkQueueWaitIdle(graphicsQueue);

Memory management with VMA

Manual Vulkan memory management — choosing heap types, satisfying alignment requirements, and suballocating from large blocks — is error-prone and time-consuming. The Vulkan Memory Allocator (VMA) library handles all of this automatically. LWJGL includes a complete Java binding:
import org.lwjgl.util.vma.*;
import static org.lwjgl.util.vma.Vma.*;

VmaAllocatorCreateInfo allocatorInfo = VmaAllocatorCreateInfo.calloc(stack)
    .physicalDevice(physicalDevice)
    .device(device)
    .instance(instance);

PointerBuffer pAllocator = stack.mallocPointer(1);
vmaCreateAllocator(allocatorInfo, pAllocator);
long allocator = pAllocator.get(0);
With VMA, creating a GPU buffer with host-visible memory becomes a single vmaCreateBuffer call instead of ten.

Further reading

  • HelloVulkan.java in the LWJGL samples — a complete textured triangle with depth buffer.
  • vulkan-tutorial.com — the most thorough Vulkan tutorial; the concepts map directly to LWJGL’s API.
  • The Vulkan specification — available at khronos.org/vulkan.

Build docs developers (and LLMs) love