Skip to content

partition: Implement followup spatial partition merging - #964

Merged
zeux merged 12 commits into
masterfrom
part-spatial
Oct 4, 2025
Merged

zeux merged 12 commits into
masterfrom
part-spatial

Conversation

@zeux

@zeux zeux commented Oct 4, 2025

Copy link
Copy Markdown
Owner

After we're done with topological merging, we may have groups that are
too small because we only consider topological adjacency when picking
merge candidates.

To avoid downstream issues from the resulting small partitions, we now
automatically merge the resulting groups further - using the same logic
where we don't try to grow groups that are already >= target unless
another group picks them as the target - using spatial partitioning.

For that, we build a tree using spatial splits until the leaves are
reasonably small, and then do pairwise merging within the leaves, using
existing bounds scoring for prioritization.

This significantly improves the partition sizes for meshes with many
disconnected clusters which can naturally result during DAG
construction, and makes it match the target much closer. To disable it,
vertex_positions = NULL can be passed (which will disable spatial
proximity scoring as well but it's much less consequential so pretty
safe to disable).

When using clodBuild, spatial partitioning is on by default but can
be disabled via clodConfig::partition_spatial.

This contribution is sponsored by Valve.

zeux added 9 commits October 3, 2025 12:29
This helps us validate the partitioning results at a glance.
Instead of using a separate array for bounds, move this data into
ClusterGroup. This makes it more obvious that the bounds data refers to
updated group bounds as the algorithm is merging groups, and makes it
easier to debug.

These were initially separate to ensure that the cost for bounds is nil
when unused, but since the algorithm is going to use bounds-only merging
as a post-process in the future, the main and preferred mode of
operation will involve included positions and bounds, so it makes less
sense to separate.

The performance effect of this change seems to be below the measurement
noise threshold when bounds are unused regardless.
When set (defaults to true), this corresponds to passing the vertex
positions to partitioning algorithm. This matches what we're doing right
now; setting it to false may be useful to understand the impact spatial
grouping has.

Note that right now, meshopt_partitionClusters behavior is similar
between having position data and not; however, that's going to change
soon and the effect will be more meaningful. For best DAG quality it's
recommented to keep this as true, hence it's true in the default
configuration.

Also slightly trim the partition size reservation to reflect the
maximum.
This is helpful to understand the corner cases in partitioning where
individual clusters have no connections to any other clusters and aren't
currently grouped.
After we're done with topological merging, we may have groups that are
too small because we only consider topological adjacency when picking
merge candidates.

To avoid downstream issues from the resulting small partitions, we now
automatically merge the resulting groups further - using the same logic
where we don't try to grow groups that are already >= target unless
another group picks them as the target - using spatial partitioning.

For that, we build a tree using spatial splits until the leaves are
reasonably small, and then do pairwise merging within the leaves, using
existing bounds scoring for prioritization.

This significantly improves the partition sizes for meshes with many
disconnected clusters which can naturally result during DAG
construction, and makes it match the target much closer. To disable it,
vertex_positions = NULL can be passed (which will disable spatial
proximity scoring as well but it's much less consequential so pretty
safe to disable).
For topological merging, because the merged group ends up back in the
heap, the merging process is symmetrical (modulo bounds based scoring
where the order matters a bit). However, since in the spatial merging we
only evaluate each group once as a first group, it's important that
first group gets merged into the second one. That way, we retain the
ability to merge more groups into the same one, which increases our
chances of reaching the target.
Use the same tail computation for both merging procedures; it's a little
cleaner compared to doing the assignment in the last loop iteration and
breaking out.
Note that if vertex positions are not specified, disconnected clusters
will not be merged.
The new test validates that we don't merge disconnected clusters without
spatial information, and merge the two closer ones together when we have
it.
@zeux

zeux commented Oct 4, 2025 •

Copy link
Copy Markdown
Owner Author

Aggregate data on partition sizes (averaged per LOD) on ~80 meshes with a target partition size of 16:

image

Zorah stats for partition size 24 cluster size 128:

  • raster before 83.6 tri/cl 2.5 cl/g, after 110.5 tri/cl 23.6 cl/g
  • RT before 93.0 tri/cl 10.5 cl/g, after 96.1 tri/cl 24.3 cl/g.

Meshes like this would be particularly subject to this in raster mode because the clusterizer would return clusters with several individual leaves, with each leaf belonging to one cluster, which after a few levels of decimation would result in singleton cluster groups:

Screenshot From 2025-10-03 23-26-20

@zeux
zeux marked this pull request as ready for review October 4, 2025 18:10
zeux added 3 commits October 4, 2025 12:40
Instead of using the bounding box size, which is a bit more prone to be
affected by one-off outliers, use variance to pick the axis; Welford's
online algorithm is used to compute both mean & variance in one pass.

The precision of computing mean is also higher compared to our old naive
computation as it doesn't suffer from catastrophic cancellation as much.

This is the same code we use in the clusterizer to build the kD-tree;
using the same approach should hopefully mean we have the same
properties between the two.
Elsewhere in the codebase we use int axis, so switch to int here as
well. The axis storage inside the KD nodes has to stay as unsigned int
as it's a bitfield.
Document the fact that absence of vertex positions can result in small
groups when the input clusters don't share vertices. Also document
partition sizing more explicitly based on current implementation
details.
@zeux
zeux merged commit 67b720d into master Oct 4, 2025
13 checks passed
@zeux
zeux deleted the part-spatial branch October 4, 2025 23: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.

1 participant