|
87 | 87 | BETA_THRESHOLD = 0.0001 # 0.1mm |
88 | 88 |
|
89 | 89 | VALUES_PER_KEY = NUM_SPATIAL_DIRECTIONS + 1 |
| 90 | +PAIRS_PER_KEY = VALUES_PER_KEY * (VALUES_PER_KEY - 1) // 2 |
| 91 | +EXPORT_REDUCED_CONTACTS_BLOCK_DIM = 32 |
| 92 | +EXPORT_REDUCED_CONTACTS_THREAD_BUDGET_MULTIPLIER = 4 |
90 | 93 |
|
91 | 94 | # Open-addressed linear probing gets expensive at high load and failed inserts |
92 | 95 | # scan the whole table. |
@@ -142,16 +145,20 @@ def _contacts_are_numerically_equivalent( |
142 | 145 | """Return whether two buffered contacts differ only by float rounding.""" |
143 | 146 | pd_a = position_depth[contact_a] |
144 | 147 | pd_b = position_depth[contact_b] |
| 148 | + if not _floats_are_near_ulps(pd_a[0], pd_b[0]): |
| 149 | + return False |
| 150 | + if not _floats_are_near_ulps(pd_a[1], pd_b[1]): |
| 151 | + return False |
| 152 | + if not _floats_are_near_ulps(pd_a[2], pd_b[2]): |
| 153 | + return False |
| 154 | + if not _floats_are_near_ulps(pd_a[3], pd_b[3]): |
| 155 | + return False |
| 156 | + |
145 | 157 | n_a = normal[contact_a] |
146 | 158 | n_b = normal[contact_b] |
147 | | - return ( |
148 | | - _floats_are_near_ulps(pd_a[0], pd_b[0]) |
149 | | - and _floats_are_near_ulps(pd_a[1], pd_b[1]) |
150 | | - and _floats_are_near_ulps(pd_a[2], pd_b[2]) |
151 | | - and _floats_are_near_ulps(pd_a[3], pd_b[3]) |
152 | | - and _floats_are_near_ulps(n_a[0], n_b[0]) |
153 | | - and _floats_are_near_ulps(n_a[1], n_b[1]) |
154 | | - ) |
| 159 | + if not _floats_are_near_ulps(n_a[0], n_b[0]): |
| 160 | + return False |
| 161 | + return _floats_are_near_ulps(n_a[1], n_b[1]) |
155 | 162 |
|
156 | 163 |
|
157 | 164 | @wp.func |
@@ -1578,165 +1585,154 @@ def write_contact_to_reducer( |
1578 | 1585 | ) |
1579 | 1586 |
|
1580 | 1587 |
|
1581 | | -def create_export_reduced_contacts_kernel(writer_func: Any): |
1582 | | - """Create a kernel that exports reduced contacts using a custom writer function. |
1583 | | -
|
1584 | | - The kernel processes one hashtable ENTRY per thread (not one value slot). |
1585 | | - Each entry has VALUES_PER_KEY value slots (``NUM_SPATIAL_DIRECTIONS`` spatial + 1 max-depth). |
1586 | | - The thread reads all slots, collects unique contact IDs, and exports each |
1587 | | - unique contact once. |
| 1588 | +@wp.func |
| 1589 | +def _roundoff_duplicate_bit_for_slot_pair( |
| 1590 | + pair_idx: int, |
| 1591 | + entry_idx: int, |
| 1592 | + ht_capacity: int, |
| 1593 | + ht_values: wp.array[wp.uint64], |
| 1594 | + position_depth: wp.array[wp.vec4], |
| 1595 | + normal: wp.array[wp.vec2], |
| 1596 | + contact_fingerprints: wp.array[wp.int32], |
| 1597 | + deterministic: int, |
| 1598 | +) -> int: |
| 1599 | + """Return the slot bit to suppress for one pair, or zero.""" |
| 1600 | + slot_b = int(1) |
| 1601 | + while pair_idx >= slot_b: |
| 1602 | + pair_idx = pair_idx - slot_b |
| 1603 | + slot_b = slot_b + 1 |
| 1604 | + slot_a = pair_idx |
| 1605 | + |
| 1606 | + value_a = ht_values[slot_a * ht_capacity + entry_idx] |
| 1607 | + value_b = ht_values[slot_b * ht_capacity + entry_idx] |
| 1608 | + if value_a == wp.uint64(0) or value_b == wp.uint64(0): |
| 1609 | + return 0 |
| 1610 | + |
| 1611 | + contact_a = unpack_contact_id(value_a, deterministic) |
| 1612 | + contact_b = unpack_contact_id(value_b, deterministic) |
| 1613 | + if contact_a == contact_b: |
| 1614 | + return 0 |
| 1615 | + if not _contacts_are_numerically_equivalent(contact_a, contact_b, position_depth, normal): |
| 1616 | + return 0 |
| 1617 | + |
| 1618 | + if contact_fingerprints[contact_b] < contact_fingerprints[contact_a]: |
| 1619 | + return 1 << slot_a |
| 1620 | + return 1 << slot_b |
1588 | 1621 |
|
1589 | | - This naturally deduplicates: one thread handles one (shape_pair, bin) entry |
1590 | | - and can locally track which contact IDs it has already exported. |
1591 | 1622 |
|
1592 | | - Args: |
1593 | | - writer_func: A warp function with signature (ContactData, writer_data, int) -> None. |
1594 | | - The third argument is an output_index (-1 indicates the writer should allocate |
1595 | | - a new slot). This follows the same pattern as narrow_phase.py's write_contact_simple. |
| 1623 | +def create_export_reduced_contacts_kernel(writer_func: Any): |
| 1624 | + """Create a tiled kernel that exports globally reduced contacts. |
1596 | 1625 |
|
1597 | | - Returns: |
1598 | | - A warp kernel that can be launched to export reduced contacts. |
| 1626 | + One thread block processes each active hashtable entry. Its first 21 lanes |
| 1627 | + compare the seven possible winner pairs in parallel, preserving the |
| 1628 | + lower-fingerprint representative for roundoff-equivalent geometry. Lane |
| 1629 | + zero then streams the surviving unique contacts to the writer. |
1599 | 1630 | """ |
1600 | | - # Define vector type for tracking exported contact IDs |
1601 | 1631 | exported_ids_vec = wp.types.vector(length=VALUES_PER_KEY, dtype=wp.int32) |
1602 | | - |
1603 | 1632 | _module = f"export_reduced_contacts_{writer_func.__name__}" |
1604 | 1633 |
|
1605 | 1634 | @wp.kernel(enable_backward=False, module=_module) |
1606 | 1635 | def export_reduced_contacts_kernel( |
1607 | | - # Hashtable arrays |
1608 | 1636 | ht_keys: wp.array[wp.uint64], |
1609 | 1637 | ht_values: wp.array[wp.uint64], |
1610 | 1638 | ht_active_slots: wp.array[wp.int32], |
1611 | | - # Contact buffer arrays |
1612 | 1639 | position_depth: wp.array[wp.vec4], |
1613 | | - normal: wp.array[wp.vec2], # Octahedral-encoded |
| 1640 | + normal: wp.array[wp.vec2], |
1614 | 1641 | shape_pairs: wp.array[wp.vec2i], |
1615 | 1642 | contact_fingerprints: wp.array[wp.int32], |
1616 | | - # Global dedup flags: one int per buffer contact, for cross-entry deduplication |
1617 | 1643 | exported_flags: wp.array[wp.int32], |
1618 | | - # Shape data for extracting margin and effective radius |
1619 | 1644 | shape_types: wp.array[int], |
1620 | 1645 | shape_data: wp.array[wp.vec4], |
1621 | | - # Per-shape contact gaps |
1622 | 1646 | shape_gap: wp.array[float], |
1623 | | - # Writer data (custom struct) |
1624 | 1647 | writer_data: Any, |
1625 | | - # Grid stride parameters |
1626 | | - total_num_threads: int, |
1627 | | - # Packing mode (non-zero = deterministic 20-bit contact IDs) |
| 1648 | + total_num_blocks: int, |
| 1649 | + parallel_pairs: int, |
1628 | 1650 | deterministic: int, |
1629 | 1651 | ): |
1630 | | - """Export reduced contacts to the writer. |
1631 | | -
|
1632 | | - Uses grid stride loop to iterate over active hashtable ENTRIES. |
1633 | | - For each entry, reads all value slots, collects unique contact IDs, |
1634 | | - and exports each unique contact once. Uses atomic flags per contact_id |
1635 | | - for cross-entry deduplication (same contact winning multiple entries). |
1636 | | - """ |
1637 | | - tid = wp.tid() |
1638 | | - |
1639 | | - # Get number of active entries (stored at index = ht_capacity) |
| 1652 | + block_id, lane = wp.tid() |
1640 | 1653 | ht_capacity = ht_keys.shape[0] |
1641 | 1654 | num_active = ht_active_slots[ht_capacity] |
1642 | | - |
1643 | | - # Early exit if no active entries (fast path for empty work) |
1644 | | - if num_active == 0: |
1645 | | - return |
1646 | | - |
1647 | | - # Grid stride loop over active entries |
1648 | | - for i in range(tid, num_active, total_num_threads): |
1649 | | - # Get the hashtable entry index |
1650 | | - entry_idx = ht_active_slots[i] |
1651 | | - |
1652 | | - # Track exported contact IDs for this entry (intra-entry dedup) |
1653 | | - exported_ids = exported_ids_vec() |
1654 | | - num_exported = int(0) |
1655 | | - |
1656 | | - # Read all value slots for this entry (slot-major layout). |
1657 | | - for slot in range(wp.static(VALUES_PER_KEY)): |
1658 | | - value = ht_values[slot * ht_capacity + entry_idx] |
1659 | | - |
1660 | | - # Skip empty slots (value = 0) |
1661 | | - if value == wp.uint64(0): |
1662 | | - continue |
1663 | | - |
1664 | | - # Extract contact ID |
1665 | | - contact_id = unpack_contact_id(value, deterministic) |
1666 | | - |
1667 | | - # Skip if already exported within this entry |
1668 | | - if is_contact_already_exported(contact_id, exported_ids, num_exported): |
1669 | | - continue |
1670 | | - |
1671 | | - # Record this contact ID for intra-entry dedup |
1672 | | - exported_ids[num_exported] = contact_id |
1673 | | - num_exported = num_exported + 1 |
1674 | | - |
1675 | | - # Suppress roundoff-equivalent winners using the lower topology |
1676 | | - # fingerprint. This examines at most 21 pairs per seven-slot entry. |
1677 | | - duplicate_mask = int(0) |
1678 | | - for local_idx in range(num_exported): |
1679 | | - contact_id = exported_ids[local_idx] |
1680 | | - fingerprint = contact_fingerprints[contact_id] |
1681 | | - for other_idx in range(local_idx): |
1682 | | - other_id = exported_ids[other_idx] |
1683 | | - if _contacts_are_numerically_equivalent(contact_id, other_id, position_depth, normal): |
1684 | | - other_fingerprint = contact_fingerprints[other_id] |
1685 | | - if fingerprint < other_fingerprint: |
1686 | | - duplicate_mask = duplicate_mask | (1 << other_idx) |
1687 | | - else: |
1688 | | - duplicate_mask = duplicate_mask | (1 << local_idx) |
1689 | | - |
1690 | | - for local_idx in range(num_exported): |
1691 | | - if duplicate_mask & (1 << local_idx) != 0: |
1692 | | - continue |
1693 | | - |
1694 | | - contact_id = exported_ids[local_idx] |
1695 | | - fingerprint = contact_fingerprints[contact_id] |
1696 | | - |
1697 | | - # Cross-entry dedup: same contact can win slots in different entries |
1698 | | - # (e.g., normal-bin AND voxel entry). Atomic flag per contact_id. |
1699 | | - old_flag = wp.atomic_add(exported_flags, contact_id, 1) |
1700 | | - if old_flag > 0: |
1701 | | - continue |
1702 | | - |
1703 | | - # Unpack contact data |
1704 | | - position, contact_normal, depth = unpack_contact(contact_id, position_depth, normal) |
1705 | | - |
1706 | | - # Get shape pair |
1707 | | - pair = shape_pairs[contact_id] |
1708 | | - shape_a = pair[0] |
1709 | | - shape_b = pair[1] |
1710 | | - |
1711 | | - # Extract margin offsets from shape_data (stored in w component) |
1712 | | - margin_offset_a = shape_data[shape_a][3] |
1713 | | - margin_offset_b = shape_data[shape_b][3] |
1714 | | - |
1715 | | - # Compute effective radius for spheres, capsules, and cones |
1716 | | - radius_eff_a = compute_effective_radius(shape_types[shape_a], shape_data[shape_a]) |
1717 | | - radius_eff_b = compute_effective_radius(shape_types[shape_b], shape_data[shape_b]) |
1718 | | - |
1719 | | - # Use additive per-shape contact gap (matching broad/narrow phase) |
1720 | | - gap_a = shape_gap[shape_a] |
1721 | | - gap_b = shape_gap[shape_b] |
1722 | | - gap_sum = gap_a + gap_b |
1723 | | - |
1724 | | - # Create ContactData struct |
1725 | | - contact_data = ContactData() |
1726 | | - contact_data.contact_point_center = position |
1727 | | - contact_data.contact_normal_a_to_b = contact_normal |
1728 | | - contact_data.contact_distance = depth |
1729 | | - contact_data.radius_eff_a = radius_eff_a |
1730 | | - contact_data.radius_eff_b = radius_eff_b |
1731 | | - contact_data.margin_a = margin_offset_a |
1732 | | - contact_data.margin_b = margin_offset_b |
1733 | | - contact_data.shape_a = shape_a |
1734 | | - contact_data.shape_b = shape_b |
1735 | | - contact_data.gap_sum = gap_sum |
1736 | | - contact_data.sort_sub_key = fingerprint |
1737 | | - |
1738 | | - # Call the writer function |
1739 | | - writer_func(contact_data, writer_data, -1) |
| 1655 | + duplicate_bits = wp.tile_zeros(shape=wp.static(EXPORT_REDUCED_CONTACTS_BLOCK_DIM), dtype=int, storage="shared") |
| 1656 | + |
| 1657 | + for active_idx in range(block_id, num_active, total_num_blocks): |
| 1658 | + entry_idx = ht_active_slots[active_idx] |
| 1659 | + duplicate_bit = int(0) |
| 1660 | + |
| 1661 | + if parallel_pairs != 0: |
| 1662 | + if lane < wp.static(PAIRS_PER_KEY): |
| 1663 | + duplicate_bit = _roundoff_duplicate_bit_for_slot_pair( |
| 1664 | + lane, |
| 1665 | + entry_idx, |
| 1666 | + ht_capacity, |
| 1667 | + ht_values, |
| 1668 | + position_depth, |
| 1669 | + normal, |
| 1670 | + contact_fingerprints, |
| 1671 | + deterministic, |
| 1672 | + ) |
| 1673 | + elif lane == 0: |
| 1674 | + for pair_idx in range(wp.static(PAIRS_PER_KEY)): |
| 1675 | + duplicate_bit = duplicate_bit | _roundoff_duplicate_bit_for_slot_pair( |
| 1676 | + pair_idx, |
| 1677 | + entry_idx, |
| 1678 | + ht_capacity, |
| 1679 | + ht_values, |
| 1680 | + position_depth, |
| 1681 | + normal, |
| 1682 | + contact_fingerprints, |
| 1683 | + deterministic, |
| 1684 | + ) |
| 1685 | + |
| 1686 | + wp.tile_scatter_masked(duplicate_bits, lane, duplicate_bit, True) |
| 1687 | + duplicate_mask = wp.tile_reduce(wp.bit_or, duplicate_bits)[0] |
| 1688 | + |
| 1689 | + if lane == 0: |
| 1690 | + exported_ids = exported_ids_vec() |
| 1691 | + num_exported = int(0) |
| 1692 | + |
| 1693 | + for slot in range(wp.static(VALUES_PER_KEY)): |
| 1694 | + if duplicate_mask & (1 << slot) != 0: |
| 1695 | + continue |
| 1696 | + value = ht_values[slot * ht_capacity + entry_idx] |
| 1697 | + if value == wp.uint64(0): |
| 1698 | + continue |
| 1699 | + |
| 1700 | + contact_id = unpack_contact_id(value, deterministic) |
| 1701 | + if is_contact_already_exported(contact_id, exported_ids, num_exported): |
| 1702 | + continue |
| 1703 | + exported_ids[num_exported] = contact_id |
| 1704 | + num_exported = num_exported + 1 |
| 1705 | + |
| 1706 | + old_flag = wp.atomic_add(exported_flags, contact_id, 1) |
| 1707 | + if old_flag > 0: |
| 1708 | + continue |
| 1709 | + |
| 1710 | + position, contact_normal, depth = unpack_contact(contact_id, position_depth, normal) |
| 1711 | + pair = shape_pairs[contact_id] |
| 1712 | + shape_a = pair[0] |
| 1713 | + shape_b = pair[1] |
| 1714 | + margin_offset_a = shape_data[shape_a][3] |
| 1715 | + margin_offset_b = shape_data[shape_b][3] |
| 1716 | + radius_eff_a = compute_effective_radius(shape_types[shape_a], shape_data[shape_a]) |
| 1717 | + radius_eff_b = compute_effective_radius(shape_types[shape_b], shape_data[shape_b]) |
| 1718 | + gap_sum = shape_gap[shape_a] + shape_gap[shape_b] |
| 1719 | + |
| 1720 | + contact_data = ContactData() |
| 1721 | + contact_data.contact_point_center = position |
| 1722 | + contact_data.contact_normal_a_to_b = contact_normal |
| 1723 | + contact_data.contact_distance = depth |
| 1724 | + contact_data.radius_eff_a = radius_eff_a |
| 1725 | + contact_data.radius_eff_b = radius_eff_b |
| 1726 | + contact_data.margin_a = margin_offset_a |
| 1727 | + contact_data.margin_b = margin_offset_b |
| 1728 | + contact_data.shape_a = shape_a |
| 1729 | + contact_data.shape_b = shape_b |
| 1730 | + contact_data.gap_sum = gap_sum |
| 1731 | + contact_data.sort_sub_key = contact_fingerprints[contact_id] |
| 1732 | + writer_func(contact_data, writer_data, -1) |
| 1733 | + |
| 1734 | + # Keep lanes together before the shared tile is reused. |
| 1735 | + _sync = wp.tile_extract(duplicate_bits, lane) |
1740 | 1736 |
|
1741 | 1737 | return export_reduced_contacts_kernel |
1742 | 1738 |
|
|
0 commit comments