diff --git a/intern/ghost/CMakeLists.txt b/intern/ghost/CMakeLists.txt index dc86cd22702..407e3b771d7 100644 --- a/intern/ghost/CMakeLists.txt +++ b/intern/ghost/CMakeLists.txt @@ -579,6 +579,14 @@ if(WITH_XR_OPENXR) # Header only library. ../../extern/tinygltf/tiny_gltf.h ) + if(WIN32) + list(APPEND SRC + intern/GHOST_XrGraphicsBindingD3D.cc + + intern/GHOST_XrGraphicsBindingD3D.hh + ) + endif() + list(APPEND INC_SYS ../../extern/json/include ../../extern/tinygltf diff --git a/intern/ghost/intern/GHOST_XrGraphicsBinding.cc b/intern/ghost/intern/GHOST_XrGraphicsBinding.cc index efad401a47c..cc29d7cc869 100644 --- a/intern/ghost/intern/GHOST_XrGraphicsBinding.cc +++ b/intern/ghost/intern/GHOST_XrGraphicsBinding.cc @@ -23,6 +23,7 @@ # include "GHOST_ContextD3D.hh" # include "GHOST_ContextWGL.hh" # include "GHOST_SystemWin32.hh" +# include "GHOST_XrGraphicsBindingD3D.hh" #endif #ifdef WITH_VULKAN_BACKEND # include "GHOST_XrGraphicsBindingVulkan.hh" @@ -303,237 +304,6 @@ class GHOST_XrGraphicsBindingOpenGL : public GHOST_IXrGraphicsBinding { GLuint m_fbo = 0; }; -#ifdef WIN32 -static void ghost_format_to_dx_format(GHOST_TXrSwapchainFormat ghost_format, - bool expects_srgb_buffer, - DXGI_FORMAT &r_dx_format) -{ - r_dx_format = DXGI_FORMAT_UNKNOWN; - - switch (ghost_format) { - case GHOST_kXrSwapchainFormatRGBA8: - r_dx_format = expects_srgb_buffer ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : - DXGI_FORMAT_R8G8B8A8_UNORM; - break; - case GHOST_kXrSwapchainFormatRGBA16: - r_dx_format = DXGI_FORMAT_R16G16B16A16_UNORM; - break; - case GHOST_kXrSwapchainFormatRGBA16F: - r_dx_format = DXGI_FORMAT_R16G16B16A16_FLOAT; - break; - case GHOST_kXrSwapchainFormatRGB10_A2: - r_dx_format = DXGI_FORMAT_R10G10B10A2_UNORM; - break; - } - - if (r_dx_format == DXGI_FORMAT_UNKNOWN) { - throw GHOST_XrException("No supported DirectX swapchain format found."); - } -} - -class GHOST_XrGraphicsBindingD3D : public GHOST_IXrGraphicsBinding { - public: - GHOST_XrGraphicsBindingD3D() : GHOST_IXrGraphicsBinding() - { - m_ghost_d3d_ctx = GHOST_SystemWin32::createOffscreenContextD3D(); - } - virtual ~GHOST_XrGraphicsBindingD3D() - { - if (m_ghost_d3d_ctx) { - GHOST_SystemWin32::disposeContextD3D(m_ghost_d3d_ctx); - } - } - - bool checkVersionRequirements( - GHOST_Context & /*ghost_ctx*/, /* Remember: This is the OpenGL context! */ - XrInstance instance, - XrSystemId system_id, - std::string *r_requirement_info) const override - { - static PFN_xrGetD3D11GraphicsRequirementsKHR s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; - // static XrInstance s_instance = XR_NULL_HANDLE; - XrGraphicsRequirementsD3D11KHR gpu_requirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR}; - - /* Although it would seem reasonable that the PROC address would not change if the instance was - * the same, in testing, repeated calls to #xrGetInstanceProcAddress() with the same instance - * can still result in changes so the workaround is to simply set the function pointer every - * time (trivializing its 'static' designation). */ - // if (instance != s_instance) { - // s_instance = instance; - s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; - //} - if (!s_xrGetD3D11GraphicsRequirementsKHR_fn && - XR_FAILED( - xrGetInstanceProcAddr(instance, - "xrGetD3D11GraphicsRequirementsKHR", - (PFN_xrVoidFunction *)&s_xrGetD3D11GraphicsRequirementsKHR_fn))) - { - s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; - return false; - } - - s_xrGetD3D11GraphicsRequirementsKHR_fn(instance, system_id, &gpu_requirements); - - if (r_requirement_info) { - std::ostringstream strstream; - strstream << "Minimum DirectX 11 Feature Level " << gpu_requirements.minFeatureLevel - << std::endl; - - *r_requirement_info = strstream.str(); - } - - return m_ghost_d3d_ctx->m_device->GetFeatureLevel() >= gpu_requirements.minFeatureLevel; - } - - void initFromGhostContext( - GHOST_Context & /*ghost_ctx*/ /* Remember: This is the OpenGL context! */, - XrInstance /*instance*/, - XrSystemId /*system_id*/ - ) override - { - oxr_binding.d3d11.type = XR_TYPE_GRAPHICS_BINDING_D3D11_KHR; - oxr_binding.d3d11.device = m_ghost_d3d_ctx->m_device; - } - - std::optional chooseSwapchainFormat(const std::vector &runtime_formats, - GHOST_TXrSwapchainFormat &r_format, - bool &r_is_srgb_format) const override - { - std::vector gpu_binding_formats = { -# if 0 /* RGB10A2, RGBA16 don't seem to work with Oculus head-sets, \ - * so move them after RGBA16F for the time being. */ - DXGI_FORMAT_R10G10B10A2_UNORM, - DXGI_FORMAT_R16G16B16A16_UNORM, -# endif - DXGI_FORMAT_R16G16B16A16_FLOAT, -# if 1 - DXGI_FORMAT_R10G10B10A2_UNORM, - DXGI_FORMAT_R16G16B16A16_UNORM, -# endif - DXGI_FORMAT_R8G8B8A8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, - }; - - std::optional result = choose_swapchain_format_from_candidates(gpu_binding_formats, - runtime_formats); - if (result) { - switch (*result) { - case DXGI_FORMAT_R10G10B10A2_UNORM: - r_format = GHOST_kXrSwapchainFormatRGB10_A2; - break; - case DXGI_FORMAT_R16G16B16A16_UNORM: - r_format = GHOST_kXrSwapchainFormatRGBA16; - break; - case DXGI_FORMAT_R16G16B16A16_FLOAT: - r_format = GHOST_kXrSwapchainFormatRGBA16F; - break; - case DXGI_FORMAT_R8G8B8A8_UNORM: - case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: - r_format = GHOST_kXrSwapchainFormatRGBA8; - break; - } - r_is_srgb_format = (*result == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB); - } - else { - r_format = GHOST_kXrSwapchainFormatRGBA8; - r_is_srgb_format = false; - } - - return result; - } - - std::vector createSwapchainImages(uint32_t image_count) override - { - std::vector d3d_images(image_count); - std::vector base_images; - - /* Need to return vector of base header pointers, so of a different type. Need to build a new - * list with this type, and keep the initial one alive. */ - for (XrSwapchainImageD3D11KHR &image : d3d_images) { - image.type = XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR; - base_images.push_back(reinterpret_cast(&image)); - } - - /* Keep alive. */ - m_image_cache.push_back(std::move(d3d_images)); - - return base_images; - } - - bool needsUpsideDownDrawing(GHOST_Context &) const - { - return m_ghost_d3d_ctx->isUpsideDown(); - } - - protected: - /** Secondary DirectX 11 context used by OpenXR. */ - GHOST_ContextD3D *m_ghost_d3d_ctx = nullptr; - - std::list> m_image_cache; -}; - -class GHOST_XrGraphicsBindingOpenGLD3D : public GHOST_XrGraphicsBindingD3D { - public: - GHOST_XrGraphicsBindingOpenGLD3D(GHOST_Context &ghost_ctx) - : GHOST_XrGraphicsBindingD3D(), m_ghost_wgl_ctx(static_cast(ghost_ctx)) - { - } - ~GHOST_XrGraphicsBindingOpenGLD3D() - { - if (m_shared_resource) { - m_ghost_d3d_ctx->disposeSharedOpenGLResource(m_shared_resource); - m_shared_resource = nullptr; - } - } - - void submitToSwapchainImage(XrSwapchainImageBaseHeader &swapchain_image, - const GHOST_XrDrawViewInfo &draw_info) override - { - XrSwapchainImageD3D11KHR &d3d_swapchain_image = reinterpret_cast( - swapchain_image); - -# if 0 - /* Ideally we'd just create a render target view for the OpenXR swap-chain image texture and - * blit from the OpenGL context into it. The NV_DX_interop extension doesn't want to work with - * this though. At least not with OPTIMUS hardware. See: - * https://github.com/mpv-player/mpv/issues/2949#issuecomment-197262807. - */ - - ID3D11RenderTargetView *rtv; - CD3D11_RENDER_TARGET_VIEW_DESC rtv_desc(D3D11_RTV_DIMENSION_TEXTURE2D, - DXGI_FORMAT_R8G8B8A8_UNORM); - - m_ghost_ctx->m_device->CreateRenderTargetView(d3d_swapchain_image.texture, &rtv_desc, &rtv); - if (!m_shared_resource) { - DXGI_FORMAT format; - ghost_format_to_dx_format(draw_info.swapchain_format, draw_info.expects_srgb_buffer, format); - m_shared_resource = m_ghost_ctx->createSharedOpenGLResource( - draw_info.width, draw_info.height, format, rtv); - } - m_ghost_ctx->blitFromOpenGLContext(m_shared_resource, draw_info.width, draw_info.height); -# else - if (!m_shared_resource) { - DXGI_FORMAT format; - ghost_format_to_dx_format(draw_info.swapchain_format, draw_info.expects_srgb_buffer, format); - m_shared_resource = m_ghost_d3d_ctx->createSharedOpenGLResource( - draw_info.width, draw_info.height, format); - } - m_ghost_d3d_ctx->blitFromOpenGLContext(m_shared_resource, draw_info.width, draw_info.height); - - m_ghost_d3d_ctx->m_device_ctx->OMSetRenderTargets(0, nullptr, nullptr); - m_ghost_d3d_ctx->m_device_ctx->CopyResource( - d3d_swapchain_image.texture, m_ghost_d3d_ctx->getSharedTexture2D(m_shared_resource)); -# endif - } - - private: - /** Primary OpenGL context for Blender to use for drawing. */ - GHOST_ContextWGL &m_ghost_wgl_ctx; - /** Handle to shared resource object. */ - GHOST_SharedOpenGLResource *m_shared_resource = nullptr; -}; -#endif // WIN32 - std::unique_ptr GHOST_XrGraphicsBindingCreateFromType( GHOST_TXrGraphicsBinding type, GHOST_Context &context) { diff --git a/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.cc b/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.cc new file mode 100644 index 00000000000..4838a8a8a75 --- /dev/null +++ b/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.cc @@ -0,0 +1,263 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup GHOST + */ + +#ifndef _WIN32 +# error "GHOST_XrGraphcisBindingD3D can only be compiled on Windows platforms." +#endif + +#include +#include + +#include "GHOST_SystemWin32.hh" +#include "GHOST_XrException.hh" +#include "GHOST_XrGraphicsBindingD3D.hh" + +static void ghost_format_to_dx_format(GHOST_TXrSwapchainFormat ghost_format, + bool expects_srgb_buffer, + DXGI_FORMAT &r_dx_format) +{ + r_dx_format = DXGI_FORMAT_UNKNOWN; + + switch (ghost_format) { + case GHOST_kXrSwapchainFormatRGBA8: + r_dx_format = expects_srgb_buffer ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : + DXGI_FORMAT_R8G8B8A8_UNORM; + break; + case GHOST_kXrSwapchainFormatRGBA16: + r_dx_format = DXGI_FORMAT_R16G16B16A16_UNORM; + break; + case GHOST_kXrSwapchainFormatRGBA16F: + r_dx_format = DXGI_FORMAT_R16G16B16A16_FLOAT; + break; + case GHOST_kXrSwapchainFormatRGB10_A2: + r_dx_format = DXGI_FORMAT_R10G10B10A2_UNORM; + break; + } + + if (r_dx_format == DXGI_FORMAT_UNKNOWN) { + throw GHOST_XrException("No supported DirectX swapchain format found."); + } +} + +static std::optional choose_swapchain_format_from_candidates( + const std::vector &gpu_binding_formats, const std::vector &runtime_formats) +{ + if (gpu_binding_formats.empty()) { + return std::nullopt; + } + + auto res = std::find_first_of(gpu_binding_formats.begin(), + gpu_binding_formats.end(), + runtime_formats.begin(), + runtime_formats.end()); + if (res == gpu_binding_formats.end()) { + return std::nullopt; + } + + return *res; +} + +/* -------------------------------------------------------------------- */ +/** \name Direct3D binding + * \{ */ + +GHOST_XrGraphicsBindingD3D::GHOST_XrGraphicsBindingD3D() : GHOST_IXrGraphicsBinding() +{ + m_ghost_d3d_ctx = GHOST_SystemWin32::createOffscreenContextD3D(); +} +GHOST_XrGraphicsBindingD3D::~GHOST_XrGraphicsBindingD3D() +{ + if (m_ghost_d3d_ctx) { + GHOST_SystemWin32::disposeContextD3D(m_ghost_d3d_ctx); + } +} + +bool GHOST_XrGraphicsBindingD3D::checkVersionRequirements( + GHOST_Context & /*ghost_ctx*/, /* Remember: This is the OpenGL context! */ + XrInstance instance, + XrSystemId system_id, + std::string *r_requirement_info) const +{ + static PFN_xrGetD3D11GraphicsRequirementsKHR s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; + // static XrInstance s_instance = XR_NULL_HANDLE; + XrGraphicsRequirementsD3D11KHR gpu_requirements = {XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR}; + + /* Although it would seem reasonable that the PROC address would not change if the instance was + * the same, in testing, repeated calls to #xrGetInstanceProcAddress() with the same instance + * can still result in changes so the workaround is to simply set the function pointer every + * time (trivializing its 'static' designation). */ + // if (instance != s_instance) { + // s_instance = instance; + s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; + //} + if (!s_xrGetD3D11GraphicsRequirementsKHR_fn && + XR_FAILED( + xrGetInstanceProcAddr(instance, + "xrGetD3D11GraphicsRequirementsKHR", + (PFN_xrVoidFunction *)&s_xrGetD3D11GraphicsRequirementsKHR_fn))) + { + s_xrGetD3D11GraphicsRequirementsKHR_fn = nullptr; + return false; + } + + s_xrGetD3D11GraphicsRequirementsKHR_fn(instance, system_id, &gpu_requirements); + + if (r_requirement_info) { + std::ostringstream strstream; + strstream << "Minimum DirectX 11 Feature Level " << gpu_requirements.minFeatureLevel + << std::endl; + + *r_requirement_info = strstream.str(); + } + + return m_ghost_d3d_ctx->m_device->GetFeatureLevel() >= gpu_requirements.minFeatureLevel; +} + +void GHOST_XrGraphicsBindingD3D::initFromGhostContext( + GHOST_Context & /*ghost_ctx*/ /* Remember: This is the OpenGL context! */, + XrInstance /*instance*/, + XrSystemId /*system_id*/ +) +{ + oxr_binding.d3d11.type = XR_TYPE_GRAPHICS_BINDING_D3D11_KHR; + oxr_binding.d3d11.device = m_ghost_d3d_ctx->m_device; +} + +std::optional GHOST_XrGraphicsBindingD3D::chooseSwapchainFormat( + const std::vector &runtime_formats, + GHOST_TXrSwapchainFormat &r_format, + bool &r_is_srgb_format) const +{ + std::vector gpu_binding_formats = { +#if 0 /* RGB10A2, RGBA16 don't seem to work with Oculus head-sets, \ + * so move them after RGBA16F for the time being. */ + DXGI_FORMAT_R10G10B10A2_UNORM, + DXGI_FORMAT_R16G16B16A16_UNORM, +#endif + DXGI_FORMAT_R16G16B16A16_FLOAT, +#if 1 + DXGI_FORMAT_R10G10B10A2_UNORM, + DXGI_FORMAT_R16G16B16A16_UNORM, +#endif + DXGI_FORMAT_R8G8B8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + }; + + std::optional result = choose_swapchain_format_from_candidates(gpu_binding_formats, + runtime_formats); + if (result) { + switch (*result) { + case DXGI_FORMAT_R10G10B10A2_UNORM: + r_format = GHOST_kXrSwapchainFormatRGB10_A2; + break; + case DXGI_FORMAT_R16G16B16A16_UNORM: + r_format = GHOST_kXrSwapchainFormatRGBA16; + break; + case DXGI_FORMAT_R16G16B16A16_FLOAT: + r_format = GHOST_kXrSwapchainFormatRGBA16F; + break; + case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + r_format = GHOST_kXrSwapchainFormatRGBA8; + break; + } + r_is_srgb_format = (*result == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB); + } + else { + r_format = GHOST_kXrSwapchainFormatRGBA8; + r_is_srgb_format = false; + } + + return result; +} + +std::vector GHOST_XrGraphicsBindingD3D::createSwapchainImages( + uint32_t image_count) +{ + std::vector d3d_images(image_count); + std::vector base_images; + + /* Need to return vector of base header pointers, so of a different type. Need to build a new + * list with this type, and keep the initial one alive. */ + for (XrSwapchainImageD3D11KHR &image : d3d_images) { + image.type = XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR; + base_images.push_back(reinterpret_cast(&image)); + } + + /* Keep alive. */ + m_image_cache.push_back(std::move(d3d_images)); + + return base_images; +} + +bool GHOST_XrGraphicsBindingD3D::needsUpsideDownDrawing(GHOST_Context &) const +{ + return m_ghost_d3d_ctx->isUpsideDown(); +} + +/* \} */ + +/* -------------------------------------------------------------------- */ +/** \name OpenGL-Direct3D bridge + * \{ */ + +GHOST_XrGraphicsBindingOpenGLD3D::GHOST_XrGraphicsBindingOpenGLD3D(GHOST_Context &ghost_ctx) + + : GHOST_XrGraphicsBindingD3D(), m_ghost_wgl_ctx(static_cast(ghost_ctx)) +{ +} + +GHOST_XrGraphicsBindingOpenGLD3D::~GHOST_XrGraphicsBindingOpenGLD3D() +{ + if (m_shared_resource) { + m_ghost_d3d_ctx->disposeSharedOpenGLResource(m_shared_resource); + m_shared_resource = nullptr; + } +} + +void GHOST_XrGraphicsBindingOpenGLD3D::submitToSwapchainImage( + XrSwapchainImageBaseHeader &swapchain_image, const GHOST_XrDrawViewInfo &draw_info) +{ + XrSwapchainImageD3D11KHR &d3d_swapchain_image = reinterpret_cast( + swapchain_image); + +#if 0 + /* Ideally we'd just create a render target view for the OpenXR swap-chain image texture and + * blit from the OpenGL context into it. The NV_DX_interop extension doesn't want to work with + * this though. At least not with OPTIMUS hardware. See: + * https://github.com/mpv-player/mpv/issues/2949#issuecomment-197262807. + */ + + ID3D11RenderTargetView *rtv; + CD3D11_RENDER_TARGET_VIEW_DESC rtv_desc(D3D11_RTV_DIMENSION_TEXTURE2D, + DXGI_FORMAT_R8G8B8A8_UNORM); + + m_ghost_ctx->m_device->CreateRenderTargetView(d3d_swapchain_image.texture, &rtv_desc, &rtv); + if (!m_shared_resource) { + DXGI_FORMAT format; + ghost_format_to_dx_format(draw_info.swapchain_format, draw_info.expects_srgb_buffer, format); + m_shared_resource = m_ghost_ctx->createSharedOpenGLResource( + draw_info.width, draw_info.height, format, rtv); + } + m_ghost_ctx->blitFromOpenGLContext(m_shared_resource, draw_info.width, draw_info.height); +#else + if (!m_shared_resource) { + DXGI_FORMAT format; + ghost_format_to_dx_format(draw_info.swapchain_format, draw_info.expects_srgb_buffer, format); + m_shared_resource = m_ghost_d3d_ctx->createSharedOpenGLResource( + draw_info.width, draw_info.height, format); + } + m_ghost_d3d_ctx->blitFromOpenGLContext(m_shared_resource, draw_info.width, draw_info.height); + + m_ghost_d3d_ctx->m_device_ctx->OMSetRenderTargets(0, nullptr, nullptr); + m_ghost_d3d_ctx->m_device_ctx->CopyResource( + d3d_swapchain_image.texture, m_ghost_d3d_ctx->getSharedTexture2D(m_shared_resource)); +#endif +} + +/* \} */ diff --git a/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.hh b/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.hh new file mode 100644 index 00000000000..9dc5d19ad80 --- /dev/null +++ b/intern/ghost/intern/GHOST_XrGraphicsBindingD3D.hh @@ -0,0 +1,71 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup GHOST + */ +#pragma once + +#ifndef _WIN32 +# error "GHOST_XrGraphcisBindingD3D can only be compiled on Windows platforms." +#endif + +#include "GHOST_ContextD3D.hh" +#include "GHOST_ContextWGL.hh" +#include "GHOST_IXrGraphicsBinding.hh" + +/** + * Base class for bridging to an OpenXR platform that only supports Direct3D. + * + * OpenGL/Vulkan have their own specific implementations. + */ +class GHOST_XrGraphicsBindingD3D : public GHOST_IXrGraphicsBinding { + public: + GHOST_XrGraphicsBindingD3D(); + ~GHOST_XrGraphicsBindingD3D() override; + + /** + * Check the version requirements to use OpenXR with the Vulkan backend. + */ + bool checkVersionRequirements(GHOST_Context &ghost_ctx, + XrInstance instance, + XrSystemId system_id, + std::string *r_requirement_info) const override; + + void initFromGhostContext(GHOST_Context &ghost_ctx, + XrInstance instance, + XrSystemId system_id) override; + std::optional chooseSwapchainFormat(const std::vector &runtime_formats, + GHOST_TXrSwapchainFormat &r_format, + bool &r_is_srgb_format) const override; + std::vector createSwapchainImages(uint32_t image_count) override; + + bool needsUpsideDownDrawing(GHOST_Context &ghost_ctx) const override; + + protected: + /** Secondary DirectX 11 context used by OpenXR. */ + GHOST_ContextD3D *m_ghost_d3d_ctx = nullptr; + + std::list> m_image_cache; +}; + +/** + * OpenXR bridge between OpenGL and D3D. + * + * The D3D swapchain image is imported into OpenGL. + */ +class GHOST_XrGraphicsBindingOpenGLD3D : public GHOST_XrGraphicsBindingD3D { + public: + GHOST_XrGraphicsBindingOpenGLD3D(GHOST_Context &ghost_ctx); + ~GHOST_XrGraphicsBindingOpenGLD3D(); + + void submitToSwapchainImage(XrSwapchainImageBaseHeader &swapchain_image, + const GHOST_XrDrawViewInfo &draw_info) override; + + private: + /** Primary OpenGL context for Blender to use for drawing. */ + GHOST_ContextWGL &m_ghost_wgl_ctx; + /** Handle to shared resource object. */ + GHOST_SharedOpenGLResource *m_shared_resource = nullptr; +};