- By default the GUI creates its own VkInstance and VkDevice, and owns them.
- You can hand it your own instead, for example when the GUI is the editor of a game engine that already has a device.
- A complete program: examples/host_device/host_device.cpp. The Host device example walks through it.
Device requirements
tachyon::vulkan::VulkanDeviceContext::GetDeviceRequirements() returns what the device must enable:
struct DeviceRequirements
{
uint32_t apiVersion = VK_API_VERSION_1_3;
VkPhysicalDeviceFeatures features{};
VkPhysicalDeviceVulkan11Features features11{};
VkPhysicalDeviceVulkan12Features features12{};
VkPhysicalDeviceVulkan13Features features13{};
std::vector<const char*> deviceExtensions;
};
The feature structs have no pNext chain; chain them yourself. With vk-bootstrap:
auto selected = vkb::PhysicalDeviceSelector{ instance }
.set_minimum_version(1, 3)
.set_required_features(requirements.features)
.set_required_features_11(requirements.features11)
.set_required_features_12(requirements.features12)
.set_required_features_13(requirements.features13)
.add_required_extensions(requirements.deviceExtensions)
.defer_surface_initialization()
.select();
static DeviceRequirements GetDeviceRequirements()
What a host's device must enable for the GUI.
- VulkanDeviceContext::CheckPhysicalDeviceSupport(physicalDevice, &problems) lists what a physical device lacks, one line per missing feature or extension.
- The GUI runs the same check when it adopts your device:
- if the device lacks something, Initialize fails with a message
- the check cannot see what you actually enabled when you created the device
std::string problems;
std::fprintf(stderr, "%s", problems.c_str());
static bool CheckPhysicalDeviceSupport(VkPhysicalDevice physicalDevice, std::string *problems)
Checks that a physical device can support GetDeviceRequirements().
Also required:
- The instance enables the window backend's surface extensions.
- The window backend is the part of the GUI that creates OS windows. The default one uses SDL.
- For SDL, get the extensions from SDL_Vulkan_GetInstanceExtensions.
- That function needs SDL running, so call SDL_Init(SDL_INIT_VIDEO) before creating the instance.
- The queue supports graphics, and presenting to the GUI's windows.
SDL_Init(SDL_INIT_VIDEO);
Uint32 count = 0;
if (const char* const* extensions = SDL_Vulkan_GetInstanceExtensions(&count))
for (Uint32 i = 0; i < count; ++i)
builder.enable_extension(extensions[i]);
Handing over the device
- The graphics backend is the part of the GUI that draws. The Vulkan one is VulkanGraphicsContext.
- Set your device on a VulkanGraphicsContext with SetExternalDevice.
- Give the backend to the app with SetGraphicsBackend.
- Do both before Initialize.
auto backend = std::make_unique<tachyon::vulkan::VulkanGraphicsContext>();
backend->SetExternalDevice(external);
auto* vk = backend.get();
app.SetGraphicsBackend(std::move(backend));
std::fprintf(stderr, "%s\n", result.GetError().message.c_str());
A value, or the Error saying why there is none.
Definition gui_result.h:36
A host's Vulkan handles for SetExternalDevice, so the GUI renders on the host's device (an engine hos...
Definition vk_device_context.h:152
VkDevice device
Made with GetDeviceRequirements() enabled.
Definition vk_device_context.h:158
VkInstance instance
Must enable the window backend's surface extensions.
Definition vk_device_context.h:154
VkPhysicalDevice physicalDevice
The GPU device was made on.
Definition vk_device_context.h:156
VkQueue graphicsQueue
Every GUI submit and present goes here; it supports graphics and presenting to the GUI's windows.
Definition vk_device_context.h:161
std::mutex * queueMutex
Optional.
Definition vk_device_context.h:172
uint32_t graphicsQueueFamily
graphicsQueue's family.
Definition vk_device_context.h:163
VmaAllocator allocator
Optional.
Definition vk_device_context.h:166
- allocator is optional.
- Without it, the GUI creates its own VmaAllocator and owns it.
- An allocator you pass must be created with VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, or Initialize fails.
VmaAllocatorCreateInfo info{};
info.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;
info.instance = hostInstance;
info.physicalDevice = hostPhysicalDevice;
info.device = hostDevice;
info.vulkanApiVersion = VK_API_VERSION_1_3;
vmaCreateAllocator(&info, &hostAllocator);
Ownership.
- The GUI destroys only what it created.
- Destroy your instance, device and allocator after Cleanup.
app.Cleanup();
vmaDestroyAllocator(hostAllocator);
vkDestroyDevice(hostDevice, nullptr);
vkDestroyInstance(hostInstance, nullptr);
Threading.
- The GUI locks queueMutex around every vkQueueSubmit2, vkQueuePresentKHR and vkDeviceWaitIdle.
- If other threads submit to the same queue, pass the mutex they lock.
- Without one, the GUI uses a private mutex that only guards its own threads.
- Presenting can block until vsync while holding the lock.
- If the device has a second graphics queue, give the GUI its own queue to avoid that.
- While recording a frame, the GUI copies uploads into mapped memory on a separate thread, and waits for it before each submit.
- That thread never touches the queue.
std::lock_guard lock(hostQueueMutex);
vkQueueSubmit2(hostGraphicsQueue, 1, &submit, fence);
Showing a host-rendered image
To show an image you render, such as a 3D viewport, register its view and sampler. You get an ImageIndex, the number the Image control takes:
vk->UpdateExternalImage(index, newView, sampler);
ImageIndex
Index of an image in the graphics context's image set, as the shaders sample it.
Definition gui_image.h:37
Definition gui_base_types.h:75
- You own the view and the sampler.
- They must outlive every GUI frame that uses the index.
- Image uses the style theme.imageStyleRef.
- A custom element or style shows the image only if its background color style type is ColorType::StretchedTexture.
Synchronization contract, all on the queue passed to SetExternalDevice:
- By the end of your frame's command buffer, leave the image in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, writes visible to the fragment stage (destination stage FRAGMENT_SHADER, access SHADER_SAMPLED_READ).
- Before writing it again, transition out of that layout with source stage FRAGMENT_SHADER, so your write waits for the GUI's earlier reads.
- Submit during OnGUI, before the GUI submits the frame. Ordering on one queue makes both barriers apply across command buffers.
- After a resize, no GUI frame still in flight may sample the old view.
- vkDeviceWaitIdle is the simplest way to make sure.
- Reference: Host::Render in examples/host_device/host_device.cpp.
- tests/gpu/gpu_external_device_test.cpp checks it under synchronization validation.
Images from another queue
An image written on another queue (async compute, for example) doesn't have to go through the GUI's queue. Instead, have the GUI's frame wait for it:
vk->AddFrameWait(computeDone, computeValue, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT);
- computeDone is a timeline semaphore on the GUI's device.
- Every window's submission of the next frame waits until it reaches computeValue, before the stages you pass.
- Two waits on one semaphore are merged:
- the higher value is used
- both stages wait
- The frame after doesn't wait on it again.
- The image's layout and queue family ownership are still yours to settle, as above.
Previous: Adding the library to a build | Integration | Next: Your own window and input