Vulkan-Tutorial-Java
Vulkan-Tutorial-Java copied to clipboard
Vulkan tutorial by Alexander Overvoorde ported to Java
Vulkan-Tutorial-Java
Java port of the great tutorial by Alexander Overvoorde. The original code can be found here.
- Introduction
-
LWJGL
- Native handles
- Pointers and references
- Stack allocation
-
Drawing a triangle
-
Setup
- Base code
- Instance
- Validation layers
- Physical devices and queue families
- Logical device and queues
-
Presentation
- Window surface
- Swap chain
- Image views
-
Graphics pipeline basics
- Introduction
- Shader Modules
- Fixed functions
- Render passes
- Conclusion
-
Drawing
- Framebuffers
- Command buffers
- Rendering and presentation
- Swapchain recreation
-
Setup
-
Vertex buffers
- Vertex input description
- Vertex buffer creation
-
Staging buffer
- Version with dedicated Transfer Queue
- Index buffer
-
Uniform buffers
- Descriptor layout and buffer
- Descriptor pool and sets
-
Texture mapping
- Images
- Image view and sampler
- Combined image sampler
- Depth buffering
- Loading models
- Generating Mipmaps
- Multisampling
Introduction
These tutorials are written to be easily followed with the C++ tutorial. However, I've made some changes to fit the Java and LWJGL styles. The repository follows the same structure as in the original one.
Every chapter have its own Java file to make them independent to each other. However, there are some common classes that many of them need:
- AlignmentUtils: Utility class for dealing with uniform buffer object alignments.
- Frame: A wrapper around all the necessary Vulkan handles for an in-flight frame (image-available semaphore, render-finished semaphore and a fence).
- ModelLoader: An utility class for loading 3D models. They are loaded with Assimp.
- ShaderSPIRVUtils: An utility class for compiling GLSL shaders into SPIRV binaries at runtime.
For maths calculations I will be using JOML, a Java math library for graphics mathematics. Its very similar to GLM.
Finally, each chapter have its own .diff file, so you can quickly see the changes made between chapters.
Please note that the Java code is more verbose than C or C++, so the source files are larger.
LWJGL
I'm going to be using LWJGL (Lightweight Java Game Library), a fantastic low level API for Java with bindings for GLFW, Vulkan, OpenGL, and other C libraries.
If you don't know LWJGL, it may be difficult to you to understand certain concepts and patterns you will see throughout this tutorials. I will briefly explain some of the most important concepts you need to know to properly follow the code.
Native handles
Vulkan has its own handles named properly, such as VkImage, VkBuffer or VkCommandPool. These are unsigned integer numbers behind the scenes, and because Java does not have typedefs, we need to use long as the type of all of those objects. For that reason, you will see lots of long variables.
Pointers and references
Some structs and functions will take as parameters references and pointers to other variables, for example to output multiple values. Consider this function in C:
int width;
int height;
glfwGetWindowSize(window, &width, &height);
// Now width and height contains the window dimension values
We pass in 2 int pointers, and the function writes the memory pointed by them. Easy and fast.
But how about in Java? There is no concept of pointer at all. While we can pass a copy of a reference and modify the object's contents inside a function, we cannot do so with primitives. We have two options. We can use either an int array, which is effectively an object, or to use Java NIO Buffers. Buffers in LWJGL are basically a windowed array, with an internal position and limit. We are going to use these buffers, since we can allocate them off heap, as we will see later.
Then, the above function will look like this with NIO Buffers:
IntBuffer width = BufferUtils.createIntBuffer(1);
IntBuffer height = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(window, width, height);
// Print the values
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));
Nice, now we can pass pointers to primitive values, but we are dynamically allocating 2 new objects for just 2 integers. And what if we only need these 2 variables for a short period of time? We need to wait for the Garbage Collector to get rid of those disposable variables.
Luckily for us, LWJGL solves this problem with its own memory management system. You can learn about that here.
Stack allocation
In C and C++, we can easily allocate objects on the stack:
VkApplicationInfo appInfo = {};
// ...
However, this is not possible in Java. Fortunately for us, LWJGL allows us to kind of stack allocate variables on the stack. For that, we need a MemoryStack instance. Since a stack frame is pushed at the beginning of a function and is popped at the end, no matter what happens in the middle, we should use try-with-resources syntax to imitate this behaviour:
try(MemoryStack stack = stackPush()) {
// ...
} // By this line, stack is popped and all the variables in this stack frame are released
Great, now we are able to use stack allocation in Java. Let's see how it looks like:
try(MemoryStack stack = stackPush()) {
IntBuffer width = stack.mallocInt(1); // 1 int unitialized
IntBuffer height = stack.ints(0); // 1 int initialized with 0
glfwGetWindowSize(window, width, height);
// Print the values
System.out.println("width = " + width.get(0));
System.out.println("height = " + height.get(0));
}
Now let's see a real Vulkan example with MemoryStack:
private void createInstance() {
try(MemoryStack stack = stackPush()) {
// Use calloc to initialize the structs with 0s. Otherwise, the program can crash due to random values
VkApplicationInfo appInfo = VkApplicationInfo.callocStack(stack);
appInfo.sType(VK_STRUCTURE_TYPE_APPLICATION_INFO);
appInfo.pApplicationName(stack.UTF8Safe("Hello Triangle"));
appInfo.applicationVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.pEngineName(stack.UTF8Safe("No Engine"));
appInfo.engineVersion(VK_MAKE_VERSION(1, 0, 0));
appInfo.apiVersion(VK_API_VERSION_1_0);
VkInstanceCreateInfo createInfo = VkInstanceCreateInfo.callocStack(stack);
createInfo.sType(VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO);
createInfo.pApplicationInfo(appInfo);
// enabledExtensionCount is implicitly set when you call ppEnabledExtensionNames
createInfo.ppEnabledExtensionNames(glfwGetRequiredInstanceExtensions());
// same with enabledLayerCount
createInfo.ppEnabledLayerNames(null);
// We need to retrieve the pointer of the created instance
PointerBuffer instancePtr = stack.mallocPointer(1);
if(vkCreateInstance(createInfo, null, instancePtr) != VK_SUCCESS) {
throw new RuntimeException("Failed to create instance");
}
instance = new VkInstance(instancePtr.get(0), createInfo);
}
}
Drawing a triangle
Setup
Base code
Java code
Instance
Java code
Diff
Validation layers
Java code
Diff
Physical devices and queue families
Java code
Diff
Logical device and queues
Java code
Diff
Presentation
Window surface
Java code
Diff
Swap chain
Java code
Diff
Image views
Java code
Diff
Graphics pipeline basics
Introduction
Java code
Diff
Shader Modules
The shaders are compiled into SPIRV at runtime using shaderc library. GLSL files are located at the resources/shaders folder.
Java code
Diff
Fixed functions
Java code
Diff
Render passes
Java code
Diff
Conclusion
Java code
Diff
Drawing
Framebuffers
Java code
Diff
Command buffers
Java code
Diff
Rendering and presentation
Java code
Diff
Swapchain recreation
Java code
Diff
Vertex buffers
Vertex input description
(Will cause Validation Layer errors, but that will be fixed in the next chapter)
Java code
Diff
Vertex buffer creation
Java code
Diff
Staging buffer
Java code
Diff
Version with dedicated Transfer Queue
Java code
Diff
Index buffer
Java code
Diff
Uniform buffers
Uniform Buffer Object
Descriptor layout and buffer
Java code
Diff
Descriptor pool and sets
Java code
Diff
Texture mapping
Images
Java code
Diff
Image view and sampler
Java code
Diff
Combined image sampler
Java code
Diff
Depth buffering
Java code
Diff
Loading models
The models will be loaded using Assimp, a library for loading 3D models in different formats which LWJGL has bindings for. I have wrapped all the model loading stuff into the ModelLoader class.
Java code
Diff
Generating Mipmaps
Java code
Diff
Multisampling
Java code
Diff
Icons made by Icon Mafia