diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf555b0d11..e48d4a24439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Main +- Added copy and move semantics for `t::geometry::RaycastingScene` in C++ and Python: copy/move constructors and assignments are now supported. Python `copy.deepcopy()` is supported for `RaycastingScene`. (issue #7014)(PR #7437) - Upgrade stdgpu third-party library to commit d7c07d0. - Fix performance for non-contiguous NumPy array conversion in pybind vector converters. This change removes restrictive `py::array::c_style` flags and adds a runtime contiguity check, improving Pandas-to-Open3D conversion speed by up to ~50×. (issue #5250)(PR #7343). - Corrected documentation for Link Open3D in C++ projects (broken links). diff --git a/cpp/open3d/t/geometry/RaycastingScene.cpp b/cpp/open3d/t/geometry/RaycastingScene.cpp index ba322023532..964db7cae5b 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.cpp +++ b/cpp/open3d/t/geometry/RaycastingScene.cpp @@ -361,12 +361,17 @@ struct RaycastingScene::Impl { RTCDevice device_; // Vector for storing some information about the added geometry. std::vector geometry_ptrs_; + // (num_vertices, num_triangles) per geometry, for copy support. + std::vector> geometry_sizes_; core::Device tensor_device_; // cpu or sycl bool devprop_join_commit; virtual ~Impl() = default; + /// Returns a deep copy of this implementation (new device and scene). + virtual std::unique_ptr Clone() const = 0; + void CommitScene() { if (!scene_committed_) { if (devprop_join_commit) { @@ -818,6 +823,59 @@ struct RaycastingScene::SYCLImpl : public RaycastingScene::Impl { void CopyArray(int* src, uint32_t* dst, size_t num_elements) override { queue_.memcpy(dst, src, num_elements * sizeof(uint32_t)).wait(); } + + std::unique_ptr Clone() const override { + auto copy = std::make_unique(); + copy->InitializeDevice(); + copy->tensor_device_ = tensor_device_; + rtcSetDeviceErrorFunction(copy->device_, ErrorFunction, NULL); + copy->scene_ = rtcNewScene(copy->device_); + rtcSetSceneFlags(copy->scene_, + RTC_SCENE_FLAG_ROBUST | + RTC_SCENE_FLAG_FILTER_FUNCTION_IN_ARGUMENTS); + copy->devprop_join_commit = rtcGetDeviceProperty( + copy->device_, RTC_DEVICE_PROPERTY_JOIN_COMMIT_SUPPORTED); + copy->scene_committed_ = false; + copy->geometry_sizes_ = geometry_sizes_; + + SYCLImpl* dst = copy.get(); + + for (size_t geom_id = 0; geom_id < geometry_ptrs_.size(); ++geom_id) { + const size_t num_vertices = geometry_sizes_[geom_id].first; + const size_t num_triangles = geometry_sizes_[geom_id].second; + const size_t vb_bytes = 3 * sizeof(float) * num_vertices; + const size_t ib_bytes = 3 * sizeof(uint32_t) * num_triangles; + + RTCGeometry src_geom = rtcGetGeometry(scene_, geom_id); + const void* vb_src = rtcGetGeometryBufferData( + src_geom, RTC_BUFFER_TYPE_VERTEX, 0); + const void* ib_src = rtcGetGeometryBufferData( + src_geom, RTC_BUFFER_TYPE_INDEX, 0); + + RTCGeometry geom = + rtcNewGeometry(dst->device_, RTC_GEOMETRY_TYPE_TRIANGLE); + void* vertex_buffer = rtcSetNewGeometryBuffer( + geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, + 3 * sizeof(float), num_vertices); + void* index_buffer = rtcSetNewGeometryBuffer( + geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, + 3 * sizeof(uint32_t), num_triangles); + + std::memcpy(vertex_buffer, vb_src, vb_bytes); + std::memcpy(index_buffer, ib_src, ib_bytes); + + rtcSetGeometryEnableFilterFunctionFromArguments(geom, true); + rtcCommitGeometry(geom); + rtcAttachGeometry(dst->scene_, geom); + rtcReleaseGeometry(geom); + + GeometryPtr geometry_ptr = {RTC_GEOMETRY_TYPE_TRIANGLE, + static_cast(vertex_buffer), + static_cast(index_buffer)}; + dst->geometry_ptrs_.push_back(geometry_ptr); + } + return copy; + } }; #endif @@ -1192,6 +1250,61 @@ struct RaycastingScene::CPUImpl : public RaycastingScene::Impl { void CopyArray(int* src, uint32_t* dst, size_t num_elements) override { std::copy(src, src + num_elements, dst); } + + std::unique_ptr Clone() const override { + auto copy = std::make_unique(); + copy->device_ = rtcNewDevice(NULL); + rtcSetDeviceErrorFunction(copy->device_, ErrorFunction, NULL); + copy->scene_ = rtcNewScene(copy->device_); + rtcSetSceneFlags(copy->scene_, + RTC_SCENE_FLAG_ROBUST | + RTC_SCENE_FLAG_FILTER_FUNCTION_IN_ARGUMENTS); + copy->devprop_join_commit = rtcGetDeviceProperty( + copy->device_, RTC_DEVICE_PROPERTY_JOIN_COMMIT_SUPPORTED); + copy->scene_committed_ = false; + copy->tensor_device_ = tensor_device_; + copy->geometry_sizes_ = geometry_sizes_; + + for (size_t geom_id = 0; geom_id < geometry_ptrs_.size(); ++geom_id) { + const size_t num_vertices = geometry_sizes_[geom_id].first; + const size_t num_triangles = geometry_sizes_[geom_id].second; + + RTCGeometry src_geom = rtcGetGeometry(scene_, geom_id); + const float* vb_src = + reinterpret_cast(rtcGetGeometryBufferData( + src_geom, RTC_BUFFER_TYPE_VERTEX, 0)); + const uint32_t* ib_src = + reinterpret_cast(rtcGetGeometryBufferData( + src_geom, RTC_BUFFER_TYPE_INDEX, 0)); + + RTCGeometry geom = + rtcNewGeometry(copy->device_, RTC_GEOMETRY_TYPE_TRIANGLE); + float* vertex_buffer = + reinterpret_cast(rtcSetNewGeometryBuffer( + geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, + 3 * sizeof(float), num_vertices)); + uint32_t* index_buffer = + reinterpret_cast(rtcSetNewGeometryBuffer( + geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, + 3 * sizeof(uint32_t), num_triangles)); + + std::memcpy(vertex_buffer, vb_src, + 3 * sizeof(float) * num_vertices); + std::memcpy(index_buffer, ib_src, + 3 * sizeof(uint32_t) * num_triangles); + + rtcSetGeometryEnableFilterFunctionFromArguments(geom, true); + rtcCommitGeometry(geom); + rtcAttachGeometry(copy->scene_, geom); + rtcReleaseGeometry(geom); + + GeometryPtr geometry_ptr = {RTC_GEOMETRY_TYPE_TRIANGLE, + static_cast(vertex_buffer), + static_cast(index_buffer)}; + copy->geometry_ptrs_.push_back(geometry_ptr); + } + return copy; + } }; RaycastingScene::RaycastingScene(int64_t nthreads, const core::Device& device) { @@ -1230,8 +1343,30 @@ RaycastingScene::RaycastingScene(int64_t nthreads, const core::Device& device) { } RaycastingScene::~RaycastingScene() { - rtcReleaseScene(impl_->scene_); - rtcReleaseDevice(impl_->device_); + if (impl_) { + rtcReleaseScene(impl_->scene_); + rtcReleaseDevice(impl_->device_); + } +} + +RaycastingScene::RaycastingScene(const RaycastingScene& other) + : impl_(other.impl_->Clone()) {} + +RaycastingScene& RaycastingScene::operator=(const RaycastingScene& other) { + if (this != &other) { + impl_ = other.impl_->Clone(); + } + return *this; +} + +RaycastingScene::RaycastingScene(RaycastingScene&& other) noexcept + : impl_(std::move(other.impl_)) {} + +RaycastingScene& RaycastingScene::operator=(RaycastingScene&& other) noexcept { + if (this != &other) { + impl_ = std::move(other.impl_); + } + return *this; } uint32_t RaycastingScene::AddTriangles(const core::Tensor& vertex_positions, @@ -1301,9 +1436,10 @@ uint32_t RaycastingScene::AddTriangles(const core::Tensor& vertex_positions, rtcReleaseGeometry(geom); GeometryPtr geometry_ptr = {RTC_GEOMETRY_TYPE_TRIANGLE, - (const void*)vertex_buffer, - (const void*)index_buffer}; + static_cast(vertex_buffer), + static_cast(index_buffer)}; impl_->geometry_ptrs_.push_back(geometry_ptr); + impl_->geometry_sizes_.push_back({num_vertices, num_triangles}); return geom_id; } diff --git a/cpp/open3d/t/geometry/RaycastingScene.h b/cpp/open3d/t/geometry/RaycastingScene.h index 37390c0c6fb..c4b09268041 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.h +++ b/cpp/open3d/t/geometry/RaycastingScene.h @@ -35,6 +35,20 @@ class RaycastingScene { ~RaycastingScene(); + /// \brief Copy constructor. + /// Creates a new object as a copy of an existing one. + RaycastingScene(const RaycastingScene &other); + + /// \brief Copy assignment operator. + /// Assigns the contents of an existing object to this object. + RaycastingScene &operator=(const RaycastingScene &other); + + /// \brief Move constructor. + RaycastingScene(RaycastingScene &&other) noexcept; + + /// \brief Move assignment operator. + RaycastingScene &operator=(RaycastingScene &&other) noexcept; + /// \brief Add a triangle mesh to the scene. /// \param vertex_positions Vertices as Tensor of dim {N,3} and dtype float. /// \param triangle_indices Triangles as Tensor of dim {M,3} and dtype diff --git a/cpp/pybind/t/geometry/raycasting_scene.cpp b/cpp/pybind/t/geometry/raycasting_scene.cpp index 0d924fc749b..85c3ebd73a1 100644 --- a/cpp/pybind/t/geometry/raycasting_scene.cpp +++ b/cpp/pybind/t/geometry/raycasting_scene.cpp @@ -67,6 +67,18 @@ Create a RaycastingScene. device (open3d.core.Device): The device to use. Currently CPU and SYCL devices are supported. )doc"); + raycasting_scene.def(py::init(), "other"_a, R"doc( +Create a RaycastingScene as a copy of another scene (copy constructor). + +Args: + other (open3d.t.geometry.RaycastingScene): The scene to copy. +)doc"); + + raycasting_scene.def("__deepcopy__", + [](const RaycastingScene& self, py::dict /* memo */) { + return RaycastingScene(self); + }); + raycasting_scene.def( "add_triangles", py::overload_cast( diff --git a/python/test/t/geometry/test_raycasting_scene.py b/python/test/t/geometry/test_raycasting_scene.py index 2164f96d6fe..b864b12da26 100755 --- a/python/test/t/geometry/test_raycasting_scene.py +++ b/python/test/t/geometry/test_raycasting_scene.py @@ -460,3 +460,130 @@ def test_sphere_wrong_occupancy(): # we should get the same result with more samples occupancy_3samples = scene.compute_occupancy(query_points, nsamples=3) np.testing.assert_equal(occupancy_3samples.numpy(), expected) + + +# --- Tests for copy constructor and copy semantics. --- + + +def _make_scene_with_triangle(device): + """Create a RaycastingScene with a single triangle for copy tests.""" + vertices = o3d.core.Tensor([[0, 0, 0], [1, 0, 0], [1, 1, 0]], + dtype=o3d.core.float32, + device=device) + triangles = o3d.core.Tensor([[0, 1, 2]], + dtype=o3d.core.uint32, + device=device) + scene = o3d.t.geometry.RaycastingScene(device=device) + scene.add_triangles(vertices, triangles) + return scene + + +@pytest.mark.parametrize("device", + list_devices(enable_cuda=False, enable_sycl=True)) +def test_copy_constructor_empty_scene(device): + scene = o3d.t.geometry.RaycastingScene(device=device) + scene_copy = o3d.t.geometry.RaycastingScene(scene) + rays = o3d.core.Tensor([[0, 0, 1, 0, 0, -1]], + dtype=o3d.core.float32, + device=device) + ans_orig = scene.cast_rays(rays) + ans_copy = scene_copy.cast_rays(rays) + invalid_id = o3d.t.geometry.RaycastingScene.INVALID_ID + assert ans_orig["geometry_ids"][0].cpu().item() == invalid_id + assert ans_copy["geometry_ids"][0].cpu().item() == invalid_id + assert np.isinf(ans_orig["t_hit"][0].item()) + assert np.isinf(ans_copy["t_hit"][0].item()) + + +@pytest.mark.parametrize("device", + list_devices(enable_cuda=False, enable_sycl=True)) +def test_copy_constructor_scene_with_geometry(device): + """Copy of a scene with geometry yields same cast_rays and compute_* results (tests clone).""" + scene = _make_scene_with_triangle(device) + scene_copy = o3d.t.geometry.RaycastingScene(scene) + + rays = o3d.core.Tensor( + [[0.2, 0.1, 1, 0, 0, -1], [10, 10, 10, 1, 0, 0]], + dtype=o3d.core.float32, + device=device, + ) + ans_orig = scene.cast_rays(rays) + ans_copy = scene_copy.cast_rays(rays) + + np.testing.assert_allclose(ans_orig["t_hit"].numpy(), + ans_copy["t_hit"].numpy()) + np.testing.assert_array_equal(ans_orig["geometry_ids"].numpy(), + ans_copy["geometry_ids"].numpy()) + + query = o3d.core.Tensor([[0.2, 0.1, 1], [10, 10, 10]], + dtype=o3d.core.float32, + device=device) + closest_orig = scene.compute_closest_points(query) + closest_copy = scene_copy.compute_closest_points(query) + np.testing.assert_allclose(closest_orig["points"].numpy(), + closest_copy["points"].numpy()) + np.testing.assert_array_equal(closest_orig["geometry_ids"].numpy(), + closest_copy["geometry_ids"].numpy()) + + dist_orig = scene.compute_distance(query) + dist_copy = scene_copy.compute_distance(query) + np.testing.assert_allclose(dist_orig.numpy(), dist_copy.numpy()) + + +@pytest.mark.parametrize("device", + list_devices(enable_cuda=False, enable_sycl=True)) +def test_copy_independence(device): + """Copy is independent: adding geometry to one does not affect the other.""" + scene = _make_scene_with_triangle(device) + scene_copy = o3d.t.geometry.RaycastingScene(scene) + + # Add another triangle only to the copy + v2 = o3d.core.Tensor([[0, 0, 1], [1, 0, 1], [0, 1, 1]], + dtype=o3d.core.float32, + device=device) + t2 = o3d.core.Tensor([[0, 1, 2]], dtype=o3d.core.uint32, device=device) + scene_copy.add_triangles(v2, t2) + + invalid_id = o3d.t.geometry.RaycastingScene.INVALID_ID + # Ray hitting first triangle: both should hit + rays_one = o3d.core.Tensor( + [[0.2, 0.1, 0.5, 0, 0, -1]], + dtype=o3d.core.float32, + device=device, + ) + ans_orig = scene.cast_rays(rays_one) + ans_copy = scene_copy.cast_rays(rays_one) + assert ans_orig["geometry_ids"][0].cpu().item() != invalid_id + assert ans_copy["geometry_ids"][0].cpu().item() != invalid_id + + # Ray that would hit second triangle (only in copy): miss in original, hit in copy + rays_two = o3d.core.Tensor( + [[0.2, 0.1, 0.5, 0, 0, 1]], + dtype=o3d.core.float32, + device=device, + ) + ans_orig2 = scene.cast_rays(rays_two) + ans_copy2 = scene_copy.cast_rays(rays_two) + assert ans_orig2["geometry_ids"][0].cpu().item() == invalid_id + assert ans_copy2["geometry_ids"][0].cpu().item() != invalid_id + + +@pytest.mark.parametrize("device", + list_devices(enable_cuda=False, enable_sycl=True)) +def test_copy_module_deepcopy(device): + """copy.deepcopy(scene) works and produces an independent copy.""" + import copy as copy_module + scene = _make_scene_with_triangle(device) + scene_copy = copy_module.deepcopy(scene) + + rays = o3d.core.Tensor( + [[0.2, 0.1, 1, 0, 0, -1]], + dtype=o3d.core.float32, + device=device, + ) + ans_orig = scene.cast_rays(rays) + ans_copy = scene_copy.cast_rays(rays) + np.testing.assert_allclose(ans_orig["t_hit"].numpy(), + ans_copy["t_hit"].numpy()) + assert ans_orig["geometry_ids"][0].cpu().item( + ) == ans_copy["geometry_ids"][0].cpu().item()