From 61eaa814dae0bf8cb9d57b6050a6c65d48431140 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:01:54 -0700 Subject: [PATCH 01/11] Add Windows MSIX packaging for Open3DViewer Adds AppxManifest, store icon assets, and a build_msix.ps1 packaging script under cpp/apps/Open3DViewer/WindowsMSIX/, wires MSIX packaging into the Open3DViewer CMake target, and adds a Windows CI workflow step to build the MSIX package. --- .github/workflows/windows.yml | 24 +++++ cpp/apps/CMakeLists.txt | 5 ++ cpp/apps/Open3DViewer/Open3DViewer.xml | 10 +++ .../Open3DViewer/WindowsMSIX/AppxManifest.xml | 85 ++++++++++++++++++ .../WindowsMSIX/Assets/Square150x150Logo.png | Bin 0 -> 8506 bytes .../WindowsMSIX/Assets/Square44x44Logo.png | Bin 0 -> 1720 bytes .../WindowsMSIX/Assets/StoreLogo.png | Bin 0 -> 1936 bytes .../Open3DViewer/WindowsMSIX/build_msix.ps1 | 57 ++++++++++++ 8 files changed, 181 insertions(+) create mode 100644 cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml create mode 100644 cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square150x150Logo.png create mode 100644 cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square44x44Logo.png create mode 100644 cpp/apps/Open3DViewer/WindowsMSIX/Assets/StoreLogo.png create mode 100644 cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b680f0a191a..70c7de2a96c 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -296,6 +296,30 @@ jobs: path: ${{ github.workspace }}/${{ env.VIEWER_ZIP_NAME }} if-no-files-found: error + - name: Setup WinApp CLI + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + uses: microsoft/setup-WinAppCli@v0.1 + + - name: Build MSIX + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + run: | + $ErrorActionPreference = 'Stop' + & "${{ env.SRC_DIR }}\cpp\apps\Open3DViewer\WindowsMSIX\build_msix.ps1" ` + -SrcDir "${{ env.SRC_DIR }}" ` + -InstallDir "C:\Program Files\Open3D\bin\Open3D" ` + -CMakeCachePath "C:/Open3D/build/CMakeCache.txt" ` + -OutDir "C:\Open3D" + + - name: Upload MSIX + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + uses: actions/upload-artifact@v4 + with: + name: open3d-app-windows-msix-amd64 + path: | + C:\Open3D\*.msix + C:\Open3D\Open3D.cer + if-no-files-found: error + - name: Update devel release with viewer if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} env: diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index 107aae62150..f2c4cce7853 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -131,6 +131,11 @@ 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 (MSVC) + target_link_options(${TARGET_NAME} PRIVATE "/ENTRY:mainCRTStartup") + endif() else() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../${APP_NAME}") add_executable(${TARGET_NAME} ${SOURCE_FILES} ${HEADER_FILES}) 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..af0132c3346 --- /dev/null +++ b/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml @@ -0,0 +1,85 @@ + + + + + + + + Open3D + Open3D + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + Assets\Square44x44Logo.png + Open3D 3D file + + .ply + .stl + .obj + .off + .fbx + .glb + .gltf + .pcd + .pts + .xyz + .splat + .spz + + + + + + + + + + + + diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square150x150Logo.png b/cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f218d5b4f32f22472f5951513bf6a177b13808cc GIT binary patch literal 8506 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 +param( + [Parameter(Mandatory = $true)][string]$SrcDir, + [Parameter(Mandatory = $true)][string]$InstallDir, + [Parameter(Mandatory = $true)][string]$CMakeCachePath, + [Parameter(Mandatory = $true)][string]$OutDir +) + +$ErrorActionPreference = 'Stop' + +# Build a four-part MSIX version from the Open3D version in CMakeCache. +# MSIX requires Major.Minor.Patch.Build (all numeric). +$RAW_VER = (Select-String -Path $CMakeCachePath ` + -Pattern "OPEN3D_VERSION_FULL:STRING=").Line.Split('=')[1].Trim() +$MSIX_VERSION = ($RAW_VER -replace '[^0-9.]', '').TrimEnd('.') + '.0' +$OPEN3D_VERSION = $RAW_VER -replace '[^0-9.]', '' + +# Stage: start from the already-installed viewer folder and add manifest + assets. +$STAGING = Join-Path $OutDir "msix-staging" +New-Item -ItemType Directory -Force -Path $STAGING | Out-Null +Copy-Item -Recurse (Join-Path $InstallDir "*") $STAGING +$SRC_MSIX = Join-Path $SrcDir "cpp\apps\Open3DViewer\WindowsMSIX" +Copy-Item (Join-Path $SRC_MSIX "AppxManifest.xml") $STAGING +Copy-Item -Recurse (Join-Path $SRC_MSIX "Assets") $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. +winapp cert generate ` + --manifest $manifestPath ` + --output (Join-Path $OutDir "Open3D.pfx") ` + --export-cer ` + --if-exists overwrite + +# Pack and sign the MSIX. +$MSIX_NAME = "Open3DViewer-$OPEN3D_VERSION-x64.msix" +winapp pack $STAGING ` + --output (Join-Path $OutDir $MSIX_NAME) ` + --cert (Join-Path $OutDir "Open3D.pfx") + +echo "MSIX_NAME=$MSIX_NAME" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append From 9a78ca1f09275d9b4c36a70f5635a7813c9acce5 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Thu, 13 Aug 2026 15:02:12 -0700 Subject: [PATCH 02/11] Fix Windows crash on Gaussian Splat GL/Vulkan interop with hybrid GPUs On hybrid-GPU systems (e.g. Intel integrated + discrete), Vulkan's discrete-GPU-preferred device selection could pick a different physical adapter than the one the OS bound the OpenGL context to, causing GL_EXT_memory_object cross-adapter texture import to silently fail with GL_OUT_OF_MEMORY and crash later with STATUS_FAIL_FAST_EXCEPTION in glTextureStorageMem2DEXT. Fix: create the shared OpenGL context before Vulkan initialization, query its DXGI adapter LUID (Windows) or GL_DEVICE_UUID_EXT (other platforms) via GaussianSplatOpenGLContext::GetAdapterId(), and thread it into GaussianSplatVulkanInteropContext::SelectPhysicalDevice() so Vulkan device selection prefers the physical device matching the GL context's adapter, falling back to best-effort scoring if no match is found. Also fixes an unrelated HANDLE-to-int truncation bug in AllocateExportableImage, sets GL_DEDICATED_MEMORY_OBJECT_EXT and GL_TEXTURE_TILING_EXT before importing Vulkan-allocated memory into GL (required since allocations use dedicated allocation and optimal tiling), and disables MSAA before render target setup in the geometry pass. --- .../rendering/filament/FilamentEngine.cpp | 58 +++++++----- .../GaussianSplatOpenGLContext.cpp | 81 +++++++++++++++++ .../GaussianSplatOpenGLContext.h | 16 ++++ .../GaussianSplatVulkanBackend.cpp | 6 +- .../GaussianSplatVulkanInteropContext.cpp | 91 ++++++++++++++++--- .../GaussianSplatVulkanInteropContext.h | 30 ++++-- 6 files changed, 239 insertions(+), 43 deletions(-) diff --git a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp index 1d0e2ffa847..b4c77f0be57 100644 --- a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp +++ b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp @@ -22,6 +22,7 @@ #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" @@ -126,32 +127,18 @@ 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()); - } - } - } - - // 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 + // Create our compute GL context BEFORE the Vulkan interop context so we + // can read the GPU adapter's GL_DEVICE_UUID_EXT and select a *matching* + // Vulkan physical device. GL_EXT_memory_object cross-adapter import is + // not supported: if Vulkan picks a different GPU than the GL context is + // bound to (e.g. multi-GPU laptops), the memory import silently fails + // (GL_OUT_OF_MEMORY) even though the Vulkan-side export succeeds. + // This must also happen before Engine::create() because GLX/WGL context // 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()) { gl_ctx.InitializeStandalone(); } @@ -163,6 +150,33 @@ EngineInstance::EngineInstance() { shared_context_); } } + + // Initialise the Vulkan interop context, preferring the physical device + // whose adapter id matches the GL context's adapter (see above). Failure + // is non-fatal: the Vulkan backend will fall back gracefully. + { + auto& vk_ctx = GaussianSplatVulkanInteropContext::GetInstance(); + if (!vk_ctx.IsValid()) { + std::uint8_t gl_adapter_id[16]; + std::size_t gl_adapter_id_size = 0; + const bool have_id = + gl_ctx.IsValid() && + gl_ctx.GetAdapterId(gl_adapter_id, gl_adapter_id_size); + if (!vk_ctx.Initialize(have_id ? gl_adapter_id : nullptr, + have_id ? gl_adapter_id_size : 0)) { + utility::LogWarning( + "EngineInstance: Vulkan interop context init failed: " + "{}", + vk_ctx.GetLastError()); + } + } + // GL context is already current at this point (created above), so + // the interop extension probe can run now. + if (vk_ctx.IsValid() && gl_ctx.IsValid() && + !vk_ctx.AreGLExtensionsReady()) { + vk_ctx.ProbeGLExtensions(); + } + } #endif filament::Engine::Config fmcfg; diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp index a1256f4a12d..5a4f6bb1505 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp @@ -20,6 +20,7 @@ #if !defined(__APPLE__) +#include #include #include @@ -41,6 +42,11 @@ #endif #include +#if defined(_WIN32) +#include +#pragma comment(lib, "dxgi.lib") +#endif + #include "open3d/utility/Logging.h" namespace open3d { @@ -171,6 +177,81 @@ void* GaussianSplatOpenGLContext::GetNativeContext() const { #endif } +bool GaussianSplatOpenGLContext::GetAdapterId(std::uint8_t out_id[16], + std::size_t& out_size) const { + if (!initialized_ || !glfw_window_) { + return false; + } +#if defined(_WIN32) + // Find the DXGI adapter driving the monitor this (hidden) window is + // associated with, and return its 8-byte LUID. This is used to match + // against Vulkan's VkPhysicalDeviceIDProperties::deviceLUID so the + // Vulkan device selected for GL_EXT_memory_object interop is guaranteed + // to be the *same* physical GPU as this GL context — required because + // cross-adapter memory import silently fails (GL_OUT_OF_MEMORY). + HWND hwnd = glfwGetWin32Window(static_cast(glfw_window_)); + if (!hwnd) return false; + HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); + + IDXGIFactory1* factory = nullptr; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), + reinterpret_cast(&factory))) || + !factory) { + return false; + } + + bool found = false; + LUID luid{}; + for (UINT i = 0;; ++i) { + IDXGIAdapter1* adapter = nullptr; + if (factory->EnumAdapters1(i, &adapter) == DXGI_ERROR_NOT_FOUND) { + break; + } + if (!adapter) continue; + for (UINT j = 0;; ++j) { + IDXGIOutput* output = nullptr; + if (adapter->EnumOutputs(j, &output) == DXGI_ERROR_NOT_FOUND) { + break; + } + if (!output) continue; + DXGI_OUTPUT_DESC odesc{}; + if (SUCCEEDED(output->GetDesc(&odesc)) && + odesc.Monitor == mon) { + DXGI_ADAPTER_DESC1 adesc{}; + if (SUCCEEDED(adapter->GetDesc1(&adesc))) { + luid = adesc.AdapterLuid; + found = true; + } + } + output->Release(); + if (found) break; + } + adapter->Release(); + if (found) break; + } + factory->Release(); + + if (!found) return false; + std::memcpy(out_id, &luid, sizeof(luid)); + out_size = sizeof(luid); + return true; +#else + if (GLEW_EXT_memory_object == 0) { + return false; + } + GLint num_uuids = 0; + glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &num_uuids); + if (num_uuids < 1) { + return false; + } + glGetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, + reinterpret_cast(out_id)); + if (glGetError() != GL_NO_ERROR) return false; + out_size = 16; + return true; +#endif +} + void GaussianSplatOpenGLContext::Shutdown() { if (!initialized_) { return; diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h index b324e76781f..65e142e1140 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,19 @@ class GaussianSplatOpenGLContext { /// Windows -> HGLRC void* GetNativeContext() const; + /// Queries an identifier for the GPU adapter this context is bound to, + /// so callers (Vulkan interop) can select a *matching* physical device. + /// GL_EXT_memory_object cross-adapter import silently fails, so this + /// match is required on multi-GPU (hybrid graphics) systems. + /// Windows -> DXGI adapter LUID (8 bytes), via the monitor the + /// context's window is associated with. More reliable than + /// GL_DEVICE_UUID_EXT, which some drivers advertise but do + /// not actually implement (observed: Intel Iris Xe/Arc + /// hybrid driver returns GL_INVALID_OPERATION for it). + /// Other platforms -> GL_DEVICE_UUID_EXT (16 bytes). + /// Returns false (leaving out_id untouched) if unavailable. + bool GetAdapterId(std::uint8_t out_id[16], std::size_t& out_size) const; + /// Destroys the context and associated resources. void Shutdown(); diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp index c2703d3e658..2bf70bedeb5 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp @@ -179,12 +179,14 @@ class GaussianSplatVulkanBackend final : public GaussianSplatRenderer::Backend { targets.render_target = resource_mgr.CreateRenderTarget(view_color, targets.depth); } - view.SetRenderTarget(targets.render_target); - + // Disable MSAA before binding the render target: Filament validates + // MSAA/sampleable-depth compatibility inside SetRenderTarget() (see #7495). auto* native = view.GetNativeView(); auto msaa = native->getMultiSampleAntiAliasingOptions(); msaa.enabled = false; native->setMultiSampleAntiAliasingOptions(msaa); + + view.SetRenderTarget(targets.render_target); view.SetPostProcessing(false); return static_cast(targets.render_target); diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp index 2a327943d6e..e7061496a7a 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp @@ -169,7 +169,8 @@ GaussianSplatVulkanInteropContext::~GaussianSplatVulkanInteropContext() { // Initialize / Shutdown // --------------------------------------------------------------------------- -bool GaussianSplatVulkanInteropContext::Initialize() { +bool GaussianSplatVulkanInteropContext::Initialize( + const std::uint8_t* gl_adapter_id, std::size_t gl_adapter_id_size) { if (initialized_) return true; // Initialize the global dynamic dispatcher with vkGetInstanceProcAddr @@ -192,7 +193,7 @@ 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(gl_adapter_id, gl_adapter_id_size)) return false; if (!CreateLogicalDevice()) return false; // After device creation, update the dispatcher with device-level @@ -314,7 +315,8 @@ bool GaussianSplatVulkanInteropContext::CreateInstance() { // Physical device selection // --------------------------------------------------------------------------- -bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice() { +bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( + const std::uint8_t* gl_adapter_id, std::size_t gl_adapter_id_size) { auto devices = instance_.enumeratePhysicalDevices(); if (devices.empty()) { last_error_ = "No Vulkan-capable devices found"; @@ -322,16 +324,61 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice() { return false; } + // GL_EXT_memory_object interop requires the Vulkan device and the GL + // context to be the *same* physical GPU adapter: cross-adapter import + // is not supported and fails silently (GL_OUT_OF_MEMORY) even though + // the Vulkan-side export succeeds. When the GL adapter id is known + // (8-byte DXGI LUID on Windows, 16-byte GL_DEVICE_UUID_EXT elsewhere), + // restrict candidates to the matching device first. + const bool have_id = gl_adapter_id != nullptr && + (gl_adapter_id_size == 8 || gl_adapter_id_size == 16); + int best_score = -1; std::size_t best_idx = devices.size(); for (std::size_t i = 0; i < devices.size(); ++i) { - const int score = ScoreDevice(devices[i]); + int score = ScoreDevice(devices[i]); + if (score < 0) continue; + if (have_id) { + const auto id_props = + devices[i].getProperties2(); + const auto& id = id_props.get(); + bool matches = false; + if (gl_adapter_id_size == 8) { + matches = id.deviceLUIDValid && + std::memcmp(id.deviceLUID.data(), gl_adapter_id, + 8) == 0; + } else { + matches = std::memcmp(id.deviceUUID.data(), gl_adapter_id, + 16) == 0; + } + if (!matches) continue; // not the GL-bound adapter; skip + score += 100; // guarantee a match wins over any non-match + } if (score > best_score) { best_score = score; best_idx = i; } } + if (best_idx == devices.size() && have_id) { + // No device matched the GL adapter. Falling back to best-effort + // scoring risks a repeat of the cross-adapter import failure, but a + // working (if unshared) device is still better than none for + // callers that only need compute, so fall back with a warning. + utility::LogWarning( + "GaussianSplat Vulkan: no physical device matched the GL " + "context's adapter; GL/Vulkan interop may fail. Falling " + "back to best-effort device scoring."); + for (std::size_t i = 0; i < devices.size(); ++i) { + const int score = ScoreDevice(devices[i]); + if (score > best_score) { + best_score = score; + best_idx = i; + } + } + } + if (best_idx == devices.size()) { last_error_ = "No suitable Vulkan device found with required interop " @@ -474,7 +521,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 +623,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 +651,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 +666,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 +690,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 +703,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 +771,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..81d4a4f6639 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 @@ -110,10 +111,22 @@ class GaussianSplatVulkanInteropContext { /// 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(); + /// + /// `gl_adapter_id`/`gl_adapter_id_size`, if `gl_adapter_id` is non-null, + /// identify the GPU adapter the GL interop context is bound to (see + /// GaussianSplatOpenGLContext::GetAdapterId()): an 8-byte DXGI LUID on + /// Windows or a 16-byte GL_DEVICE_UUID_EXT elsewhere. When provided, + /// only physical devices whose VkPhysicalDeviceIDProperties deviceLUID + /// (8 bytes) or deviceUUID (16 bytes) matches are eligible: + /// GL_EXT_memory_object import requires the Vulkan device and the GL + /// context to be on the *same* GPU adapter, otherwise the import + /// silently fails (GL_OUT_OF_MEMORY) even though the Vulkan-side export + /// succeeds. If null, or no Vulkan device matches, falls back to the + /// old best-effort scoring (may pick a mismatched adapter on multi-GPU + /// systems). + /// Returns false on failure; call GetLastError() for a diagnostic string. + bool Initialize(const std::uint8_t* gl_adapter_id = nullptr, + std::size_t gl_adapter_id_size = 0); /// Release all Vulkan resources and invalidate the context. void Shutdown(); @@ -209,7 +222,8 @@ class GaussianSplatVulkanInteropContext { // --- Internal helpers ------------------------------------------------- bool CreateInstance(); - bool SelectPhysicalDevice(); + bool SelectPhysicalDevice(const std::uint8_t* gl_adapter_id, + std::size_t gl_adapter_id_size); bool CreateLogicalDevice(); /// Allocate a VkImage with a dedicated exportable memory allocation and @@ -220,11 +234,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, From ff19e0eb058451fc39053c45b5e7fcc020c65682 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:45:20 -0700 Subject: [PATCH 03/11] Fix Gaussian Splat GPU adapter interop --- cpp/open3d/visualization/CMakeLists.txt | 1 + cpp/open3d/visualization/app/Viewer.cpp | 2 + .../rendering/GpuAdapterSelection.cpp | 292 ++++++++++++++++++ .../rendering/GpuAdapterSelection.h | 101 ++++++ .../rendering/filament/FilamentEngine.cpp | 90 ++++-- .../GaussianSplatOpenGLContext.cpp | 80 ----- .../GaussianSplatOpenGLContext.h | 15 +- .../GaussianSplatPassRunner.cpp | 11 +- .../GaussianSplatVulkanInteropContext.cpp | 117 ++++--- .../GaussianSplatVulkanInteropContext.h | 26 +- 10 files changed, 527 insertions(+), 208 deletions(-) create mode 100644 cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp create mode 100644 cpp/open3d/visualization/rendering/GpuAdapterSelection.h 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/app/Viewer.cpp b/cpp/open3d/visualization/app/Viewer.cpp index c4b965bbb9c..84684b68c00 100644 --- a/cpp/open3d/visualization/app/Viewer.cpp +++ b/cpp/open3d/visualization/app/Viewer.cpp @@ -24,6 +24,8 @@ static const int width = 1280; static const int height = 960; void RunViewer(int argc, const char *argv[]) { + utility::Logger::GetInstance().SetVerbosityLevel( + utility::VerbosityLevel::Debug); std::function print_fcn = utility::Logger::GetInstance().GetPrintFunction(); utility::Logger::GetInstance().ResetPrintFunction(); diff --git a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp new file mode 100644 index 00000000000..870717ef132 --- /dev/null +++ b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp @@ -0,0 +1,292 @@ +// ---------------------------------------------------------------------------- +// - 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, 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 b4c77f0be57..38b49053558 100644 --- a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp +++ b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp @@ -27,6 +27,7 @@ #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 @@ -127,20 +128,58 @@ EngineInstance::EngineInstance() { backend = filament::backend::Backend::OPENGL; } - // Create our compute GL context BEFORE the Vulkan interop context so we - // can read the GPU adapter's GL_DEVICE_UUID_EXT and select a *matching* - // Vulkan physical device. GL_EXT_memory_object cross-adapter import is - // not supported: if Vulkan picks a different GPU than the GL context is - // bound to (e.g. multi-GPU laptops), the memory import silently fails - // (GL_OUT_OF_MEMORY) even though the Vulkan-side export succeeds. - // This must also happen before Engine::create() because GLX/WGL context - // sharing can only be established at context creation time. + // 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()); + } + auto& gl_ctx = GaussianSplatOpenGLContext::GetInstance(); if ((backend == filament::backend::Backend::OPENGL || backend == filament::backend::Backend::DEFAULT) && !shared_context_) { 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(); @@ -151,32 +190,21 @@ EngineInstance::EngineInstance() { } } - // Initialise the Vulkan interop context, preferring the physical device - // whose adapter id matches the GL context's adapter (see above). Failure - // is non-fatal: the Vulkan backend will fall back gracefully. - { - auto& vk_ctx = GaussianSplatVulkanInteropContext::GetInstance(); - if (!vk_ctx.IsValid()) { - std::uint8_t gl_adapter_id[16]; - std::size_t gl_adapter_id_size = 0; - const bool have_id = - gl_ctx.IsValid() && - gl_ctx.GetAdapterId(gl_adapter_id, gl_adapter_id_size); - if (!vk_ctx.Initialize(have_id ? gl_adapter_id : nullptr, - have_id ? gl_adapter_id_size : 0)) { - utility::LogWarning( - "EngineInstance: Vulkan interop context init failed: " - "{}", - vk_ctx.GetLastError()); - } - } - // GL context is already current at this point (created above), so - // the interop extension probe can run now. - if (vk_ctx.IsValid() && gl_ctx.IsValid() && - !vk_ctx.AreGLExtensionsReady()) { + 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/GaussianSplatOpenGLContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp index 5a4f6bb1505..22a0c97e722 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp @@ -42,11 +42,6 @@ #endif #include -#if defined(_WIN32) -#include -#pragma comment(lib, "dxgi.lib") -#endif - #include "open3d/utility/Logging.h" namespace open3d { @@ -177,81 +172,6 @@ void* GaussianSplatOpenGLContext::GetNativeContext() const { #endif } -bool GaussianSplatOpenGLContext::GetAdapterId(std::uint8_t out_id[16], - std::size_t& out_size) const { - if (!initialized_ || !glfw_window_) { - return false; - } -#if defined(_WIN32) - // Find the DXGI adapter driving the monitor this (hidden) window is - // associated with, and return its 8-byte LUID. This is used to match - // against Vulkan's VkPhysicalDeviceIDProperties::deviceLUID so the - // Vulkan device selected for GL_EXT_memory_object interop is guaranteed - // to be the *same* physical GPU as this GL context — required because - // cross-adapter memory import silently fails (GL_OUT_OF_MEMORY). - HWND hwnd = glfwGetWin32Window(static_cast(glfw_window_)); - if (!hwnd) return false; - HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); - - IDXGIFactory1* factory = nullptr; - if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), - reinterpret_cast(&factory))) || - !factory) { - return false; - } - - bool found = false; - LUID luid{}; - for (UINT i = 0;; ++i) { - IDXGIAdapter1* adapter = nullptr; - if (factory->EnumAdapters1(i, &adapter) == DXGI_ERROR_NOT_FOUND) { - break; - } - if (!adapter) continue; - for (UINT j = 0;; ++j) { - IDXGIOutput* output = nullptr; - if (adapter->EnumOutputs(j, &output) == DXGI_ERROR_NOT_FOUND) { - break; - } - if (!output) continue; - DXGI_OUTPUT_DESC odesc{}; - if (SUCCEEDED(output->GetDesc(&odesc)) && - odesc.Monitor == mon) { - DXGI_ADAPTER_DESC1 adesc{}; - if (SUCCEEDED(adapter->GetDesc1(&adesc))) { - luid = adesc.AdapterLuid; - found = true; - } - } - output->Release(); - if (found) break; - } - adapter->Release(); - if (found) break; - } - factory->Release(); - - if (!found) return false; - std::memcpy(out_id, &luid, sizeof(luid)); - out_size = sizeof(luid); - return true; -#else - if (GLEW_EXT_memory_object == 0) { - return false; - } - GLint num_uuids = 0; - glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &num_uuids); - if (num_uuids < 1) { - return false; - } - glGetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, - reinterpret_cast(out_id)); - if (glGetError() != GL_NO_ERROR) return false; - out_size = 16; - return true; -#endif -} - void GaussianSplatOpenGLContext::Shutdown() { if (!initialized_) { return; diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h index 65e142e1140..d34ba8e719e 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.h @@ -53,18 +53,9 @@ class GaussianSplatOpenGLContext { /// Windows -> HGLRC void* GetNativeContext() const; - /// Queries an identifier for the GPU adapter this context is bound to, - /// so callers (Vulkan interop) can select a *matching* physical device. - /// GL_EXT_memory_object cross-adapter import silently fails, so this - /// match is required on multi-GPU (hybrid graphics) systems. - /// Windows -> DXGI adapter LUID (8 bytes), via the monitor the - /// context's window is associated with. More reliable than - /// GL_DEVICE_UUID_EXT, which some drivers advertise but do - /// not actually implement (observed: Intel Iris Xe/Arc - /// hybrid driver returns GL_INVALID_OPERATION for it). - /// Other platforms -> GL_DEVICE_UUID_EXT (16 bytes). - /// Returns false (leaving out_id untouched) if unavailable. - bool GetAdapterId(std::uint8_t out_id[16], std::size_t& out_size) 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..d10b6f4cb0d 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp @@ -190,9 +190,14 @@ 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/GaussianSplatVulkanInteropContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp index e7061496a7a..a4a6df38158 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp @@ -41,8 +41,11 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #include #include +#include #include +#include + #include "open3d/utility/Logging.h" // Initialize VMA in this single translation unit. @@ -113,11 +116,15 @@ 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 +135,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 +144,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 @@ -170,7 +194,7 @@ GaussianSplatVulkanInteropContext::~GaussianSplatVulkanInteropContext() { // --------------------------------------------------------------------------- bool GaussianSplatVulkanInteropContext::Initialize( - const std::uint8_t* gl_adapter_id, std::size_t gl_adapter_id_size) { + const GpuAdapterInfo* required_adapter) { if (initialized_) return true; // Initialize the global dynamic dispatcher with vkGetInstanceProcAddr @@ -193,7 +217,8 @@ bool GaussianSplatVulkanInteropContext::Initialize( // function pointers (required for physical device enumeration etc.). VULKAN_HPP_DEFAULT_DISPATCHER.init(static_cast(*instance_)); - if (!SelectPhysicalDevice(gl_adapter_id, gl_adapter_id_size)) return false; + if (!SelectPhysicalDevice(required_adapter)) return false; + if (!CreateLogicalDevice()) return false; // After device creation, update the dispatcher with device-level @@ -316,7 +341,7 @@ bool GaussianSplatVulkanInteropContext::CreateInstance() { // --------------------------------------------------------------------------- bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( - const std::uint8_t* gl_adapter_id, std::size_t gl_adapter_id_size) { + const GpuAdapterInfo* required_adapter) { auto devices = instance_.enumeratePhysicalDevices(); if (devices.empty()) { last_error_ = "No Vulkan-capable devices found"; @@ -324,36 +349,16 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( return false; } - // GL_EXT_memory_object interop requires the Vulkan device and the GL - // context to be the *same* physical GPU adapter: cross-adapter import - // is not supported and fails silently (GL_OUT_OF_MEMORY) even though - // the Vulkan-side export succeeds. When the GL adapter id is known - // (8-byte DXGI LUID on Windows, 16-byte GL_DEVICE_UUID_EXT elsewhere), - // restrict candidates to the matching device first. - const bool have_id = gl_adapter_id != nullptr && - (gl_adapter_id_size == 8 || gl_adapter_id_size == 16); - - 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) { - int score = ScoreDevice(devices[i]); - if (score < 0) continue; - if (have_id) { - const auto id_props = - devices[i].getProperties2(); - const auto& id = id_props.get(); - bool matches = false; - if (gl_adapter_id_size == 8) { - matches = id.deviceLUIDValid && - std::memcmp(id.deviceLUID.data(), gl_adapter_id, - 8) == 0; - } else { - matches = std::memcmp(id.deviceUUID.data(), gl_adapter_id, - 16) == 0; - } - if (!matches) continue; // not the GL-bound adapter; skip - score += 100; // guarantee a match wins over any non-match + 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; @@ -361,29 +366,13 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( } } - if (best_idx == devices.size() && have_id) { - // No device matched the GL adapter. Falling back to best-effort - // scoring risks a repeat of the cross-adapter import failure, but a - // working (if unshared) device is still better than none for - // callers that only need compute, so fall back with a warning. - utility::LogWarning( - "GaussianSplat Vulkan: no physical device matched the GL " - "context's adapter; GL/Vulkan interop may fail. Falling " - "back to best-effort device scoring."); - for (std::size_t i = 0; i < devices.size(); ++i) { - const int score = ScoreDevice(devices[i]); - if (score > best_score) { - best_score = score; - best_idx = i; - } - } - } - 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; } diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h index 81d4a4f6639..5346b814301 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.h @@ -49,6 +49,8 @@ #endif #include +#include "open3d/visualization/rendering/GpuAdapterSelection.h" + namespace open3d { namespace visualization { namespace rendering { @@ -110,23 +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. - /// - /// `gl_adapter_id`/`gl_adapter_id_size`, if `gl_adapter_id` is non-null, - /// identify the GPU adapter the GL interop context is bound to (see - /// GaussianSplatOpenGLContext::GetAdapterId()): an 8-byte DXGI LUID on - /// Windows or a 16-byte GL_DEVICE_UUID_EXT elsewhere. When provided, - /// only physical devices whose VkPhysicalDeviceIDProperties deviceLUID - /// (8 bytes) or deviceUUID (16 bytes) matches are eligible: - /// GL_EXT_memory_object import requires the Vulkan device and the GL - /// context to be on the *same* GPU adapter, otherwise the import - /// silently fails (GL_OUT_OF_MEMORY) even though the Vulkan-side export - /// succeeds. If null, or no Vulkan device matches, falls back to the - /// old best-effort scoring (may pick a mismatched adapter on multi-GPU - /// systems). + /// 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 std::uint8_t* gl_adapter_id = nullptr, - std::size_t gl_adapter_id_size = 0); + bool Initialize(const GpuAdapterInfo* required_adapter = nullptr); /// Release all Vulkan resources and invalidate the context. void Shutdown(); @@ -222,8 +213,7 @@ class GaussianSplatVulkanInteropContext { // --- Internal helpers ------------------------------------------------- bool CreateInstance(); - bool SelectPhysicalDevice(const std::uint8_t* gl_adapter_id, - std::size_t gl_adapter_id_size); + bool SelectPhysicalDevice(const GpuAdapterInfo* required_adapter); bool CreateLogicalDevice(); /// Allocate a VkImage with a dedicated exportable memory allocation and From d1515e8bb2a004bb0ec2f3307d4e15ca4625a44f Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:22:29 -0700 Subject: [PATCH 04/11] Package Open3D Viewer as signed MSIX --- .github/workflows/windows.yml | 49 ++++++------------ cpp/apps/CMakeLists.txt | 14 +++++ .../Open3DViewer/WindowsMSIX/AppxManifest.xml | 14 ++--- .../Open3DViewer/WindowsMSIX/build_msix.ps1 | 37 +++++++------ .../visualization/gui/GLFWWindowSystem.cpp | 4 ++ cpp/open3d/visualization/gui/Native.h | 5 ++ cpp/open3d/visualization/gui/NativeWin32.cpp | 21 ++++++++ .../gui/Resources}/Square150x150Logo.png | Bin .../gui/Resources}/Square44x44Logo.png | Bin .../gui/Resources}/StoreLogo.png | Bin .../visualizer/GuiVisualizer.cpp | 2 +- 11 files changed, 85 insertions(+), 61 deletions(-) rename cpp/{apps/Open3DViewer/WindowsMSIX/Assets => open3d/visualization/gui/Resources}/Square150x150Logo.png (100%) rename cpp/{apps/Open3DViewer/WindowsMSIX/Assets => open3d/visualization/gui/Resources}/Square44x44Logo.png (100%) rename cpp/{apps/Open3DViewer/WindowsMSIX/Assets => open3d/visualization/gui/Resources}/StoreLogo.png (100%) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 70c7de2a96c..a26c8d28f02 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -272,52 +272,33 @@ jobs: $ErrorActionPreference = 'Stop' 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 }} - - - 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 }} - if-no-files-found: error - name: Setup WinApp CLI - if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + 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 - if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + working-directory: ${{ env.BUILD_DIR }} + if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} run: | $ErrorActionPreference = 'Stop' - & "${{ env.SRC_DIR }}\cpp\apps\Open3DViewer\WindowsMSIX\build_msix.ps1" ` - -SrcDir "${{ env.SRC_DIR }}" ` - -InstallDir "C:\Program Files\Open3D\bin\Open3D" ` - -CMakeCachePath "C:/Open3D/build/CMakeCache.txt" ` - -OutDir "C:\Open3D" + cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` + --target Open3DViewerMSIX + + - name: Generate MSIX 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: ${{ env.MSIX_PATH }} - name: Upload MSIX - if: ${{ matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.BUILD_CUDA_MODULE == 'OFF' && matrix.CONFIG == 'Release' }} + 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: | - C:\Open3D\*.msix - C:\Open3D\Open3D.cer + ${{ env.MSIX_PATH }} + ${{ env.BUILD_DIR }}\msix-package\Open3D.cer if-no-files-found: error - name: Update devel release with viewer @@ -325,7 +306,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - bash .github/workflows/update_release.sh "${{ env.VIEWER_ZIP_NAME }}" + bash .github/workflows/update_release.sh "${{ env.MSIX_PATH }}" "${{ env.BUILD_DIR }}\msix-package\Open3D.cer" - name: Run C++ unit tests if: ${{ matrix.device != 'cuda' }} diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index f2c4cce7853..c2276cf338f 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) @@ -160,6 +162,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/WindowsMSIX/AppxManifest.xml b/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml index af0132c3346..2940e2a2131 100644 --- a/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml +++ b/cpp/apps/Open3DViewer/WindowsMSIX/AppxManifest.xml @@ -6,13 +6,13 @@ - Checked in as source; only the Version attribute is substituted at pack time in CI (placeholder @OPEN3D_MSIX_VERSION@ → four-part version, e.g. 0.18.0.0). - Publisher "CN=Open3D" is matched by the self-signed dev cert generated in CI - via: winapp cert generate --manifest AppxManifest.xml + using the manifest publisher value - runFullTrust is the only required capability for a packaged Win32 desktop app. - File associations are derived from cpp/apps/Open3DViewer/Open3DViewer.xml. The viewer receives the opened file path as argv[1] (standard packaged app full-trust activation behaviour). - - Assets/ PNGs are checked in (generated once from icon.ico with - winapp manifest update-assets; regenerate and commit on icon changes). + - MSIX PNGs are kept with the shared GUI resources and installed under + resources/ alongside the viewer's other runtime resources. --> Open3D Open3D - Assets\StoreLogo.png + resources\StoreLogo.png @@ -48,15 +48,15 @@ - Assets\Square44x44Logo.png + resources\Square44x44Logo.png Open3D 3D file .ply diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 index 6be89d86cc3..ee532f1a196 100644 --- a/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 +++ b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 @@ -2,38 +2,32 @@ .SYNOPSIS Stages, signs, and packs the Open3D Viewer as a side-load MSIX. -.PARAMETER SrcDir - Open3D repository root (contains cpp/apps/Open3DViewer/WindowsMSIX). .PARAMETER InstallDir Directory the viewer was installed to (contains Open3D.exe + resources/). -.PARAMETER CMakeCachePath - Path to CMakeCache.txt, used to read OPEN3D_VERSION_FULL. +.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]$SrcDir, [Parameter(Mandatory = $true)][string]$InstallDir, - [Parameter(Mandatory = $true)][string]$CMakeCachePath, + [Parameter(Mandatory = $true)][string]$Version, [Parameter(Mandatory = $true)][string]$OutDir ) $ErrorActionPreference = 'Stop' -# Build a four-part MSIX version from the Open3D version in CMakeCache. -# MSIX requires Major.Minor.Patch.Build (all numeric). -$RAW_VER = (Select-String -Path $CMakeCachePath ` - -Pattern "OPEN3D_VERSION_FULL:STRING=").Line.Split('=')[1].Trim() -$MSIX_VERSION = ($RAW_VER -replace '[^0-9.]', '').TrimEnd('.') + '.0' -$OPEN3D_VERSION = $RAW_VER -replace '[^0-9.]', '' +$OPEN3D_VERSION = $Version +$MSIX_VERSION = "$Version.0" -# Stage: start from the already-installed viewer folder and add manifest + assets. +# 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 -Recurse (Join-Path $InstallDir "*") $STAGING -$SRC_MSIX = Join-Path $SrcDir "cpp\apps\Open3DViewer\WindowsMSIX" -Copy-Item (Join-Path $SRC_MSIX "AppxManifest.xml") $STAGING -Copy-Item -Recurse (Join-Path $SRC_MSIX "Assets") $STAGING +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" @@ -50,8 +44,13 @@ winapp cert generate ` # Pack and sign the MSIX. $MSIX_NAME = "Open3DViewer-$OPEN3D_VERSION-x64.msix" +$MSIX_PATH = Join-Path $OutDir $MSIX_NAME winapp pack $STAGING ` - --output (Join-Path $OutDir $MSIX_NAME) ` + --output $MSIX_PATH ` --cert (Join-Path $OutDir "Open3D.pfx") -echo "MSIX_NAME=$MSIX_NAME" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append +# 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/gui/GLFWWindowSystem.cpp b/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp index 23f24921387..bd3dcc944c4 100644 --- a/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp +++ b/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp @@ -172,6 +172,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..c3ddd2813f5 100644 --- a/cpp/open3d/visualization/gui/NativeWin32.cpp +++ b/cpp/open3d/visualization/gui/NativeWin32.cpp @@ -20,6 +20,27 @@ void* GetNativeDrawable(GLFWwindow* glfw_window) { return glfwGetWin32Window(glfw_window); } +void SetNativeWindowIcon(GLFWwindow* glfw_window) { + HWND window = glfwGetWin32Window(glfw_window); + 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/apps/Open3DViewer/WindowsMSIX/Assets/Square150x150Logo.png b/cpp/open3d/visualization/gui/Resources/Square150x150Logo.png similarity index 100% rename from cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square150x150Logo.png rename to cpp/open3d/visualization/gui/Resources/Square150x150Logo.png diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square44x44Logo.png b/cpp/open3d/visualization/gui/Resources/Square44x44Logo.png similarity index 100% rename from cpp/apps/Open3DViewer/WindowsMSIX/Assets/Square44x44Logo.png rename to cpp/open3d/visualization/gui/Resources/Square44x44Logo.png diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/Assets/StoreLogo.png b/cpp/open3d/visualization/gui/Resources/StoreLogo.png similarity index 100% rename from cpp/apps/Open3DViewer/WindowsMSIX/Assets/StoreLogo.png rename to cpp/open3d/visualization/gui/Resources/StoreLogo.png 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)"); From 250089cd5572112be95805bd3b6179bff83c2e16 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:42:43 -0700 Subject: [PATCH 05/11] Fix cross-platform CI build failures --- cpp/apps/CMakeLists.txt | 3 +++ cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index c2276cf338f..838f7f9921f 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -137,6 +137,9 @@ macro(open3d_add_app_common SRC_DIR APP_NAME TARGET_NAME) set_target_properties(${TARGET_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) if (MSVC) target_link_options(${TARGET_NAME} PRIVATE "/ENTRY:mainCRTStartup") + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM") + target_link_options(${TARGET_NAME} PRIVATE + "-Xlinker" "/ENTRY:mainCRTStartup") endif() else() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../${APP_NAME}") diff --git a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp index 870717ef132..98ef22ff429 100644 --- a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp +++ b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp @@ -230,7 +230,7 @@ bool SteerNextGLContextToAdapter(const GpuAdapterInfo& info) { // 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, 1); + 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); From 91e1e3d92f9bca0ccd997e0c995618715d0ebf8a Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:39:22 -0700 Subject: [PATCH 06/11] formatting. IntelLLVM support. msix not in main-devel - only for windows store, since msix canot be installed easily without trusted CA signature.msix + public key cer is available in github artifact for testing. --- .github/workflows/windows.yml | 39 ++++++++++++------- cpp/apps/CMakeLists.txt | 2 +- .../Open3DViewer/WindowsMSIX/build_msix.ps1 | 26 ++++++++----- cpp/open3d/visualization/gui/NativeWin32.cpp | 20 +++++----- .../rendering/GpuAdapterSelection.cpp | 31 +++++++-------- .../rendering/filament/FilamentEngine.cpp | 7 ++-- .../GaussianSplatPassRunner.cpp | 8 ++-- .../GaussianSplatVulkanBackend.cpp | 3 +- .../GaussianSplatVulkanInteropContext.cpp | 22 ++++++----- cpp/pybind/t/geometry/pointcloud.cpp | 7 ++-- 10 files changed, 93 insertions(+), 72 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a26c8d28f02..359cf153865 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -272,6 +272,30 @@ jobs: $ErrorActionPreference = 'Stop' cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` --target Open3DViewer + cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` + --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: ${{ env.VIEWER_ZIP_PATH }} + if-no-files-found: error + + - name: Update devel release with viewer + if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + 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' }} @@ -285,13 +309,7 @@ jobs: cmake --build . --parallel ${{ env.NPROC }} --config ${{ matrix.CONFIG }} ` --target Open3DViewerMSIX - - name: Generate MSIX 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: ${{ env.MSIX_PATH }} - - - name: Upload MSIX + - 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: @@ -301,13 +319,6 @@ jobs: ${{ env.BUILD_DIR }}\msix-package\Open3D.cer if-no-files-found: error - - name: Update devel release with viewer - if: ${{ github.ref == 'refs/heads/main' && matrix.BUILD_SHARED_LIBS == 'OFF' && matrix.STATIC_RUNTIME == 'ON' && matrix.device == 'cpu' && matrix.CONFIG == 'Release' }} - env: - GH_TOKEN: ${{ github.token }} - run: | - bash .github/workflows/update_release.sh "${{ env.MSIX_PATH }}" "${{ env.BUILD_DIR }}\msix-package\Open3D.cer" - - name: Run C++ unit tests if: ${{ matrix.device != 'cuda' }} working-directory: ${{ env.BUILD_DIR }} diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index 838f7f9921f..807aa111066 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -137,7 +137,7 @@ macro(open3d_add_app_common SRC_DIR APP_NAME TARGET_NAME) set_target_properties(${TARGET_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) if (MSVC) target_link_options(${TARGET_NAME} PRIVATE "/ENTRY:mainCRTStartup") - elseif (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM") + else # (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM" or "Clang") target_link_options(${TARGET_NAME} PRIVATE "-Xlinker" "/ENTRY:mainCRTStartup") endif() diff --git a/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 index ee532f1a196..43a5cfaeffe 100644 --- a/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 +++ b/cpp/apps/Open3DViewer/WindowsMSIX/build_msix.ps1 @@ -36,18 +36,24 @@ $manifestPath = Join-Path $STAGING "AppxManifest.xml" # Generate self-signed cert whose subject matches Publisher="CN=Open3D". # Export .cer (public key only) so users can install it to trust the package. -winapp cert generate ` - --manifest $manifestPath ` - --output (Join-Path $OutDir "Open3D.pfx") ` - --export-cer ` - --if-exists overwrite - -# Pack and sign the MSIX. $MSIX_NAME = "Open3DViewer-$OPEN3D_VERSION-x64.msix" $MSIX_PATH = Join-Path $OutDir $MSIX_NAME -winapp pack $STAGING ` - --output $MSIX_PATH ` - --cert (Join-Path $OutDir "Open3D.pfx") +$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) { diff --git a/cpp/open3d/visualization/gui/NativeWin32.cpp b/cpp/open3d/visualization/gui/NativeWin32.cpp index c3ddd2813f5..3f7b365c5cc 100644 --- a/cpp/open3d/visualization/gui/NativeWin32.cpp +++ b/cpp/open3d/visualization/gui/NativeWin32.cpp @@ -24,20 +24,20 @@ void SetNativeWindowIcon(GLFWwindow* glfw_window) { HWND window = glfwGetWin32Window(glfw_window); HINSTANCE instance = GetModuleHandle(nullptr); HICON small_icon = static_cast(LoadImage( - instance, "IDI_ICON1", IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), - GetSystemMetrics(SM_CYSMICON), 0)); + 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)); + 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)); + 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)); + reinterpret_cast(large_icon)); + SetClassLongPtr(window, GCLP_HICON, + reinterpret_cast(large_icon)); + SetClassLongPtr(window, GCLP_HICONSM, + reinterpret_cast(small_icon)); } } diff --git a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp index 98ef22ff429..e520e28d8fd 100644 --- a/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp +++ b/cpp/open3d/visualization/rendering/GpuAdapterSelection.cpp @@ -15,9 +15,8 @@ #ifndef NOMINMAX #define NOMINMAX #endif -#include - #include +#include #pragma comment(lib, "dxgi.lib") #endif @@ -38,13 +37,12 @@ #endif #include +#include #include #include #include -#include - #include "open3d/utility/Logging.h" namespace open3d { @@ -74,8 +72,8 @@ bool FindMonitorRectForAdapter(const GpuAdapterInfo& info, RECT* out_rect) { if (!adapter) continue; DXGI_ADAPTER_DESC1 adesc{}; if (SUCCEEDED(adapter->GetDesc1(&adesc)) && - std::memcmp(&adesc.AdapterLuid, info.luid, - sizeof(info.luid)) == 0) { + std::memcmp(&adesc.AdapterLuid, info.luid, sizeof(info.luid)) == + 0) { IDXGIOutput* output = nullptr; if (adapter->EnumOutputs(0, &output) != DXGI_ERROR_NOT_FOUND && output) { @@ -99,11 +97,10 @@ bool FindMonitorRectForAdapter(const GpuAdapterInfo& info, RECT* out_rect) { GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { GpuAdapterInfo info; vk::PhysicalDevice pd(physical_device); - const auto chain = - pd.getProperties2(); + const auto chain = pd.getProperties2(); info.device_name = chain.get() - .properties.deviceName.data(); + .properties.deviceName.data(); const auto& id = chain.get(); if (id.deviceLUIDValid) { std::memcpy(info.luid, id.deviceLUID.data(), sizeof(info.luid)); @@ -203,8 +200,7 @@ GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { const auto chain = pd.getProperties2(); - const auto& pci = - chain.get(); + const auto& pci = chain.get(); info.pci_domain = pci.pciDomain; info.pci_bus = pci.pciBus; info.pci_device = pci.pciDevice; @@ -213,8 +209,9 @@ GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { const auto driver_chain = pd.getProperties2(); - info.is_nvidia = driver_chain.get() - .driverID == vk::DriverId::eNvidiaProprietary; + info.is_nvidia = + driver_chain.get().driverID == + vk::DriverId::eNvidiaProprietary; info.valid = true; return info; } @@ -222,9 +219,9 @@ GpuAdapterInfo GetAdapterInfo(VkPhysicalDevice physical_device) { 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); + 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 diff --git a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp index 38b49053558..1e8781e2453 100644 --- a/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp +++ b/cpp/open3d/visualization/rendering/filament/FilamentEngine.cpp @@ -163,8 +163,8 @@ EngineInstance::EngineInstance() { // 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()); + const GpuAdapterInfo gl_actual = + GetAdapterInfoForWindow(gl_ctx.GetNativeWindowHandle()); if (gl_actual.valid && !SameAdapter(gl_actual, vk_adapter_info)) { utility::LogWarning( @@ -176,7 +176,8 @@ EngineInstance::EngineInstance() { if (!vk_ctx.Initialize(&gl_actual)) { utility::LogWarning( "EngineInstance: Vulkan reinit to match GL " - "adapter failed: {}", vk_ctx.GetLastError()); + "adapter failed: {}", + vk_ctx.GetLastError()); } } } diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp index d10b6f4cb0d..9017fb1a020 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatPassRunner.cpp @@ -194,9 +194,11 @@ bool RunGaussianGeometryPasses( // 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); + // 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"); } diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp index 2bf70bedeb5..6c79be47cb8 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanBackend.cpp @@ -180,7 +180,8 @@ class GaussianSplatVulkanBackend final : public GaussianSplatRenderer::Backend { resource_mgr.CreateRenderTarget(view_color, targets.depth); } // Disable MSAA before binding the render target: Filament validates - // MSAA/sampleable-depth compatibility inside SetRenderTarget() (see #7495). + // MSAA/sampleable-depth compatibility inside SetRenderTarget() (see + // #7495). auto* native = view.GetNativeView(); auto msaa = native->getMultiSampleAntiAliasingOptions(); msaa.enabled = false; diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp index a4a6df38158..c007f92b35e 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp @@ -38,14 +38,13 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #include +#include #include #include #include #include -#include - #include "open3d/utility/Logging.h" // Initialize VMA in this single translation unit. @@ -123,7 +122,8 @@ bool CheckExtensions(const std::vector& available, constexpr int kDeviceRejected = std::numeric_limits::min(); /// Score a physical device for interop suitability. Higher is better. -/// Native discrete > native integrated > other native > D3D12 translation > CPU. +/// 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. @@ -152,11 +152,11 @@ int ScoreDevice(const vk::raii::PhysicalDevice& dev) { 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; + 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; @@ -357,7 +357,7 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( if (required_adapter && required_adapter->valid && !SameAdapter(GetAdapterInfo(static_cast( *devices[i])), - *required_adapter)) { + *required_adapter)) { continue; // not the required adapter; skip } if (score > best_score) { @@ -372,7 +372,9 @@ bool GaussianSplatVulkanInteropContext::SelectPhysicalDevice( ? "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; + "interop extensions. Required " + "extensions:" + " " VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME; utility::LogWarning("GaussianSplat Vulkan: {}", last_error_); return false; } diff --git a/cpp/pybind/t/geometry/pointcloud.cpp b/cpp/pybind/t/geometry/pointcloud.cpp index 9f3a045a693..c2afb26bb3b 100644 --- a/cpp/pybind/t/geometry/pointcloud.cpp +++ b/cpp/pybind/t/geometry/pointcloud.cpp @@ -261,9 +261,10 @@ void pybind_pointcloud_definitions(py::module& m) { "non-negative number less than number of points in the " "input pointcloud.", "start_index"_a = 0); - pointcloud.def("remove_radius_outliers", &PointCloud::RemoveRadiusOutliers, - "nb_points"_a, "search_radius"_a, - R"(Remove points that have less than nb_points neighbors in a + pointcloud.def( + "remove_radius_outliers", &PointCloud::RemoveRadiusOutliers, + "nb_points"_a, "search_radius"_a, + R"(Remove points that have less than nb_points neighbors in a sphere of a given search radius. Args: From a6bf9312aa324ede3dd2c9536c8570b4f9822a44 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:38:45 -0700 Subject: [PATCH 07/11] Fix CMake Windows compiler conditional --- cpp/apps/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index 807aa111066..554078eadef 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -137,7 +137,8 @@ macro(open3d_add_app_common SRC_DIR APP_NAME TARGET_NAME) set_target_properties(${TARGET_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) if (MSVC) target_link_options(${TARGET_NAME} PRIVATE "/ENTRY:mainCRTStartup") - else # (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM" or "Clang") + else() + # IntelLLVM and Clang on Windows use the standard main entry point. target_link_options(${TARGET_NAME} PRIVATE "-Xlinker" "/ENTRY:mainCRTStartup") endif() From 04789ba59f0b93541924c6a118a0d1a1b6812ec9 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey Date: Sat, 15 Aug 2026 23:54:01 -0700 Subject: [PATCH 08/11] Linux multi-gpu fix: PRIME steering before glfwInit(), use doube buffered hidden window. --- .../visualization/gui/GLFWWindowSystem.cpp | 14 ++++++++++++++ .../gaussian_splat/GaussianSplatDesign.md | 5 +++++ .../GaussianSplatOpenGLContext.cpp | 5 ++++- .../visualization/gaussian_splatting.ipynb | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp b/cpp/open3d/visualization/gui/GLFWWindowSystem.cpp index bd3dcc944c4..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 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 22a0c97e722..fd7ddfffebc 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp @@ -94,7 +94,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/docs/jupyter/visualization/gaussian_splatting.ipynb b/docs/jupyter/visualization/gaussian_splatting.ipynb index f5795cef1f2..1c166ea84e3 100644 --- a/docs/jupyter/visualization/gaussian_splatting.ipynb +++ b/docs/jupyter/visualization/gaussian_splatting.ipynb @@ -87,6 +87,22 @@ "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", + "\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, From e14a1cd8c5ba5e6ab4b40d459392edda1cc84fdd Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:27:21 -0700 Subject: [PATCH 09/11] Fix IntelLLVM Windows linker options --- cpp/apps/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/apps/CMakeLists.txt b/cpp/apps/CMakeLists.txt index 554078eadef..6ff0f87b12e 100644 --- a/cpp/apps/CMakeLists.txt +++ b/cpp/apps/CMakeLists.txt @@ -135,10 +135,12 @@ macro(open3d_add_app_common SRC_DIR APP_NAME TARGET_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 (MSVC) + 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() - # IntelLLVM and Clang on Windows use the standard main entry point. target_link_options(${TARGET_NAME} PRIVATE "-Xlinker" "/ENTRY:mainCRTStartup") endif() From 2c83d75445ce657a0e83c119d22a4445427998ed Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:28:31 -0700 Subject: [PATCH 10/11] address copilot review --- cpp/open3d/visualization/app/Viewer.cpp | 2 -- cpp/open3d/visualization/gui/NativeWin32.cpp | 3 +++ .../rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp | 1 - .../gaussian_splat/GaussianSplatVulkanInteropContext.cpp | 1 - docs/jupyter/visualization/gaussian_splatting.ipynb | 3 +++ 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cpp/open3d/visualization/app/Viewer.cpp b/cpp/open3d/visualization/app/Viewer.cpp index 84684b68c00..c4b965bbb9c 100644 --- a/cpp/open3d/visualization/app/Viewer.cpp +++ b/cpp/open3d/visualization/app/Viewer.cpp @@ -24,8 +24,6 @@ static const int width = 1280; static const int height = 960; void RunViewer(int argc, const char *argv[]) { - utility::Logger::GetInstance().SetVerbosityLevel( - utility::VerbosityLevel::Debug); std::function print_fcn = utility::Logger::GetInstance().GetPrintFunction(); utility::Logger::GetInstance().ResetPrintFunction(); diff --git a/cpp/open3d/visualization/gui/NativeWin32.cpp b/cpp/open3d/visualization/gui/NativeWin32.cpp index 3f7b365c5cc..2f5571cf4b2 100644 --- a/cpp/open3d/visualization/gui/NativeWin32.cpp +++ b/cpp/open3d/visualization/gui/NativeWin32.cpp @@ -22,6 +22,9 @@ void* GetNativeDrawable(GLFWwindow* 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), diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp index fd7ddfffebc..b5fb63465d4 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatOpenGLContext.cpp @@ -20,7 +20,6 @@ #if !defined(__APPLE__) -#include #include #include diff --git a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp index c007f92b35e..476d9146ce6 100644 --- a/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp +++ b/cpp/open3d/visualization/rendering/gaussian_splat/GaussianSplatVulkanInteropContext.cpp @@ -38,7 +38,6 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #include -#include #include #include diff --git a/docs/jupyter/visualization/gaussian_splatting.ipynb b/docs/jupyter/visualization/gaussian_splatting.ipynb index 1c166ea84e3..d617fe9e118 100644 --- a/docs/jupyter/visualization/gaussian_splatting.ipynb +++ b/docs/jupyter/visualization/gaussian_splatting.ipynb @@ -98,6 +98,9 @@ "\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." From 089e99cba2f98e599afe91fee0769c69c56f0bc2 Mon Sep 17 00:00:00 2001 From: Sameer Sheorey Date: Tue, 18 Aug 2026 23:50:12 -0700 Subject: [PATCH 11/11] style --- cpp/pybind/t/geometry/pointcloud.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cpp/pybind/t/geometry/pointcloud.cpp b/cpp/pybind/t/geometry/pointcloud.cpp index c2afb26bb3b..9f3a045a693 100644 --- a/cpp/pybind/t/geometry/pointcloud.cpp +++ b/cpp/pybind/t/geometry/pointcloud.cpp @@ -261,10 +261,9 @@ void pybind_pointcloud_definitions(py::module& m) { "non-negative number less than number of points in the " "input pointcloud.", "start_index"_a = 0); - pointcloud.def( - "remove_radius_outliers", &PointCloud::RemoveRadiusOutliers, - "nb_points"_a, "search_radius"_a, - R"(Remove points that have less than nb_points neighbors in a + pointcloud.def("remove_radius_outliers", &PointCloud::RemoveRadiusOutliers, + "nb_points"_a, "search_radius"_a, + R"(Remove points that have less than nb_points neighbors in a sphere of a given search radius. Args: