From 37d8a938392f052dd79fc74369fe6abea3402fed Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 16:49:31 +0200 Subject: [PATCH 1/3] point-cloud-utils: add patches for riscv64 build --- ...v64-like-arm64-aarch64-for-the-SSE-v.patch | 34 ++ ...cmake-keep-geogram-s-HLBFGS-disabled.patch | 34 ++ ...mbree-backed-ray-intersection-bindin.patch | 486 ++++++++++++++++++ 3 files changed, 554 insertions(+) create mode 100644 patches/point-cloud-utils/0.34.0/0001-cmake-treat-riscv64-like-arm64-aarch64-for-the-SSE-v.patch create mode 100644 patches/point-cloud-utils/0.34.0/0002-cmake-keep-geogram-s-HLBFGS-disabled.patch create mode 100644 patches/point-cloud-utils/0.34.0/0003-build-drop-the-Embree-backed-ray-intersection-bindin.patch diff --git a/patches/point-cloud-utils/0.34.0/0001-cmake-treat-riscv64-like-arm64-aarch64-for-the-SSE-v.patch b/patches/point-cloud-utils/0.34.0/0001-cmake-treat-riscv64-like-arm64-aarch64-for-the-SSE-v.patch new file mode 100644 index 000000000..3e63103b6 --- /dev/null +++ b/patches/point-cloud-utils/0.34.0/0001-cmake-treat-riscv64-like-arm64-aarch64-for-the-SSE-v.patch @@ -0,0 +1,34 @@ +From bbddcc6e9ef3ff669010b28a7812b0c11409f5e0 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 16:46:36 +0200 +Subject: [PATCH 1/3] cmake: treat riscv64 like arm64/aarch64 for the + SSE-via-simde path + +igl's math code hardcodes SSE intrinsics; this project already redirects +those to the platform's native SIMD through simde on any non-x86 +CMAKE_SYSTEM_PROCESSOR (used today for the upstream macOS arm64 CI +job). riscv64 has no SSE either, and the old regex left it falling +through to the x86 branch, which then passes -msse3 to a riscv64 +compiler. + +Upstream-Status: Inappropriate [riscv64-only build config; every architecture upstream actually builds for is already covered by NATIVE_SSE's two existing branches] +--- + CMakeLists.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index f0a7953..2f989b7 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -7,7 +7,7 @@ option(GEOGRAM_WITH_HLBFGS "Non-linear solver (Yang Liu's HLBFGS)" O + # option(NPE_WITH_EIGEN "Whether to use the bundled version of Eigen" ON) + option(EIGEN_WITH_MKL "Whether to build Eigen with intel MKL or not" OFF) + +-if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "(arm64)|(ARM64)|(aarch64)|(AARCH64)") ++if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "(arm64)|(ARM64)|(aarch64)|(AARCH64)|(riscv64)") + set(NOT_USING_ARM OFF) + else() + set(NOT_USING_ARM ON) +-- +2.50.1 (Apple Git-155) + diff --git a/patches/point-cloud-utils/0.34.0/0002-cmake-keep-geogram-s-HLBFGS-disabled.patch b/patches/point-cloud-utils/0.34.0/0002-cmake-keep-geogram-s-HLBFGS-disabled.patch new file mode 100644 index 000000000..20fc9b9f4 --- /dev/null +++ b/patches/point-cloud-utils/0.34.0/0002-cmake-keep-geogram-s-HLBFGS-disabled.patch @@ -0,0 +1,34 @@ +From 80a32eeb8380ce7dab804e61bc5adf0d8031181c Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 16:46:47 +0200 +Subject: [PATCH 2/3] cmake: keep geogram's HLBFGS disabled + +geogram's HLBFGS/README.txt: "freely available for non-commercial +purposes" -- not an OSI-approved licence, so it cannot go into a wheel +RISE redistributes publicly. None of point-cloud-utils' own bindings +(grep for hlbfgs/bfgs/lscm across src/) call into geogram's optimizer, +so turning it off changes nothing observable; upstream's own default +already builds it, so this is a riscv64-only narrowing, not a fix +upstream needs. + +Upstream-Status: Inappropriate [avoids redistributing a non-commercial-only licensed dependency in a publicly-published wheel; unused by point-cloud-utils' own bindings] +--- + CMakeLists.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 2f989b7..1078118 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -3,7 +3,7 @@ project(point-cloud-utils) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_BUILD_TYPE "Release") + +-option(GEOGRAM_WITH_HLBFGS "Non-linear solver (Yang Liu's HLBFGS)" ON) ++option(GEOGRAM_WITH_HLBFGS "Non-linear solver (Yang Liu's HLBFGS)" OFF) + # option(NPE_WITH_EIGEN "Whether to use the bundled version of Eigen" ON) + option(EIGEN_WITH_MKL "Whether to build Eigen with intel MKL or not" OFF) + +-- +2.50.1 (Apple Git-155) + diff --git a/patches/point-cloud-utils/0.34.0/0003-build-drop-the-Embree-backed-ray-intersection-bindin.patch b/patches/point-cloud-utils/0.34.0/0003-build-drop-the-Embree-backed-ray-intersection-bindin.patch new file mode 100644 index 000000000..d68ae4532 --- /dev/null +++ b/patches/point-cloud-utils/0.34.0/0003-build-drop-the-Embree-backed-ray-intersection-bindin.patch @@ -0,0 +1,486 @@ +From 21c9d4e8bad75724335195c63adf784cc098a36f Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 16:48:28 +0200 +Subject: [PATCH 3/3] build: drop the Embree-backed ray-intersection bindings + +Embree's CMake ISA detection (EMBREE_MAX_ISA=DEFAULT) only recognizes +x86 (SSE2..AVX512) and ARM (NEON/NEON2X) CMAKE_SYSTEM_PROCESSOR +values; riscv64 has no Embree ISA at all (same finding as the riscv64 +libigl port, which carries the identical CMakeLists.txt shape and +drops Embree via LIBIGL_EMBREE=OFF). Unlike libigl, Embree isn't behind +a CMake option here -- it's unconditionally download_dep()'d and +target_link_libraries()'d into _pcu_internal -- so removing it means +removing every binding that calls into it: + +- src/ray_mesh_intersection.cpp (ray_mesh_intersection, + _RayMeshIntersectorInternal, and the RayMeshIntersector class' + internal helpers) is deleted outright; nothing else in the tree + references it. +- src/ray_point_cloud_intersection.cpp keeps + point_cloud_splatting_geometry_internal_ (pure geometry generation, + no Embree, and imported at module scope by + point_cloud_utils/_point_cloud_geometry.py so the package must keep + importing cleanly) and drops the three Embree-backed functions plus + the now-unused trace_rays_point_cloud/validate_rays helpers. +- point_cloud_utils/__init__.py drops the top-level + ray_mesh_intersection import (that symbol no longer exists in + _pcu_internal). RayMeshIntersector/RaySurfelIntersector/ + ray_surfel_intersection stay importable -- their _pcu_internal + lookups are already lazy (inside __init__/call bodies), so only + calling them now raises ImportError naming the missing symbol, + matching how the rest of the package behaves when a feature isn't + built in. + +Ray-mesh/ray-surfel intersection is unavailable in the riscv64 wheel; +everything else (sampling, normals, marching cubes, decimation, mesh +IO, signed distance, KNN, Hausdorff/Chamfer distance, morton codes, +voxelization, connected components, curvature, smoothing, winding +numbers, Lloyd relaxation, adjacency lists, splat geometry) is +unaffected. + +Upstream-Status: Inappropriate [riscv64-only build config; Embree works fine for every architecture upstream actually builds for (x86_64/aarch64/macOS/Windows), this narrows only the riscv64 wheel] +--- + CMakeLists.txt | 26 ---- + point_cloud_utils/__init__.py | 2 +- + src/ray_mesh_intersection.cpp | 177 --------------------------- + src/ray_point_cloud_intersection.cpp | 147 ---------------------- + 4 files changed, 1 insertion(+), 351 deletions(-) + delete mode 100644 src/ray_mesh_intersection.cpp + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 1078118..3d050ad 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -109,27 +109,6 @@ endif() + # target_compile_definitions(Eigen3::Eigen INTERFACE EIGEN_USE_MKL_ALL) + #endif(${EIGEN_WITH_MKL}) + +-# Embree +-set(EMBREE_DIR "${EXTERNAL_DEP_DIR}/embree") +-if(NOT TARGET embree) +- download_dep(embree +- GIT_REPOSITORY https://github.com/embree/embree.git +- GIT_TAG v4.3.3 +- ) +- +- # Note: On macOS, building embree as a static lib can only be done with a single ISA target. +- set(EMBREE_MAX_ISA "DEFAULT" CACHE STRING "Selects highest ISA to support.") +- set(EMBREE_TESTING_INTENSITY 0 CACHE STRING "") +- set(EMBREE_ISPC_SUPPORT OFF CACHE BOOL " ") +- set(EMBREE_TASKING_SYSTEM "INTERNAL" CACHE BOOL " ") +- set(EMBREE_TUTORIALS OFF CACHE BOOL " ") +- set(EMBREE_STATIC_LIB ON CACHE BOOL " ") +- if(MSVC) +- set(EMBREE_STATIC_RUNTIME ON CACHE BOOL "Use the static version of the C/C++ runtime library.") +- endif() +- +- add_subdirectory("${EMBREE_DIR}" "embree" EXCLUDE_FROM_ALL) +-endif() + #compile_igl_module("embree") + #target_link_libraries(igl_embree INTERFACE embree) + #target_include_directories(igl_embree INTERFACE ${EMBREE_DIR}/include) +@@ -172,7 +151,6 @@ set(PCU_BINDING_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/src/signed_distance.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/closest_point_on_mesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/connected_components.cpp +- ${CMAKE_CURRENT_SOURCE_DIR}/src/ray_mesh_intersection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ray_point_cloud_intersection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/smooth.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/manifold.cpp +@@ -199,13 +177,11 @@ npe_add_module(_pcu_internal + ${PCU_BINDING_SOURCES} + EXTRA_MODULE_FUNCTIONS + hack_extra_bindings +- hack_extra_ray_mesh_bindings + ) + + # TODO: Make common into its own library that we link statically + target_sources(_pcu_internal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/common/morton_code.cpp) + target_sources(_pcu_internal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/common/geogram_utils.cpp) +-target_sources(_pcu_internal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/common/embree_intersector.cpp) + # target_sources(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external/tinyply/source/tinyply.cpp) + target_include_directories(_pcu_internal PRIVATE ${EXTERNAL_DEP_DIR}/tinyply/source) + target_include_directories(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +@@ -213,8 +189,6 @@ target_include_directories(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ext + target_include_directories(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external) + target_include_directories(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external/igl/include) + target_include_directories(_pcu_internal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external/tinyobjloader) +-target_include_directories(_pcu_internal PRIVATE ${EMBREE_DIR}/include) +-target_link_libraries(_pcu_internal PRIVATE embree) + target_link_libraries(_pcu_internal PRIVATE geogram) + target_link_libraries(_pcu_internal PRIVATE manifold) + # target_link_libraries(_pcu_internal PRIVATE mls_utils) +diff --git a/point_cloud_utils/__init__.py b/point_cloud_utils/__init__.py +index 3838b4c..8e93d6b 100644 +--- a/point_cloud_utils/__init__.py ++++ b/point_cloud_utils/__init__.py +@@ -8,7 +8,7 @@ from ._pcu_internal import sample_mesh_poisson_disk, sample_mesh_random, \ + morton_encode, morton_decode, morton_knn, \ + lloyd_2d, lloyd_3d, voronoi_centroids_unit_cube, sample_mesh_lloyd, \ + deduplicate_point_cloud, deduplicate_mesh_vertices, signed_distance_to_mesh, \ +- closest_points_on_mesh, connected_components, ray_mesh_intersection, laplacian_smooth_mesh, \ ++ closest_points_on_mesh, connected_components, laplacian_smooth_mesh, \ + make_mesh_watertight, \ + morton_add, morton_subtract, point_cloud_fast_winding_number, \ + sparse_voxel_grid_boundary, marching_cubes_sparse_voxel_grid, decimate_triangle_mesh, \ +diff --git a/src/ray_mesh_intersection.cpp b/src/ray_mesh_intersection.cpp +deleted file mode 100644 +index 2d2065f..0000000 +--- a/src/ray_mesh_intersection.cpp ++++ /dev/null +@@ -1,177 +0,0 @@ +-#include +-#include +-#include +-#include +- +-#include "common/common.h" +- +-namespace py = pybind11; +- +-using Intersector = EmbreeIntersector; +-void hack_extra_ray_mesh_bindings(pybind11::module& m) { +- py::class_>(m, "_RayMeshIntersectorInternal") +- .def(py::init([]() { +- return std::shared_ptr(new EmbreeIntersector()); +- })); +-} +- +- +-npe_function(_populate_ray_intersector_internal) +-npe_arg(v, dense_float, dense_double) +-npe_arg(f, dense_int32, dense_int64, dense_uint32, dense_uint64) +-npe_arg(isector, std::shared_ptr) +-npe_begin_code() +- EmbreeIntersector::PointMatrixType v_copy = v.template cast(); +- EmbreeIntersector::FaceMatrixType f_copy = f.template cast(); +- isector->init(v_copy, f_copy, true /*is_static*/); +-npe_end_code() +- +- +-npe_function(_intersect_ray_intersector_internal) +-npe_arg(ray_o, dense_float, dense_double) +-npe_arg(ray_d, npe_matches(ray_o)) +-npe_arg(isector, std::shared_ptr) +-npe_default_arg(ray_near, double, 0.0) +-npe_default_arg(ray_far, double, std::numeric_limits::infinity()) +-npe_begin_code() +- bool use_single_ray_origin = false; +- if (ray_o.size() == 3) { +- use_single_ray_origin = true; +- } else { +- if (ray_o.rows() != ray_d.rows()) { +- throw pybind11::value_error("ray_o and ray_d must have the same number of rows (one ray origin per ray direction). " +- "(Note: ray_o can have one row to use the same origin for all directions)"); +- } +- } +- +- if (ray_o.cols() != 3 && !use_single_ray_origin) { +- throw pybind11::value_error("Invalid shape for ray_o must have shape (N, 3) but got (" + +- std::to_string(ray_o.rows()) + ", " + std::to_string(ray_o.cols()) + ")."); +- } +- if (ray_d.cols() != 3) { +- throw pybind11::value_error("Invalid shape for ray_d must have shape (N, 3) but got (" + +- std::to_string(ray_d.rows()) + ", " + std::to_string(ray_d.cols()) + ")."); +- } +- +- EigenDense ret_fid(ray_d.rows(), 1); +- npe_Matrix_ray_o ret_bc(ray_d.rows(), 3); +- npe_Matrix_ray_o ret_t(ray_d.rows(), 1); +- for (int i = 0; i < ray_d.rows(); i++) { +- Eigen::RowVector3f o_i; +- if (use_single_ray_origin) { +- o_i = Eigen::RowVector3f((float)ray_o(0, 0), (float)ray_o(1, 0), (float)ray_o(2, 0)); +- } else { +- o_i = Eigen::RowVector3f((float)ray_o(i, 0), (float)ray_o(i, 1), (float)ray_o(i, 2)); +- } +- Eigen::RowVector3f d_i((float)ray_d(i, 0), (float)ray_d(i, 1), (float)ray_d(i, 2)); +- Hit hit; +- +- bool is_hit = isector->intersectRay(o_i, d_i, hit, ray_near, ray_far); +- if (is_hit) { +- ret_fid(i, 0) = (int) hit.id; +- ret_bc(i, 0) = 1.0 - hit.u - hit.v; +- ret_bc(i, 1) = hit.u; +- ret_bc(i, 2) = hit.v; +- ret_t(i, 0) = hit.t; +- } else { +- ret_fid(i, 0) = -1; +- ret_t(i, 0) = std::numeric_limits::infinity(); +- ret_bc(i, 0) = 0.0; +- ret_bc(i, 1) = 0.0; +- ret_bc(i, 2) = 0.0; +- } +- +- } +- +- return std::make_tuple(npe::move(ret_fid), npe::move(ret_bc), npe::move(ret_t)); +-npe_end_code() +- +- +- +-const char* ray_mesh_intersection_doc = R"Qu8mg5v7( +-Compute intersection between a set of rays and a triangle mesh +- +-Args: +- v : \#v by 3 array of vertex positions (each row is a vertex) +- f : \#f by 3 Matrix of face (triangle) indices +- ray_o : array of shape (#rays, 3) of ray origins (one per row) or a single array of shape (3,) to use +- ray_d : array of shape (#rays, 3) of ray directions (one per row) +- ray_near : an optional floating point value indicating the distance along each ray to start searching (default 0.0) +- ray_far : an optional floating point value indicating the maximum distance along each ray to search (default inf) +- +-Returns: +- f_id : an array of shape (#rays,) representing the face id hit by each ray +- bc : an array of shape (#rays, 3) where each row is the barycentric coordinates within each face of the ray intersection +- t : the distance along each ray to the intersection +-)Qu8mg5v7"; +-npe_function(ray_mesh_intersection) +-npe_arg(v, dense_float, dense_double) +-npe_arg(f, dense_int32, dense_int64, dense_uint32, dense_uint64) +-npe_arg(ray_o, npe_matches(v)) +-npe_arg(ray_d, npe_matches(v)) +-npe_default_arg(ray_near, double, 0.0) +-npe_default_arg(ray_far, double, std::numeric_limits::infinity()) +-npe_doc(ray_mesh_intersection_doc) +-npe_begin_code() +-{ +- bool use_single_ray_origin = false; +- if (ray_o.size() == 3) { +- use_single_ray_origin = true; +- } else { +- if (ray_o.rows() != ray_d.rows()) { +- throw pybind11::value_error("ray_o and ray_d must have the same number of rows (one ray origin per ray direction). " +- "(Note: ray_o can have one row to use the same origin for all directions)"); +- } +- } +- +- if (ray_o.cols() != 3 && !use_single_ray_origin) { +- throw pybind11::value_error("Invalid shape for ray_o must have shape (N, 3) but got (" + +- std::to_string(ray_o.rows()) + ", " + std::to_string(ray_o.cols()) + ")."); +- } +- if (ray_d.cols() != 3) { +- throw pybind11::value_error("Invalid shape for ray_d must have shape (N, 3) but got (" + +- std::to_string(ray_d.rows()) + ", " + std::to_string(ray_d.cols()) + ")."); +- } +- validate_mesh(v, f); +- +- +- EmbreeIntersector isector; +- +- EmbreeIntersector::PointMatrixType v_copy = v.template cast(); +- EmbreeIntersector::FaceMatrixType f_copy = f.template cast(); +- isector.init(v_copy, f_copy, true /*is_static*/); +- +- npe_Matrix_f ret_fid(ray_d.rows(), 1); +- npe_Matrix_v ret_bc(ray_d.rows(), 3); +- npe_Matrix_v ret_t(ray_d.rows(), 1); +- for (int i = 0; i < ray_d.rows(); i++) { +- Eigen::RowVector3f o_i; +- if (use_single_ray_origin) { +- o_i = Eigen::RowVector3f((float)ray_o(0, 0), (float)ray_o(1, 0), (float)ray_o(2, 0)); +- } else { +- o_i = Eigen::RowVector3f((float)ray_o(i, 0), (float)ray_o(i, 1), (float)ray_o(i, 2)); +- } +- Eigen::RowVector3f d_i((float)ray_d(i, 0), (float)ray_d(i, 1), (float)ray_d(i, 2)); +- Hit hit; +- +- bool is_hit = isector.intersectRay(o_i, d_i, hit, ray_near, ray_far); +- if (is_hit) { +- ret_fid(i, 0) = (npe_Scalar_f) hit.id; +- ret_bc(i, 0) = 1.0 - hit.u - hit.v; +- ret_bc(i, 1) = hit.u; +- ret_bc(i, 2) = hit.v; +- ret_t(i, 0) = hit.t; +- } else { +- ret_fid(i, 0) = -1; +- ret_t(i, 0) = std::numeric_limits::infinity(); +- ret_bc(i, 0) = 0.0; +- ret_bc(i, 1) = 0.0; +- ret_bc(i, 2) = 0.0; +- } +- +- } +- +- return std::make_tuple(npe::move(ret_fid), npe::move(ret_bc), npe::move(ret_t)); +- +-} +-npe_end_code() +diff --git a/src/ray_point_cloud_intersection.cpp b/src/ray_point_cloud_intersection.cpp +index b9fff2e..1ea9d1e 100644 +--- a/src/ray_point_cloud_intersection.cpp ++++ b/src/ray_point_cloud_intersection.cpp +@@ -1,5 +1,4 @@ + #include +-#include + #include + #include + +@@ -24,28 +23,6 @@ namespace { + } + } + +- template +- bool validate_rays(T1 ray_o, T2 ray_d) { +- bool use_single_ray_origin = false; +- if (ray_o.size() == 3) { +- use_single_ray_origin = true; +- } else { +- if (ray_o.rows() != ray_d.rows()) { +- throw pybind11::value_error("ray_o and ray_d must have the same number of rows (one ray origin per ray direction). " +- "(Note: ray_o can have one row to use the same origin for all directions)"); +- } +- } +- if (ray_o.cols() != 3 && !use_single_ray_origin) { +- throw pybind11::value_error("Invalid shape for ray_o must have shape (N, 3) but got (" + +- std::to_string(ray_o.rows()) + ", " + std::to_string(ray_o.cols()) + ")."); +- } +- if (ray_d.cols() != 3) { +- throw pybind11::value_error("Invalid shape for ray_d must have shape (N, 3) but got (" + +- std::to_string(ray_d.rows()) + ", " + std::to_string(ray_d.cols()) + ")."); +- } +- return use_single_ray_origin; +- } +- + template + GeometryType validate_point_geometry(T1 v, T2 n, T3 geometry_radius, int geometry_subdivisions_1, int geometry_subdivisions_2, std::string geometry_type) { + validate_point_cloud_normals(v, n); +@@ -236,87 +213,7 @@ namespace { + return num_faces_per_geom; + } + +- template +- void trace_rays_point_cloud(const TRO& ray_o, const TRD& ray_d, double ray_near, double ray_far, +- bool use_single_ray_origin, +- int num_faces_per_geometry, +- const EmbreeIntersector& isector, +- Eigen::Matrix& out_t, +- Eigen::Matrix& out_pid) { +- for (int i = 0; i < ray_d.rows(); i++) { +- Eigen::RowVector3f o_i; +- if (use_single_ray_origin) { +- o_i = Eigen::RowVector3f((float) ray_o(0, 0), (float) ray_o(1, 0), (float) ray_o(2, 0)); +- } else { +- o_i = Eigen::RowVector3f((float) ray_o(i, 0), (float) ray_o(i, 1), (float) ray_o(i, 2)); +- } +- Eigen::RowVector3f d_i((float) ray_d(i, 0), (float) ray_d(i, 1), (float) ray_d(i, 2)); +- Hit hit; +- +- bool is_hit = isector.intersectRay(o_i, d_i, hit, ray_near, ray_far); +- if (is_hit) { +- out_pid(i, 0) = (int) hit.id / num_faces_per_geometry; +- out_t(i, 0) = (typename TRO::Scalar) hit.t; +- } else { +- out_pid(i, 0) = -1; +- out_t(i, 0) = std::numeric_limits::infinity(); +- } +- } +- } +-} +- +-npe_function(_populate_ray_point_cloud_intersector_internal) +-npe_arg(v, dense_float, dense_double) +-npe_arg(n, npe_matches(v)) +-npe_arg(geometry_type, std::string) +-npe_arg(geometry_radius, npe_matches(v)) +-npe_default_arg(geometry_subdivisions_1, int, 4) +-npe_default_arg(geometry_subdivisions_2, int, 4) +-npe_arg(isector, std::shared_ptr) +-npe_begin_code() +- using MatrixI = Eigen::Matrix; +- using MatrixF = Eigen::Matrix; +- +- GeometryType geom_type = validate_point_geometry(v, n, geometry_radius, +- geometry_subdivisions_1, +- geometry_subdivisions_2, +- geometry_type); +- MatrixF geom_vertices; +- MatrixI geom_faces; +- +- int num_faces_per_geometry = generate_splat_geometry(geom_type, geometry_subdivisions_1, geometry_subdivisions_2, +- v, n, geometry_radius, geom_vertices, geom_faces); +- isector->init(geom_vertices, geom_faces, true /*is_static*/); +- +- return num_faces_per_geometry; +-npe_end_code() +- +- +-npe_function(_intersect_ray_point_cloud_intersector_internal) +-npe_arg(ray_o, dense_float, dense_double) +-npe_arg(ray_d, npe_matches(ray_o)) +-npe_arg(isector, std::shared_ptr) +-npe_arg(num_faces_per_geometry, int) +-npe_default_arg(ray_near, double, 0.0) +-npe_default_arg(ray_far, double, std::numeric_limits::infinity()) +-npe_begin_code() +-{ +- using MatrixI = Eigen::Matrix; +- using MatrixFOut = Eigen::Matrix; +- +- bool use_single_ray_origin = validate_rays(ray_o, ray_d); +- +- MatrixI ret_pid(ray_d.rows(), 1); +- MatrixFOut ret_t(ray_d.rows(), 1); +- +- trace_rays_point_cloud(ray_o, ray_d, ray_near, ray_far, +- use_single_ray_origin, num_faces_per_geometry, +- *isector, ret_t, ret_pid); +- +- return std::make_tuple(npe::move(ret_pid), npe::move(ret_t)); + } +-npe_end_code() +- + + npe_function(point_cloud_splatting_geometry_internal_) + npe_arg(v, dense_float, dense_double) +@@ -338,47 +235,3 @@ npe_begin_code() + return std::make_tuple(npe::move(geom_vertices), npe::move(geom_faces)); + } + npe_end_code() +- +- +-npe_function(ray_point_cloud_intersection_internal_) +-npe_arg(v, dense_float, dense_double) +-npe_arg(n, npe_matches(v)) +-npe_arg(ray_o, npe_matches(v)) +-npe_arg(ray_d, npe_matches(v)) +-npe_arg(geometry_type, std::string) +-npe_arg(geometry_radius, npe_matches(v)) +-npe_default_arg(geometry_subdivisions_1, int, 4) +-npe_default_arg(geometry_subdivisions_2, int, 4) +-npe_default_arg(ray_near, double, 0.0) +-npe_default_arg(ray_far, double, std::numeric_limits::infinity()) +-npe_begin_code() +- { +- using MatrixI = Eigen::Matrix; +- using MatrixF = Eigen::Matrix; +- using MatrixFOut = Eigen::Matrix; +- +- bool use_single_ray_origin = validate_rays(ray_o, ray_d); +- GeometryType geom_type = validate_point_geometry(v, n, geometry_radius, +- geometry_subdivisions_1, +- geometry_subdivisions_2, +- geometry_type); +- +- MatrixF geom_vertices; +- MatrixI geom_faces; +- +- int num_faces_per_geometry = generate_splat_geometry(geom_type, geometry_subdivisions_1, geometry_subdivisions_2, +- v, n, geometry_radius, geom_vertices, geom_faces); +- +- EmbreeIntersector isector; +- isector.init(geom_vertices, geom_faces, true /*is_static*/); +- +- MatrixI ret_pid(ray_d.rows(), 1); +- MatrixFOut ret_t(ray_d.rows(), 1); +- +- trace_rays_point_cloud(ray_o, ray_d, ray_near, ray_far, +- use_single_ray_origin, num_faces_per_geometry, +- isector, ret_t, ret_pid); +- +- return std::make_tuple(npe::move(ret_pid), npe::move(ret_t)); +-} +-npe_end_code() +-- +2.50.1 (Apple Git-155) + From aff063dec8baa16a58ae1a18f6aa459eaa15c014 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 18:34:15 +0200 Subject: [PATCH 2/3] point-cloud-utils: add build-point-cloud-utils.yml for riscv64 wheels --- .github/workflows/build-point-cloud-utils.yml | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 .github/workflows/build-point-cloud-utils.yml diff --git a/.github/workflows/build-point-cloud-utils.yml b/.github/workflows/build-point-cloud-utils.yml new file mode 100644 index 000000000..38e07846a --- /dev/null +++ b/.github/workflows/build-point-cloud-utils.yml @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build_wheels` job of +# https://github.com/fwilliams/point-cloud-utils/blob/v0.34/.github/workflows/build-wheels-and-publish-to-pipy.yml +name: Build point-cloud-utils wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'point-cloud-utils git tag to build (e.g. v0.34)' + required: true + default: 'v0.34' + pull_request: + paths: + - '.github/workflows/build-point-cloud-utils.yml' + - 'patches/point-cloud-utils/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || 'v0.34' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + POINT_CLOUD_UTILS_TAG: ${{ inputs.version || 'v0.34' }} + # v0.34's setup.py hardcodes version="0.34.0"; the git tag itself doesn't + # carry the patch component. + POINT_CLOUD_UTILS_VERSION: '0.34.0' + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build point-cloud-utils ${{ inputs.version || 'v0.34' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + python: [cp312, cp313, cp314, cp314t] + + steps: + - name: Checkout point-cloud-utils ${{ env.POINT_CLOUD_UTILS_TAG }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: fwilliams/point-cloud-utils + ref: ${{ env.POINT_CLOUD_UTILS_TAG }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply python-wheels/patches/point-cloud-utils/${{ env.POINT_CLOUD_UTILS_VERSION }}/*.patch + + # numpyeigen, igl, tinyply, tinyobjloader, simde, manifold and geogram + # (plus eigen and pybind11, which numpyeigen itself pulls in) are + # download_dep()'d by CMake at build time, not vendored as submodules, + # so their own licences aren't in this checkout; stage each at the + # root for setuptools' default LICEN[CS]E*/COPYING* wheel.license-files + # glob to pick up. Embree is no longer one of them (see the patches). + - name: Stage the vendored dependencies' licences + run: | + set -euo pipefail + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/numpyeigen/master/LICENSE" -o LICENSE.numpyeigen + { + curl -fsSL --retry 5 "https://gitlab.com/libeigen/eigen/-/raw/3.4.0/COPYING.README" + curl -fsSL --retry 5 "https://gitlab.com/libeigen/eigen/-/raw/3.4.0/COPYING.MPL2" + curl -fsSL --retry 5 "https://gitlab.com/libeigen/eigen/-/raw/3.4.0/COPYING.BSD" + curl -fsSL --retry 5 "https://gitlab.com/libeigen/eigen/-/raw/3.4.0/COPYING.LGPL" + } > LICENSE.eigen + curl -fsSL --retry 5 "https://raw.githubusercontent.com/pybind/pybind11/v2.13.6/LICENSE" -o LICENSE.pybind11 + curl -fsSL --retry 5 "https://raw.githubusercontent.com/libigl/libigl/6868413/LICENSE.MPL2" -o LICENSE.igl + # tinyply carries no LICENSE file; tinyply.h's own header dedicates + # it to the public domain. + curl -fsSL --retry 5 "https://raw.githubusercontent.com/ddiakopoulos/tinyply/2.4/source/tinyply.h" | sed -n '1,16p' > LICENSE.tinyply + curl -fsSL --retry 5 "https://raw.githubusercontent.com/tinyobjloader/tinyobjloader/release/LICENSE" -o LICENSE.tinyobjloader + curl -fsSL --retry 5 "https://raw.githubusercontent.com/simd-everywhere/simde/dd0b662fd8cf4b1617dbbb4d08aa053e512b08e4/COPYING" -o COPYING.simde + curl -fsSL --retry 5 "https://raw.githubusercontent.com/AppledoreM/Manifold/28e335f8bfde0721f39fc439e362727c6ad47987/LICENSE.txt" -o LICENSE.manifold + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/LICENSE" -o LICENSE.geogram + # geogram's own third_party/{LM7,rply,zlib,gzstream,PoissonRecon} + # are compiled in unconditionally (aux_source_directories() outside + # any GEOGRAM_WITH_* option); HLBFGS is excluded by the patches. + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/src/lib/geogram/third_party/LM7/LICENSE_lgpl.txt" -o LICENSE.LM7 + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/src/lib/geogram/third_party/rply/LICENSE" -o LICENSE.rply + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/src/lib/geogram/third_party/zlib/LICENSE" -o LICENSE.geogram-zlib + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/src/lib/geogram/third_party/gzstream/COPYING.LIB" -o LICENSE.gzstream + curl -fsSL --retry 5 "https://raw.githubusercontent.com/fwilliams/geogram-pcu/main/src/lib/geogram/third_party/PoissonRecon/LICENSE.txt" -o LICENSE.poissonrecon + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=numpy,scipy,cmake + CIBW_TEST_SOURCES: tests data + CIBW_TEST_REQUIRES: pytest + # ray_mesh_intersection/RaySurfelIntersector need Embree, which the + # patches drop for riscv64 (no Embree ISA exists for it). + CIBW_TEST_COMMAND: >- + pytest tests -k "not test_ray_mesh_intersection and not test_ray_surfel_intersection" + + - name: Check wheel contents + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + path = sys.argv[1] + names = zipfile.ZipFile(path).namelist() + libs = [n for n in names if n.endswith(".so")] + assert libs, names + assert not any("embree" in n.lower() for n in names), names + licences = {n.rsplit("/", 1)[-1] for n in names + if ".dist-info/licenses/" in n and not n.endswith("/")} + expected = {"LICENSE", "LICENSE.numpyeigen", "LICENSE.eigen", "LICENSE.pybind11", + "LICENSE.igl", "LICENSE.tinyply", "LICENSE.tinyobjloader", "COPYING.simde", + "LICENSE.manifold", "LICENSE.geogram", "LICENSE.LM7", "LICENSE.rply", + "LICENSE.geogram-zlib", "LICENSE.gzstream", "LICENSE.poissonrecon"} + assert licences == expected, licences + print(path, "ok") + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: point-cloud-utils-${{ env.POINT_CLOUD_UTILS_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + gpl_sources: + needs: [setup] + name: Collect GPL sources (gcc) for point-cloud-utils ${{ inputs.version || 'v0.34' }} + runs-on: ubuntu-24.04-riscv + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Collect gcc source RPM from manylinux_riscv64 + uses: ./actions/collect-gpl-sources + with: + image: ${{ env.MANYLINUX_RISCV64_IMAGE }} + packages: gcc + output: gpl-sources.tar + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: point-cloud-utils-${{ env.POINT_CLOUD_UTILS_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish point-cloud-utils 0.34.0 + needs: [setup, build_wheels, gpl_sources] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: point-cloud-utils-0.34.0-*-manylinux_riscv64 + gpl-sources-artifact: point-cloud-utils-0.34.0-gpl-sources + gpl-sources-description: gcc (geogram links OpenMP/libgomp) From 9c8cf53d27b0def48ae88134d87647dcbe3ce8a2 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 12 Sep 2026 04:59:22 +0200 Subject: [PATCH 3/3] point-cloud-utils: fix riscv64 import crash and OpenMP signal-check segfault Two independent bugs surfaced once the wheel actually built on riscv64: - Every test failed importing _pcu_internal.so with an undefined Eigen::MatrixBase::cross() symbol. nm -D --undefined-only on the built .so showed these were the only two unexplained undefined symbols. Forces a genuine out-of-line instantiation by taking their address. - With that fixed, tests started passing but the suite then segfaulted partway through on unrelated tests (remove_duplicate_points, mesh_face_areas -- varies by run). A gdb backtrace (gotcha 115) caught it live: SIGSEGV inside PyErr_CheckSignals(), called from OpenMP worker threads with no GIL/Python thread state. Several '#pragma omp for' loops (face_areas.cpp, morton.cpp x4, point_cloud_distance.cpp, point_cloud_normals.cpp) call PyErr_CheckSignals() from every thread on every iteration for Ctrl-C cancellation; only the thread that entered the parallel region from Python may touch the interpreter. This is a pre-existing upstream bug, not riscv64-specific in nature -- it likely races harmlessly elsewhere and reproduces reliably here. See the patch commit messages for the full diagnosis. --- ...-of-line-Eigen-cross-symbols-on-risc.patch | 53 ++++++++ ...Err_CheckSignals-from-the-OpenMP-mas.patch | 116 ++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 patches/point-cloud-utils/0.34.0/0004-common-force-out-of-line-Eigen-cross-symbols-on-risc.patch create mode 100644 patches/point-cloud-utils/0.34.0/0005-src-only-call-PyErr_CheckSignals-from-the-OpenMP-mas.patch diff --git a/patches/point-cloud-utils/0.34.0/0004-common-force-out-of-line-Eigen-cross-symbols-on-risc.patch b/patches/point-cloud-utils/0.34.0/0004-common-force-out-of-line-Eigen-cross-symbols-on-risc.patch new file mode 100644 index 000000000..4fce8dad8 --- /dev/null +++ b/patches/point-cloud-utils/0.34.0/0004-common-force-out-of-line-Eigen-cross-symbols-on-risc.patch @@ -0,0 +1,53 @@ +From f630bb41f8f8758eb9828f8d3ad170c118ce71c1 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 23:20:30 +0200 +Subject: [PATCH 4/4] common: force out-of-line Eigen::cross() symbols on + riscv64 + +Every riscv64 CI build failed importing _pcu_internal.so with: +undefined symbol: ...MatrixBase>::cross<...> +(and the same for Matrix). nm -D --undefined-only on +the built .so shows these are the *only* two unexplained undefined +symbols -- everything else is normal versioned libstdc++/libgcc/Python +C-API symbols resolved at load time. Eigen's cross() is a plain +'inline' (EIGEN_STRONG_INLINE expands to plain 'inline' on GCC, not +always_inline) template method, so every call site that doesn't fully +inline it should itself emit a fallback out-of-line weak definition; +somewhere in this build that guarantee isn't holding on riscv64 +(disabling EIGEN_DONT_VECTORIZE entirely, tried first, made no +difference, ruling out the SSE/NEON Geometry_SIMD.h specialization). +Force a genuine out-of-line, address-taken instantiation so the +dynamic symbol always resolves regardless of what upstream's own +translation units decide to inline. + +Upstream-Status: Inappropriate [riscv64-only workaround for a linkage issue that doesn't reproduce on any architecture upstream actually builds for] +--- + src/common/geogram_utils.cpp | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/src/common/geogram_utils.cpp b/src/common/geogram_utils.cpp +index db7d349..9f948d5 100644 +--- a/src/common/geogram_utils.cpp ++++ b/src/common/geogram_utils.cpp +@@ -37,3 +37,18 @@ void init_geogram_only_once() { + geogram_is_initialized = true; + } + } ++ ++#if defined(__riscv) ++#include ++ ++namespace { ++// MatrixBase::cross()/MatrixBase::cross() end up as ++// unresolved dynamic symbols in the built .so (every call site elsewhere is ++// fully inlined, so no translation unit ever emits an out-of-line copy). ++// Taking their address here forces GCC to emit a real, linkable definition. ++__attribute__((used)) ++auto force_riscv64_vector3_cross_f = &Eigen::MatrixBase::cross; ++__attribute__((used)) ++auto force_riscv64_vector3_cross_d = &Eigen::MatrixBase::cross; ++} ++#endif +-- +2.50.1 (Apple Git-155) + diff --git a/patches/point-cloud-utils/0.34.0/0005-src-only-call-PyErr_CheckSignals-from-the-OpenMP-mas.patch b/patches/point-cloud-utils/0.34.0/0005-src-only-call-PyErr_CheckSignals-from-the-OpenMP-mas.patch new file mode 100644 index 000000000..52d0bbe50 --- /dev/null +++ b/patches/point-cloud-utils/0.34.0/0005-src-only-call-PyErr_CheckSignals-from-the-OpenMP-mas.patch @@ -0,0 +1,116 @@ +From b8a21e3c1b589622180ca50e7fe64ca390809ff4 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 12 Sep 2026 04:55:11 +0200 +Subject: [PATCH 5/5] src: only call PyErr_CheckSignals() from the OpenMP + master thread + +test_mesh_face_areas segfaulted on riscv64 with a gdb backtrace +(gotcha 115's technique) showing a SIGSEGV inside PyErr_CheckSignals(), +called from three separate OpenMP worker threads that were all +spawned by mesh_face_areas' #pragma omp for loop, none of which hold +the GIL or have a registered Python thread state. PyErr_CheckSignals() +touches interpreter-global signal-handling state and is documented as +callable only from the main thread; the Ctrl-C-cancellation checks +this project adds inside several "#pragma omp for" loops +(face_areas.cpp, morton.cpp x4, point_cloud_distance.cpp, +point_cloud_normals.cpp) call it from every worker thread on every +loop iteration instead. This is a pre-existing bug in the upstream +source, not a riscv64-only gap -- it likely races harmlessly on other +platforms/allocators but reproduces reliably here. Guard each call +with omp_get_thread_num() == 0 so only the thread that entered the +parallel region from Python ever calls it; the shared threw_exception +flag still lets every other thread observe the cancellation. +Call sites outside any "#pragma omp for" (connected_components.cpp, +sparse_voxel_grid.cpp, morton.cpp's commented-out-parallel loop, and +point_cloud_normals.cpp's single-threaded fallback) are untouched. + +Upstream-Status: To upstream [real bug, not riscv64-specific, but this session is barred from filing issues/PRs on third-party repos] +--- + src/face_areas.cpp | 2 +- + src/morton.cpp | 8 ++++---- + src/point_cloud_distance.cpp | 2 +- + src/point_cloud_normals.cpp | 2 +- + 4 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/src/face_areas.cpp b/src/face_areas.cpp +index 02ca3b1..76bb4d9 100644 +--- a/src/face_areas.cpp ++++ b/src/face_areas.cpp +@@ -37,7 +37,7 @@ npe_begin_code() + #pragma omp for nowait + #endif + for (int i = 0; i < f.rows(); i += 1) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +diff --git a/src/morton.cpp b/src/morton.cpp +index 33e1ddc..d53107c 100644 +--- a/src/morton.cpp ++++ b/src/morton.cpp +@@ -62,7 +62,7 @@ npe_begin_code() + #pragma omp for + #endif + for(int i = 0; i < codes_1.rows(); i += 1) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +@@ -142,7 +142,7 @@ npe_begin_code() + #pragma omp for + #endif + for(int i = 0; i < codes_1.rows(); i += 1) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +@@ -210,7 +210,7 @@ npe_begin_code() + #pragma omp for + #endif + for(int i = 0; i < pts.rows(); i += 1) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +@@ -278,7 +278,7 @@ npe_begin_code() + #pragma omp for + #endif + for(int i = 0; i < codes.rows(); i += 1) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +diff --git a/src/point_cloud_distance.cpp b/src/point_cloud_distance.cpp +index f04b3e7..d1dd15d 100644 +--- a/src/point_cloud_distance.cpp ++++ b/src/point_cloud_distance.cpp +@@ -57,7 +57,7 @@ void shortest_distances_nanoflann(Source& query_mat, + #pragma omp for + #endif + for(int i = 0; i < query_mat.rows(); ++i) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +diff --git a/src/point_cloud_normals.cpp b/src/point_cloud_normals.cpp +index 41ab7d6..6c36abe 100644 +--- a/src/point_cloud_normals.cpp ++++ b/src/point_cloud_normals.cpp +@@ -205,7 +205,7 @@ void estimate_normals_parallel(const InPointsType& points, const InViewDirsType& + #pragma omp for nowait + #endif + for (int i = 0; i < points.rows(); i++) { +- if (PyErr_CheckSignals() != 0) { ++ if (omp_get_thread_num() == 0 && PyErr_CheckSignals() != 0) { + #if defined(_OPENMP) + if (threw_exception) { + continue; +-- +2.50.1 (Apple Git-155) +