evan 2026.1.4
Evan
Loading...
Searching...
No Matches
XrSwapchainContext.cpp
1/*
2** ETIB PROJECT, 2026
3** evan
4** File description:
5** XrSwapchainContext
6*/
7
8#include "evan/openxr/XrSwapchainContext.hpp"
9
10#include "evan/CheckedCast.hpp"
11
12#include <algorithm>
13
14#include <glm/gtc/quaternion.hpp>
15
16namespace
17{
25 VkSampleCountFlagBits toVkSampleCountFlagBits(uint32_t sampleCount)
26 {
27 switch (sampleCount) {
28 case 2:
29 return VK_SAMPLE_COUNT_2_BIT;
30 case 4:
31 return VK_SAMPLE_COUNT_4_BIT;
32 case 8:
33 return VK_SAMPLE_COUNT_8_BIT;
34 case 16:
35 return VK_SAMPLE_COUNT_16_BIT;
36 case 32:
37 return VK_SAMPLE_COUNT_32_BIT;
38 case 64:
39 return VK_SAMPLE_COUNT_64_BIT;
40 default:
41 return VK_SAMPLE_COUNT_1_BIT;
42 }
43 }
44} // namespace
45
47{
48 this->getLogger().info() << "Initializing XrSwapchainContext";
49
51 *deviceContext.getDeviceBackend());
52
54 deviceContext.getDeviceBackend()->countSwapchainFormats();
55 auto swapchainFormats =
56 deviceContext.getDeviceBackend()->enumerateSwapchainFormats(
58
59 _viewsConfigurations = backend.enumerateViewConfigurations();
60 _views.resize(_viewsConfigurations.size(), { XR_TYPE_VIEW });
61
62 _viewSet.resize(_viewsConfigurations.size());
63
64 selectMsaaSamples(deviceContext);
65
66 // Multiview needs the device feature, a stereo view count and one image
67 // layout every eye agrees on. Whether the runtime actually accepts the
68 // layered swapchain is only known once xrCreateSwapchain returns, so the
69 // tentative mode is confirmed by the creation below and falls back to one
70 // swapchain per view.
71 const bool multiviewWanted = backend.supportsMultiview()
72 && multiviewMode() != MultiviewMode::Off
73 && _viewsConfigurations.size() >= 2
74 && _viewsConfigurations.size()
75 <= static_cast<std::size_t>(MAX_MULTIVIEW_VIEWS)
76 && viewConfigurationsShareLayout();
77
79 deviceContext.getDeviceBackend()->getPhysicalDevice(),
81
82 std::vector<SwapchainHandle> handles;
83 if (multiviewWanted) {
84 auto handle = createMultiviewSwapchain(deviceContext, swapchainFormat);
85 if (handle.has_value()) {
86 _multiviewEnabled = true;
87 handles.push_back(handle.value());
88 this->getLogger().info()
89 << "Using a single layered swapchain with "
90 << handles.back().createInfo.arraySize
91 << " array layers to render all views in one pass.";
92 } else {
93 this->getLogger().warning()
94 << "The runtime did not provide a layered swapchain; falling "
95 "back to one swapchain per view.";
96 }
97 }
98 if (!_multiviewEnabled) {
99 handles = createPerViewSwapchains(deviceContext, swapchainFormat);
100 }
101 if (multiviewMode() == MultiviewMode::On && !_multiviewEnabled) {
102 this->getLogger().error()
103 << "EVAN_MULTIVIEW=on but multiview could not be enabled; "
104 "falling back to the two-pass path.";
105 }
106
107 // Every view targets the single layered image set in multiview mode, and
108 // its own swapchain otherwise. syncViewSet() re-applies this mapping every
109 // frame, so both have to agree.
110 for (std::size_t i = 0; i < _viewSet.size(); ++i) {
111 _viewSet[i].swapchainIndex = _multiviewEnabled ? 0 : i;
112 }
113
115 deviceContext.getDeviceBackend(), _msaaSamples, _colorAttachmentMode,
116 _multiviewEnabled ? multiviewViewMask(_viewSet.size()) : 0u);
117
118 buildSwapchainImages(deviceContext, handles);
119}
120
121std::optional<evan::XrSwapchainContext::SwapchainHandle>
122 evan::XrSwapchainContext::createMultiviewSwapchain(
123 const DeviceContext &deviceContext, VkFormat swapchainFormat)
124{
126 *deviceContext.getDeviceBackend());
127
128 // viewConfigurationsShareLayout() guarantees the first configuration is
129 // representative of every eye.
130 const auto &viewConfig = _viewsConfigurations.front();
131
132 SwapchainHandle handle {};
133 handle.createInfo.type = XR_TYPE_SWAPCHAIN_CREATE_INFO;
134 handle.createInfo.arraySize = static_cast<uint32_t>(_viewSet.size());
135 handle.createInfo.format = swapchainFormat;
136 handle.createInfo.width = viewConfig.recommendedImageRectWidth;
137 handle.createInfo.height = viewConfig.recommendedImageRectHeight;
138 handle.createInfo.mipCount = 1;
139 handle.createInfo.faceCount = 1;
140 // The render pass and framebuffers are built for this sample count (see
141 // selectMsaaSamples), so it must stay the runtime's recommended value
142 // rather than the application one.
143 handle.createInfo.sampleCount = viewConfig.recommendedSwapchainSampleCount;
144 handle.createInfo.usageFlags =
145 backend.getOpenXrOptions().swapchainUsageFlags;
146
147 this->getLogger().info()
148 << "Creating layered swapchain for multiview rendering...";
149
150 XrResult result = xrCreateSwapchain(backend.getSession(),
151 &handle.createInfo, &handle.swapchain);
152 if (result != XR_SUCCESS) {
153 this->getLogger().warning()
154 << "Failed to create a swapchain with arraySize "
155 << handle.createInfo.arraySize << " with error code: " << result;
156 return std::nullopt;
157 }
158 return handle;
159}
160
161std::vector<evan::XrSwapchainContext::SwapchainHandle>
162 evan::XrSwapchainContext::createPerViewSwapchains(
163 const DeviceContext &deviceContext, VkFormat swapchainFormat)
164{
166 *deviceContext.getDeviceBackend());
167
168 std::vector<SwapchainHandle> handles;
169 for (const auto &viewConfig: _viewsConfigurations) {
170 this->getLogger().info() << "Creating swapchain for view configuration";
171
172 SwapchainHandle handle {};
173 handle.createInfo.type = XR_TYPE_SWAPCHAIN_CREATE_INFO;
174 handle.createInfo.arraySize = 1;
175 handle.createInfo.format = swapchainFormat;
176 handle.createInfo.width = viewConfig.recommendedImageRectWidth;
177 handle.createInfo.height = viewConfig.recommendedImageRectHeight;
178 handle.createInfo.mipCount = 1;
179 handle.createInfo.faceCount = 1;
180 // The render pass and framebuffers are built for this sample count
181 // (see selectMsaaSamples), so it must stay the runtime's recommended
182 // value rather than the application one.
183 handle.createInfo.sampleCount =
184 viewConfig.recommendedSwapchainSampleCount;
185 handle.createInfo.usageFlags =
186 backend.getOpenXrOptions().swapchainUsageFlags;
187
188 XrResult result = xrCreateSwapchain(
189 backend.getSession(), &handle.createInfo, &handle.swapchain);
190 if (result != XR_SUCCESS) {
191 this->getLogger().error()
192 << "Failed to create swapchain for view configuration "
193 "with error code: "
194 << result;
195 continue;
196 }
197 handles.push_back(handle);
198 }
199 return handles;
200}
201
202void evan::XrSwapchainContext::buildSwapchainImages(
203 const DeviceContext &deviceContext,
204 const std::vector<SwapchainHandle> &handles)
205{
206 for (const auto &handle: handles) {
208 .swapchain = handle.swapchain,
209 .createInfo = handle.createInfo,
210 .renderPass = _renderPass,
211 .deviceContext = deviceContext,
212 .msaaSamples = _msaaSamples,
213 .colorAttachmentMode = _colorAttachmentMode
214 };
215 _swapchainImages.push_back(
216 std::make_shared<XrSwapchainImage>(properties));
217 }
218}
219
220bool evan::XrSwapchainContext::viewConfigurationsShareLayout() const
221{
222 if (_viewsConfigurations.empty()) {
223 return false;
224 }
225
226 const auto &reference = _viewsConfigurations.front();
227 for (const auto &viewConfig: _viewsConfigurations) {
228 if (viewConfig.recommendedImageRectWidth
229 != reference.recommendedImageRectWidth
230 || viewConfig.recommendedImageRectHeight
231 != reference.recommendedImageRectHeight
232 || viewConfig.recommendedSwapchainSampleCount
233 != reference.recommendedSwapchainSampleCount) {
234 return false;
235 }
236 }
237 return true;
238}
239
240void evan::XrSwapchainContext::selectMsaaSamples(
241 const DeviceContext &deviceContext)
242{
243 // The swapchain images belong to the runtime, and their sample count is
244 // whatever it handed out (XrSwapchainCreateInfo::sampleCount asks for the
245 // recommended value). The render pass and the framebuffers have to match
246 // that count, so a runtime that only offers multisampled swapchain images
247 // overrides the application setting.
248 const uint32_t recommended = _viewsConfigurations.empty()
249 ? 0u
250 : _viewsConfigurations.front().recommendedSwapchainSampleCount;
251 const VkSampleCountFlagBits runtimeSamples =
252 toVkSampleCountFlagBits(recommended);
253 const VkSampleCountFlagBits requested = deviceContext.getMsaaSamples();
254
255 if (_viewsConfigurations.empty()) {
256 this->getLogger().warning()
257 << "No OpenXR view configuration available. Using " << requested
258 << " sample(s).";
259 _msaaSamples = requested;
260 _colorAttachmentMode = requested == VK_SAMPLE_COUNT_1_BIT
261 ? ColorAttachmentMode::SwapchainImageAttachment
262 : ColorAttachmentMode::ResolveToSwapchain;
263 return;
264 }
265
266 if (runtimeSamples != VK_SAMPLE_COUNT_1_BIT) {
267 // The runtime only hands out multisampled swapchain images: render
268 // into them directly, their sample count cannot be lowered.
269 _msaaSamples = runtimeSamples;
270 _colorAttachmentMode = ColorAttachmentMode::SwapchainImageAttachment;
271 if (requested != runtimeSamples) {
272 this->getLogger().warning()
273 << "The OpenXR runtime recommends a " << recommended
274 << "-sample swapchain, so the application request of "
275 << requested << " sample(s) is ignored.";
276 }
277 } else if (requested == VK_SAMPLE_COUNT_1_BIT) {
278 // Default path: no multisampling anywhere and no resolve. The render
279 // pass targets the single-sampled swapchain image directly.
280 _msaaSamples = VK_SAMPLE_COUNT_1_BIT;
281 _colorAttachmentMode = ColorAttachmentMode::SwapchainImageAttachment;
282 } else {
283 // The application asked for multisampling: shade into an engine-owned
284 // multisampled color image and resolve it into the single-sampled
285 // swapchain image.
286 _msaaSamples = requested;
287 _colorAttachmentMode = ColorAttachmentMode::ResolveToSwapchain;
288 }
289
290 this->getLogger().info()
291 << "OpenXR recommended swapchain sample count: " << recommended
292 << ", using MSAA samples: " << _msaaSamples
293 << ", resolve to swapchain: "
294 << (_colorAttachmentMode == ColorAttachmentMode::ResolveToSwapchain
295 ? "yes"
296 : "no");
297}
298
300// Public Methods //
302
304{
305 this->getLogger().info()
306 << "Destroying XrSwapchainContext and releasing resources";
307
308 for (const auto &swapchainImage: _swapchainImages) {
309 this->getLogger().info()
310 << "Destroying swapchain image and releasing resources";
311 swapchainImage->destroy(device);
312 }
313 _swapchainImages.clear();
314}
315
317 const DeviceContext &deviceContext, VkRenderPass renderpass)
318{
320 this->getLogger().info() << "Recreating swapchain and associated resources "
321 "for XrSwapchainContext";
322
323 for (const auto &swapchainImage: _swapchainImages) {
324 this->getLogger().info()
325 << "Destroying swapchain image and releasing resources";
326 swapchainImage->destroy(deviceContext.getDeviceBackend()->getDevice());
327 }
328 _swapchainImages.clear();
329
330 auto swapchainFormat = selectSwapchainFormat(
331 deviceContext.getDeviceBackend()->getPhysicalDevice(),
332 deviceContext.getDeviceBackend()->enumerateSwapchainFormats(
333 deviceContext.getDeviceBackend()->countSwapchainFormats()));
334
335 // The render pass (and therefore the view mask and the pipeline
336 // compatibility) is fixed for the session, so recreation has to reuse the
337 // mode that was established at construction.
338 std::vector<SwapchainHandle> handles;
339 if (_multiviewEnabled) {
340 auto handle = createMultiviewSwapchain(deviceContext, swapchainFormat);
341 if (handle.has_value()) {
342 handles.push_back(handle.value());
343 } else {
344 this->getLogger().error()
345 << "Failed to recreate the layered swapchain; leaving the "
346 "context without swapchain images.";
347 }
348 } else {
349 handles = createPerViewSwapchains(deviceContext, swapchainFormat);
350 }
351
352 buildSwapchainImages(deviceContext, handles);
353
354 this->getLogger().info() << "Finished recreating swapchain and associated "
355 "resources for XrSwapchainContext";
356}
357
361{
362 this->getLogger().info() << "Acquiring swapchain image";
363
364 XrSwapchain swapchain =
365 evan::checkedCast<XrSwapchainImage>(*_swapchainImages[index])
366 ._swapchain;
371 if (result != XR_SUCCESS) {
372 this->getLogger().error()
373 << "Failed to acquire swapchain image with error code: " << result;
375 }
376 return VK_SUCCESS;
377}
378
380{
381 this->getLogger().info() << "Waiting for swapchain image";
382
383 XrSwapchain swapchain =
384 evan::checkedCast<XrSwapchainImage>(*_swapchainImages[index])
385 ._swapchain;
390 if (result != XR_SUCCESS) {
391 this->getLogger().error()
392 << "Failed to wait for swapchain image with error code: " << result;
393 return;
394 }
395 this->getLogger().info() << "Successfully waited for swapchain image";
396}
397
399{
400 this->getLogger().info()
401 << "Updating projection layer views based on current swapchain images "
402 "and view configurations";
403
404 _projectionLayerViews.resize(_views.size());
405 for (size_t i = 0; i < _views.size(); ++i) {
406 this->getLogger().info()
407 << "Updating projection layer view for index: " << i;
408
409 // Each view reaches its image set through the ViewSet mapping: in
410 // multiview mode every eye points at the single layered swapchain,
411 // otherwise at its own.
412 if (i >= _viewSet.size()) {
413 continue;
414 }
415 const std::size_t swapchainIndex = _viewSet[i].swapchainIndex;
416 if (swapchainIndex >= _swapchainImages.size()) {
417 continue;
418 }
419 auto *viewSwapchain = dynamic_cast<XrSwapchainImage *>(
420 _swapchainImages[swapchainIndex].get());
421
422 if (viewSwapchain == nullptr) {
423 continue;
424 }
425
426 _projectionLayerViews[i] = {};
427 _projectionLayerViews[i].type =
429 _projectionLayerViews[i].pose = _views[i].pose;
430 _projectionLayerViews[i].fov = _views[i].fov;
431 _projectionLayerViews[i].subImage.swapchain = viewSwapchain->_swapchain;
432 // In multiview mode both eyes share the layered swapchain and select
433 // their own array layer; in the fallback every swapchain has a single
434 // layer, so the index stays zero.
435 _projectionLayerViews[i].subImage.imageArrayIndex =
436 _multiviewEnabled ? static_cast<uint32_t>(i) : 0u;
437 _projectionLayerViews[i].subImage.imageRect.offset = { 0, 0 };
438 _projectionLayerViews[i].subImage.imageRect.extent = {
439 static_cast<int32_t>(viewSwapchain->_width),
440 static_cast<int32_t>(viewSwapchain->_height)
441 };
442 }
443 this->getLogger().info() << "Finished updating projection layer views";
444}
445
447// Getters //
449
450const std::vector<XrCompositionLayerProjectionView> &
452{
453 return _projectionLayerViews;
454}
455
457{
458 return _viewSet;
459}
460
462{
463 return _viewSet;
464}
465
467{
468 return _msaaSamples;
469}
470
472{
473 return _multiviewEnabled;
474}
475
477{
478 this->getLogger().info() << "Syncing ViewSet from OpenXR view state";
479
480 if (_viewSet.size() != _views.size()) {
481 _viewSet.resize(_views.size());
482 }
483
484 // Track the raw head center before applying the locomotion offset so that
485 // setView can reduce the requested pose to an offset relative to it.
486 if (!_views.empty()) {
487 glm::vec3 centerPosition(0.0f);
488 for (const auto &xrView: _views) {
490 glm::vec3(xrView.pose.position.x, xrView.pose.position.y,
491 xrView.pose.position.z);
492 }
493 centerPosition /= static_cast<float>(_views.size());
494
495 glm::quat centerOrientation(1.0f, 0.0f, 0.0f, 0.0f);
496 if (_views.size() == 1) {
497 const auto &orientation = _views[0].pose.orientation;
500 } else {
501 const auto &left = _views[0].pose.orientation;
502 const auto &right = _views[1].pose.orientation;
503 glm::quat leftQ(left.w, left.x, left.y, left.z);
504 glm::quat rightQ(right.w, right.x, right.y, right.z);
505 centerOrientation = glm::normalize(glm::slerp(leftQ, rightQ, 0.5f));
506 }
507
508 _baseCenterPose = utility::graphic::PoseF(
509 utility::graphic::PositionF(centerPosition.x, centerPosition.y,
511 utility::graphic::OrientationF(
514 _hasBaseCenter = true;
515 }
516
517 const auto &offsetPose = _viewOffset.getPosition();
518 const auto &offsetOrient = _viewOffset.getOrientation();
519 const glm::quat offsetQ(offsetOrient.w, offsetOrient.x, offsetOrient.y,
520 offsetOrient.z);
521 const glm::vec3 offsetPosition(offsetPose.getX(), offsetPose.getY(),
522 offsetPose.getZ());
523
524 for (std::size_t i = 0; i < _views.size(); ++i) {
525 // Multiview renders every eye from the single layered swapchain; the
526 // fallback keeps one image set per view. Re-applying the same mapping
527 // the constructor chose keeps syncViewSet and recreation consistent.
528 _viewSet[i].swapchainIndex = _multiviewEnabled ? 0 : i;
529
530 utility::graphic::ViewF view;
531 const auto &xrPose = _views[i].pose;
532
533 glm::quat baseQ(xrPose.orientation.w, xrPose.orientation.x,
534 xrPose.orientation.y, xrPose.orientation.z);
535 glm::quat finalQ = glm::normalize(offsetQ * baseQ);
536
537 glm::vec3 basePosition(xrPose.position.x, xrPose.position.y,
538 xrPose.position.z);
540
541 utility::graphic::PoseF pose(
542 utility::graphic::PositionF(finalPosition.x, finalPosition.y,
543 finalPosition.z),
544 utility::graphic::OrientationF(finalQ.x, finalQ.y, finalQ.z,
545 finalQ.w));
546 utility::graphic::FieldOfViewF fov(
547 _views[i].fov.angleUp, _views[i].fov.angleDown,
548 _views[i].fov.angleLeft, _views[i].fov.angleRight);
549
550 view.setPose(pose);
551 view.setFieldOfView(fov);
552 view.setClippingPlanes(_nearPlane, _farPlane);
553
554 _viewSet[i].view = view;
555 }
556}
557
559 const utility::graphic::ViewF &view)
560
561{
562 (void)index;
563
564 _nearPlane = view.getNearPlane();
565 _farPlane = view.getFarPlane();
566
567 if (!_hasBaseCenter) {
568 return;
569 }
570
571 // The runtime refreshes the tracked eye poses every frame, so the requested
572 // pose is stored as a world-space offset relative to the tracked head
573 // center and re-applied by syncViewSet.
574 const auto requestedPosition = view.getPose().getPosition();
575 const auto requestedOrient = view.getPose().getOrientation();
576 const auto basePosition = _baseCenterPose.getPosition();
577 const auto baseOrient = _baseCenterPose.getOrientation();
578
581 glm::quat baseQ(baseOrient.w, baseOrient.x, baseOrient.y, baseOrient.z);
582
583 glm::quat offsetQ = glm::normalize(requestedQ * glm::inverse(baseQ));
584
585 _viewOffset = utility::graphic::PoseF(
586 utility::graphic::PositionF(
587 requestedPosition.getX() - basePosition.getX(),
588 requestedPosition.getY() - basePosition.getY(),
589 requestedPosition.getZ() - basePosition.getZ()),
590 utility::graphic::OrientationF(offsetQ.x, offsetQ.y, offsetQ.z,
591 offsetQ.w));
592}
VkFormat selectSwapchainFormat(VkPhysicalDevice physicalDevice, const std::vector< int64_t > &swapchainFormats)
Selects the appropriate swapchain format from the available formats.
void createRenderPass(const std::shared_ptr< ADeviceBackend > &deviceBackend, VkSampleCountFlagBits msaaSamples, ColorAttachmentMode mode, uint32_t viewMask=0)
Creates the Vulkan render pass for the swapchain context.
Represents the device context for the engine, managing Vulkan-related resources and operations.
std::shared_ptr< ADeviceBackend > getDeviceBackend() const
Retrieves a shared pointer to the ADeviceBackend instance associated with this device context.
Describes a set of views to render, independent of the swapchain image sets.
Definition ViewSet.hpp:37
void resize(std::size_t count)
Resizes the view set to contain exactly count views.
std::size_t size() const
void updateProjectionLayerViews()
Updates the projection layer views based on current swapchain images and view data.
void syncViewSet()
Rebuilds the ViewSet from the latest OpenXR view state.
std::vector< XrView > _views
void recreateSwapchain(const DeviceContext &deviceContext, VkRenderPass renderpass) override
Recreates the swapchain and associated resources.
bool usesMultiview() const override
Indicates whether all views render into one layered swapchain.
void destroy(VkDevice device) override
Destroys all swapchain images and clears the swapchain image collection.
VkResult aquireImage(uint32_t index, VkDevice device, VkSemaphore imageAvailableSemaphore, VkFence inFlightFence, uint32_t &imageIndex) override
Acquires an image from the swapchain for rendering.
VkSampleCountFlagBits getMsaaSamples() const override
Retrieves the MSAA sample count used by this swapchain context.
XrSwapchainContext(const DeviceContext &deviceContext)
Constructs an XrSwapchainContext instance using the provided DeviceContext.
std::vector< XrViewConfigurationView > _viewsConfigurations
ViewSet & getViewSet() override
Retrieves the set of views to render (mutable).
const std::vector< XrCompositionLayerProjectionView > & getProjectionLayerViews() const
Retrieves the projection layer views for the current view configurations.
void setView(std::size_t index, const utility::graphic::ViewF &view) override
Sets the view matrix for a specific view index.
void waitForImage(uint32_t index) override
Waits for a swapchain image to become available.
Represents a swapchain image for OpenXR rendering.
Small result wrapper used by helpers that must return both a status and a value (e....
Definition Error.hpp:89
Encapsulates properties for creating an XrSwapchainImage instance.
XrSwapchain swapchain
The OpenXR swapchain handle associated with the swapchain images.