Skip to content

Add clay_mesh_from_arrays: a mesh constructor that keeps uvs, normals and colours (#661) - #685

Merged
leonardoaraujosantos merged 2 commits into
mainfrom
feat/661-mesh-from-arrays
Oct 4, 2026
Merged

leonardoaraujosantos merged 2 commits into
mainfrom
feat/661-mesh-from-arrays

Conversation

@leonardoaraujosantos

Copy link
Copy Markdown
Contributor

Why

clay_mesh_from_triangles and clay_mesh_from_quads copy positions and indices and nothing else. A host that computed its own per-vertex attributes had no constructor that keeps them. ClaySpaceDesktop's retopology comes back from CyberRemesher with a UV layout (ClaySpaceDesktop#213), and the only entry point that attached uvs was the OBJ reader. So the host wrote vertex-aligned v/vn/vt text into memory and parsed it back through clay_mesh_load_memory(..., "obj", ...): a text round trip standing in for a copy.

Nothing below the boundary was missing. mesh::Mesh already holds normals / colors / uvs, each empty or vertex-aligned. clay_mesh_normals / _colors / _uvs already read them, and mesh_stream already writes them into a document. Only the constructor was missing, so there is no data-model or format change.

What lands

  • clay_mesh_arrays + clay_mesh_from_arrays (bindings/c/clay.h). The descriptor has a leading struct_size and is laid out as the issue sketched: positions/vertex_count, normals, colors, uvs (each NULL or vertex-aligned), indices/index_count, quad_indices/quad_index_count.
    • Exactly one index kind. Triangles are taken as given. Quads derive the triangles by the (a,b,c),(a,c,d) rule clay_mesh_from_quads uses, so quads_consistent holds by construction. A kind counts as supplied by its pointer or its count, so a count without its pointer is refused, not ignored.
    • A NULL attribute leaves it absent. A supplied one is copied for vertex_count entries, bit-exactly.
    • CLAY_ERROR_INVALID_ARGUMENT with out_mesh left NULL for: NULL descriptor/out, struct_size below the layout, NULL positions, zero vertices, neither or both index kinds, non-whole triangle/quad counts, an index past the vertices.
    • The header says what the call does not validate: attribute lengths (a pointer cannot carry one, the same contract positions already had) and attribute values (no renormalising, clamping, wrapping or NaN rejection).
  • One copy path (bindings/c/clay_c.cpp). start_mesh, take_triangles, take_quads and take_attributes are shared. from_triangles and from_quads are rebuilt on them and behave as before. The only visible difference is that the clay_last_error text for a NULL pointer now names the one argument that was NULL. Every function is a handful of branches, well inside the backend complexity target.
  • OpenSpec openspec/changes/add-mesh-from-arrays (c-abi ADDED requirement, proposal, design, tasks).
  • Docs: docs/08-mesh-readback.md lists the constructor in the producer, ownership and attribute tables, with a C example in the rebuild section.

ABI 0.123.0 -> 0.124.0 (CMakeLists.txt, clay.h, pyproject.toml). Additive.

What building it found

  • The helpers sit inside clay_c.cpp's extern "C" block, where returning a std::vector is -Wreturn-type-c-linkage under -Werror. They take out-parameters instead.
  • pyclay is not extended. check_binding_parity.py runs pyclay -> C, so a C-only entry point passes it, but pyclay has the same gap: Mesh.normals/colors/uvs are read-only views and Mesh.from_triangles/_from_quads take positions and indices only. The host that asked is a C host. A Mesh.from_arrays would be checked against this entry point by the existing clay_mesh_ prefix rule; it is left as a follow-up.

Regression test

tests/unit/test_c_mesh_from_arrays.cpp (5 cases, 91 assertions):

  • uvs, normals and colours come back bit-exactly (memcmp, not a tolerance). Values are chosen so a dropped, reordered or renormalised channel cannot pass.
  • NULL attributes stay absent, and uvs alone works.
  • A quad list gives the same indices as clay_mesh_from_quads and keeps its quads.
  • The attributes survive a mesh-layer attach, an undo/redo of the layer creation, and a .clayspace save and load.
  • Every refusal returns INVALID_ARGUMENT and clears out_mesh: NULL positions, zero vertices, a bad triangle or quad count, an out-of-range triangle index or quad corner, both kinds, neither kind, a count without its pointer, and struct_size 4 and 0.

Proof it gates:

  • On unfixed main, the test does not compile (unknown type name 'clay_mesh_arrays'): the capability did not exist.
  • Mutant with the attribute copy dropped (take_attributes commented out): 4 of 5 cases fail, 16 assertions.
  • Mutant accepting both index kinds: the refusal case fails, 2 assertions.

Verification

  • cmake --preset cpu-only -DCLAY_BUILD_TESTS=ON -DCLAY_BUILD_PYTHON=ON, full build, ctest: 11/11 passed (the four unit shards, shard partition, pyclay pytest).
  • GCC 16 -Wall -Wextra -Wpedantic -Wshadow -Werror -fno-exceptions -fno-rtti syntax check of bindings/c/clay_c.cpp and the new test: clean (exit codes read directly, not through a pipe).
  • npx @fission-ai/openspec@1.12.0 validate --all --strict: 77/77.
  • tools/release_check.py --skip-slow: version, configure, build, tests, parity, layering, dialect, licenses, task-symbols, bindings (imported .../build/release/bindings/python/pyclay...so, a real check), kernels, abi (hygiene + ctypes FFI) and openspec all pass. The rows still red are the release-time hardware rows: device (the engine changed since the last device run) and the four hardware/* waivers, which are stale against include/clay/eval/bake_volume.h, a file this PR does not touch.

Closes #661

)

clay_mesh_from_triangles and _from_quads copy positions and indices
only, so a host with its own uvs, normals or colours had to round-trip
them through OBJ text via clay_mesh_load_memory. clay_mesh_arrays takes
them directly, each NULL or vertex-aligned, with exactly one of a
triangle or a quad index list; quads derive the triangles by the rule
from_quads already uses.

The three constructors now share one position/triangle/quad path.
ABI 0.123.0 -> 0.124.0.
UVs, normals and colours read back bit-exactly and survive a mesh-layer
attach, undo/redo and a save/load; NULL attributes stay absent; quads
match from_quads; each malformed call is INVALID_ARGUMENT with out_mesh
left NULL.
@leonardoaraujosantos
leonardoaraujosantos merged commit 64c5ecf into main Oct 4, 2026
16 checks passed
@leonardoaraujosantos
leonardoaraujosantos deleted the feat/661-mesh-from-arrays branch October 4, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A mesh constructor that accepts vertex-aligned UVs, normals and colours

1 participant