Skip to content

Latest commit

 

History

History
536 lines (311 loc) · 21.6 KB

README.md

File metadata and controls

536 lines (311 loc) · 21.6 KB

Vulkan-Tutorial-Java

Java port of the great tutorial by Alexander Overvoorde. The original code can be found here.

Tutorial Image 3


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.calloc(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.calloc(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

Read the tutorial

c++Original code

javaJava code

Instance

Read the tutorial

c++Original code

javaJava code

diffDiff

Validation layers

Read the tutorial

c++Original code

javaJava code

diffDiff

Physical devices and queue families

Read the tutorial

c++Original code

javaJava code

diffDiff

Logical device and queues

Read the tutorial

c++Original code

javaJava code

diffDiff

Presentation

Window surface

Read the tutorial

c++Original code

javaJava code

diffDiff

Swap chain

Read the tutorial

c++Original code

javaJava code

diffDiff

Image views

Read the tutorial

c++Original code

javaJava code

diffDiff

Graphics pipeline basics

Introduction

Read the tutorial

c++Original code

javaJava code

diffDiff

Shader Modules

The shaders are compiled into SPIRV at runtime using shaderc library. GLSL files are located at the resources/shaders folder.

Read the tutorial

c++Original code

javaJava code

diffDiff

Fixed functions

Read the tutorial

c++Original code

javaJava code

diffDiff

Render passes

Read the tutorial

c++Original code

javaJava code

diffDiff

Conclusion

Read the tutorial

c++Original code

javaJava code

diffDiff

Drawing

Framebuffers

Read the tutorial

c++Original code

javaJava code

diffDiff

Command buffers

Read the tutorial

c++Original code

javaJava code

diffDiff

Rendering and presentation

Read the tutorial

c++Original code

javaJava code

diffDiff

Swapchain recreation

Read the tutorial

c++Original code

javaJava code

diffDiff

Vertex buffers

Vertex input description

(Will cause Validation Layer errors, but that will be fixed in the next chapter)

Read the tutorial

c++Original code

javaJava code

diffDiff

Vertex buffer creation

Read the tutorial

c++Original code

javaJava code

diffDiff

Staging buffer

Read the tutorial

c++Original code

javaJava code

diffDiff

Version with dedicated Transfer Queue

javaJava code

diffDiff

Index buffer

Read the tutorial

c++Original code

javaJava code

diffDiff

Uniform buffers

Uniform Buffer Object

Descriptor layout and buffer

Read the tutorial

c++Original code

javaJava code

diffDiff

Descriptor pool and sets

Read the tutorial

c++Original code

javaJava code

diffDiff

Texture mapping

Images

Read the tutorial

c++Original code

javaJava code

diffDiff

Image view and sampler

Read the tutorial

c++Original code

javaJava code

diffDiff

Combined image sampler

Read the tutorial

c++Original code

javaJava code

diffDiff

Depth buffering

Read the tutorial

c++Original code

javaJava code

diffDiff

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.

Read the tutorial

c++Original code

javaJava code

diffDiff

Generating Mipmaps

Read the tutorial

c++Original code

javaJava code

diffDiff

Multisampling

Read the tutorial

c++Original code

javaJava code

diffDiff


Icons made by Icon Mafia