Jump to content

Getting Started with Vulkan

Josh

596 views

The latest design of my OpenGL renderer using bindless textures has some problems, and although these can be resolved, I think I have hit the limit on how useful an initial OpenGL implementation will be for the new engine. I decided it was time to dive into the Vulkan API. This is sort of scary, because I feel like it sets me back quite a lot, but at the same time the work I do with this will carry forward much better. A Vulkan-based renderer can run on Windows, Linux, Mac, iOS, Android, PS4, and Nintendo Switch.

So far my impressions of the API are pretty good. Although it is very verbose, it gives you a lot of control over things that were previously undefined or vendor-specific hacks. Below is code that initializes Vulkan and chooses a rendering device, with a preference for discrete GPUs over integrated graphics.

VkInstance inst;
VkResult res;
VkDevice device;

VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "MyGame";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "TurboEngine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;

// Get extensions
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, availableExtensions.data());
std::vector<const char*> extensions;

VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)extensions.size();
createInfo.ppEnabledExtensionNames = extensions.data();

#ifdef DEBUG
createInfo.enabledLayerCount = 1;
const char* DEBUG_LAYER = "VK_LAYER_LUNARG_standard_validation";
createInfo.ppEnabledLayerNames = &DEBUG_LAYER;
#endif

res = vkCreateInstance(&createInfo, NULL, &inst);
if (res == VK_ERROR_INCOMPATIBLE_DRIVER)
{
	std::cout << "cannot find a compatible Vulkan ICD\n";
	exit(-1);
}
else if (res)
{
	std::cout << "unknown error\n";
	exit(-1);
}

//Enumerate devices
uint32_t gpu_count = 1;
std::vector<VkPhysicalDevice> devices;
res = vkEnumeratePhysicalDevices(inst, &gpu_count, NULL);
if (gpu_count > 0)
{			
	devices.resize(gpu_count);
	res = vkEnumeratePhysicalDevices(inst, &gpu_count, &devices[0]);
	assert(!res && gpu_count >= 1);
}

//Sort list with discrete GPUs at the beginning
std::vector<VkPhysicalDevice> sorteddevices;
for (int n = 0; n < devices.size(); n++)
{
	VkPhysicalDeviceProperties deviceprops = VkPhysicalDeviceProperties{};
	vkGetPhysicalDeviceProperties(devices[n], &deviceprops);
	if (deviceprops.deviceType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
	{
		sorteddevices.insert(sorteddevices.begin(),devices[n]);
	}
	else
	{
		sorteddevices.push_back(devices[n]);
	}
}
devices = sorteddevices;

VkDeviceQueueCreateInfo queue_info = {};
unsigned int queue_family_count;

for (int n = 0; n < devices.size(); ++n)
{
	vkGetPhysicalDeviceQueueFamilyProperties(devices[n], &queue_family_count, NULL);
	if (queue_family_count >= 1)
	{
		std::vector<VkQueueFamilyProperties> queue_props;
		queue_props.resize(queue_family_count);
		vkGetPhysicalDeviceQueueFamilyProperties(devices[n], &queue_family_count, queue_props.data());

		if (queue_family_count >= 1)
		{
			bool found = false;
			for (int i = 0; i < queue_family_count; i++)
			{
				if (queue_props[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
				{
					queue_info.queueFamilyIndex = i;
					found = true;
					break;
				}
			}
			if (!found) continue;

			float queue_priorities[1] = { 0.0 };
			queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
			queue_info.pNext = NULL;
			queue_info.queueCount = 1;
			queue_info.pQueuePriorities = queue_priorities;

			VkDeviceCreateInfo device_info = {};
			device_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
			device_info.pNext = NULL;
			device_info.queueCreateInfoCount = 1;
			device_info.pQueueCreateInfos = &queue_info;
			device_info.enabledExtensionCount = 0;
			device_info.ppEnabledExtensionNames = NULL;
			device_info.enabledLayerCount = 0;
			device_info.ppEnabledLayerNames = NULL;
			device_info.pEnabledFeatures = NULL;

			res = vkCreateDevice(devices[n], &device_info, NULL, &device);
			if (res == VK_SUCCESS)
			{
				VkPhysicalDeviceProperties deviceprops = VkPhysicalDeviceProperties{};
				vkGetPhysicalDeviceProperties(devices[n], &deviceprops);
						
				std::cout << deviceprops.deviceName;

				vkDestroyDevice(device, NULL);
				break;
			}
		}
	}
}

vkDestroyInstance(inst, NULL);

 

  • Like 3


9 Comments


Recommended Comments

Do you already know if it will be faster than OpenGL and if so, by how much?  Or is that subject to testing?

Share this comment


Link to comment
5 hours ago, gamecreator said:

Do you already know if it will be faster than OpenGL and if so, by how much?  Or is that subject to testing?

Our new engine makes speed gains in a different way and I don't think Vulkan will make anything faster at all, honestly, over what the engine could do with OpenGL 4.3. We already have zero driver overhead, but Vulkan is more widely supported and the brand is synonymous with speed in the customer's mind. I know this because when I talk to people in person the first thing they say is "does it use Vulkan?"

It will be a lot faster than Leadwerks 4 but so was the OpenGL implementation.

Using Vulkan does not automatically make anything faster. No one will listen to me if I try to argue with that, which is fine, because I am just going to let everyone else mess up and I will make the fastest engine.

  • Confused 1

Share this comment


Link to comment

The best wishes in this project will surely be very pleasant results for users like us who are not real programmers.  :)

Share this comment


Link to comment

No, I don't consider myself a real programmer, that is to say the magic is done by the engine, I have no idea how to create a light, I have no idea how that light casts shadows on a static or dynamic mesh, I think that in this era of globalization we are made to believe that we are programmers when we use powerful tools such as Leadwerks, where the greatest work is already done, on the other hand we are users, creators of possible games, but the programmer in all this riddle is really you, the one who has the necessary tools for people like us to think we are programmers and create something fun.  


Translated with www.DeepL.com/Translator

Share this comment


Link to comment
1 hour ago, Josh said:

Using Vulkan does not automatically make anything faster.

This could be true.  I did some searches and one response said that it depends on if you can put it to use for your engine or game.  Wiki says something similar:

Quote

Vulkan is intended to offer higher performance and more balanced CPU/GPU usage ... Vulkan is said to induce anywhere from a marginal to polynomial speedup in run time relative to other APIs if implemented properly on the same hardware

 

Share this comment


Link to comment

This is really complicated stuff, for really no good reason. I'm 600 lines into it and still can't even make a blue screen. :blink:

  • Confused 1

Share this comment


Link to comment

That confirms my suspicions, you are a programmer, I play to believe that I am, I know that you will succeed. 

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 4
      For finer control over what 2D elements appear on what camera, I have implemented a system of "Sprite Layers". Here's how it works:
      A sprite layer is created in a world. Sprites are created in a layer. Layers are attached to a camera (in the same world). The reason the sprite layer is linked to the world is because the render tweening operates on a per-world basis, and it works with the sprite system just like the entity system. In fact, the rendering thread uses the same RenderNode class for both.
      I have basic GUI functionality working now. A GUI can be created directly on a window and use the OS drawing commands, or it can be created on a sprite layer and rendered with 3D graphics. The first method is how I plan to make the new editor user interface, while the second is quite flexible. The most common usage will be to create a sprite layer, attach it to the main camera, and add a GUI to appear in-game. However, you can just as easily attach a sprite layer to a camera that has a texture render target, and make the GUI appear in-game on a panel in 3D. Because of these different usages, you must manually insert events like mouse movements into the GUI in order for it to process them:
      while true do local event = GetEvent() if event.id == EVENT_NONE then break end if event.id == EVENT_MOUSE_DOWN or event.id == EVENT_MOUSE_MOVE or event.id == EVENT_MOUSE_UP or event.id == EVENT_KEY_DOWN or event.id == EVENT_KEY_UP then gui:ProcessEvent(event) end end You could also input your own events from the mouse position to create interactive surfaces, like in games like DOOM and Soma. Or you can render the GUI to a texture and interact with it by feeding in input from VR controllers.

      Because the new 2D drawing system uses persistent objects instead of drawing commands the code to display elements has changed quite a lot. Here is my current button script. I implemented a system of abstract GUI "rectangles" the script can create and modify. If the GUI is attached to a sprite layer these get translated into sprites, and if it is attached directly to a window they get translated into system drawing commands. Note that the AddTextRect doesn't even allow you to access the widget text directly because the widget text is stored in a wstring, which supports Unicode characters but is not supported by Lua.
      --Default values widget.pushed=false widget.hovered=false widget.textindent=4 widget.checkboxsize=14 widget.checkboxindent=5 widget.radius=3 widget.textcolor = Vec4(1,1,1,1) widget.bordercolor = Vec4(0,0,0,0) widget.hoverbordercolor = Vec4(51/255,151/255,1) widget.backgroundcolor = Vec4(0.2,0.2,0.2,1) function widget:MouseEnter(x,y) self.hovered = true self:Redraw() end function widget:MouseLeave(x,y) self.hovered = false self:Redraw() end function widget:MouseDown(button,x,y) if button == MOUSE_LEFT then self.pushed=true self:Redraw() end end function widget:MouseUp(button,x,y) if button == MOUSE_LEFT then self.pushed = false if self.hovered then EmitEvent(EVENT_WIDGET_ACTION,self) end self:Redraw() end end function widget:OK() EmitEvent(EVENT_WIDGET_ACTION,self) end function widget:KeyDown(keycode) if keycode == KEY_ENTER then EmitEvent(EVENT_WIDGET_ACTION,self) self:Redraw() end end function widget:Start() --Background self:AddRect(self.position, self.size, self.backgroundcolor, false, self.radius) --Border if self.hovered == true then self:AddRect(self.position, self.size, self.hoverbordercolor, true, self.radius) else self:AddRect(self.position, self.size, self.bordercolor, true, self.radius) end --Text if self.pushed == true then self:AddTextRect(self.position + iVec2(1,1), self.size, self.textcolor, TEXT_CENTER + TEXT_MIDDLE) else self:AddTextRect(self.position, self.size, self.textcolor, TEXT_CENTER + TEXT_MIDDLE) end end function widget:Draw() --Update position and size self.primitives[1].position = self.position self.primitives[1].size = self.size self.primitives[2].position = self.position self.primitives[2].size = self.size self.primitives[3].size = self.size --Update the border color based on the current hover state if self.hovered == true then self.primitives[2].color = self.hoverbordercolor else self.primitives[2].color = self.bordercolor end --Offset the text when button is pressed if self.pushed == true then self.primitives[3].position = self.position + iVec2(1,1) else self.primitives[3].position = self.position end end This is arguably harder to use than the Leadwerks 4 system, but it gives you advanced capabilities and better performance that the previous design did not allow.
    • By Josh in Josh's Dev Blog 4
      I have been working on 2D rendering off and on since October. Why am I putting so much effort into something that was fairly simple in Leadwerks 4? I have been designing a system in anticipation of some features I want to see in the GUI, namely VR support and in-game 3D user interfaces. These are both accomplished with 2D drawing performed on a texture. Our system of sprite layers, cameras, and sprites was necessary in order to provide enough control to accomplish this.
      I now have 2D drawing to a texture working, this time as an official supported feature. In Leadwerks 4, some draw-to-texture features were supported, but it was through undocumented commands due to the complex design of shared resources between OpenGL contexts. Vulkan does not have this problem because everything, including contexts (or rather, the VK equivalent) is bound to an abstract VkInstance object.

      Here is the Lua code that makes this program:
      --Get the primary display local displaylist = ListDisplays() local display = displaylist[1]; if display == nil then DebugError("Primary display not found.") end local displayscale = display:GetScale() --Create a window local window = CreateWindow(display, "2D Drawing to Texture", 0, 0, math.min(1280 * displayscale.x, display.size.x), math.min(720 * displayscale.y, display.size.y), WINDOW_TITLEBAR) --Create a rendering framebuffer local framebuffer = CreateFramebuffer(window); --Create a world local world = CreateWorld() --Create second camera local texcam = CreateCamera(world) --Create a camera local camera = CreateCamera(world) camera:Turn(45,-45,0) camera:Move(0,0,-2) camera:SetClearColor(0,0,1,1) --Create a texture buffer local texbuffer = CreateTextureBuffer(512,512,1,true) texcam:SetRenderTarget(texbuffer) --Create scene local box = CreateBox(world) --Create render-to-texture material local material = CreateMaterial() local tex = texbuffer:GetColorBuffer() material:SetTexture(tex, TEXTURE_BASE) box:SetMaterial(material) --Create a light local light = CreateLight(world,LIGHT_DIRECTIONAL) light:SetRotation(55,-55,0) light:SetColor(2,2,2,1) --Create a sprite layer. This can be shared across different cameras for control over which cameras display the 2D elements local layer = CreateSpriteLayer(world) texcam:AddSpriteLayer(layer) texcam:SetPosition(0,1000,0)--put the camera really far away --Load a sprite to display local sprite = LoadSprite(layer, "Materials/Sprites/23.svg", 0, 0.5) sprite:MidHandle(true,true) sprite:SetPosition(texbuffer.size.x * 0.5, texbuffer.size.y * 0.5) --Load font local font = LoadFont("Fonts/arial.ttf", 0) --Text shadow local textshadow = CreateText(layer, font, "Hello!", 36 * displayscale.y, TEXT_LEFT, 1) textshadow:SetColor(0,0,0,1) textshadow:SetPosition(50,30) textshadow:SetRotation(90) --Create text text = textshadow:Instantiate(layer) text:SetColor(1,1,1,1) text:SetPosition(52,32) text:SetRotation(90) --Main loop while window:Closed() == false do sprite:SetRotation(CurrentTime() / 30) world:Update() world:Render(framebuffer) end I have also added a GetTexCoords() command to the PickInfo structure. This will calculate the tangent and bitangent for the picked triangle and then calculate the UV coordinate at the picked position. It is necessary to calculate the non-normalized tangent and bitangent to get the texture coordinate, because the values that are stored in the vertex array are normalized and do not include the length of the vectors.
      local pick = camera:Pick(framebuffer, mousepos.x, mousepos.y, 0, true, 0) if pick ~= nil then local texcoords = pick:GetTexCoords() Print(texcoords) end Maybe I will make this into a Mesh method like GetPolygonTexCoord(), which would work just as well but could potentially be useful for other things. I have not decided yet.
      Now that we have 2D drawing to a texture, and the ability to calculate texture coordinates at a position on a mesh, the next step will be to set up a GUI displayed on a 3D surface, and to send input events to the GUI based on the user interactions in 3D space. The texture could be applied to a computer panel, like many of the interfaces in the newer DOOM games, or it could be used as a panel floating in the air that can be interacted with VR controllers.
    • By Josh in Josh's Dev Blog 0
      Putting all the pieces together, I was able to create a GUI with a sprite layer, attach it to a camera with a texture buffer render target, and render the GUI onto a texture applied to a 3D surface. Then I used the picked UV coords to convert to mouse coordinates and send user events to the GUI. Here is the result:

      This can be used for GUIs rendered onto surfaces in your game, or for a user interface that can be interacted with in VR. This example will be included in the next beta update.
×
×
  • Create New...