Pico Headers
Loading...
Searching...
No Matches
pico_bvh.h
Go to the documentation of this file.
1
69#ifndef PICO_BVH_H
70#define PICO_BVH_H
71
72#include <stdbool.h>
73#include <stdint.h>
74
75// --- User Data Type Override -------------------------------------------------
76
77#ifndef PICO_BVH_UDATA_TYPE
78 #define PICO_BVH_UDATA_TYPE uint64_t
79#endif
80
82
83// --- Math Primitives ---------------------------------------------------------
84
85typedef struct
86{
87 float x;
88 float y;
90
91typedef struct
92{
96
97// --- Public Types ------------------------------------------------------------
98
99#define BVH_NULL_ID (-1)
100
104typedef bool (*bvh_query_cb)(int leaf_id, bvh_udata_t udata, void* ctx);
105
109typedef void (*bvh_walk_cb)(bvh_aabb_t aabb, int depth, bool is_leaf,
110 bvh_udata_t udata, void* ctx);
111
115typedef struct bvh_t bvh_t;
116
117// --- Helpers -----------------------------------------------------------------
118
122bvh_aabb_t bvh_make_aabb(float x, float y, float w, float h);
123
124// --- Lifecycle ---------------------------------------------------------------
125
130
134void bvh_destroy(bvh_t* tree);
135
136// --- Modification ------------------------------------------------------------
137
145int bvh_insert(bvh_t* tree, bvh_aabb_t aabb, float padding, bvh_udata_t udata);
146
150void bvh_remove(bvh_t* tree, int leaf_id);
151
157bool bvh_move(bvh_t* tree, int leaf_id, bvh_aabb_t new_aabb, float padding);
158
159// --- Queries -----------------------------------------------------------------
160
164void bvh_query_aabb(const bvh_t* tree, bvh_aabb_t query,
165 bvh_query_cb cb, void* ctx);
171void bvh_query_ray(const bvh_t* tree,
172 bvh_vec2_t origin, bvh_vec2_t dir, float max_t,
173 bvh_query_cb cb, void* ctx);
174
175// --- Accessors ---------------------------------------------------------------
176
180bvh_udata_t bvh_get_udata(const bvh_t* tree, int leaf_id);
181
185bvh_aabb_t bvh_get_padded_aabb(const bvh_t* tree, int leaf_id);
186
190int bvh_get_leaf_count(const bvh_t* tree);
191
195void bvh_walk(const bvh_t* tree, bvh_walk_cb cb, void* ctx);
196
200float bvh_get_cost(const bvh_t* tree);
201
202#endif // PICO_BVH_H
203
204#ifdef PICO_BVH_IMPLEMENTATION
205
206/*
207 * Design notes
208 * ------------
209 * The tree is a pool-allocated binary tree stored in a flat array.
210 * Every node keeps:
211 * • A "padded" AABB (tight AABB padded by a user-supplied padding).
212 * • child[0], child[1] - BVH_NULL_ID for leaves.
213 * • parent - BVH_NULL_ID for the root.
214 * • height - 0 for leaves, max(child heights)+1 otherwise.
215 * • udata - only meaningful for leaves.
216 *
217 * Insertion (O(log n) expected)
218 * ------------------------------
219 * We pick the best sibling for a new leaf using the surface-area heuristic:
220 * the sibling that minimises the total induced cost increase walking back to
221 * the root. This is the exact O(log n) algorithm described by:
222 * Bittner et al. "Fast, Effective BVH Updates for Animated Scenes" (2015)
223 * (also used by Box2D's b2DynamicTree).
224 *
225 * After every insertion / removal we walk back to the root and:
226 * 1. Refit the ancestor AABBs.
227 * 2. Apply one SAH rotation at each ancestor to reduce surface-area cost.
228 *
229 * Rotations (SAH balance)
230 * ------------------------
231 * At each internal node we consider swapping one of its grandchildren with
232 * the other child. If any swap reduces the node's induced surface area we
233 * apply it. This keeps the tree height near O(log n) without a full rebuild.
234 */
235
236#include <float.h>
237#include <math.h>
238
239#ifdef NDEBUG
240 #define PICO_BVH_ASSERT(expr) ((void)0)
241#else
242 #ifndef PICO_BVH_ASSERT
243 #include <assert.h>
244 #define PICO_BVH_ASSERT(expr) (assert(expr))
245 #endif
246#endif
247
248#if !defined(PICO_BVH_CALLOC) || \
249 !defined(PICO_BVH_REALLOC) || \
250 !defined(PICO_BVH_FREE)
251 #include <stdlib.h>
252 #define PICO_BVH_CALLOC(num, size) (calloc(num, size))
253 #define PICO_BVH_REALLOC(ptr, size) (realloc(ptr, size))
254 #define PICO_BVH_FREE(ptr) (free(ptr))
255#endif
256
257#ifndef PICO_BVH_MEMSET
258 #include <string.h>
259 #define PICO_BVH_MEMSET memset
260#endif
261
262#ifndef PICO_BVH_MEMCPY
263 #include <string.h>
264 #define PICO_BVH_MEMCPY memcpy
265#endif
266
267#ifndef PICO_BVH_STACK_SIZE
268 #define PICO_BVH_STACK_SIZE 1024
269#endif
270
271#define BVH_IS_LEAF(n) ((n)->child[0] == BVH_NULL_ID)
272#define BVH_INITIAL_CAPACITY 64
273#define PICO_BVH_EPSILON 1e-9f
274
275// --- Internal Node -----------------------------------------------------------
276
277typedef struct
278{
279 bvh_aabb_t aabb;
280 int parent;
281 int child[2]; // BVH_NULL_ID for leaves
282 int height; // 0 = leaf
283 bvh_udata_t udata; // Valid only for leaves
284 bool allocated;
285} bvh_node_t;
286
287// --- Tree Structure ----------------------------------------------------------
288
289struct bvh_t
290{
291 bvh_node_t* nodes;
292 int capacity;
293 int root;
294 int free_list; // Singly-linked free list via child[0]
295 int leaf_count;
296};
297
298// --- Best-Sibling Heap Types -------------------------------------------------
299
300typedef struct
301{
302 int id;
303 float inherited_cost;
304} bvh_heap_entry_t;
305
306typedef struct
307{
308 bvh_heap_entry_t* data;
309 int size;
310 int cap;
311} bvh_min_heap_t;
312
313// --- Forward Declarations ----------------------------------------------------
314
315static inline bvh_aabb_t bvh_aabb_pad(bvh_aabb_t a, float m);
316static inline bvh_aabb_t bvh_aabb_union(bvh_aabb_t a, bvh_aabb_t b);
317static inline float bvh_aabb_perimeter(bvh_aabb_t a);
318static inline bool bvh_aabb_overlaps(bvh_aabb_t a, bvh_aabb_t b);
319static inline bool bvh_aabb_contains(bvh_aabb_t outer, bvh_aabb_t inner);
320static void bvh_grow(bvh_t* t);
321static int bvh_alloc_node(bvh_t* t);
322static void bvh_free_node(bvh_t* t, int id);
323static void bvh_refit(bvh_t* t, int id);
324static void bvh_rotate(bvh_t* t, int a_id);
325static void bvh_refit_and_rotate(bvh_t* t, int start);
326static void bvh_heap_push(bvh_min_heap_t* h, bvh_heap_entry_t e);
327static bvh_heap_entry_t bvh_heap_pop(bvh_min_heap_t* h);
328static int bvh_best_sibling(bvh_t* t, bvh_aabb_t new_aabb);
329static bool bvh_ray_aabb(bvh_vec2_t origin, bvh_vec2_t inv_dir, bvh_aabb_t aabb, float max_t);
330static void bvh_walk_rec(const bvh_t* t, int id, int depth, bvh_walk_cb cb, void* ctx);
331static float bvh_get_cost_rec(const bvh_t* t, int id);
332
333// --- Public: Lifecycle -------------------------------------------------------
334
335bvh_t* bvh_create(void)
336{
337 bvh_t* t = (bvh_t*)PICO_BVH_CALLOC(1, sizeof(bvh_t));
338 PICO_BVH_ASSERT(t);
339
340 t->capacity = BVH_INITIAL_CAPACITY;
341 t->nodes = (bvh_node_t*)PICO_BVH_CALLOC((size_t)t->capacity, sizeof(bvh_node_t));
342
343 PICO_BVH_ASSERT(t->nodes);
344
345 t->root = BVH_NULL_ID;
346 t->leaf_count = 0;
347
348 // Build free list
349 for (int i = 0; i < t->capacity - 1; ++i)
350 {
351 t->nodes[i].child[0] = i + 1;
352 }
353
354 t->nodes[t->capacity - 1].child[0] = BVH_NULL_ID;
355 t->free_list = 0;
356
357 return t;
358}
359
360void bvh_destroy(bvh_t* t)
361{
362 if (!t)
363 {
364 return;
365 }
366
367 PICO_BVH_FREE(t->nodes);
368 PICO_BVH_FREE(t);
369}
370
371// --- Public: Insert ----------------------------------------------------------
372
373int bvh_insert(bvh_t* t, bvh_aabb_t aabb, float padding, bvh_udata_t udata)
374{
375 int leaf_id = bvh_alloc_node(t);
376 bvh_node_t* leaf = &t->nodes[leaf_id];
377 leaf->aabb = padding > 0.f ? bvh_aabb_pad(aabb, padding) : aabb;
378 leaf->udata = udata;
379 leaf->height = 0;
380 t->leaf_count++;
381
382 // Empty tree
383 if (t->root == BVH_NULL_ID)
384 {
385 t->root = leaf_id;
386 leaf->parent = BVH_NULL_ID;
387 return leaf_id;
388 }
389
390 // Find best sibling
391 int sib_id = bvh_best_sibling(t, leaf->aabb);
392
393 // Create a new internal node to replace sibling
394 int new_id = bvh_alloc_node(t);
395 bvh_node_t* new_node = &t->nodes[new_id];
396 int old_parent = t->nodes[sib_id].parent;
397
398 new_node->parent = old_parent;
399 new_node->child[0] = sib_id;
400 new_node->child[1] = leaf_id;
401 new_node->height = 0; // Will be set by bvh_refit
402
403 t->nodes[sib_id].parent = new_id;
404 t->nodes[leaf_id].parent = new_id;
405
406 if (old_parent == BVH_NULL_ID)
407 {
408 t->root = new_id;
409 }
410 else
411 {
412 bvh_node_t* op = &t->nodes[old_parent];
413
414 if (op->child[0] == sib_id)
415 {
416 op->child[0] = new_id;
417 }
418 else
419 {
420 op->child[1] = new_id;
421 }
422 }
423
424 bvh_refit_and_rotate(t, new_id);
425
426 return leaf_id;
427}
428
429// --- Public: Remove ----------------------------------------------------------
430
431void bvh_remove(bvh_t* t, int leaf_id)
432{
433 PICO_BVH_ASSERT(leaf_id >= 0 && leaf_id < t->capacity);
434 PICO_BVH_ASSERT(BVH_IS_LEAF(&t->nodes[leaf_id]));
435
436 int parent_id = t->nodes[leaf_id].parent;
437 bvh_free_node(t, leaf_id);
438 t->leaf_count--;
439
440 if (parent_id == BVH_NULL_ID)
441 {
442 // Was the only node
443 t->root = BVH_NULL_ID;
444 return;
445 }
446
447 // Find the sibling, pull it up to replace the parent
448 bvh_node_t* parent = &t->nodes[parent_id];
449 int sibling = parent->child[0] == leaf_id
450 ? parent->child[1] : parent->child[0];
451
452 int grandparent = parent->parent;
453 bvh_free_node(t, parent_id);
454
455 t->nodes[sibling].parent = grandparent;
456
457 if (grandparent == BVH_NULL_ID)
458 {
459 t->root = sibling;
460 }
461 else
462 {
463 bvh_node_t* gp = &t->nodes[grandparent];
464
465 if (gp->child[0] == parent_id)
466 {
467 gp->child[0] = sibling;
468 }
469 else
470 {
471 gp->child[1] = sibling;
472 }
473
474 bvh_refit_and_rotate(t, grandparent);
475 }
476}
477
478// --- Public: Move ------------------------------------------------------------
479
480bool bvh_move(bvh_t* t, int leaf_id, bvh_aabb_t new_aabb, float padding)
481{
482 PICO_BVH_ASSERT(BVH_IS_LEAF(&t->nodes[leaf_id]));
483
484 bvh_aabb_t padded = padding > 0.f ? bvh_aabb_pad(new_aabb, padding) : new_aabb;
485
486 // No restructuring needed if the padded AABB still contains the new one
487 if (bvh_aabb_contains(t->nodes[leaf_id].aabb, new_aabb))
488 {
489 // Optionally shrink if the padded box is much bigger than needed
490 bvh_aabb_t big = bvh_aabb_pad(new_aabb, padding * 4.f);
491
492 if (bvh_aabb_contains(big, t->nodes[leaf_id].aabb))
493 {
494 return false;
495 }
496 }
497
498 bvh_udata_t ud = t->nodes[leaf_id].udata;
499 bvh_remove(t, leaf_id);
500
501 // bvh_remove freed leaf_id (and its parent). Ensure leaf_id sits at the
502 // front of the free list so bvh_alloc_node returns it first, preserving
503 // the caller's handle.
504 if (t->free_list != leaf_id)
505 {
506 int prev = t->free_list;
507
508 while (t->nodes[prev].child[0] != leaf_id)
509 {
510 prev = t->nodes[prev].child[0];
511 }
512
513 t->nodes[prev].child[0] = t->nodes[leaf_id].child[0];
514 t->nodes[leaf_id].child[0] = t->free_list;
515 t->free_list = leaf_id;
516 }
517
518 int new_id = bvh_insert(t, padded, 0.f, ud);
519 (void)new_id;
520 PICO_BVH_ASSERT(new_id == leaf_id);
521
522 return true;
523}
524
525// --- Public: Query (AABB) ----------------------------------------------------
526
527// Iterative DFS using an explicit stack to avoid recursion overhead.
528void bvh_query_aabb(const bvh_t* t, bvh_aabb_t query, bvh_query_cb cb, void* ctx)
529{
530 if (t->root == BVH_NULL_ID)
531 {
532 return;
533 }
534
535 // Stack: fixed-size. 2*height+2 suffices for a balanced tree.
536 // We allocate generously; could also be dynamic.
537 int stack[PICO_BVH_STACK_SIZE];
538 int top = 0;
539 stack[top++] = t->root;
540
541 while (top > 0)
542 {
543 int id = stack[--top];
544 if (id == BVH_NULL_ID)
545 {
546 continue;
547 }
548
549 const bvh_node_t* n = &t->nodes[id];
550
551 if (!bvh_aabb_overlaps(n->aabb, query))
552 {
553 continue;
554 }
555
556 if (BVH_IS_LEAF(n))
557 {
558 if (!cb(id, n->udata, ctx))
559 {
560 return;
561 }
562 }
563 else
564 {
565 PICO_BVH_ASSERT(top + 2 <= PICO_BVH_STACK_SIZE);
566 stack[top++] = n->child[0];
567 stack[top++] = n->child[1];
568 }
569 }
570}
571
572// --- Public: Query (Ray) -----------------------------------------------------
573
574void bvh_query_ray(const bvh_t* t,
575 bvh_vec2_t origin, bvh_vec2_t dir, float max_t,
576 bvh_query_cb cb, void* ctx)
577{
578 if (t->root == BVH_NULL_ID)
579 {
580 return;
581 }
582
583 // Precompute reciprocal direction (handle zero-components)
584 bvh_vec2_t inv_dir =
585 {
586 fabsf(dir.x) > PICO_BVH_EPSILON ? 1.f / dir.x : (dir.x >= 0.f ? FLT_MAX : -FLT_MAX),
587 fabsf(dir.y) > PICO_BVH_EPSILON ? 1.f / dir.y : (dir.y >= 0.f ? FLT_MAX : -FLT_MAX)
588 };
589
590 int stack[PICO_BVH_STACK_SIZE]; // TODO: define constant
591 int top = 0;
592 stack[top++] = t->root;
593
594 while (top > 0)
595 {
596 int id = stack[--top];
597 if (id == BVH_NULL_ID)
598 {
599 continue;
600 }
601
602 const bvh_node_t* n = &t->nodes[id];
603
604 if (!bvh_ray_aabb(origin, inv_dir, n->aabb, max_t))
605 {
606 continue;
607 }
608
609 if (BVH_IS_LEAF(n))
610 {
611 if (!cb(id, n->udata, ctx))
612 {
613 return;
614 }
615 }
616 else
617 {
618 PICO_BVH_ASSERT(top + 2 <= PICO_BVH_STACK_SIZE);
619 stack[top++] = n->child[0];
620 stack[top++] = n->child[1];
621 }
622 }
623}
624
625// --- Public: Accessors -------------------------------------------------------
626
627bvh_udata_t bvh_get_udata(const bvh_t* t, int leaf_id)
628{
629 PICO_BVH_ASSERT(BVH_IS_LEAF(&t->nodes[leaf_id]));
630 return t->nodes[leaf_id].udata;
631}
632
633bvh_aabb_t bvh_get_padded_aabb(const bvh_t* t, int leaf_id)
634{
635 return t->nodes[leaf_id].aabb;
636}
637
638int bvh_get_leaf_count(const bvh_t* t) { return t->leaf_count; }
639
640// --- Public: Walk ------------------------------------------------------------
641
642void bvh_walk(const bvh_t* t, bvh_walk_cb cb, void* ctx)
643{
644 bvh_walk_rec(t, t->root, 0, cb, ctx);
645}
646
647// --- Public: Cost ------------------------------------------------------------
648
649float bvh_get_cost(const bvh_t* t)
650{
651 return bvh_get_cost_rec(t, t->root);
652}
653
654// --- Math Primitives ---------------------------------------------------------
655
656bvh_aabb_t bvh_make_aabb(float x, float y, float w, float h)
657{
658 return (bvh_aabb_t){ {x, y}, {x + w, y + h} };
659}
660
661static inline bvh_aabb_t bvh_aabb_pad(bvh_aabb_t a, float m)
662{
663 return (bvh_aabb_t){ {a.min.x - m, a.min.y - m}, {a.max.x + m, a.max.y + m} };
664}
665
666static inline bvh_aabb_t bvh_aabb_union(bvh_aabb_t a, bvh_aabb_t b)
667{
668 return (bvh_aabb_t){
669 { a.min.x < b.min.x ? a.min.x : b.min.x,
670 a.min.y < b.min.y ? a.min.y : b.min.y },
671 { a.max.x > b.max.x ? a.max.x : b.max.x,
672 a.max.y > b.max.y ? a.max.y : b.max.y }
673 };
674}
675
676static inline float bvh_aabb_perimeter(bvh_aabb_t a)
677{
678 return 2.f * ((a.max.x - a.min.x) + (a.max.y - a.min.y));
679}
680
681static inline bool bvh_aabb_overlaps(bvh_aabb_t a, bvh_aabb_t b)
682{
683 return a.min.x <= b.max.x && a.max.x >= b.min.x
684 && a.min.y <= b.max.y && a.max.y >= b.min.y;
685}
686
687static inline bool bvh_aabb_contains(bvh_aabb_t outer, bvh_aabb_t inner)
688{
689 return outer.min.x <= inner.min.x && inner.max.x <= outer.max.x
690 && outer.min.y <= inner.min.y && inner.max.y <= outer.max.y;
691}
692
693// --- Node Pool ---------------------------------------------------------------
694
695static void bvh_grow(bvh_t* t)
696{
697 int old_cap = t->capacity;
698 int new_cap = old_cap * 2;
699 t->nodes = (bvh_node_t*)PICO_BVH_REALLOC(t->nodes, (size_t)new_cap * sizeof(bvh_node_t));
700
701 PICO_BVH_ASSERT(t->nodes);
702
703 PICO_BVH_MEMSET(t->nodes + old_cap, 0, (size_t)(new_cap - old_cap) * sizeof(bvh_node_t));
704
705 // Chain new slots onto the free list
706 for (int i = old_cap; i < new_cap - 1; ++i)
707 {
708 t->nodes[i].child[0] = i + 1;
709 }
710
711 t->nodes[new_cap - 1].child[0] = t->free_list;
712 t->free_list = old_cap;
713 t->capacity = new_cap;
714}
715
716static int bvh_alloc_node(bvh_t* t)
717{
718 if (t->free_list == BVH_NULL_ID)
719 {
720 bvh_grow(t);
721 }
722
723 int id = t->free_list;
724 t->free_list = t->nodes[id].child[0];
725 bvh_node_t* n = &t->nodes[id];
726 n->parent = BVH_NULL_ID;
727 n->child[0] = BVH_NULL_ID;
728 n->child[1] = BVH_NULL_ID;
729 n->height = 0;
730 n->udata = 0;
731 n->allocated = true;
732 return id;
733}
734
735static void bvh_free_node(bvh_t* t, int id)
736{
737 PICO_BVH_ASSERT(id >= 0 && id < t->capacity);
738 t->nodes[id].allocated = false;
739 t->nodes[id].child[0] = t->free_list;
740 t->free_list = id;
741}
742
743// --- Helpers -----------------------------------------------------------------
744
745static void bvh_refit(bvh_t* t, int id)
746{
747 bvh_node_t* n = &t->nodes[id];
748 n->aabb = bvh_aabb_union(t->nodes[n->child[0]].aabb,
749 t->nodes[n->child[1]].aabb);
750 int h0 = t->nodes[n->child[0]].height;
751 int h1 = t->nodes[n->child[1]].height;
752 n->height = 1 + (h0 > h1 ? h0 : h1);
753}
754
755// --- SAH Rotation ------------------------------------------------------------
756/*
757 * Local tree at A before rotation:
758 *
759 * A
760 * / \
761 * B C
762 * / \ / \
763 * b0 b1 c0 c1
764 *
765 * We evaluate 4 one-step rotations (swapping a grandchild with the opposite
766 * subtree) and keep only the one that reduces perimeter(A) the most. Only two
767 * cases are considered for brevity.
768 *
769 * Candidate 0 (costs[0]): swap b0 <-> C
770 *
771 * A
772 * / \
773 * B b0
774 * / \
775 * C b1
776 * / \
777 * c0 c1
778 *
779 *
780 * Candidate 2 (costs[2]): swap c0 <-> B
781 *
782 * A
783 * / \
784 * c0 C
785 * / \
786 * B c1
787 * / \
788 * b0 b11
789 *
790 * We evaluate all valid candidates and apply the one that gives the largest
791 * reduction in perimeter(A). If none improves cost, no rotation is applied.
792 */
793static void bvh_rotate(bvh_t* t, int a_id)
794{
795 bvh_node_t* A = &t->nodes[a_id];
796
797 if (A->height < 2)
798 {
799 return;
800 }
801
802 int b_id = A->child[0];
803 int c_id = A->child[1];
804 bvh_node_t* B = &t->nodes[b_id];
805 bvh_node_t* C = &t->nodes[c_id];
806
807 float base_cost = bvh_aabb_perimeter(A->aabb);
808
809 // Candidate costs
810 float costs[4] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX };
811
812 if (!BVH_IS_LEAF(B))
813 {
814 costs[0] = bvh_aabb_perimeter(bvh_aabb_union(C->aabb,
815 t->nodes[B->child[1]].aabb)); // Swap b0 <-> C
816 costs[1] = bvh_aabb_perimeter(bvh_aabb_union(C->aabb,
817 t->nodes[B->child[0]].aabb)); // Swap b1 <-> C
818 }
819
820 if (!BVH_IS_LEAF(C))
821 {
822 costs[2] = bvh_aabb_perimeter(bvh_aabb_union(B->aabb,
823 t->nodes[C->child[1]].aabb)); // Swap c0 <-> B
824 costs[3] = bvh_aabb_perimeter(bvh_aabb_union(B->aabb,
825 t->nodes[C->child[0]].aabb)); // Swap c1 <-> B
826 }
827
828 // Find best candidate
829 int best = -1;
830 float best_cost = base_cost;
831
832 for (int i = 0; i < 4; ++i)
833 {
834 if (costs[i] < best_cost)
835 {
836 best_cost = costs[i];
837 best = i;
838 }
839 }
840
841 if (best < 0)
842 {
843 return; // No improvement
844 }
845
846 switch (best)
847 {
848 case 0:
849 { // Swap B->child[0] <-> C
850 int x = B->child[0];
851 B->child[0] = c_id;
852 t->nodes[c_id].parent = b_id;
853 A->child[1] = x;
854 t->nodes[x].parent = a_id;
855 bvh_refit(t, b_id);
856 bvh_refit(t, a_id);
857 break;
858 }
859
860 case 1:
861 { // Swap B->child[1] <-> C
862 int x = B->child[1];
863 B->child[1] = c_id;
864 t->nodes[c_id].parent = b_id;
865 A->child[1] = x;
866 t->nodes[x].parent = a_id;
867 bvh_refit(t, b_id);
868 bvh_refit(t, a_id);
869 break;
870 }
871
872 case 2:
873 { // Swap C->child[0] <-> B
874 int x = C->child[0];
875 C->child[0] = b_id;
876 t->nodes[b_id].parent = c_id;
877 A->child[0] = x;
878 t->nodes[x].parent = a_id;
879 bvh_refit(t, c_id);
880 bvh_refit(t, a_id);
881 break;
882 }
883
884 case 3:
885 { // Swap C->child[1] <-> B
886 int x = C->child[1];
887 C->child[1] = b_id;
888 t->nodes[b_id].parent = c_id;
889 A->child[0] = x;
890 t->nodes[x].parent = a_id;
891 bvh_refit(t, c_id);
892 bvh_refit(t, a_id);
893 break;
894 }
895 }
896}
897
898// Walk from `start` toward the root: bvh_refit + bvh_rotate each ancestor.
899static void bvh_refit_and_rotate(bvh_t* t, int start)
900{
901 int id = start;
902 while (id != BVH_NULL_ID)
903 {
904 if (!BVH_IS_LEAF(&t->nodes[id]))
905 {
906 bvh_refit(t, id);
907 }
908
909 bvh_rotate(t, id);
910 id = t->nodes[id].parent;
911 }
912}
913
914// --- Best-Sibling Search (SAH) -----------------------------------------------
915/*
916 * Branch-and-bound traversal to find the node that, when used as a sibling
917 * for the new leaf nl, minimises the total induced cost increase up to root.
918 *
919 * induced_cost(node) = cost(union(node, nl)) - cost(node)
920 * + sum of [cost(union(anc, nl)) - cost(anc)] for each ancestor
921 *
922 * We maintain a priority queue (simple binary heap) keyed on a lower bound
923 * of the induced cost to prune branches early.
924 */
925static void bvh_heap_push(bvh_min_heap_t* h, bvh_heap_entry_t entry)
926{
927 if (h->size == h->cap)
928 {
929 h->cap = h->cap ? h->cap * 2 : 16;
930 h->data = (bvh_heap_entry_t*)PICO_BVH_REALLOC(h->data,
931 (size_t)h->cap * sizeof(bvh_heap_entry_t));
932
933 PICO_BVH_ASSERT(h->data);
934 }
935
936 PICO_BVH_ASSERT(h->size < h->cap);
937
938 // Sift-up
939 int i = h->size++;
940
941 while (i > 0)
942 {
943 int parent = (i - 1) / 2;
944
945 if (h->data[parent].inherited_cost <= entry.inherited_cost)
946 {
947 break;
948 }
949
950 h->data[i] = h->data[parent];
951 i = parent;
952 }
953
954 h->data[i] = entry;
955}
956
957static bvh_heap_entry_t bvh_heap_pop(bvh_min_heap_t* h)
958{
959 PICO_BVH_ASSERT(h->size > 0);
960
961 bvh_heap_entry_t top = h->data[0];
962 bvh_heap_entry_t last = h->data[--h->size];
963
964 if (h->size == 0)
965 {
966 return top;
967 }
968
969 int i = 0;
970
971 while (true)
972 {
973 int left = i * 2 + 1;
974
975 if (left >= h->size)
976 {
977 break;
978 }
979
980 int right = left + 1;
981 int child = left;
982
983 if (right < h->size
984 && h->data[right].inherited_cost < h->data[left].inherited_cost)
985 {
986 child = right;
987 }
988
989 if (h->data[child].inherited_cost >= last.inherited_cost)
990 {
991 break;
992 }
993
994 h->data[i] = h->data[child];
995 i = child;
996 }
997
998 h->data[i] = last;
999 return top;
1000}
1001
1002static int bvh_best_sibling(bvh_t* t, bvh_aabb_t new_aabb)
1003{
1004 float new_cost = bvh_aabb_perimeter(new_aabb);
1005
1006 bvh_min_heap_t heap = {0};
1007 bvh_heap_push(&heap, (bvh_heap_entry_t){ t->root, 0.f });
1008
1009 int best_id = t->root;
1010 float best_cost = FLT_MAX;
1011
1012 while (heap.size > 0)
1013 {
1014 bvh_heap_entry_t entry = bvh_heap_pop(&heap);
1015
1016 if (entry.inherited_cost >= best_cost)
1017 {
1018 break; // Prune
1019 }
1020
1021 bvh_node_t* node = &t->nodes[entry.id];
1022 bvh_aabb_t combined = bvh_aabb_union(node->aabb, new_aabb);
1023 float direct_cost = bvh_aabb_perimeter(combined);
1024 float total_cost = direct_cost + entry.inherited_cost;
1025
1026 if (total_cost < best_cost)
1027 {
1028 best_cost = total_cost;
1029 best_id = entry.id;
1030 }
1031
1032 if (!BVH_IS_LEAF(node))
1033 {
1034 // Inherited cost that any descendant must pay
1035 float inherited_cost = entry.inherited_cost + direct_cost
1036 - bvh_aabb_perimeter(node->aabb);
1037
1038 float lower_bound = inherited_cost + new_cost; // <= P(union(child, new)) + inherited
1039
1040 if (lower_bound < best_cost)
1041 {
1042 bvh_heap_push(&heap, (bvh_heap_entry_t){ node->child[0], inherited_cost });
1043 bvh_heap_push(&heap, (bvh_heap_entry_t){ node->child[1], inherited_cost });
1044 }
1045 }
1046 }
1047
1048 PICO_BVH_FREE(heap.data); // TODO: store heap in bvh_t
1049 return best_id;
1050}
1051
1052// --- Ray Test ----------------------------------------------------------------
1053/*
1054 * Slab test for ray vs AABB intersection.
1055 * Returns true if the ray hits the AABB within [0, max_t].
1056 */
1057static bool bvh_ray_aabb(bvh_vec2_t origin, bvh_vec2_t inv_dir, bvh_aabb_t aabb, float max_t)
1058{
1059 float tx1 = (aabb.min.x - origin.x) * inv_dir.x;
1060 float tx2 = (aabb.max.x - origin.x) * inv_dir.x;
1061 float tmin = tx1 < tx2 ? tx1 : tx2;
1062 float tmax = tx1 > tx2 ? tx1 : tx2;
1063
1064 float ty1 = (aabb.min.y - origin.y) * inv_dir.y;
1065 float ty2 = (aabb.max.y - origin.y) * inv_dir.y;
1066 float tymin = ty1 < ty2 ? ty1 : ty2;
1067 float tymax = ty1 > ty2 ? ty1 : ty2;
1068
1069 tmin = tmin > tymin ? tmin : tymin;
1070 tmax = tmax < tymax ? tmax : tymax;
1071
1072 return tmax >= 0.f && tmin <= tmax && tmin <= max_t;
1073}
1074
1075// --- Walk Helper -------------------------------------------------------------
1076
1077static void bvh_walk_rec(const bvh_t* t, int id, int depth, bvh_walk_cb cb, void* ctx)
1078{
1079 if (id == BVH_NULL_ID)
1080 {
1081 return;
1082 }
1083
1084 const bvh_node_t* n = &t->nodes[id];
1085 cb(n->aabb, depth, BVH_IS_LEAF(n), n->udata, ctx);
1086
1087 if (!BVH_IS_LEAF(n))
1088 {
1089 bvh_walk_rec(t, n->child[0], depth + 1, cb, ctx);
1090 bvh_walk_rec(t, n->child[1], depth + 1, cb, ctx);
1091 }
1092}
1093
1094// --- Cost Helper -------------------------------------------------------------
1095
1096static float bvh_get_cost_rec(const bvh_t* t, int id)
1097{
1098 if (id == BVH_NULL_ID)
1099 {
1100 return 0.f;
1101 }
1102
1103 const bvh_node_t* n = &t->nodes[id];
1104 float c = bvh_aabb_perimeter(n->aabb);
1105
1106 if (!BVH_IS_LEAF(n))
1107 {
1108 c += bvh_get_cost_rec(t, n->child[0]) + bvh_get_cost_rec(t, n->child[1]);
1109 }
1110
1111 return c;
1112}
1113
1114#endif // PICO_BVH_IMPLEMENTATION
1115
1116/*
1117 ---------------------------------------------------------------------------
1118 This software is available under two licenses (A) or (B). You may choose
1119 either one as you wish:
1120 ---------------------------------------------------------------------------
1121
1122 (A) The MIT License
1123
1124 Copyright (c) 2026 James McLean
1125
1126 Permission is hereby granted, free of charge, to any person obtaining a copy
1127 of this software and associated documentation files (the "Software"), to
1128 deal in the Software without restriction, including without limitation the
1129 rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
1130 sell copies of the Software, and to permit persons to whom the Software is
1131 furnished to do so, subject to the following conditions:
1132
1133 The above copyright notice and this permission notice shall be included in
1134 all copies or substantial portions of the Software.
1135
1136 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1137 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1138 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1139 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1140 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1141 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
1142 IN THE SOFTWARE.
1143
1144 ---------------------------------------------------------------------------
1145
1146 (B) Public Domain (www.unlicense.org)
1147
1148 This is free and unencumbered software released into the public domain.
1149
1150 Anyone is free to copy, modify, publish, use, compile, sell, or distribute
1151 this software, either in source code form or as a compiled binary, for any
1152 purpose, commercial or non-commercial, and by any means.
1153
1154 In jurisdictions that recognize copyright laws, the author or authors of
1155 this software dedicate any and all copyright interest in the software to the
1156 public domain. We make this dedication for the benefit of the public at
1157 large and to the detriment of our heirs and successors. We intend this
1158 dedication to be an overt act of relinquishment in perpetuity of all present
1159 and future rights to this software under copyright law.
1160
1161 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1162 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1163 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1164 AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
1165 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
1166 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1167*/
void bvh_destroy(bvh_t *tree)
Destroys and deallocates a BVH instance.
void bvh_query_aabb(const bvh_t *tree, bvh_aabb_t query, bvh_query_cb cb, void *ctx)
Queries the tree against an AABB.
bvh_udata_t bvh_get_udata(const bvh_t *tree, int leaf_id)
Returns the user data from the specified leaf node.
bool bvh_move(bvh_t *tree, int leaf_id, bvh_aabb_t new_aabb, float padding)
Update a leaf's AABB.
float bvh_get_cost(const bvh_t *tree)
Total surface-area cost (lower = better balanced).
int bvh_get_leaf_count(const bvh_t *tree)
Returns number of leaves in the tree.
struct bvh_t bvh_t
BVH instance.
Definition pico_bvh.h:115
bvh_aabb_t bvh_make_aabb(float x, float y, float w, float h)
Constructs an AABB from a position and dimensions.
bvh_aabb_t bvh_get_padded_aabb(const bvh_t *tree, int leaf_id)
Returns the enlarged bounds from the specified leaf node.
void bvh_remove(bvh_t *tree, int leaf_id)
Remove a leaf.
#define BVH_NULL_ID
Definition pico_bvh.h:99
void(* bvh_walk_cb)(bvh_aabb_t aabb, int depth, bool is_leaf, bvh_udata_t udata, void *ctx)
Called for every node during bvh_walk(); depth=0 at root.
Definition pico_bvh.h:109
PICO_BVH_UDATA_TYPE bvh_udata_t
Definition pico_bvh.h:81
void bvh_walk(const bvh_t *tree, bvh_walk_cb cb, void *ctx)
Depth-first walk over every node (internal + leaf).
bvh_t * bvh_create(void)
Allocates and initializes a BVH instances.
void bvh_query_ray(const bvh_t *tree, bvh_vec2_t origin, bvh_vec2_t dir, float max_t, bvh_query_cb cb, void *ctx)
Queries the tree against a ray.
int bvh_insert(bvh_t *tree, bvh_aabb_t aabb, float padding, bvh_udata_t udata)
Inserts a new leaf.
bool(* bvh_query_cb)(int leaf_id, bvh_udata_t udata, void *ctx)
Return false to stop traversal early.
Definition pico_bvh.h:104
#define PICO_BVH_UDATA_TYPE
Definition pico_bvh.h:78
Definition pico_bvh.h:92
bvh_vec2_t max
Definition pico_bvh.h:94
bvh_vec2_t min
Definition pico_bvh.h:93
Definition pico_bvh.h:86
float x
Definition pico_bvh.h:87
float y
Definition pico_bvh.h:88