Own bulk-API ndarray buffers with std::vector, pin nanobind below 3
face_grid leaked both buffers when the kernel threw mid-grid, and tess copied every mesh array a second time. Also drop the stale S5 references.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
std::vector -> numpy without a copy.
|
||||
|
||||
The bulk APIs fill plain vectors while the GIL is released, then hand each
|
||||
one to numpy. Moving the vector into a capsule makes it the array's
|
||||
storage, so there is no second buffer to copy into, and nothing leaks if
|
||||
the kernel throws halfway through filling it.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/ndarray.h>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
/// Wrap `data` as a numpy array of `shape`; the array owns the buffer.
|
||||
template <typename T>
|
||||
nb::object ocp_ndarray(std::vector<T> &&data,
|
||||
std::initializer_list<size_t> shape) {
|
||||
auto *owned = new std::vector<T>(std::move(data));
|
||||
// An empty vector may have no buffer at all; numpy wants a real pointer
|
||||
// even for a zero-length array.
|
||||
owned->reserve(1);
|
||||
nb::capsule owner(owned, [](void *p) noexcept {
|
||||
delete static_cast<std::vector<T> *>(p);
|
||||
});
|
||||
return nb::cast(nb::ndarray<nb::numpy, T>(owned->data(), shape, owner));
|
||||
}
|
||||
+9
-17
@@ -18,8 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "../common/occt_module.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include "../common/occt_ndarray.h"
|
||||
|
||||
#include <BRepGProp.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
@@ -35,11 +34,6 @@
|
||||
|
||||
namespace {
|
||||
|
||||
/// Own a heap buffer for as long as the numpy array that wraps it lives.
|
||||
template <typename T> nb::capsule owner_of(T *data) {
|
||||
return nb::capsule(data, [](void *p) noexcept { delete[] (T *) p; });
|
||||
}
|
||||
|
||||
void measure_range(const TopTools_IndexedMapOfShape &faces, double eps,
|
||||
double *areas, double *centroids, int begin, int end) {
|
||||
for (int i = begin; i < end; ++i) {
|
||||
@@ -81,27 +75,25 @@ void register_ext_measure() {
|
||||
TopExp::MapShapes(shape, TopAbs_FACE, faces);
|
||||
const int count = faces.Extent();
|
||||
|
||||
double *areas = new double[count > 0 ? count : 1];
|
||||
double *centroids = new double[count > 0 ? 3 * count : 1];
|
||||
std::vector<double> areas((size_t) count);
|
||||
std::vector<double> centroids(3 * (size_t) count);
|
||||
|
||||
{
|
||||
nb::gil_scoped_release nogil;
|
||||
if (parallel && count > 1) {
|
||||
OSD_Parallel::For(0, count, [&](int i) {
|
||||
measure_range(faces, eps, areas, centroids, i, i + 1);
|
||||
measure_range(faces, eps, areas.data(), centroids.data(), i,
|
||||
i + 1);
|
||||
});
|
||||
} else {
|
||||
measure_range(faces, eps, areas, centroids, 0, count);
|
||||
measure_range(faces, eps, areas.data(), centroids.data(), 0,
|
||||
count);
|
||||
}
|
||||
}
|
||||
|
||||
size_t area_shape[1] = {(size_t) count};
|
||||
size_t centroid_shape[2] = {(size_t) count, 3};
|
||||
return nb::make_tuple(
|
||||
nb::ndarray<nb::numpy, double, nb::ndim<1>>(areas, 1, area_shape,
|
||||
owner_of(areas)),
|
||||
nb::ndarray<nb::numpy, double, nb::ndim<2>>(
|
||||
centroids, 2, centroid_shape, owner_of(centroids)));
|
||||
ocp_ndarray(std::move(areas), {(size_t) count}),
|
||||
ocp_ndarray(std::move(centroids), {(size_t) count, 3}));
|
||||
},
|
||||
"shape"_a, nb::kw_only(), "parallel"_a = true, "eps"_a = nb::none(),
|
||||
R"doc(Area and centre of mass of every face, in MapShapes(FACE) order.
|
||||
|
||||
+6
-13
@@ -21,8 +21,7 @@
|
||||
*/
|
||||
|
||||
#include "../common/occt_module.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include "../common/occt_ndarray.h"
|
||||
|
||||
#include <BRepAdaptor_Surface.hxx>
|
||||
#include <BRepTools.hxx>
|
||||
@@ -33,13 +32,10 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T> nb::capsule owner_of(T *data) {
|
||||
return nb::capsule(data, [](void *p) noexcept { delete[] (T *) p; });
|
||||
}
|
||||
|
||||
/// numpy's linspace endpoint handling: the last sample is the bound itself,
|
||||
/// not start + (n-1)*step, which can miss it by an ulp.
|
||||
inline double linspace_at(double lo, double hi, int i, int n) {
|
||||
@@ -67,8 +63,8 @@ void register_ext_sample() {
|
||||
face.Orientation() == TopAbs_REVERSED ? -1.0 : 1.0;
|
||||
|
||||
const size_t count = (size_t) n * (size_t) n;
|
||||
double *points = new double[3 * count];
|
||||
double *normals = new double[3 * count];
|
||||
std::vector<double> points(3 * count);
|
||||
std::vector<double> normals(3 * count);
|
||||
|
||||
{
|
||||
nb::gil_scoped_release nogil;
|
||||
@@ -103,12 +99,9 @@ void register_ext_sample() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t shape[3] = {(size_t) n, (size_t) n, 3};
|
||||
return nb::make_tuple(
|
||||
nb::ndarray<nb::numpy, double, nb::ndim<3>>(points, 3, shape,
|
||||
owner_of(points)),
|
||||
nb::ndarray<nb::numpy, double, nb::ndim<3>>(normals, 3, shape,
|
||||
owner_of(normals)),
|
||||
ocp_ndarray(std::move(points), {(size_t) n, (size_t) n, 3}),
|
||||
ocp_ndarray(std::move(normals), {(size_t) n, (size_t) n, 3}),
|
||||
nb::make_tuple(umin, umax, vmin, vmax));
|
||||
},
|
||||
"face"_a, "n"_a,
|
||||
|
||||
+8
-29
@@ -26,8 +26,7 @@
|
||||
*/
|
||||
|
||||
#include "../common/occt_module.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include "../common/occt_ndarray.h"
|
||||
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
@@ -48,18 +47,6 @@
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T> nb::capsule owner_of(T *data) {
|
||||
return nb::capsule(data, [](void *p) noexcept { delete[] (T *) p; });
|
||||
}
|
||||
|
||||
/// Wrap an owned buffer as an (n, cols) numpy array.
|
||||
template <typename T>
|
||||
nb::object as_matrix(T *data, size_t rows, size_t cols) {
|
||||
size_t shape[2] = {rows, cols};
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, T, nb::ndim<2>>(data, 2, shape, owner_of(data)));
|
||||
}
|
||||
|
||||
/// One face's mesh, already flattened and oriented, ready to hand to Python.
|
||||
struct FaceMesh {
|
||||
int index = 0;
|
||||
@@ -140,25 +127,18 @@ void register_ext_tess() {
|
||||
}
|
||||
|
||||
nb::list out;
|
||||
for (const FaceMesh &mesh : meshes) {
|
||||
for (FaceMesh &mesh : meshes) {
|
||||
const size_t n = mesh.nodes.size() / 3;
|
||||
const size_t t = mesh.tris.size() / 3;
|
||||
|
||||
double *nodes = new double[mesh.nodes.size()];
|
||||
std::copy(mesh.nodes.begin(), mesh.nodes.end(), nodes);
|
||||
uint32_t *tris = new uint32_t[mesh.tris.size()];
|
||||
std::copy(mesh.tris.begin(), mesh.tris.end(), tris);
|
||||
|
||||
nb::dict entry;
|
||||
entry["face_index"] = mesh.index;
|
||||
entry["nodes"] = as_matrix(nodes, n, 3);
|
||||
entry["triangles"] = as_matrix(tris, t, 3);
|
||||
entry["nodes"] = ocp_ndarray(std::move(mesh.nodes), {n, 3});
|
||||
entry["triangles"] = ocp_ndarray(std::move(mesh.tris), {t, 3});
|
||||
if (mesh.normals.empty()) {
|
||||
entry["normals"] = nb::none();
|
||||
} else {
|
||||
double *normals = new double[mesh.normals.size()];
|
||||
std::copy(mesh.normals.begin(), mesh.normals.end(), normals);
|
||||
entry["normals"] = as_matrix(normals, n, 3);
|
||||
entry["normals"] = ocp_ndarray(std::move(mesh.normals), {n, 3});
|
||||
}
|
||||
out.append(entry);
|
||||
}
|
||||
@@ -203,12 +183,11 @@ the app-wide face identity, not a position in this list.)doc");
|
||||
}
|
||||
|
||||
nb::list out;
|
||||
for (const auto &entry : polylines) {
|
||||
double *points = new double[entry.second.size()];
|
||||
std::copy(entry.second.begin(), entry.second.end(), points);
|
||||
for (auto &entry : polylines) {
|
||||
const size_t count = entry.second.size() / 3;
|
||||
nb::dict row;
|
||||
row["edge_index"] = entry.first;
|
||||
row["points"] = as_matrix(points, entry.second.size() / 3, 3);
|
||||
row["points"] = ocp_ndarray(std::move(entry.second), {count, 3});
|
||||
out.append(row);
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
The GIL stays held throughout, per the note in mod_XSControl.cpp: XSTEP is
|
||||
built on process-global state and the app serialises imports anyway.
|
||||
|
||||
This is also where the S5 question lands (design.md): whether the wheel has
|
||||
to ship OCCT's share/ resources for STEP to resolve units in a container
|
||||
with no CSF_* environment. tests/test_inc3_io.py answers it.
|
||||
The wheel ships none of OCCT's share/ resources; STEP still resolves units
|
||||
in a container with no CSF_* environment, which tests/test_inc3_io.py pins.
|
||||
*/
|
||||
|
||||
#include "../common/occt_module.h"
|
||||
|
||||
Reference in New Issue
Block a user