meshopt 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ # On a version tag: a wheel for each system, tested where it is built, published to PyPI by trusted publishing, no token
2
+ # stored anywhere, and attached to a GitHub release. One wheel a system serves every Python from 3.12 through the stable
3
+ # ABI; 3.11 and free-threaded 3.14 get their own.
4
+ name: Release
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ permissions:
11
+ contents: write
12
+
13
+ jobs:
14
+ wheels:
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest]
19
+ runs-on: ${{ matrix.os }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - name: The tag must be the version in pyproject.toml
23
+ shell: bash
24
+ run: test "v$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)" = "${GITHUB_REF_NAME}"
25
+ - uses: pypa/cibuildwheel@v3.2
26
+ env:
27
+ CIBW_BUILD: "cp311-* cp312-* cp314t-*"
28
+ CIBW_SKIP: "*-musllinux_* *-win32 *-manylinux_i686"
29
+ CIBW_ENABLE: cpython-freethreading
30
+ CIBW_TEST_REQUIRES: pytest
31
+ CIBW_TEST_COMMAND: pytest -q {project}/tests
32
+ MACOSX_DEPLOYMENT_TARGET: "10.14" # nanobind's aligned allocation needs 10.14; arm64 builds start at 11 regardless
33
+ - uses: actions/upload-artifact@v4
34
+ with:
35
+ name: wheels-${{ matrix.os }}
36
+ path: wheelhouse/*.whl
37
+
38
+ sdist:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v6
43
+ - run: uv build --sdist
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: sdist
47
+ path: dist/*.tar.gz
48
+
49
+ release:
50
+ needs: [wheels, sdist]
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/download-artifact@v4
54
+ with:
55
+ path: dist
56
+ merge-multiple: true
57
+ - run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --notes "meshoptimizer 1.3"
58
+ env:
59
+ GH_TOKEN: ${{ github.token }}
60
+
61
+ publish:
62
+ needs: [wheels, sdist]
63
+ runs-on: ubuntu-latest
64
+ environment: pypi
65
+ permissions:
66
+ id-token: write
67
+ steps:
68
+ - uses: actions/download-artifact@v4
69
+ with:
70
+ path: dist
71
+ merge-multiple: true
72
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ # On every push and pull request: built from source and tested on each system, with the oldest and newest Python and
2
+ # free-threaded Python, which must keep its lock off.
3
+ name: Test
4
+
5
+ on:
6
+ push:
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
15
+ python: ["3.11", "3.14", "3.14t"]
16
+ runs-on: ${{ matrix.os }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v6
20
+ with:
21
+ python-version: ${{ matrix.python }}
22
+ - run: uv sync
23
+ - run: uv run pytest -q
24
+ - name: Free-threaded Python keeps its lock off
25
+ if: matrix.python == '3.14t'
26
+ run: uv run python -c "import sys, meshopt; assert not sys._is_gil_enabled(), 'meshopt turned the lock back on'"
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ dist/
6
+ build/
@@ -0,0 +1,19 @@
1
+ cmake_minimum_required(VERSION 3.18...3.31)
2
+ project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)
3
+
4
+ find_package(Python 3.11 REQUIRED COMPONENTS Interpreter Development.Module ${SKBUILD_SABI_COMPONENT})
5
+ find_package(nanobind CONFIG REQUIRED)
6
+
7
+ # The binding and meshoptimizer's own sources, the files the encoder and decoder need, in one module.
8
+ nanobind_add_module(_core STABLE_ABI FREE_THREADED NB_STATIC LTO
9
+ src/meshopt.cpp
10
+ vendor/meshoptimizer/allocator.cpp
11
+ vendor/meshoptimizer/vertexcodec.cpp
12
+ vendor/meshoptimizer/indexcodec.cpp
13
+ vendor/meshoptimizer/vertexfilter.cpp
14
+ vendor/meshoptimizer/vcacheoptimizer.cpp
15
+ vendor/meshoptimizer/vfetchoptimizer.cpp
16
+ vendor/meshoptimizer/spatialorder.cpp
17
+ )
18
+
19
+ install(TARGETS _core LIBRARY DESTINATION meshopt)
meshopt-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aidan Newsome
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
meshopt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: meshopt
3
+ Version: 0.1.0
4
+ Summary: meshoptimizer's encoder and decoder for Python, shaped like its JavaScript module.
5
+ Author-Email: Aidan Newsome <aidan.newsome@me.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ License-File: vendor/meshoptimizer/LICENSE.md
9
+ Project-URL: Source, https://github.com/aidannewsome/meshopt
10
+ Project-URL: Releases, https://github.com/aidannewsome/meshopt/releases
11
+ Project-URL: meshoptimizer, https://github.com/zeux/meshoptimizer
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: numpy>=1.26
14
+ Description-Content-Type: text/markdown
15
+
16
+ # meshopt
17
+
18
+ An independent Python binding for [meshoptimizer](https://github.com/zeux/meshoptimizer)'s encoder and decoder, modelled
19
+ on its JavaScript module. Not affiliated with the meshoptimizer project.
20
+
21
+ ## Use
22
+
23
+ ```python
24
+ import meshopt
25
+
26
+ indices, remap, unique = meshopt.reorder_mesh(indices, triangles=True, optsize=False)
27
+ vertices = vertices[np.argsort(remap)[:unique]] # each vertex moved to its new place
28
+
29
+ data = meshopt.encode_gltf_buffer(vertices, "ATTRIBUTES") # bytes for an EXT_meshopt_compression bufferView
30
+ rows = meshopt.decode_gltf_buffer(data, len(vertices), vertices.itemsize * vertices.shape[1], "ATTRIBUTES")
31
+ ```
32
+
33
+ The functions are the JavaScript module's (`MeshoptEncoder` and `MeshoptDecoder`), in snake case, with counts and sizes
34
+ taken from the arrays where they can be. Encoders give bytes; filters give rows of bytes as an array; decoders give rows
35
+ of bytes, or indices for an index buffer. The tests are the JavaScript module's tests, one for one.
36
+
37
+ Every call releases Python's lock, and the module runs on free-threaded Python with the lock off.
38
+
39
+ ## Build
40
+
41
+ A short C++ binding made with nanobind over meshoptimizer's own sources, vendored from release 1.3 in `vendor/meshoptimizer`.
42
+ `uv sync` builds it and `uv run pytest` tests it. Releases are a wheel for each system, so installing needs no compiler.
43
+
44
+ ## Licence
45
+
46
+ MIT, as meshoptimizer is. meshoptimizer is Arseny Kapoulkine's; its licence is in `vendor/meshoptimizer/LICENSE.md`.
@@ -0,0 +1,31 @@
1
+ # meshopt
2
+
3
+ An independent Python binding for [meshoptimizer](https://github.com/zeux/meshoptimizer)'s encoder and decoder, modelled
4
+ on its JavaScript module. Not affiliated with the meshoptimizer project.
5
+
6
+ ## Use
7
+
8
+ ```python
9
+ import meshopt
10
+
11
+ indices, remap, unique = meshopt.reorder_mesh(indices, triangles=True, optsize=False)
12
+ vertices = vertices[np.argsort(remap)[:unique]] # each vertex moved to its new place
13
+
14
+ data = meshopt.encode_gltf_buffer(vertices, "ATTRIBUTES") # bytes for an EXT_meshopt_compression bufferView
15
+ rows = meshopt.decode_gltf_buffer(data, len(vertices), vertices.itemsize * vertices.shape[1], "ATTRIBUTES")
16
+ ```
17
+
18
+ The functions are the JavaScript module's (`MeshoptEncoder` and `MeshoptDecoder`), in snake case, with counts and sizes
19
+ taken from the arrays where they can be. Encoders give bytes; filters give rows of bytes as an array; decoders give rows
20
+ of bytes, or indices for an index buffer. The tests are the JavaScript module's tests, one for one.
21
+
22
+ Every call releases Python's lock, and the module runs on free-threaded Python with the lock off.
23
+
24
+ ## Build
25
+
26
+ A short C++ binding made with nanobind over meshoptimizer's own sources, vendored from release 1.3 in `vendor/meshoptimizer`.
27
+ `uv sync` builds it and `uv run pytest` tests it. Releases are a wheel for each system, so installing needs no compiler.
28
+
29
+ ## Licence
30
+
31
+ MIT, as meshoptimizer is. meshoptimizer is Arseny Kapoulkine's; its licence is in `vendor/meshoptimizer/LICENSE.md`.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "meshopt"
3
+ version = "0.1.0"
4
+ description = "meshoptimizer's encoder and decoder for Python, shaped like its JavaScript module."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE", "vendor/meshoptimizer/LICENSE.md"]
8
+ authors = [{ name = "Aidan Newsome", email = "aidan.newsome@me.com" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "numpy>=1.26",
12
+ ]
13
+
14
+ [project.urls]
15
+ Source = "https://github.com/aidannewsome/meshopt"
16
+ Releases = "https://github.com/aidannewsome/meshopt/releases"
17
+ meshoptimizer = "https://github.com/zeux/meshoptimizer"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["scikit-build-core>=0.11", "nanobind>=2.9"]
26
+ build-backend = "scikit_build_core.build"
27
+
28
+ [tool.scikit-build]
29
+ minimum-version = "build-system.requires"
30
+ build-dir = "build/{wheel_tag}"
31
+ wheel.packages = ["python/meshopt"]
32
+ wheel.py-api = "cp312" # one wheel a system for every Python from 3.12, through the stable ABI; 3.11 and free-threaded Python get their own
33
+
34
+ [tool.ruff]
35
+ line-length = 160
36
+
37
+ [tool.ruff.lint.isort]
38
+ known-first-party = ["meshopt"]
39
+
40
+ [tool.ruff.format]
41
+ exclude = ["tests/test_decoder.py"] # its numbers stay in rows, as the JavaScript test has them
@@ -0,0 +1,168 @@
1
+ """meshoptimizer's encoder and decoder, shaped like its JavaScript module: the same functions in the same order,
2
+ counts and sizes read from the arrays where they can be. Codecs give bytes; filters and decoders give arrays."""
3
+
4
+ from typing import Literal
5
+
6
+ import numpy as np
7
+
8
+ from . import _core
9
+
10
+ __all__ = [
11
+ "reorder_mesh",
12
+ "reorder_points",
13
+ "encode_vertex_buffer",
14
+ "encode_vertex_buffer_level",
15
+ "encode_index_buffer",
16
+ "encode_index_sequence",
17
+ "encode_gltf_buffer",
18
+ "encode_filter_oct",
19
+ "encode_filter_quat",
20
+ "encode_filter_exp",
21
+ "encode_filter_color",
22
+ "decode_vertex_buffer",
23
+ "decode_index_buffer",
24
+ "decode_index_sequence",
25
+ "decode_gltf_buffer",
26
+ ]
27
+
28
+ Mode = Literal["ATTRIBUTES", "TRIANGLES", "INDICES"]
29
+ Filter = Literal["NONE", "OCTAHEDRAL", "QUATERNION", "EXPONENTIAL", "COLOR"]
30
+ ExpMode = Literal["Separate", "SharedVector", "SharedComponent", "Clamped"]
31
+ EXP_MODES = {"Separate": 0, "SharedVector": 1, "SharedComponent": 2, "Clamped": 3}
32
+
33
+
34
+ def check(condition: bool, message: str) -> None:
35
+ if not condition:
36
+ raise ValueError(message)
37
+
38
+
39
+ def rows(data: np.ndarray) -> tuple[np.ndarray, int, int]:
40
+ """An array as its bytes, with a row each element: the bytes, the count and each row's size."""
41
+ data = np.ascontiguousarray(data)
42
+ check(data.ndim >= 1 and len(data) > 0, "data must have a row for each element")
43
+ return data.view(np.uint8).reshape(-1), len(data), data.nbytes // len(data)
44
+
45
+
46
+ def index32(indices: np.ndarray) -> np.ndarray:
47
+ indices = np.asarray(indices)
48
+ check(indices.dtype in (np.uint16, np.uint32, np.int32), "indices must be 16 or 32 bit integers")
49
+ return np.ascontiguousarray(indices.reshape(-1), dtype=np.uint32)
50
+
51
+
52
+ def reorder_mesh(indices: np.ndarray, triangles: bool, optsize: bool) -> tuple[np.ndarray, np.ndarray, int]:
53
+ """Indices renumbered so vertices come in the order they are first used, the triangles first put in the order a
54
+ GPU's vertex cache likes, or a strip's when optsize. The new indices, each vertex's new place, and how many are used."""
55
+ check(np.asarray(indices).dtype in (np.uint32, np.int32), "indices must be 32 bit integers")
56
+ indices = index32(indices)
57
+ check(not triangles or len(indices) % 3 == 0, "triangles need indices in threes")
58
+ if triangles:
59
+ indices = _core.optimize_vertex_cache(indices, optsize)
60
+ remap, unique = _core.optimize_vertex_fetch_remap(indices)
61
+ return remap[indices], remap, unique
62
+
63
+
64
+ def reorder_points(positions: np.ndarray) -> np.ndarray:
65
+ """The order to put points in so near ones are near in the list: each place's point."""
66
+ positions = np.ascontiguousarray(positions, dtype=np.float32)
67
+ check(positions.ndim == 2 and positions.shape[1] >= 3, "positions must be n by 3 or more")
68
+ return _core.spatial_sort_remap(positions.reshape(-1), *positions.shape)
69
+
70
+
71
+ def encode_vertex_buffer(data: np.ndarray) -> bytes:
72
+ return encode_vertex_buffer_level(data, 2)
73
+
74
+
75
+ def encode_vertex_buffer_level(data: np.ndarray, level: int, version: int | None = None) -> bytes:
76
+ """Rows of any type, each a multiple of four bytes, compressed harder from level 0 to 3."""
77
+ data, count, size = rows(data)
78
+ check(0 < size <= 256 and size % 4 == 0, "a row must be 4 to 256 bytes, in fours")
79
+ check(0 <= level <= 3, "level must be 0 to 3")
80
+ check(version in (None, 0, 1), "version must be 0 or 1")
81
+ return _core.encode_vertex_buffer(data, count, size, level, -1 if version is None else version)
82
+
83
+
84
+ def encode_index_buffer(indices: np.ndarray) -> bytes:
85
+ indices = index32(indices)
86
+ check(len(indices) % 3 == 0, "triangles need indices in threes")
87
+ return _core.encode_index_buffer(indices)
88
+
89
+
90
+ def encode_index_sequence(indices: np.ndarray) -> bytes:
91
+ return _core.encode_index_sequence(index32(indices))
92
+
93
+
94
+ def encode_gltf_buffer(data: np.ndarray, mode: Mode, version: int = 0) -> bytes:
95
+ """Data as EXT_meshopt_compression stores it, for a bufferView of the mode."""
96
+ check(mode in ("ATTRIBUTES", "TRIANGLES", "INDICES"), "mode must be ATTRIBUTES, TRIANGLES or INDICES")
97
+ if mode == "ATTRIBUTES":
98
+ return encode_vertex_buffer_level(data, 2, version)
99
+ return encode_index_buffer(data) if mode == "TRIANGLES" else encode_index_sequence(data)
100
+
101
+
102
+ def encode_filter(filter: str, values: np.ndarray, stride: int, bits: int, mode: int = 0, width: int = 4) -> np.ndarray:
103
+ values = np.ascontiguousarray(values, dtype=np.float32)
104
+ check(values.ndim == 2 and values.shape[1] == width, f"values must be n by {width}")
105
+ return _core.encode_filter(filter, values.reshape(-1), len(values), stride, bits, mode).reshape(-1, stride)
106
+
107
+
108
+ def encode_filter_oct(values: np.ndarray, stride: int, bits: int) -> np.ndarray:
109
+ """Unit vectors, n by 4 with w kept as a sign, in octahedral form: n rows of stride bytes."""
110
+ check(stride in (4, 8), "stride must be 4 or 8")
111
+ check(2 <= bits <= 16, "bits must be 2 to 16")
112
+ return encode_filter("OCTAHEDRAL", values, stride, bits)
113
+
114
+
115
+ def encode_filter_quat(values: np.ndarray, stride: int, bits: int) -> np.ndarray:
116
+ """Unit quaternions, n by 4, in their smallest three components: n rows of 8 bytes."""
117
+ check(stride == 8, "stride must be 8")
118
+ check(4 <= bits <= 16, "bits must be 4 to 16")
119
+ return encode_filter("QUATERNION", values, stride, bits)
120
+
121
+
122
+ def encode_filter_exp(values: np.ndarray, bits: int, mode: ExpMode = "SharedVector") -> np.ndarray:
123
+ """Floats, n by k, as a mantissa of the bits and an exponent each, shared as the mode says: n rows of 4k bytes."""
124
+ check(mode in EXP_MODES, "mode must be Separate, SharedVector, SharedComponent or Clamped")
125
+ check(1 <= bits <= 24, "bits must be 1 to 24")
126
+ values = np.asarray(values)
127
+ check(values.ndim == 2 and values.shape[1] > 0, "values must be n by k")
128
+ return encode_filter("EXPONENTIAL", values, 4 * values.shape[1], bits, EXP_MODES[mode], values.shape[1])
129
+
130
+
131
+ def encode_filter_color(values: np.ndarray, stride: int, bits: int) -> np.ndarray:
132
+ """Colours, n by 4 from 0 to 1, as YCoCg with alpha: n rows of stride bytes."""
133
+ check(stride in (4, 8), "stride must be 4 or 8")
134
+ check(2 <= bits <= 16, "bits must be 2 to 16")
135
+ return encode_filter("COLOR", values, stride, bits)
136
+
137
+
138
+ def source(data: bytes | np.ndarray) -> np.ndarray:
139
+ return np.frombuffer(data, dtype=np.uint8) if isinstance(data, (bytes, bytearray, memoryview)) else np.ascontiguousarray(data, dtype=np.uint8).reshape(-1)
140
+
141
+
142
+ def decode_vertex_buffer(data: bytes, count: int, size: int, filter: Filter | None = None) -> np.ndarray:
143
+ """Encoded rows back as count rows of size bytes, the filter undone if one is named."""
144
+ check(0 < size <= 256 and size % 4 == 0, "a row must be 4 to 256 bytes, in fours")
145
+ check(filter in (None, "NONE", "OCTAHEDRAL", "QUATERNION", "EXPONENTIAL", "COLOR"), "filter must be NONE, OCTAHEDRAL, QUATERNION, EXPONENTIAL or COLOR")
146
+ check(filter not in ("OCTAHEDRAL", "COLOR") or size in (4, 8), "this filter's rows are 4 or 8 bytes")
147
+ check(filter != "QUATERNION" or size == 8, "quaternion rows are 8 bytes")
148
+ return _core.decode_vertex_buffer(source(data), count, size, filter).reshape(count, size)
149
+
150
+
151
+ def decode_index_buffer(data: bytes, count: int, size: int) -> np.ndarray:
152
+ """Encoded triangles back as count indices of size bytes, 2 or 4."""
153
+ check(size in (2, 4), "size must be 2 or 4")
154
+ check(count % 3 == 0, "triangles need indices in threes")
155
+ return _core.decode_index_buffer(source(data), count, size).view(np.uint16 if size == 2 else np.uint32)
156
+
157
+
158
+ def decode_index_sequence(data: bytes, count: int, size: int) -> np.ndarray:
159
+ check(size in (2, 4), "size must be 2 or 4")
160
+ return _core.decode_index_sequence(source(data), count, size).view(np.uint16 if size == 2 else np.uint32)
161
+
162
+
163
+ def decode_gltf_buffer(data: bytes, count: int, size: int, mode: Mode, filter: Filter | None = None) -> np.ndarray:
164
+ """An EXT_meshopt_compression bufferView back as plain data: rows of bytes, or indices."""
165
+ check(mode in ("ATTRIBUTES", "TRIANGLES", "INDICES"), "mode must be ATTRIBUTES, TRIANGLES or INDICES")
166
+ if mode == "ATTRIBUTES":
167
+ return decode_vertex_buffer(data, count, size, filter)
168
+ return decode_index_buffer(data, count, size) if mode == "TRIANGLES" else decode_index_sequence(data, count, size)
@@ -0,0 +1,164 @@
1
+ // meshoptimizer's C functions, each called once on whole arrays with Python's lock let go. The checks that keep a call
2
+ // in bounds are here; what makes a call sensible, as the JavaScript module asserts it, is in python/meshopt.
3
+
4
+ #include <nanobind/nanobind.h>
5
+ #include <nanobind/ndarray.h>
6
+ #include <nanobind/stl/optional.h>
7
+ #include <nanobind/stl/pair.h>
8
+ #include <nanobind/stl/string.h>
9
+
10
+ #include <algorithm>
11
+ #include <cstdint>
12
+ #include <optional>
13
+ #include <string>
14
+ #include <vector>
15
+
16
+ #include "../vendor/meshoptimizer/meshoptimizer.h"
17
+
18
+ namespace nb = nanobind;
19
+
20
+ template <typename T> using In = nb::ndarray<const T, nb::ndim<1>, nb::c_contig, nb::device::cpu>;
21
+ template <typename T> using Out = nb::ndarray<nb::numpy, T, nb::ndim<1>>;
22
+
23
+ template <typename T> Out<T> out(std::vector<T>&& values)
24
+ {
25
+ auto* held = new std::vector<T>(std::move(values));
26
+ nb::capsule owner(held, [](void* p) noexcept { delete static_cast<std::vector<T>*>(p); });
27
+ return Out<T>(held->data(), {held->size()}, owner);
28
+ }
29
+
30
+ static void sized(const char* name, size_t length, size_t count, size_t size)
31
+ {
32
+ if (size != 0 && count > SIZE_MAX / size)
33
+ throw nb::value_error("too many values");
34
+ if (count * size != length)
35
+ throw nb::value_error((std::string(name) + " holds " + std::to_string(length) + " values, not " + std::to_string(count) + " by " + std::to_string(size)).c_str());
36
+ }
37
+
38
+ static size_t vertices(const unsigned int* indices, size_t count)
39
+ {
40
+ return count ? size_t(*std::max_element(indices, indices + count)) + 1 : 0;
41
+ }
42
+
43
+ // Triangles reordered for a GPU's vertex cache, as a strip likes them when strip is set.
44
+ static Out<uint32_t> optimize_vertex_cache(In<uint32_t> indices, bool strip)
45
+ {
46
+ std::vector<uint32_t> result(indices.size());
47
+ {
48
+ nb::gil_scoped_release release;
49
+ size_t count = vertices(indices.data(), indices.size());
50
+ if (strip)
51
+ meshopt_optimizeVertexCacheStrip(result.data(), indices.data(), indices.size(), count);
52
+ else
53
+ meshopt_optimizeVertexCache(result.data(), indices.data(), indices.size(), count);
54
+ }
55
+ return out(std::move(result));
56
+ }
57
+
58
+ // Each vertex's new place, in the order the indices first use them, and how many are used.
59
+ static std::pair<Out<uint32_t>, size_t> optimize_vertex_fetch_remap(In<uint32_t> indices)
60
+ {
61
+ std::vector<uint32_t> remap;
62
+ size_t unique;
63
+ {
64
+ nb::gil_scoped_release release;
65
+ remap.resize(vertices(indices.data(), indices.size()));
66
+ unique = meshopt_optimizeVertexFetchRemap(remap.data(), indices.data(), indices.size(), remap.size());
67
+ }
68
+ return {out(std::move(remap)), unique};
69
+ }
70
+
71
+ // The points' order along a space-filling curve.
72
+ static Out<uint32_t> spatial_sort_remap(In<float> positions, size_t count, size_t stride)
73
+ {
74
+ sized("positions", positions.size(), count, stride);
75
+ if (stride < 3)
76
+ throw nb::value_error("a point has three coordinates at least");
77
+ std::vector<uint32_t> remap(count);
78
+ {
79
+ nb::gil_scoped_release release;
80
+ meshopt_spatialSortRemap(remap.data(), positions.data(), count, stride * sizeof(float));
81
+ }
82
+ return out(std::move(remap));
83
+ }
84
+
85
+ static nb::bytes encode_vertex_buffer(In<uint8_t> data, size_t count, size_t size, int level, int version)
86
+ {
87
+ sized("data", data.size(), count, size);
88
+ std::vector<unsigned char> buffer;
89
+ {
90
+ nb::gil_scoped_release release;
91
+ buffer.resize(meshopt_encodeVertexBufferBound(count, size));
92
+ buffer.resize(meshopt_encodeVertexBufferLevel(buffer.data(), buffer.size(), data.data(), count, size, level, version));
93
+ }
94
+ return nb::bytes(buffer.data(), buffer.size());
95
+ }
96
+
97
+ template <size_t (*Bound)(size_t, size_t), size_t (*Encode)(unsigned char*, size_t, const unsigned int*, size_t)>
98
+ static nb::bytes encode_indices(In<uint32_t> indices)
99
+ {
100
+ std::vector<unsigned char> buffer;
101
+ {
102
+ nb::gil_scoped_release release;
103
+ buffer.resize(Bound(indices.size(), vertices(indices.data(), indices.size())));
104
+ buffer.resize(Encode(buffer.data(), buffer.size(), indices.data(), indices.size()));
105
+ }
106
+ return nb::bytes(buffer.data(), buffer.size());
107
+ }
108
+
109
+ // Floats in a filter's form: count elements of stride bytes, each read from four floats, or stride / 4 for exponents.
110
+ static Out<uint8_t> encode_filter(const std::string& filter, In<float> data, size_t count, size_t stride, int bits, int mode)
111
+ {
112
+ sized("data", data.size(), count, filter == "EXPONENTIAL" ? stride / 4 : 4);
113
+ std::vector<uint8_t> result(count * stride);
114
+ {
115
+ nb::gil_scoped_release release;
116
+ if (filter == "OCTAHEDRAL")
117
+ meshopt_encodeFilterOct(result.data(), count, stride, bits, data.data());
118
+ else if (filter == "QUATERNION")
119
+ meshopt_encodeFilterQuat(result.data(), count, stride, bits, data.data());
120
+ else if (filter == "EXPONENTIAL")
121
+ meshopt_encodeFilterExp(result.data(), count, stride, bits, data.data(), meshopt_EncodeExpMode(mode));
122
+ else
123
+ meshopt_encodeFilterColor(result.data(), count, stride, bits, data.data());
124
+ }
125
+ return out(std::move(result));
126
+ }
127
+
128
+ template <int (*Decode)(void*, size_t, size_t, const unsigned char*, size_t)>
129
+ static Out<uint8_t> decode(In<uint8_t> source, size_t count, size_t size, std::optional<std::string> filter)
130
+ {
131
+ if (size != 0 && count > SIZE_MAX / size)
132
+ throw nb::value_error("too many values");
133
+ std::vector<uint8_t> result(count * size);
134
+ int status;
135
+ {
136
+ nb::gil_scoped_release release;
137
+ status = Decode(result.data(), count, size, source.data(), source.size());
138
+ if (status == 0 && filter == "OCTAHEDRAL")
139
+ meshopt_decodeFilterOct(result.data(), count, size);
140
+ else if (status == 0 && filter == "QUATERNION")
141
+ meshopt_decodeFilterQuat(result.data(), count, size);
142
+ else if (status == 0 && filter == "EXPONENTIAL")
143
+ meshopt_decodeFilterExp(result.data(), count, size);
144
+ else if (status == 0 && filter == "COLOR")
145
+ meshopt_decodeFilterColor(result.data(), count, size);
146
+ }
147
+ if (status != 0)
148
+ throw nb::value_error(("Malformed buffer data: " + std::to_string(status)).c_str());
149
+ return out(std::move(result));
150
+ }
151
+
152
+ NB_MODULE(_core, m)
153
+ {
154
+ m.def("optimize_vertex_cache", &optimize_vertex_cache);
155
+ m.def("optimize_vertex_fetch_remap", &optimize_vertex_fetch_remap);
156
+ m.def("spatial_sort_remap", &spatial_sort_remap);
157
+ m.def("encode_vertex_buffer", &encode_vertex_buffer);
158
+ m.def("encode_index_buffer", &encode_indices<meshopt_encodeIndexBufferBound, meshopt_encodeIndexBuffer>);
159
+ m.def("encode_index_sequence", &encode_indices<meshopt_encodeIndexSequenceBound, meshopt_encodeIndexSequence>);
160
+ m.def("encode_filter", &encode_filter);
161
+ m.def("decode_vertex_buffer", &decode<meshopt_decodeVertexBuffer>, nb::arg("source"), nb::arg("count"), nb::arg("size"), nb::arg("filter") = nb::none());
162
+ m.def("decode_index_buffer", [](In<uint8_t> source, size_t count, size_t size) { return decode<meshopt_decodeIndexBuffer>(source, count, size, std::nullopt); });
163
+ m.def("decode_index_sequence", [](In<uint8_t> source, size_t count, size_t size) { return decode<meshopt_decodeIndexSequence>(source, count, size, std::nullopt); });
164
+ }