diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4385e2b8643..41399a07b91 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -274,27 +274,21 @@ jobs: cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` --target Open3DViewer cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` - --target INSTALL - $cmakeCachePath = Join-Path $env:BUILD_DIR "CMakeCache.txt" - $Env:OPEN3D_VERSION_FULL = (Select-String -Path $cmakeCachePath -Pattern "OPEN3D_VERSION_FULL").Line.Split('=')[1] - $open3dAppPath = Join-Path $env:INSTALL_DIR "bin\Open3D" - Compress-Archive -Path $open3dAppPath -DestinationPath ` - "$Env:GITHUB_WORKSPACE/open3d-$Env:OPEN3D_VERSION_FULL-app-windows-amd64.zip" - echo "VIEWER_ZIP_NAME=open3d-$Env:OPEN3D_VERSION_FULL-app-windows-amd64.zip" | Out-File -FilePath ` - $Env:GITHUB_ENV -Encoding utf8 -Append - - - name: Generate viewer attestation - if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} - uses: actions/attest@v4 - with: - subject-path: ${{ github.workspace }}/${{ env.VIEWER_ZIP_NAME }} + --target install + $version = (Select-String -Path "${{ env.BUILD_DIR }}/CMakeCache.txt" ` + -Pattern "OPEN3D_VERSION_FULL").Line.Split('=')[1] + $viewerZip = "${{ env.BUILD_DIR }}/open3d-$version-app-windows-amd64.zip" + Compress-Archive -Path "${{ env.INSTALL_DIR }}/bin/Open3D" ` + -DestinationPath $viewerZip + "VIEWER_ZIP_PATH=$viewerZip" | Out-File -FilePath $Env:GITHUB_ENV ` + -Encoding utf8 -Append - name: Upload Viewer if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} uses: actions/upload-artifact@v4 with: name: open3d-app-windows-amd64 - path: ${{ github.workspace }}/${{ env.VIEWER_ZIP_NAME }} + path: ${{ env.VIEWER_ZIP_PATH }} if-no-files-found: error - name: Update devel release with viewer @@ -302,7 +296,29 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - bash .github/workflows/update_release.sh "${{ env.VIEWER_ZIP_NAME }}" + bash .github/workflows/update_release.sh "${{ env.VIEWER_ZIP_PATH }}" + + - name: Setup WinApp CLI + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} + uses: microsoft/setup-WinAppCli@v0.1 + + - name: Build MSIX + working-directory: ${{ env.BUILD_DIR }} + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} + run: | + $ErrorActionPreference = 'Stop' + cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` + --target Open3DViewerMSIX + + - name: Upload MSIX test package + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: open3d-app-windows-msix-amd64 + path: | + ${{ env.MSIX_PATH }} + ${{ env.BUILD_DIR }}\msix-package\Open3D.cer + if-no-files-found: error - name: Run C++ unit tests if: ${{ matrix.device != 'cuda' }} diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index 107aae62150..6ff0f87b12e 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -55,6 +55,8 @@ macro(open3d_add_app_gui SRC_DIR APP_NAME TARGET_NAME) COMMAND ${CMAKE_COMMAND} -E copy_directory "${GUI_RESOURCE_DIR}" "${APP_DIR}/${RESOURCE_DIR_NAME}" ) if (UNIX) + install(FILES "$" + DESTINATION "${CMAKE_INSTALL_PREFIX}/bin/${APP_NAME}") install(DIRECTORY "${APP_DIR}" DESTINATION "${CMAKE_INSTALL_PREFIX}/bin" USE_SOURCE_PERMISSIONS) @@ -131,6 +133,17 @@ macro(open3d_add_app_common SRC_DIR APP_NAME TARGET_NAME) elseif (WIN32) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../${APP_NAME}") add_executable(${TARGET_NAME} ${SOURCE_FILES} ${HEADER_FILES}) + # Suppress the console window while keeping the standard main() entry point (no WinMain). + set_target_properties(${TARGET_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) + if (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM") + target_link_options(${TARGET_NAME} PRIVATE + "-Xlinker" "/ENTRY:mainCRTStartup") + elseif (MSVC) + target_link_options(${TARGET_NAME} PRIVATE "/ENTRY:mainCRTStartup") + else() + target_link_options(${TARGET_NAME} PRIVATE + "-Xlinker" "/ENTRY:mainCRTStartup") + endif() else() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../${APP_NAME}") add_executable(${TARGET_NAME} ${SOURCE_FILES} ${HEADER_FILES}) @@ -155,6 +168,18 @@ endmacro() if (BUILD_GUI) open3d_add_app_common(Open3DViewer Open3D Open3DViewer) open3d_add_app_gui(Open3DViewer Open3D Open3DViewer) + if (WIN32) + add_custom_target(Open3DViewerMSIX + COMMAND "${CMAKE_COMMAND}" --install "${CMAKE_BINARY_DIR}" + --config "$" + COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass + -File "${PROJECT_SOURCE_DIR}/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1" + -InstallDir "${CMAKE_INSTALL_PREFIX}/bin/Open3D" + -Version "${PROJECT_VERSION}" + -OutDir "${CMAKE_BINARY_DIR}/msix-package" + VERBATIM) + add_dependencies(Open3DViewerMSIX Open3DViewer) + endif() endif() open3d_add_app_common(OfflineReconstruction OfflineReconstruction OfflineReconstruction) diff --git a/cpp/apps/Open3DViewer/Open3DViewer.xml b/cpp/apps/Open3DViewer/Open3DViewer.xml index 8300b67da16..6b789ca76ca 100644 --- a/cpp/apps/Open3DViewer/Open3DViewer.xml +++ b/cpp/apps/Open3DViewer/Open3DViewer.xml @@ -83,4 +83,14 @@ + + 3D Gaussian Splat + + + + + 3D Gaussian Splat (compressed) + + + diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml b/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml new file mode 100644 index 00000000000..2940e2a2131 --- /dev/null +++ b/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml @@ -0,0 +1,85 @@ + + + + + + + + Open3D + Open3D + resources\StoreLogo.png + + + + + + + + + + + + + + + + + + + resources\Square44x44Logo.png + Open3D 3D file + + .ply + .stl + .obj + .off + .fbx + .glb + .gltf + .pcd + .pts + .xyz + .splat + .spz + + + + + + + + + + + + diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 new file mode 100644 index 00000000000..43a5cfaeffe --- /dev/null +++ b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Stages, signs, and packs the Open3D Viewer as a side-load MSIX. + +.PARAMETER InstallDir + Directory the viewer was installed to (contains Open3D.exe + resources/). +.PARAMETER Version + Three-part Open3D version from CMake. +.PARAMETER OutDir + Directory to write the staged files, cert, and final .msix/.cer into. +#> +param( + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string]$OutDir +) + +$ErrorActionPreference = 'Stop' + +$OPEN3D_VERSION = $Version +$MSIX_VERSION = "$Version.0" + +# Stage the already-installed viewer and add the manifest. +$STAGING = Join-Path $OutDir "msix-staging" +Remove-Item -Recurse -Force $STAGING -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $STAGING | Out-Null +Copy-Item (Join-Path $InstallDir "Open3D.exe") $STAGING +Copy-Item (Join-Path $InstallDir "tbb*.dll") $STAGING +Copy-Item -Recurse (Join-Path $InstallDir "resources") $STAGING +Copy-Item (Join-Path $PSScriptRoot "AppxManifest.xml") $STAGING + +# Substitute version placeholder in the staged manifest. +$manifestPath = Join-Path $STAGING "AppxManifest.xml" +(Get-Content $manifestPath) -replace '@OPEN3D_MSIX_VERSION@', $MSIX_VERSION | + Set-Content $manifestPath + +# Generate self-signed cert whose subject matches Publisher="CN=Open3D". +# Export .cer (public key only) so users can install it to trust the package. +$MSIX_NAME = "Open3DViewer-$OPEN3D_VERSION-x64.msix" +$MSIX_PATH = Join-Path $OutDir $MSIX_NAME +$PFX_PATH = Join-Path $OutDir "Open3D.pfx" +try { + winapp cert generate ` + --manifest $manifestPath ` + --output $PFX_PATH ` + --export-cer ` + --if-exists overwrite + + # Pack and sign the MSIX. The private key is removed immediately after use. + winapp pack $STAGING ` + --output $MSIX_PATH ` + --cert $PFX_PATH +} +finally { + Remove-Item $PFX_PATH -Force -ErrorAction SilentlyContinue +} + +# If running in GitHub Actions, export the MSIX name and path to the environment. +if ($env:GITHUB_ENV) { + echo "MSIX_NAME=$MSIX_NAME" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append + echo "MSIX_PATH=$MSIX_PATH" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append +} diff --git a/cpp/open3d/visualization/CMakeLists.txt b/cpp/open3d/visualization/CMakeLists.txt index aa23190770a..a49870d7149 100644 --- a/cpp/open3d/visualization/CMakeLists.txt +++ b/cpp/open3d/visualization/CMakeLists.txt @@ -110,6 +110,7 @@ if (BUILD_GUI) else() target_sources(visualization_impl PRIVATE rendering/gaussian_splat/ComputeGPUVulkan.cpp + rendering/GpuAdapterSelection.cpp rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp diff --git a/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp b/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp index 23f24921387..30204ef46f0 100644 --- a/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp +++ b/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp @@ -21,6 +21,10 @@ #endif #include "open3d/visualization/gui/Native.h" #include "open3d/visualization/gui/Window.h" +#if defined(__linux__) +#include "open3d/visualization/rendering/GpuAdapterSelection.h" +#include "open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h" +#endif #include "open3d/visualization/rendering/filament/FilamentEngine.h" #include "open3d/visualization/rendering/filament/FilamentRenderer.h" @@ -111,6 +115,16 @@ void GLFWWindowSystem::Initialize() { glfwInitHint(GLFW_COCOA_CHDIR_RESOURCES, GLFW_FALSE); #endif #if defined(__linux__) + // GLFW/GLX caches the vendor selection during glfwInit(). Select the + // Vulkan adapter and apply PRIME steering before initializing GLFW so the + // helper OpenGL context can be created on the same GPU. + auto& vk_ctx = rendering::GaussianSplatVulkanInteropContext::GetInstance(); + if (!vk_ctx.IsValid() && vk_ctx.Initialize() && vk_ctx.IsValid()) { + const rendering::GpuAdapterInfo adapter = + rendering::GetAdapterInfo(vk_ctx.GetPhysicalDevice()); + rendering::SteerNextGLContextToAdapter(adapter); + } + // Filament (April 2026) selects PlatformGLX exclusively on Linux // (compile-time decision in PlatformFactory.cpp). Force GLFW to X11 so the // native window handle is an X11 Window (XID), matching what PlatformGLX @@ -172,6 +186,10 @@ GLFWWindowSystem::OSWindow GLFWWindowSystem::CreateOSWindow(Window* o3d_window, auto* glfw_window = glfwCreateWindow(width, height, title, NULL, NULL); +#if defined(_WIN32) + SetNativeWindowIcon(glfw_window); +#endif + glfwSetWindowUserPointer(glfw_window, o3d_window); glfwSetWindowSizeCallback(glfw_window, ResizeCallback); glfwSetWindowPosCallback(glfw_window, WindowMovedCallback); diff --git a/cpp/open3d/visualization/gui/Native.h b/cpp/open3d/visualization/gui/Native.h index 6089723342d..4cb48db9729 100644 --- a/cpp/open3d/visualization/gui/Native.h +++ b/cpp/open3d/visualization/gui/Native.h @@ -19,6 +19,11 @@ namespace visualization { namespace gui { void* GetNativeDrawable(GLFWwindow* glfw_window); +// GLFW uses the generic Windows application icon unless the native window is +// assigned the icon embedded in the executable. +#ifdef _WIN32 +void SetNativeWindowIcon(GLFWwindow* glfw_window); +#endif // _WIN32 // Note that Windows cannot post an expose event so it must draw immediately. // Therefore this function cannot be called while drawing. void PostNativeExposeEvent(GLFWwindow* glfw_window); diff --git a/cpp/open3d/visualization/gui/NativeWin32.cpp b/cpp/open3d/visualization/gui/NativeWin32.cpp index 42c2f48b1e0..2f5571cf4b2 100644 --- a/cpp/open3d/visualization/gui/NativeWin32.cpp +++ b/cpp/open3d/visualization/gui/NativeWin32.cpp @@ -20,6 +20,30 @@ void* GetNativeDrawable(GLFWwindow* glfw_window) { return glfwGetWin32Window(glfw_window); } +void SetNativeWindowIcon(GLFWwindow* glfw_window) { + HWND window = glfwGetWin32Window(glfw_window); + if (!window) { + return; + } + HINSTANCE instance = GetModuleHandle(nullptr); + HICON small_icon = static_cast(LoadImage( + instance, "IDI_ICON1", IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), 0)); + HICON large_icon = static_cast(LoadImage( + instance, "IDI_ICON1", IMAGE_ICON, GetSystemMetrics(SM_CXICON), + GetSystemMetrics(SM_CYICON), 0)); + if (small_icon && large_icon) { + SendMessage(window, WM_SETICON, ICON_SMALL, + reinterpret_cast(small_icon)); + SendMessage(window, WM_SETICON, ICON_BIG, + reinterpret_cast(large_icon)); + SetClassLongPtr(window, GCLP_HICON, + reinterpret_cast(large_icon)); + SetClassLongPtr(window, GCLP_HICONSM, + reinterpret_cast(small_icon)); + } +} + void PostNativeExposeEvent(GLFWwindow* glfw_window) { InvalidateRect(glfwGetWin32Window(glfw_window), NULL, FALSE); // InvalidateRect() does not actually post an event to the message queue. diff --git a/cpp/open3d/visualization/gui/Resources/Square150x150Logo.png b/cpp/open3d/visualization/gui/Resources/Square150x150Logo.png new file mode 100644 index 00000000000..f218d5b4f32 Binary files /dev/null and b/cpp/open3d/visualization/gui/Resources/Square150x150Logo.png differ diff --git a/cpp/open3d/visualization/gui/Resources/Square44x44Logo.png b/cpp/open3d/visualization/gui/Resources/Square44x44Logo.png new file mode 100644 index 00000000000..8f99f1e89dd Binary files /dev/null and b/cpp/open3d/visualization/gui/Resources/Square44x44Logo.png differ diff --git a/cpp/open3d/visualization/gui/Resources/StoreLogo.png b/cpp/open3d/visualization/gui/Resources/StoreLogo.png new file mode 100644 index 00000000000..8ba914f2abd Binary files /dev/null and b/cpp/open3d/visualization/gui/Resources/StoreLogo.png differ diff --git a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp new file mode 100644 index 00000000000..e520e28d8fd --- /dev/null +++ b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp @@ -0,0 +1,289 @@ +// ---------------------------------------------------------------------------- +// - Open3D: www.open3d.org - +// ---------------------------------------------------------------------------- +// Copyright (c) 2018-2024 www.open3d.org +// SPDX-License-Identifier: MIT +// ---------------------------------------------------------------------------- + +#if defined(_WIN32) +#ifndef VK_USE_PLATFORM_WIN32_KHR +#define VK_USE_PLATFORM_WIN32_KHR 1 +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#pragma comment(lib, "dxgi.lib") +#endif + +#include "open3d/visualization/rendering/GpuAdapterSelection.h" + +#if !defined(__APPLE__) + +#ifndef VK_NO_PROTOTYPES +#define VK_NO_PROTOTYPES +#endif +#include + +#define GLFW_INCLUDE_NONE +#include +#if defined(_WIN32) +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#endif + +#include +#include + +#include +#include +#include + +#include "open3d/utility/Logging.h" + +namespace open3d { +namespace visualization { +namespace rendering { + +#if defined(_WIN32) + +namespace { + +/// Finds the DXGI adapter identified by `info` and returns the desktop rect +/// of its first output, if it drives one at all. +bool FindMonitorRectForAdapter(const GpuAdapterInfo& info, RECT* out_rect) { + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), + reinterpret_cast(&factory))) || + !factory) { + return false; + } + + bool found = false; + for (UINT i = 0;; ++i) { + IDXGIAdapter1* adapter = nullptr; + if (factory->EnumAdapters1(i, &adapter) == DXGI_ERROR_NOT_FOUND) { + break; + } + if (!adapter) continue; + DXGI_ADAPTER_DESC1 adesc{}; + if (SUCCEEDED(adapter->GetDesc1(&adesc)) && + std::memcmp(&adesc.AdapterLuid, info.luid, sizeof(info.luid)) == + 0) { + IDXGIOutput* output = nullptr; + if (adapter->EnumOutputs(0, &output) != DXGI_ERROR_NOT_FOUND && + output) { + DXGI_OUTPUT_DESC odesc{}; + if (SUCCEEDED(output->GetDesc(&odesc))) { + *out_rect = odesc.DesktopCoordinates; + found = true; + } + output->Release(); + } + } + adapter->Release(); + if (found) break; + } + factory->Release(); + return found; +} + +} // namespace + +GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { + GpuAdapterInfo info; + vk::PhysicalDevice pd(physical_device); + const auto chain = pd.getProperties2(); + info.device_name = chain.get() + .properties.deviceName.data(); + const auto& id = chain.get(); + if (id.deviceLUIDValid) { + std::memcpy(info.luid, id.deviceLUID.data(), sizeof(info.luid)); + info.valid = true; + } + return info; +} + +bool SteerNextGLContextToAdapter(const GpuAdapterInfo& info) { + if (!info.valid) return false; + + RECT rect{}; + if (!FindMonitorRectForAdapter(info, &rect)) { + utility::LogWarning( + "GpuAdapterSelection: could not find a monitor driven by " + "adapter '{}'; GL context will use its default adapter.", + info.device_name); + return false; + } + + // GLFW binds a WGL context to whatever adapter drives the monitor the + // window is created on, so positioning the (still hidden) window on + // that monitor before glfwCreateWindow() steers the context there. + glfwWindowHint(GLFW_POSITION_X, rect.left); + glfwWindowHint(GLFW_POSITION_Y, rect.top); + return true; +} + +GpuAdapterInfo GetAdapterInfoForWindow(void* glfw_window) { + if (!glfw_window) return GpuAdapterInfo(); + HWND hwnd = glfwGetWin32Window(static_cast(glfw_window)); + if (!hwnd) return GpuAdapterInfo(); + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); + + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), + reinterpret_cast(&factory))) || + !factory) { + return GpuAdapterInfo(); + } + + GpuAdapterInfo info; + for (UINT i = 0;; ++i) { + IDXGIAdapter1* adapter = nullptr; + if (factory->EnumAdapters1(i, &adapter) == DXGI_ERROR_NOT_FOUND) { + break; + } + if (!adapter) continue; + bool found = false; + for (UINT j = 0;; ++j) { + IDXGIOutput* output = nullptr; + if (adapter->EnumOutputs(j, &output) == DXGI_ERROR_NOT_FOUND) { + break; + } + if (!output) continue; + DXGI_OUTPUT_DESC output_desc{}; + if (SUCCEEDED(output->GetDesc(&output_desc)) && + output_desc.Monitor == monitor) { + DXGI_ADAPTER_DESC1 adapter_desc{}; + if (SUCCEEDED(adapter->GetDesc1(&adapter_desc))) { + std::memcpy(info.luid, &adapter_desc.AdapterLuid, + sizeof(info.luid)); + const std::wstring name(adapter_desc.Description); + info.device_name.assign(name.begin(), name.end()); + info.valid = true; + found = true; + } + } + output->Release(); + if (found) break; + } + adapter->Release(); + if (found) break; + } + factory->Release(); + return info; +} + +#else // !_WIN32 + +GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { + GpuAdapterInfo info; + vk::PhysicalDevice pd(physical_device); + const auto base_props = pd.getProperties(); + info.device_name = base_props.deviceName.data(); + + bool has_pci_ext = false; + for (const auto& ext : pd.enumerateDeviceExtensionProperties()) { + if (std::strcmp(ext.extensionName, + VK_EXT_PCI_BUS_INFO_EXTENSION_NAME) == 0) { + has_pci_ext = true; + break; + } + } + if (!has_pci_ext) return info; // info.valid stays false + + const auto chain = + pd.getProperties2(); + const auto& pci = chain.get(); + info.pci_domain = pci.pciDomain; + info.pci_bus = pci.pciBus; + info.pci_device = pci.pciDevice; + info.pci_function = pci.pciFunction; + + const auto driver_chain = + pd.getProperties2(); + info.is_nvidia = + driver_chain.get().driverID == + vk::DriverId::eNvidiaProprietary; + info.valid = true; + return info; +} + +bool SteerNextGLContextToAdapter(const GpuAdapterInfo& info) { + if (!info.valid) return false; + + const std::string pci_id = + fmt::format("pci-{:04x}_{:02x}_{:02x}_{:01x}", info.pci_domain, + info.pci_bus, info.pci_device, info.pci_function); + + // EXPERIMENTAL: no portable GLX API exists to select a specific GPU, so + // this relies on the Mesa/NVIDIA PRIME-offload env var convention, which + // only takes effect if set before the first GL/GLX context is created + // in this process. + setenv("DRI_PRIME", pci_id.c_str(), 1); + if (info.is_nvidia) { + setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 1); + setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 1); + } + utility::LogDebug( + "GpuAdapterSelection: best-effort steering next GL context to " + "'{}' ({}) via DRI_PRIME{}. This is experimental; verify with " + "GetCurrentGLAdapterUUID().", + info.device_name, pci_id, info.is_nvidia ? "/NVIDIA PRIME" : ""); + return true; +} + +#endif // _WIN32 + +#if !defined(_WIN32) +GpuAdapterInfo GetAdapterInfoForWindow(void* /*glfw_window*/) { + // No portable GLX/EGL API exists to query the GPU adapter backing an + // existing context, so verifying SteerNextGLContextToAdapter() actually + // took effect is not currently supported on this platform. + return GpuAdapterInfo(); +} + +#endif + +bool SameAdapter(const GpuAdapterInfo& a, const GpuAdapterInfo& b) { + if (!a.valid || !b.valid) return false; +#if defined(_WIN32) + return std::memcmp(a.luid, b.luid, sizeof(a.luid)) == 0; +#else + return a.pci_domain == b.pci_domain && a.pci_bus == b.pci_bus && + a.pci_device == b.pci_device && a.pci_function == b.pci_function; +#endif +} + +std::string GetCurrentGLAdapterUUID() { + if (!GLEW_EXT_memory_object) return {}; + GLint num_uuids = 0; + glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &num_uuids); + if (num_uuids < 1) return {}; + GLubyte uuid[16] = {}; + glGetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, uuid); + if (glGetError() != GL_NO_ERROR) return {}; + return std::string(reinterpret_cast(uuid), sizeof(uuid)); +} + +std::string HexEncode(const std::string& raw_bytes) { + std::string out; + out.reserve(raw_bytes.size() * 2); + for (unsigned char b : raw_bytes) { + fmt::format_to(std::back_inserter(out), "{:02x}", b); + } + return out; +} + +} // namespace rendering +} // namespace visualization +} // namespace open3d + +#endif // !defined(__APPLE__) diff --git a/cpp/open3d/visualization/rendering/GpuAdapterSelection.h b/cpp/open3d/visualization/rendering/GpuAdapterSelection.h new file mode 100644 index 00000000000..387ac1736ce --- /dev/null +++ b/cpp/open3d/visualization/rendering/GpuAdapterSelection.h @@ -0,0 +1,101 @@ +// ---------------------------------------------------------------------------- +// - Open3D: www.open3d.org - +// ---------------------------------------------------------------------------- +// Copyright (c) 2018-2024 www.open3d.org +// SPDX-License-Identifier: MIT +// ---------------------------------------------------------------------------- + +// General-purpose (non-Gaussian-Splat-specific) helpers for keeping a Vulkan +// device and an OpenGL context on the *same* physical GPU adapter. Needed +// because GL_EXT_memory_object cross-adapter texture import silently fails +// (GL_OUT_OF_MEMORY) on multi-GPU (hybrid graphics) systems. +// +// Architecture: Vulkan selects its physical device first (see +// GaussianSplatVulkanInteropContext), then GetAdapterInfo() + the +// SteerNextGLContextToAdapter() helpers below let the *following* OpenGL +// context creation be steered onto that same adapter. + +#pragma once + +#include +#include + +#if !defined(__APPLE__) + +typedef struct VkPhysicalDevice_T* VkPhysicalDevice; + +namespace open3d { +namespace visualization { +namespace rendering { + +/// Identifies the physical GPU adapter backing a Vulkan device. +struct GpuAdapterInfo { + bool valid = false; +#if defined(_WIN32) + std::uint8_t luid[8] = {}; +#else + std::uint32_t pci_domain = 0; + std::uint32_t pci_bus = 0; + std::uint32_t pci_device = 0; + std::uint32_t pci_function = 0; + bool is_nvidia = false; ///< driverID == eNvidiaProprietary +#endif + std::string device_name; ///< For logging only. +}; + +/// Extracts the adapter identity from a Vulkan physical device: the 8-byte +/// DXGI LUID on Windows (via VkPhysicalDeviceIDProperties), or the PCI bus +/// address on other platforms (via the optional VK_EXT_pci_bus_info device +/// extension). Returns GpuAdapterInfo::valid == false if unavailable. +GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device); + +/// Best-effort: steers the *next* OpenGL context created in this process +/// onto the physical GPU described by `info`. No-op (returns false) if +/// `info.valid` is false. +/// Windows: sets GLFW window-position hints so the next glfwCreateWindow() +/// lands on the monitor driven by the matching DXGI adapter, +/// causing its WGL context to bind to that adapter. Must be +/// called before glfwCreateWindow(). +/// Other platforms: EXPERIMENTAL. Sets Mesa/NVIDIA PRIME-offload +/// environment variables (DRI_PRIME / __NV_PRIME_RENDER_OFFLOAD) +/// matching the PCI bus address. No portable GLX API exists to +/// force adapter selection, so this only has a chance of taking +/// effect if called before *any* GL context has been created in +/// this process (Mesa/GLVND cache the driver choice on first +/// load), and success is not guaranteed. Verify after the fact +/// with GetCurrentGLAdapterUUID(). +bool SteerNextGLContextToAdapter(const GpuAdapterInfo& info); + +/// Reverse lookup: identifies the physical GPU adapter actually backing an +/// already-created OpenGL context, given its GLFW window handle. Used to +/// verify SteerNextGLContextToAdapter() actually took effect, since it can +/// silently fail (e.g. the target adapter drives no monitor at all, which +/// happens on some hybrid-graphics laptops where the discrete GPU is +/// render-only). +/// Windows: looks up the DXGI adapter driving the monitor the window is +/// on. Always succeeds if the window has a monitor, regardless +/// of whether that adapter was the intended steering target. +/// Other platforms: not implemented (returns GpuAdapterInfo::valid == +/// false) — no portable way to query the GPU behind an existing +/// GLX context. +GpuAdapterInfo GetAdapterInfoForWindow(void* glfw_window); + +/// True if `a` and `b` identify the same physical GPU adapter. False if +/// either is invalid. +bool SameAdapter(const GpuAdapterInfo& a, const GpuAdapterInfo& b); + +/// Diagnostic-only: returns the current GL context's GL_DEVICE_UUID_EXT (16 +/// raw bytes), or an empty string if unavailable/unsupported. Requires a +/// current GL context with GLEW already initialized. Never used to gate +/// device-selection behavior: some drivers advertise this extension but +/// fail the query (observed: Intel Iris Xe/Arc hybrid Windows driver). +std::string GetCurrentGLAdapterUUID(); + +/// Hex-encodes raw bytes for log messages (e.g. GetCurrentGLAdapterUUID()). +std::string HexEncode(const std::string& raw_bytes); + +} // namespace rendering +} // namespace visualization +} // namespace open3d + +#endif // !defined(__APPLE__) diff --git a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp index 1d0e2ffa847..1e8781e2453 100644 --- a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp +++ b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp @@ -22,10 +22,12 @@ #endif // _MSC_VER #include // recursive includes needs this, std::size_t especially +#include #include "open3d/utility/FileSystem.h" #include "open3d/visualization/rendering/filament/FilamentResourceManager.h" #if !defined(__APPLE__) +#include "open3d/visualization/rendering/GpuAdapterSelection.h" #include "open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h" #include "open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h" #endif @@ -126,34 +128,59 @@ EngineInstance::EngineInstance() { backend = filament::backend::Backend::OPENGL; } - // Initialise the Vulkan interop context BEFORE the GL context so that - // Vulkan device memory allocations and exported FDs are ready for the - // GL EXT_memory_object import calls made during PrepareOutputTextures(). - // Failure is non-fatal: the Vulkan backend will fall back gracefully. - { - auto& vk_ctx = GaussianSplatVulkanInteropContext::GetInstance(); - if (!vk_ctx.IsValid()) { - if (!vk_ctx.Initialize()) { - utility::LogWarning( - "EngineInstance: Vulkan interop context init failed: " - "{}", - vk_ctx.GetLastError()); - } - } + // Vulkan selects its physical device first (discrete-GPU-preferred, + // emulated/software devices down-weighted — see ScoreDevice()), honoring + // any loader-level device reordering (e.g. VK_LOADER_DEVICE_SELECT on + // Linux) for free. The compute GL context is then steered onto that same + // adapter before creation: GL_EXT_memory_object cross-adapter import is + // not supported and silently fails (GL_OUT_OF_MEMORY) on multi-GPU + // (hybrid graphics) systems if Vulkan and GL end up on different GPUs. + auto& vk_ctx = GaussianSplatVulkanInteropContext::GetInstance(); + if (!vk_ctx.IsValid() && !vk_ctx.Initialize()) { + utility::LogWarning( + "EngineInstance: Vulkan interop context init failed: {}", + vk_ctx.GetLastError()); } - // On Linux (X11/XWayland via GLX) and Windows (WGL), create our compute - // GL context BEFORE the Filament engine so we can pass it as the - // sharedGLContext. Filament then creates its own context sharing our GL - // namespace, enabling zero-copy texture import() between the two - // contexts. This must happen before Engine::create() because GLX/WGL - // sharing can only be established at context creation time. + auto& gl_ctx = GaussianSplatOpenGLContext::GetInstance(); if ((backend == filament::backend::Backend::OPENGL || backend == filament::backend::Backend::DEFAULT) && !shared_context_) { - auto& gl_ctx = GaussianSplatOpenGLContext::GetInstance(); if (!gl_ctx.IsValid()) { + GpuAdapterInfo vk_adapter_info; + if (vk_ctx.IsValid()) { + vk_adapter_info = GetAdapterInfo(vk_ctx.GetPhysicalDevice()); + SteerNextGLContextToAdapter(vk_adapter_info); + } gl_ctx.InitializeStandalone(); + + // Safety net only: GaussianSplatVulkanInteropContext::Initialize() + // already avoids picking a monitor-less adapter when it can, so + // steering above should normally succeed. This still guards + // against rarer cases (e.g. the window manager not honoring the + // position hint) by making Vulkan follow GL if they still + // disagree — guaranteeing the two share a GPU (required for + // GL_EXT_memory_object import) matters more than which GPU is + // used. + if (gl_ctx.IsValid() && vk_adapter_info.valid) { + const GpuAdapterInfo gl_actual = + GetAdapterInfoForWindow(gl_ctx.GetNativeWindowHandle()); + if (gl_actual.valid && + !SameAdapter(gl_actual, vk_adapter_info)) { + utility::LogWarning( + "EngineInstance: GL landed on adapter '{}' but " + "Vulkan selected '{}'; reinitializing Vulkan to " + "match GL so compute interop works correctly.", + gl_actual.device_name, vk_adapter_info.device_name); + vk_ctx.Shutdown(); + if (!vk_ctx.Initialize(&gl_actual)) { + utility::LogWarning( + "EngineInstance: Vulkan reinit to match GL " + "adapter failed: {}", + vk_ctx.GetLastError()); + } + } + } } if (gl_ctx.IsValid()) { shared_context_ = gl_ctx.GetNativeContext(); @@ -163,6 +190,22 @@ EngineInstance::EngineInstance() { shared_context_); } } + + if (vk_ctx.IsValid() && gl_ctx.IsValid() && + !vk_ctx.AreGLExtensionsReady()) { + if (gl_ctx.MakeCurrent()) { + vk_ctx.ProbeGLExtensions(); + gl_ctx.ReleaseCurrent(); + } + } + // Diagnostic-only verification that GL and Vulkan ended up on the same + // adapter; never gates behavior (some drivers fail this query, see + // GetCurrentGLAdapterUUID()'s doc comment). + if (gl_ctx.IsValid() && gl_ctx.MakeCurrent()) { + utility::LogDebug("EngineInstance: GL adapter UUID = {}", + HexEncode(GetCurrentGLAdapterUUID())); + gl_ctx.ReleaseCurrent(); + } #endif filament::Engine::Config fmcfg; diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatDesign.md b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatDesign.md index e06fe73340b..84e791e535c 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatDesign.md +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatDesign.md @@ -163,6 +163,11 @@ is set at creation time and cannot be added retroactively. (`PlatformGLX` on Linux, `PlatformWGL` on Windows). 5. Both contexts share the same GL object namespace; texture handles are valid in both. +On Linux, `GLFWWindowSystem::Initialize()` initializes the Vulkan interop device +and applies PRIME steering before calling `glfwInit()`. GLFW/GLX caches the +vendor selection during initialization, so steering after `glfwInit()` can make +NVIDIA GLX fail to find a compatible framebuffer configuration. + ### Backend Abstraction ``` diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp index a1256f4a12d..b5fb63465d4 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp @@ -93,7 +93,10 @@ bool GaussianSplatOpenGLContext::InitializeStandalone() { glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_FALSE); + // NVIDIA's GLX driver does not expose a compatible single-buffered + // framebuffer configuration for this core-profile context. The helper + // window never presents, so double buffering has no runtime cost here. + glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE); GLFWwindow* window = glfwCreateWindow(1, 1, "O3D_GS_Helper", nullptr, nullptr); diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h index b324e76781f..d34ba8e719e 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h @@ -15,6 +15,9 @@ #pragma once +#include +#include + #if !defined(__APPLE__) namespace open3d { @@ -50,6 +53,10 @@ class GaussianSplatOpenGLContext { /// Windows -> HGLRC void* GetNativeContext() const; + /// Returns the underlying GLFWwindow* (as void*), for adapter-identity + /// lookups (see GpuAdapterSelection.h's GetAdapterInfoForWindow()). + void* GetNativeWindowHandle() const { return glfw_window_; } + /// Destroys the context and associated resources. void Shutdown(); diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp index c209fc695b3..9017fb1a020 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp @@ -190,9 +190,16 @@ bool RunGaussianGeometryPasses( vs.dc_opacity_buf, attrs.dc_opacity.size() * sizeof(std::uint32_t), "gs.dc_opacity"); - vs.sh_buf = ctx.ResizeBuffer( - vs.sh_buf, attrs.sh_coefficients.size() * sizeof(std::uint32_t), - "gs.sh_coeffs"); + // Always keep this buffer non-empty: the project shader statically + // references binding 5 even when guarded by "if (sh_degree >= 1u)", + // so a size-0 (i.e. unallocated) buffer leaves the descriptor unwritten + // and unbound, which some drivers (e.g. Intel Arc) fault on even + // though it's never read at that degree + // (VUID-vkCmdDispatch-None-08114). + const std::size_t sh_bytes = + std::max(1, attrs.sh_coefficients.size()) * + sizeof(std::uint32_t); + vs.sh_buf = ctx.ResizeBuffer(vs.sh_buf, sh_bytes, "gs.sh_coeffs"); } // GPU-only intermediate buffers: private storage for better cache diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp index a19d202c749..b7547780891 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp @@ -179,13 +179,13 @@ class GaussianSplatVulkanBackend final : public GaussianSplatRenderer::Backend { targets.render_target = resource_mgr.CreateRenderTarget(view_color, targets.depth); } + // Disable MSAA before binding the render target: Filament validates + // MSAA/sampleable-depth compatibility inside SetRenderTarget() auto* native = view.GetNativeView(); auto msaa = native->getMultiSampleAntiAliasingOptions(); msaa.enabled = false; native->setMultiSampleAntiAliasingOptions(msaa); - // Filament rejects sampleable depth targets while MSAA is enabled, so - // update the view before binding the shared depth render target. view.SetRenderTarget(targets.render_target); view.SetPostProcessing(false); diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp index 2a327943d6e..476d9146ce6 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp @@ -41,6 +41,7 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #include #include +#include #include #include "open3d/utility/Logging.h" @@ -113,11 +114,16 @@ bool CheckExtensions(const std::vector& available, return true; } +/// Sentinel returned by ScoreDevice() for devices that must never be picked +/// (no compute queue, missing required extensions, or API version too low). +/// Distinct from the emulated-device penalty below, which keeps the device +/// eligible as a last resort when nothing better is available. +constexpr int kDeviceRejected = std::numeric_limits::min(); + /// Score a physical device for interop suitability. Higher is better. -/// +2 : discrete GPU -/// +1 : integrated GPU -/// 0 : any compute-capable device -/// -∞ : no compute queue or missing required extensions → reject +/// Native discrete > native integrated > other native > D3D12 translation > +/// CPU. +/// kDeviceRejected : no compute queue or missing required extensions/version int ScoreDevice(const vk::raii::PhysicalDevice& dev) { // Check for a compute queue. const auto qfams = dev.getQueueFamilyProperties(); @@ -128,7 +134,7 @@ int ScoreDevice(const vk::raii::PhysicalDevice& dev) { break; } } - if (!has_compute) return -1; + if (!has_compute) return kDeviceRejected; // Check device extensions. const auto exts = dev.enumerateDeviceExtensionProperties(); @@ -137,16 +143,33 @@ int ScoreDevice(const vk::raii::PhysicalDevice& dev) { const bool ok = CheckExtensions(exts, kRequiredDeviceExtensions, std::size(kRequiredDeviceExtensions), missing); - if (!ok) return -1; + if (!ok) return kDeviceRejected; - // Shaders are compiled for Vulkan 1.3 (SPIR-V 1.6); reject older devices. const auto props = dev.getProperties(); - if (props.apiVersion < VK_API_VERSION_1_3) return -1; - // Score device type. - if (props.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) return 2; - if (props.deviceType == vk::PhysicalDeviceType::eIntegratedGpu) return 1; - return 0; + // Shaders are compiled for Vulkan 1.3 (SPIR-V 1.6); reject older devices. + if (props.apiVersion < VK_API_VERSION_1_3) return kDeviceRejected; + + const std::string renderer(props.deviceName.data()); + const bool software = + renderer.find("llvmpipe") != std::string::npos || + renderer.find("SwiftShader") != std::string::npos || + renderer.find("WARP") != std::string::npos || + renderer.find("Basic Render Driver") != std::string::npos; + if (software) return 0; + + int score = 10; + if (props.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + score = 30; + } else if (props.deviceType == vk::PhysicalDeviceType::eIntegratedGpu) { + score = 20; + } + // Penalize emulated Vulkan-on-D3D12 devices (e.g. WSL2) + if (renderer.find("D3D12") != std::string::npos || + renderer.find("Dozen") != std::string::npos) { + --score; + } + return score; } } // namespace @@ -169,7 +192,8 @@ GaussianSplatVulkanInteropContext::~GaussianSplatVulkanInteropContext() { // Initialize / Shutdown // --------------------------------------------------------------------------- -bool GaussianSplatVulkanInteropContext::Initialize() { +bool GaussianSplatVulkanInteropContext::Initialize( + const GpuAdapterInfo* required_adapter) { if (initialized_) return true; // Initialize the global dynamic dispatcher with vkGetInstanceProcAddr @@ -192,7 +216,8 @@ bool GaussianSplatVulkanInteropContext::Initialize() { // function pointers (required for physical device enumeration etc.). VULKAN_HPP_DEFAULT_DISPATCHER.init(static_cast(*instance_)); - if (!SelectPhysicalDevice()) return false; + if (!SelectPhysicalDevice(required_adapter)) return false; + if (!CreateLogicalDevice()) return false; // After device creation, update the dispatcher with device-level @@ -314,7 +339,8 @@ bool GaussianSplatVulkanInteropContext::CreateInstance() { // Physical device selection // --------------------------------------------------------------------------- -bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice() { +bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( + const GpuAdapterInfo* required_adapter) { auto devices = instance_.enumeratePhysicalDevices(); if (devices.empty()) { last_error_ = "No Vulkan-capable devices found"; @@ -322,10 +348,17 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice() { return false; } - int best_score = -1; + int best_score = kDeviceRejected; std::size_t best_idx = devices.size(); for (std::size_t i = 0; i < devices.size(); ++i) { const int score = ScoreDevice(devices[i]); + if (score == kDeviceRejected) continue; + if (required_adapter && required_adapter->valid && + !SameAdapter(GetAdapterInfo(static_cast( + *devices[i])), + *required_adapter)) { + continue; // not the required adapter; skip + } if (score > best_score) { best_score = score; best_idx = i; @@ -334,9 +367,13 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice() { if (best_idx == devices.size()) { last_error_ = - "No suitable Vulkan device found with required interop " - "extensions. Required " - "extensions: " VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; + required_adapter && required_adapter->valid + ? "No Vulkan device matched the required adapter '" + + required_adapter->device_name + "'" + : "No suitable Vulkan device found with required " + "interop extensions. Required " + "extensions:" + " " VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; utility::LogWarning("GaussianSplat Vulkan: {}", last_error_); return false; } @@ -474,7 +511,7 @@ bool GaussianSplatVulkanInteropContext::AllocateExportableImage( VkImageUsageFlags usage, VkImage& out_image, VkDeviceMemory& out_memory, - int& out_fd) const { + intptr_t& out_fd) const { const vk::Format format = static_cast(vk_format); const vk::ImageUsageFlags vk_usage = static_cast(usage); @@ -576,10 +613,12 @@ bool GaussianSplatVulkanInteropContext::AllocateExportableImage( dev.destroyImage(image); return false; } - out_fd = static_cast(reinterpret_cast(win32_handle)); + // Stored as intptr_t (not int) to avoid truncating/sign-extending the + // 64-bit HANDLE, which previously corrupted the value passed to GL. + out_fd = reinterpret_cast(win32_handle); // NOTE: on Windows, HANDLE values are not real file descriptors and should // be closed with CloseHandle() after being imported into GL. However, the - // current design passes them as int (out_fd) to ImportFDIntoGL, which will + // current design passes them as out_fd to ImportFDIntoGL, which will // pass them to glImportMemoryWin32HandleEXT. GL takes ownership of the // HANDLE import, so we cannot close it here. The HANDLE lifetime is managed // by GL and Vulkan. @@ -602,7 +641,7 @@ bool GaussianSplatVulkanInteropContext::AllocateExportableImage( } bool GaussianSplatVulkanInteropContext::ImportFDIntoGL( - int fd, + intptr_t fd, std::uint32_t width, std::uint32_t height, VkDeviceSize memory_size, @@ -617,9 +656,20 @@ bool GaussianSplatVulkanInteropContext::ImportFDIntoGL( return false; } + // AllocateExportableImage() always allocates via + // vk::MemoryDedicatedAllocateInfo, so GL must be told this memory object + // is dedicated to a single resource before the handle/fd is imported. + // Omitting this causes some Windows GL drivers to silently fail the + // import (GL_OUT_OF_MEMORY) and crash on the subsequent storage call. + { + const GLint dedicated = GL_TRUE; + glMemoryObjectParameterivEXT(out_gl_memory_object, + GL_DEDICATED_MEMORY_OBJECT_EXT, + &dedicated); + } + #if defined(_WIN32) - const HANDLE win32_handle = - reinterpret_cast(static_cast(fd)); + const HANDLE win32_handle = reinterpret_cast(fd); glImportMemoryWin32HandleEXT(out_gl_memory_object, static_cast(memory_size), GL_HANDLE_TYPE_OPAQUE_WIN32_EXT, win32_handle); @@ -630,7 +680,7 @@ bool GaussianSplatVulkanInteropContext::ImportFDIntoGL( #else glImportMemoryFdEXT(out_gl_memory_object, static_cast(memory_size), - GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); + GL_HANDLE_TYPE_OPAQUE_FD_EXT, static_cast(fd)); // FD ownership is transferred to GL; do not close it. #endif @@ -643,6 +693,13 @@ bool GaussianSplatVulkanInteropContext::ImportFDIntoGL( return false; } + // The imported Vulkan image was allocated with optimal (driver-opaque) + // tiling; GL defaults imported textures to linear tiling, so this must be + // set explicitly or glTextureStorageMem2DEXT operates on a mismatched + // memory layout (crashes some Windows drivers instead of erroring). + glTextureParameteri(out_gl_texture, GL_TEXTURE_TILING_EXT, + GL_OPTIMAL_TILING_EXT); + // Allocate GL texture storage bound to the imported memory. GLenum gl_internal_format = 0; switch (format) { @@ -704,7 +761,7 @@ SharedImageDesc GaussianSplatVulkanInteropContext::CreateSharedImage( break; } - int export_fd = -1; + intptr_t export_fd = -1; { // Probe memory requirements using a temporary image (no exportable // memory yet) to determine the actual allocation size for GL import. diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h index e7927feae41..5346b814301 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h @@ -32,6 +32,7 @@ #if !defined(__APPLE__) +#include #include #include @@ -48,6 +49,8 @@ #endif #include +#include "open3d/visualization/rendering/GpuAdapterSelection.h" + namespace open3d { namespace visualization { namespace rendering { @@ -109,11 +112,12 @@ class GaussianSplatVulkanInteropContext { static GaussianSplatVulkanInteropContext& GetInstance(); /// Load Vulkan via BlueVK, select a physical device with external-memory - /// extension support, and create a compute queue. - /// Must be called BEFORE - /// GaussianSplatOpenGLContext::InitializeStandalone(). Returns false on - /// failure; call GetLastError() for a diagnostic string. - bool Initialize(); + /// extension support, and create a compute queue. If `required_adapter` + /// is valid, only that physical GPU is selected; otherwise native discrete + /// GPUs are preferred over integrated, translation, and software devices. + /// Vulkan and OpenGL must use the same adapter for memory-object interop. + /// Returns false on failure; call GetLastError() for a diagnostic string. + bool Initialize(const GpuAdapterInfo* required_adapter = nullptr); /// Release all Vulkan resources and invalidate the context. void Shutdown(); @@ -209,7 +213,7 @@ class GaussianSplatVulkanInteropContext { // --- Internal helpers ------------------------------------------------- bool CreateInstance(); - bool SelectPhysicalDevice(); + bool SelectPhysicalDevice(const GpuAdapterInfo* required_adapter); bool CreateLogicalDevice(); /// Allocate a VkImage with a dedicated exportable memory allocation and @@ -220,11 +224,13 @@ class GaussianSplatVulkanInteropContext { VkImageUsageFlags usage, VkImage& out_image, VkDeviceMemory& out_memory, - int& out_fd) const; + // intptr_t (not int): must hold a 64-bit + // Windows HANDLE without truncation. + intptr_t& out_fd) const; /// Import a Vulkan FD into an OpenGL memory-object and create a GL /// texture backed by that memory object. - bool ImportFDIntoGL(int fd, + bool ImportFDIntoGL(intptr_t fd, std::uint32_t width, std::uint32_t height, VkDeviceSize memory_size, diff --git a/cpp/open3d/visualization/visualizer/GuiVisualizer.cpp b/cpp/open3d/visualization/visualizer/GuiVisualizer.cpp index ac1495d663b..f33c7bc637b 100644 --- a/cpp/open3d/visualization/visualizer/GuiVisualizer.cpp +++ b/cpp/open3d/visualization/visualizer/GuiVisualizer.cpp @@ -1312,7 +1312,7 @@ void GuiVisualizer::OnMenuItemSelected(gui::Menu::ItemId item_id) { "Point cloud files (.xyz, .xyzn, .xyzrgb, .ply, " ".pcd, .pts)"); dlg->AddFilter(".ply .splat .spz", - "Gaussian Splat files (.ply, .splat,.spz)"); + "Gaussian Splat files (.ply, .splat, .spz)"); dlg->AddFilter(".ply", "Polygon files (.ply)"); dlg->AddFilter(".stl", "Stereolithography files (.stl)"); dlg->AddFilter(".fbx", "Autodesk Filmbox files (.fbx)"); diff --git a/docs/jupyter/visualization/gaussian_splatting.ipynb b/docs/jupyter/visualization/gaussian_splatting.ipynb index f5795cef1f2..d617fe9e118 100644 --- a/docs/jupyter/visualization/gaussian_splatting.ipynb +++ b/docs/jupyter/visualization/gaussian_splatting.ipynb @@ -87,6 +87,25 @@ "assets from a CSV manifest file from the command line." ] }, + { + "cell_type": "markdown", + "id": "5802328f", + "metadata": {}, + "source": [ + "## Multi-GPU systems\n", + "\n", + "On Linux, select the Vulkan GPU before starting Python or Open3D. For example, to select Intel devices:\n", + "\n", + "```bash\n", + "VK_LOADER_DRIVERS_SELECT='*intel*' python your_script.py\n", + "\n", + "# To select a specific physical device \"vendorID:deviceID\" instead of a vendor's drivers:\n", + "VK_LOADER_DEVICE_SELECT='0x8086:0x56a1' python your_script.py\n", + "```\n", + "\n", + "Replace `*intel*` with an appropriate Vulkan loader filter for another GPU. On Windows, the GPU used for 3DGS must be connected to a display; render-only adapters without a display output cannot be selected for the shared OpenGL/Vulkan rendering path." + ] + }, { "cell_type": "code", "execution_count": 1,