Skip to content

MeshData: build attributes from mdspans - #23

Merged
mtao merged 7 commits into
feature/controls-modelfrom
refactor/mesh-attribute-mdspan
Oct 13, 2026
Merged

mtao merged 7 commits into
feature/controls-modelfrom
refactor/mesh-attribute-mdspan

Conversation

@mtao

@mtao mtao commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stacked on feature/controls-model. Depends on mtao/zipper#41, which is now merged. subprojects/zipper.wrap is back on main.

Changes

  • MeshAttribute built from an mdspan. It takes a rank-1 mdspan (one scalar per element) or a rank-2 mdspan with extents (components, elements), with any arithmetic type, layout (including layout_stride) or accessor. Values are stored in a column-major zipper DataArray (plain data with no arithmetic semantics), which is the only source of the shape: component_count() and count() read it, so there are no separately kept counters.
  • Values keep their type. Storage is one of float, double, int32 or uint32, held in a std::variant. Narrower integers widen to the 32-bit type of the same sign (e.g. uint8 becomes uint32). Access is through type(), holds<T>(), view<T>() / view<T, D>() (std::optional, empty on a type or component-count mismatch) and visit(f). The conversion goes through a zipper cast<T>().
  • Typed views report why they fail. MeshAttribute::view<T, D>() and RoleBinding::view<T, D>() return std::expected<view, ViewError>. The error says whether the binding was Unbound, the values have the WrongType (with the actual stored type), or the view had the WrongComponentCount (with the actual count).
  • Bindings keep the attribute's type. A RoleBinding views either the whole attribute, one selected component (a strided view into the attribute, with no copy), or the per-element magnitude. The magnitude is computed into a derived attribute: float stays float, and other types become double. type() and visit(f) expose the bound values in their own type.
  • Float conversion happens at upload. VulkanMeshDrawable uploads contiguous float bindings in place and converts anything else into a staging buffer. normalize_mesh works in float from any stored type.
  • MeshData::add_attribute(name, domain, mdspan) rejects shapes with 0 or more than 255 components. The flat contiguous-range overload is now a thin adapter, and it rejects data whose length isn't a multiple of the component count. Before, such data was added with only a warning.
  • Stride and ordering fixes. View extents are (components, count). Scalar extraction now uses zipper: row(component) for one component and colwise().norm() for the magnitude. The previous code used a squared rowwise() norm, which reduced over the wrong axis.
  • mesh_conversion passes the OBJ vertex and triangle matrices directly via as_mdspan(), which removes flatten_columns.
  • Property::Vec3Value::min uses numeric_limits<float>::lowest(). min() is the smallest positive float, so negative values were blocked.

Tests

  • New: mesh_attribute_from_mdspan covers column- and row-major input, type preservation (double, int32, and uint8 widened to uint32), view<T, D>, rejection of bad shapes, and strided (interleaved-buffer) input.
  • New: role_binding_keeps_attribute_type covers a double position binding viewing the attribute in place, a strided uint32 component view, a double magnitude, and visit(). role_binding_view checks each ViewError reason.
  • New: scalar_binding_magnitude.
  • meson test: 13/13 suites pass. test_scene_graph: 476 assertions in 58 test cases.

mtao added 7 commits October 12, 2026 18:16
- MeshAttribute is constructed from a rank-1 or rank-2 (components x
  elements) mdspan of any arithmetic type, layout and accessor; it derives
  component_count()/count() from the shape and stores column-major float
  (element-major memory).  Data is private: data(), view(), view<D>().
- MeshData::add_attribute takes an mdspan; the flat-range overload is a
  thin adapter and rejects sizes that are not a multiple of the component
  count.
- Fix view extents order ((components, count), not (count, components));
  view() is always valid (possibly empty), view<D>() is optional.
- Scalar extraction uses zipper: row(component) or colwise().norm()
  (previously rowwise squared norm over the wrong axis).
- mesh_conversion passes OBJ vertex/triangle matrices as mdspans directly.
- Property: Vec3Value min uses numeric_limits::lowest() (min() is the
  smallest positive float).
- Point zipper.wrap at refactor/defer-mdspan-accessor (zipper#41) for
  const as_mdspan().
zipper#41 now honors mdspan layouts (incl. layout_stride) and accessors, so
the hand-written copy loop and memcpy fast path become a zipper cast
assignment.  Adds a strided (interleaved buffer) input test.
- Store values in a zipper column-major matrix (components x count) per
  element type, held in a variant; component_count()/count() come from the
  matrix instead of separately kept counters.
- Input keeps its type, widened to float/double/int32/uint32 (e.g. uint8 ->
  uint32); type()/holds<T>()/view<T[, D]>()/visit(f) replace the float-only
  data()/view().
- RoleBinding records its own component count and converts non-float
  attributes into its float buffer (also used for scalar component and
  magnitude extraction); raw_data() returns const float *, and
  is_converted() reports whether it points at a copy.
Attribute values are plain data, not a linear-algebra matrix: use zipper's
DataArray (no arithmetic semantics) instead of ColVectors.  Bindings still
wrap views in MatrixBase for the float conversion and magnitudes.  Note
DataArray's LeftMajor = true is row-major, so column-major storage is
DataArray_<T, dextents<2>, false>.
Replace the nested conditional_t with a trait specialized per supported
input type (float, double, signed/unsigned integers up to 32 bits); its
missing ::type for other types drives an AttributeValue concept that
constrains the MeshAttribute constructor and MeshData::add_attribute
instead of a static_assert.
- MeshAttribute::view<T, D>() and RoleBinding::view<T, D>() return
  std::expected<view, ViewError> (Unbound / WrongType /
  WrongComponentCount, with the actual type and component count).
- RoleBinding no longer converts to float: it views the whole attribute,
  a selected component (strided view, no copy), or the magnitude (derived
  attribute; float stays float, other types become double).  type() and
  visit(f) expose the bound values in their own type.
- VulkanMeshDrawable converts to float at upload (contiguous float data is
  uploaded in place); normalize_mesh computes in float from any type.
@mtao
mtao merged commit 79101d1 into feature/controls-model Oct 13, 2026
2 checks passed
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.

1 participant