Our Mac console will be the first target to setup Vulkan, though incidentally it will be via the MoltenVK library which gives us the ability to use the Vulkan SDK on Mac and iOS. In this article we will:
VulkanApplication
class which will be a sibling to our existing OpenGLApplication
class.Engine
class to try and initialise Vulkan, and fall back to OpenGL if it cannot.Note: During the Vulkan setup articles we will only go as far as initialising Vulkan - leaving the rest of the rendering implementation code until all platforms are able to run it.
We’ll be writing some new setup scripts to download the Vulkan SDK for Mac and iOS from the official Vulkan SDK site: https://vulkan.lunarg.com/sdk/home. We’ll put the ability to download the SDK into our existing shared-scripts.sh
as we have three targets that will need it - Mac console, Mac app, iOS.
Dust off your shared-scripts.sh
file and add the following new method to download the SDK:
fetch_third_party_lib_vulkan_macos() {
verify_third_party_folder_exists
pushd ../../third-party
if [ ! -d "vulkan-mac" ]; then
echo "Fetching Vulkan SDK (Mac) from: https://sdk.lunarg.com/sdk/download/1.1.92.1/mac/vulkansdk-macos-1.1.92.1.tar.gz?Human=true"
wget --no-cookies https://sdk.lunarg.com/sdk/download/1.1.92.1/mac/vulkansdk-macos-1.1.92.1.tar.gz?Human=true -O vulkan-mac.tar.gz
echo "Unzipping Vulkan SDK (Mac) into 'third-party/vulkan-mac' ..."
tar -xf vulkan-mac.tar.gz
rm vulkan-mac.tar.gz
mv vulkansdk-macos-1.1.92.1 vulkan-mac
fi
popd
}
Note: We are using version 1.1.92 deliberately - we want to be using as close to the same version of the Vulkan SDK across Mac, iOS, Android and Windows. Android in particular is not at the bleeding edge for its Vulkan support and the Android NDK version we will be using (version 20 as at the time of writing these articles) is bundled with its
vulkan.hpp
at version90
which is close to the92
that’s available for other platforms. As time passes the Vulkan SDK could be updated across all platforms to a later version if needed.
This setup script looks similar to others we’ve already written - we check if there is a vulkan-mac
folder in the third-party
folder and if not go and fetch it.
The wget
command is slightly different in that it has the --no-cookies
argument because the download site tries to set cookies during the request. We also need the peculiar ?Human=true
- without this the download link seems to not work. We also use the -O vulkan-mac.tar.gz
to tell wget
to change the name of the downloaded file.
The file itself is not a zip
file, but instead a tar
file so we need to use the tar
command to uncompress it.
Save and close the shared-scripts.sh
file then edit the setup.sh
script in your console
folder, adding the new shared method invocation to the end of the script to fetch Vulkan:
fetch_third_party_lib_vulkan_macos
Now you can navigate into the console
folder in Terminal and run the setup script:
$ ./setup.sh
Fetching Brew dependency: 'wget'.
Fetching Brew dependency: 'cmake'.
Fetching Brew dependency: 'ninja'.
SDL library already exists in third party folder.
SDL2.framework already exists ...
Fetching Vulkan SDK (Mac) from: https://sdk.lunarg.com/sdk/download/1.1.92.1/mac/vulkansdk-macos-1.1.92.1.tar.gz?Human=true
Saving to: ‘vulkan-mac.tar.gz’
Unzipping Vulkan SDK (Mac) into 'third-party/vulkan-mac' ...
After running the script, observe that you now have a vulkan-mac
folder in your third-party
folder:
: root
+ third-party
+ vulkan-mac
Next we’ll walk through what different bits of the SDK we’ll need to integrate into our console project.
The header files we will need to write our code against can be found here:
third-party/vulkan-mac/macOS/include
There aren’t too many of them and the main one we are interested in is the vulkan.hpp
file which provides the C++ interface to the Vulkan SDK. There is also a vulkan.h
file for the C based interface.
We need to add the header location to our CMakeLists.txt
file so they are included for us in our code base. Open console/CMakeLists.txt
and add the following definition to the include_directories
section to include the Vulkan headers:
include_directories(${THIRD_PARTY_DIR}/vulkan-mac/macOS/include)
We will be using the following dynamic library files that come bundled with the SDK - note that there is one main file for Vulkan integration but also another one for MoltenVK support:
third-party/vulkan-mac/macOS/lib/libvulkan.1.1.92.dylib
third-party/vulkan-mac/macOS/lib/libMoltenVK.dylib
We’ll be placing these two files in our console/Frameworks
folder so they can be found at runtime and we’ll be linking them during the build. The Vulkan system will actually be looking for a file named libvulkan.1.dylib
so we’ll also need to do a file naming change as well.
Open the shared-scripts.sh
file again and add the following at the end:
setup_vulkan_libs_macos() {
verify_frameworks_folder_exists
pushd "Frameworks"
if [ ! -e "libvulkan.1.dylib" ]; then
cp ../../../third-party/vulkan-mac/macOS/lib/libvulkan.1.1.92.dylib libvulkan.1.dylib
fi
if [ ! -e "libMoltenVK.dylib" ]; then
cp ../../../third-party/vulkan-mac/macOS/lib/libMoltenVK.dylib libMoltenVK.dylib
fi
popd
}
This script will check that we have a Frameworks
folder, then check that we have the two dynamic library files that we require. Note that the first cp
command renames libvulkan.1.1.92.dylib
to libvulkan.1.dylib
on the way through - if we didn’t do this then Vulkan would fail to initialise.
Save and close the shared-scripts.sh
file and edit your console/setup.sh
again, adding the following to the bottom:
setup_vulkan_libs_macos
Save and exit then run setup.sh
in your console
folder. After running it you will see your folder structure look like this:
: root
+ console
+ Frameworks
libMoltenVK.dylib
libvulkan.1.dylib
As well as copying the dylib
files we need to register them in our CMakeLists.txt
file to link them with our executable.
In the console/CMakeLists.txt
file add the following lines just before the add_executable
block to declare build variables representing the two dylib
files we need to link:
set(DYLIB_VULKAN ${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/libvulkan.1.dylib)
set(DYLIB_MOLTEN_VK ${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/libMoltenVK.dylib)
Then just before the add_custom_command
block, add the following to cause the dylib
files to be linked to our main build target:
target_link_libraries(
a-simple-triangle-console
${DYLIB_VULKAN}
${DYLIB_MOLTEN_VK}
)
If we don’t link these libraries to our target our compilation will fail.
Vulkan supports Installable Client Drivers
or ICD
s. To get Vulkan to run on Apple platforms we actually need to use the MoltenVK ICD. Here is a bit of info to explain what an ICD
needs to do: https://vulkan.lunarg.com/doc/view/1.0.54.0/windows/LoaderAndLayerInterface.html#user-content-installable-client-drivers. And here is the official MoltenVK site: https://github.com/KhronosGroup/MoltenVK.
When our application tries to initialise Vulkan, it will check to see if there are any ICDs it should use. In the Vulkan SDK for MacOS there is actually a bundled JSON definition that uses the MoltenVK ICD, which we can use as a clue on how to configure our own ICD
:
third-party/vulkan-mac/macOS/etc/vulkan/icd.d/MoltenVK_icd.json
If you examine the content of the MoltenVK_icd.json
file you will see this:
{
"file_format_version" : "1.0.0",
"ICD": {
"library_path": "../../../lib/libMoltenVK.dylib",
"api_version" : "1.0.0"
}
}
The api_version
represents Vulkan 1.0.0 - which is what MoltenVK is aligned with - and the library_path
defines a relative location to find the ICD
dynamic library to use at run time - observe that it points at the libMoltenVK.dylib
file within the main Vulkan SDK folder.
We need to replicate this ICD configuration for our console target by adding a vulkan/icd.d/MoltenVK_icd.json
file which has a library_path
pointing into our console/Frameworks
folder.
We won’t copy the one from the Vulkan SDK - the library_path
wouldn’t work for us anyway - we will just create our own MoltenVK_icd.json
file under a vulkan/icd.d
folder within our console
folder.
Important: Vulkan will look for a folder named
vulkan/icd.d
in the same location as the executable - if it isn’t in exactly that path it won’t find it and therefore Vulkan will fail to initialise for MoltenVK based platforms.
To include a MoltenVK ICD in our console project, create a file named MoltenVK_icd.json
in the following folder structure - you’ll need to create the vulkan/icd.d
folders too. We only need to do this once so you can check this new file into version control afterward:
: root
+ console
+ vulkan
+ icd.d
MoltenVK_icd.json
Edit MoltenVK_icd.json
with the following content then close it - noting that the library_path
points down into our Frameworks
folder to find the MoltenVK dynamic library which is the implementation of the ICD for MacOS:
{
"file_format_version" : "1.0.0",
"ICD": {
"library_path": "../../Frameworks/libMoltenVK.dylib",
"api_version" : "1.0.0"
}
}
We will need to make sure this new vulkan
folder is available within the out
folder after a build so Vulkan within our application executable can find it when it starts - remember it will be looking for a vulkan/icd.d/MoltenVK_icd.json
path to boostrap Vulkan. We will use a symlink again, very similar to how we included the assets
folder. Edit console/cmake-post-build.sh
and update it to look like the following:
#!/bin/bash
echo "Adding Frameworks @rpath to binary ..."
install_name_tool -add_rpath @loader_path/../Frameworks out/a-simple-triangle-console
pushd out
# See if there is an `assets` folder already.
if [ ! -d "assets" ]; then
# If there isn't create a new symlink named `assets`.
echo "Linking 'assets' path to '../../main/assets'"
ln -s ../../main/assets assets
fi
# See if there is a `vulkan` folder already.
if [ ! -d "vulkan" ]; then
# If there isn't create a new symlink named `vulkan`.
echo "Linking 'vulkan' path to '../vulkan'"
ln -s ../vulkan vulkan
fi
popd
Note the addition of the vulkan
folder check and symlink setup near the bottom - the rest of the script hasn’t changed.
Perform a CMake refresh then if you run your project now you will find a new vulkan
symlink in the out
folder.
Before adding our new Vulkan application we need to update our graphics wrapper and SDL wrapper headers to import the Vulkan C++ dependencies. Once we do this, the Vulkan APIs will become available in our code base since we previously included them in our CMakeLists.txt
file.
First off, edit main/src/core/graphics-wrapper.hpp
and add the Vulkan header include statement within the __APPLE__
block - the vulkan.hpp
file is the C++ Vulkan wrapper header since we would prefer it over the C style APIs found in vulkan.h
:
#elif __APPLE__
#include <vulkan/vulkan.hpp>
...
Note: There are advantages to using the C++ vs the C header for Vulkan, a key one is the access to a range of
Unique*
Vulkan class variants that basically act like smart pointers and clean themselves up when they go out of scope. If we were to use the C header instead, it would force us to do all the memory clean up manually - and tediously - which is error prone and requires heaps of boilerplate code to run at the correct points in time. The C++ Vulkan header is actually just a wrapper over the top of the C header with some helpful components to allow us to write more C++ styled code.
Next we’ll add some of the built in SDL Vulkan functionality to expose some Vulkan related SDL APIs that we’ll need. Edit main/src/core/sdl-wrapper.hpp
and add the following include statement:
#include <SDL_vulkan.h>
Our Vulkan application and associated classes will live in a new folder named main/src/application/vulkan
. Create it now then within it create the files vulkan-application.hpp
and vulkan-application.cpp
.
Edit vulkan-application.hpp
with the following:
#pragma once
#include "../../core/internal-ptr.hpp"
#include "../application.hpp"
namespace ast
{
struct VulkanApplication : public ast::Application
{
VulkanApplication();
void update(const float& delta) override;
void render() override;
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
The header looks almost identical to the existing opengl-application.hpp
header.
Now edit vulkan-application.cpp
to add the skeleton of our implementation:
#include "vulkan-application.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/sdl-wrapper.hpp"
#include "vulkan-context.hpp"
using ast::VulkanApplication;
struct VulkanApplication::Internal
{
const ast::VulkanContext context;
SDL_Window* window;
Internal() : context(ast::VulkanContext()),
window(ast::sdl::createWindow(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI)) {}
void update(const float& delta) {}
void render() {}
~Internal()
{
if (window)
{
SDL_DestroyWindow(window);
}
}
};
VulkanApplication::VulkanApplication() : internal(ast::make_internal_ptr<Internal>()) {}
void VulkanApplication::update(const float& delta)
{
internal->update(delta);
}
void VulkanApplication::render()
{
internal->render();
}
Our internal struct creates an SDL window in a very similar way to our OpenGL application, but passes in the SDL_WINDOW_VULKAN
argument instead.
The context
field will hold the Vulkan instance itself along with a bunch of state that it requires to run. We’ll author the VulkanContext
class next:
const ast::VulkanContext context;
Unlike the OpenGL context from our OpenGL application, the Vulkan context is not part of any Vulkan SDK - it is completely our own invention. To use Vulkan in an application we need a Vulkan instance, which for us will be of the type vk::UniqueInstance
. Once we have an instance we need to construct and configure many other Vulkan components before we can do anything useful with it. We are writing the context class as a tidy way to encapsulate the instance and all the other code that will follow for Vulkan setup.
Create the files vulkan-context.hpp
and vulkan-context.cpp
under main/src/application/vulkan/
. Edit the header file with the following:
#pragma once
#include "../../core/internal-ptr.hpp"
namespace ast
{
struct VulkanContext
{
VulkanContext();
private:
struct Internal;
ast::internal_ptr<Internal> internal;
};
} // namespace ast
Very standard stuff - actually nothing special at all about the header. Next edit the implementation file with the following:
#include "vulkan-context.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/log.hpp"
#include "vulkan-common.hpp"
using ast::VulkanContext;
namespace
{
vk::UniqueInstance createInstance()
{
vk::ApplicationInfo applicationInfo{
"A Simple Triangle", // Application name
VK_MAKE_VERSION(1, 0, 0), // Application version
"A Simple Triangle", // Engine name
VK_MAKE_VERSION(1, 0, 0), // Engine version
VK_MAKE_VERSION(1, 0, 0) // Vulkan API version
};
// Find out what the mandatory Vulkan extensions are on the current device,
// by this stage we would have already determined that the extensions are
// available via the 'ast::vulkan::isVulkanAvailable()' call in our main engine.
std::vector<std::string> requiredExtensionNames{
ast::vulkan::getRequiredVulkanExtensionNames()};
// Pack the extension names into a data format consumable by Vulkan.
std::vector<const char*> extensionNames;
for (const auto& extension : requiredExtensionNames)
{
extensionNames.push_back(extension.c_str());
}
// Define the info for creating our Vulkan instance.
vk::InstanceCreateInfo instanceCreateInfo{
vk::InstanceCreateFlags(), // Flags
&applicationInfo, // Application info
0, // Enabled layer count
nullptr, // Enabled layer names
static_cast<uint32_t>(extensionNames.size()), // Enabled extension count
extensionNames.data() // Enabled extension names
};
// Build a new Vulkan instance from the configuration.
return vk::createInstanceUnique(instanceCreateInfo);
}
} // namespace
struct VulkanContext::Internal
{
const vk::UniqueInstance instance;
Internal() : instance(::createInstance())
{
ast::log("ast::VulkanContext", "Initialized Vulkan context successfully.");
}
};
VulkanContext::VulkanContext() : internal(ast::make_internal_ptr<Internal>()) {}
Note: You will get syntax errors as we haven’t yet authored the
vulkan-common.hpp
header or its functions but don’t worry we’ll do that shortly.
Let’s digest the implementation. Our Internal
struct simply holds a Vulkan instance and initialises it through the ::createInstance()
free function. As previously mentioned, by using the Unique*
variants of Vulkan classes we get automatic lifecycle management of them:
struct VulkanContext::Internal
{
const vk::UniqueInstance instance;
Internal() : instance(::createInstance())
{
ast::log("ast::VulkanContext", "Initialized Vulkan context successfully.");
}
};
The ::createInstance()
free function needs a bit of explaining. It starts off by creating a structure that describes our application to Vulkan. Most of the fields are up to us to decide on their values except the final API version which should reflect which version of Vulkan we are targetting:
vk::ApplicationInfo applicationInfo{
"A Simple Triangle", // Application name
VK_MAKE_VERSION(1, 0, 0), // Application version
"A Simple Triangle", // Engine name
VK_MAKE_VERSION(1, 0, 0), // Engine version
VK_MAKE_VERSION(1, 0, 0) // Vulkan API version
};
Note: Vulkan makes very heavy use of
Info
configuration structures to perform many of its operations. Get used to this style of creating a pile of info objects to perform Vulkan commands!
The next line is probably quite mysterious as we haven’t actually written the ast::vulkan::getRequiredVulkanExtensionNames()
function yet.
std::vector<std::string> requiredExtensionNames{
ast::vulkan::getRequiredVulkanExtensionNames()};
The function will return a list of what Vulkan extensions are required by the current device to operate successfully. For example on my Macbook Air the list of extensions required to create an instance successfully looks like this:
VK_KHR_surface
VK_MVK_macos_surface
The next block of code simply takes this vector of std::string
extension names and converts them to vector of const char*
so we can feed them into the subsequent Vulkan configuration which can’t take std::string
objects:
std::vector<const char*> extensionNames;
for (const auto& extension : requiredExtensionNames)
{
extensionNames.push_back(extension.c_str());
}
We use the applicationInfo
object along with the extension names to form the configuration of the Vulkan instance we want to create:
vk::InstanceCreateInfo instanceCreateInfo{
vk::InstanceCreateFlags(), // Flags
&applicationInfo, // Application info
0, // Enabled layer count
nullptr, // Enabled layer names
static_cast<uint32_t>(extensionNames.size()), // Enabled extension count
extensionNames.data() // Enabled extension names
};
The final statement creates a new vk::UniqueInstance
using the instance configuration object and returns it to the caller:
return vk::createInstanceUnique(instanceCreateInfo);
The Vulkan instance forms the core of our implementation - we will be adding many more Vulkan configuration objects and data structures in the next bunch of articles to get ourselves into a state where we can actually render something - so hang in there!
We’ll now author the ast::vulkan::getRequiredVulkanExtensionNames()
function used in our instance creation as a free function within the ast::vulkan
namespace. We will write it as a free function as we’ll need to use it again later in our implementation elsewhere.
Create vulkan-common.hpp
and vulkan-common.cpp
within the main/src/application/vulkan
folder. We’ll actually be authoring two functions, the getRequiredVulkanExtensionNames
function but also another named isVulkanAvailable
that can tell us whether or not we should even attempt to initialise Vulkan on the current device.
Edit vulkan-common.hpp
with the following - noting that there is no class
or struct
definition, just free functions within the ast::vulkan
namespace:
#pragma once
#include <string>
#include <vector>
namespace ast::vulkan
{
std::vector<std::string> getRequiredVulkanExtensionNames();
bool isVulkanAvailable();
} // namespace ast::vulkan
Then edit vulkan-common.cpp
with the following to implement the functions:
#include "vulkan-common.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/log.hpp"
#include "../../core/sdl-wrapper.hpp"
#include <set>
std::vector<std::string> ast::vulkan::getRequiredVulkanExtensionNames()
{
uint32_t extensionCount;
SDL_Vulkan_GetInstanceExtensions(nullptr, &extensionCount, nullptr);
auto extensionNames{std::make_unique<const char**>(new const char*[extensionCount])};
SDL_Vulkan_GetInstanceExtensions(nullptr, &extensionCount, *extensionNames);
std::vector<std::string> result(*extensionNames, *extensionNames + extensionCount);
return result;
}
bool ast::vulkan::isVulkanAvailable()
{
static const std::string logTag{"ast::vulkan::isVulkanAvailable"};
// Check if SDL itself can load Vulkan.
if (SDL_Vulkan_LoadLibrary(nullptr) != 0)
{
ast::log(logTag, "No SDL Vulkan support found.");
return false;
}
// Determine what Vulkan extensions are required by SDL to be able to run Vulkan then pump
// them into a 'set' so we can evaluate them easily.
std::vector<std::string> requiredExtensionNamesSource{
ast::vulkan::getRequiredVulkanExtensionNames()};
std::set<std::string> requiredExtensionNames(
requiredExtensionNamesSource.begin(),
requiredExtensionNamesSource.end());
// There should always be required extensions so we should never get 0.
if (requiredExtensionNames.empty())
{
ast::log(logTag, "No Vulkan required extensions found.");
return false;
}
// Iterate all the available Vulkan extensions on the current device, draining
// each one from the required extensions set along the way.
for (auto const& availableExtension : vk::enumerateInstanceExtensionProperties())
{
requiredExtensionNames.erase(availableExtension.extensionName);
}
// If our required extensions set isn't empty it means that one or more of
// them were not found in the available extensions, so we don't have what
// we require to successfully create a Vulkan instance.
if (!requiredExtensionNames.empty())
{
ast::log(logTag, "Missing one or more required Vulkan extensions.");
return false;
}
ast::log(logTag, "Vulkan is available.");
return true;
}
getRequiredVulkanExtensionNames function
This is the function we called to create the Vulkan instance earlier:
std::vector<std::string> ast::vulkan::getRequiredVulkanExtensionNames()
First we need to query SDL to find out how many Vulkan extensions are required to support provisioning a Vulkan instance, asking for the result to be inserted into the extensionCount
field:
uint32_t extensionCount;
SDL_Vulkan_GetInstanceExtensions(nullptr, &extensionCount, nullptr);
We then query SDL a second time, using the extensionCount
from the first query to extract the names of the required extensions. The double query seems very quirky but due to the way the SDL function operates we have to call the same SDL_Vulkan_GetInstanceExtensions
twice but parametised differently to get the different attributes. The SDL collection of extension names is in a format that isn’t particularly friendly for us in C++ land so we need to coerce the result into a vector of std::string
objects before returning it:
auto extensionNames{std::make_unique<const char**>(new const char*[extensionCount])};
SDL_Vulkan_GetInstanceExtensions(nullptr, &extensionCount, *extensionNames);
std::vector<std::string> result(*extensionNames, *extensionNames + extensionCount);
return result;
isVulkanAvailable
This function will be used in our core engine shortly to evaluate whether there is any point in attempting to initialise Vulkan on the current device.
bool ast::vulkan::isVulkanAvailable()
The first criteria is to query the SDL Vulkan implementation to find out if it can successfully start the Vulkan loader - for our Mac console this would mean bootstrapping the MoltenVK ICD. On other platforms such as Windows this might mean trying to locate and initialise the Vulkan graphics driver implementation. Of course if there isn’t one, then Vulkan is pretty much not an option …
if (SDL_Vulkan_LoadLibrary(nullptr) != 0)
{
ast::log(logTag, "No SDL Vulkan support found.");
return false;
}
Next we use our getRequiredVulkanExtensionNames
function to grab the collection of what Vulkan extensions must be supported on the device to be able to create a Vulkan instance. We then create a set
populated with the collection of extension names to make it easier to evaluate how many of the required extensions are available.
std::vector<std::string> requiredExtensionNamesSource{
ast::vulkan::getRequiredVulkanExtensionNames()};
std::set<std::string> requiredExtensionNames(
requiredExtensionNamesSource.begin(),
requiredExtensionNamesSource.end());
The collection of required extensions should at a minimum contain more than zero elements - if for some reason there are no required extensions it is a sign that Vulkan probably can’t be run on the device:
if (requiredExtensionNames.empty())
{
ast::log(logTag, "No Vulkan required extensions found.");
return false;
}
The set
of required extensions is then drained by all the currently available extensions that are reported via the result of the vk::enumerateInstanceExtensionProperties()
command:
for (auto const& availableExtension : vk::enumerateInstanceExtensionProperties())
{
requiredExtensionNames.erase(availableExtension.extensionName);
}
If our set
still contains any elements it means that there were required extensions that couldn’t be found in the collection of available extensions - ultimately meaning that we don’t in fact have the capability to successfully create a Vulkan instance:
if (!requiredExtensionNames.empty())
{
ast::log(logTag, "Missing one or more required Vulkan extensions.");
return false;
}
If all our required extensions could be found then we are in business!
ast::log(logTag, "Vulkan is available.");
return true;
Save your code and observe that the syntax errors should now be resolved in the vulkan-context
class.
The final change we need to make is in our core engine
code which creates our application. At the moment it only tries to create the OpenGL application like so:
std::unique_ptr<ast::Application> resolveApplication()
{
static const std::string logTag{classLogTag + "resolveApplication"};
try
{
ast::log(logTag, "Creating OpenGL application ...");
return std::make_unique<ast::OpenGLApplication>();
}
catch (const std::exception& error)
{
ast::log(logTag, "OpenGL application failed to initialize.", error);
}
throw std::runtime_error(logTag + " No applications can run in the current environment");
}
We will update this method to do the following:
ast::vulkan::isVulkanAvailable()
function.ast::VulkanApplication
and return it.Add the following header includes to engine.cpp
:
#include "../application/vulkan/vulkan-application.hpp"
#include "../application/vulkan/vulkan-common.hpp"
Then update the resolveApplication
function with the following:
std::unique_ptr<ast::Application> resolveApplication()
{
static const std::string logTag{classLogTag + "resolveApplication"};
if (ast::vulkan::isVulkanAvailable())
{
try
{
ast::log(logTag, "Creating Vulkan application ...");
return std::make_unique<ast::VulkanApplication>();
}
catch (const std::exception& error)
{
ast::log(logTag, "Vulkan application failed to initialize.", error);
}
}
try
{
ast::log(logTag, "Creating OpenGL application ...");
return std::make_unique<ast::OpenGLApplication>();
}
catch (const std::exception& error)
{
ast::log(logTag, "OpenGL application failed to initialize.", error);
}
throw std::runtime_error(logTag + " No applications can run in the current environment");
}
Save and run the console project and as long as you are running on a Mac with Metal support you should see an empty black window appear but with the Vulkan console output indicating that it worked successfully.
Note: In the screenshot below I’m directly running
./a-simple-triangle-console
via Terminal in theout
folder to make it a bit easier to see the logging output from our application. When running through Visual Studio Code you will see the same thing except the logging statements would be displayed within the appropriate console output panel.
Next up we’ll setup the MacOS application to initialise Vulkan.
The code for this article can be found here.
Continue to Part 15: Vulkan setup MacOS.
End of part 14