Skip to main content

box3d_rust/recording/
query_replay.rs

1//! Spatial-query replay helpers. Port of recording_replay.c query dispatch.
2//!
3//! SPDX-FileCopyrightText: 2026 Erin Catto
4//! SPDX-License-Identifier: MIT
5
6use crate::distance::ShapeProxy;
7use crate::geometry::{Capsule, PlaneResult};
8use crate::id::ShapeId;
9use crate::math_functions::{Aabb, Plane, Pos, Vec3, POS_ZERO, VEC3_ZERO};
10use crate::recording::dispatch::RecReader;
11use crate::types::{QueryFilter, RayResult};
12use crate::world::{
13    world_cast_mover, world_cast_ray, world_cast_ray_closest, world_cast_shape,
14    world_collide_mover, world_overlap_aabb, world_overlap_shape, World,
15};
16
17#[derive(Clone, Default)]
18struct RecordedHit {
19    id: ShapeId,
20    point: Pos,
21    normal: Vec3,
22    fraction: f32,
23    user_material_id: u64,
24    triangle_index: i32,
25    child_index: i32,
26    user_return_f: f32,
27    user_return_b: bool,
28    plane: PlaneResult,
29    plane_count: i32,
30}
31
32struct ReplayCtx<'a> {
33    rdr: *mut RecReader<'a>,
34    hits: Vec<RecordedHit>,
35    cursor: usize,
36}
37
38fn f32_differs(a: f32, b: f32) -> bool {
39    a.to_bits() != b.to_bits()
40}
41
42fn vec3_differs(a: Vec3, b: Vec3) -> bool {
43    f32_differs(a.x, b.x) || f32_differs(a.y, b.y) || f32_differs(a.z, b.z)
44}
45
46fn pos_differs(a: Pos, b: Pos) -> bool {
47    #[cfg(feature = "double-precision")]
48    {
49        a.x.to_bits() != b.x.to_bits()
50            || a.y.to_bits() != b.y.to_bits()
51            || a.z.to_bits() != b.z.to_bits()
52    }
53    #[cfg(not(feature = "double-precision"))]
54    {
55        vec3_differs(a, b)
56    }
57}
58
59fn mark_diverged(rdr: &mut RecReader<'_>) {
60    rdr.diverged = true;
61}
62
63fn make_shape_id(rdr: &RecReader<'_>, recorded: ShapeId) -> ShapeId {
64    rdr.make_shape_id(recorded)
65}
66
67/// (b3RecDispatch_QueryOverlapAABB)
68pub fn dispatch_query_overlap_aabb(
69    rdr: &mut RecReader<'_>,
70    world: &mut World,
71    aabb: Aabb,
72    filter: QueryFilter,
73) {
74    let mut s = rdr.snap();
75    let n = s.u32() as usize;
76    let mut hits = Vec::with_capacity(n);
77    for _ in 0..n {
78        let id = make_shape_id(rdr, s.shape_id());
79        let user_return_b = s.bool();
80        hits.push(RecordedHit {
81            id,
82            user_return_b,
83            ..Default::default()
84        });
85    }
86    let _ = s.i32(); // nodeVisits
87    let _ = s.i32(); // leafVisits
88    rdr.sync_from(&s);
89    if !rdr.ok {
90        return;
91    }
92
93    let mut ctx = ReplayCtx {
94        rdr: rdr as *mut _,
95        hits,
96        cursor: 0,
97    };
98    world_overlap_aabb(world, aabb, &filter, |id| {
99        if ctx.cursor >= ctx.hits.len() {
100            unsafe { mark_diverged(&mut *ctx.rdr) };
101            return false;
102        }
103        let h = &ctx.hits[ctx.cursor];
104        ctx.cursor += 1;
105        if id.index1 != h.id.index1 || id.generation != h.id.generation {
106            unsafe { mark_diverged(&mut *ctx.rdr) };
107        }
108        h.user_return_b
109    });
110    if ctx.cursor != ctx.hits.len() {
111        rdr.diverged = true;
112    }
113    stash_simple(
114        rdr,
115        RecQueryKind::OverlapAabb,
116        &ctx.hits,
117        aabb,
118        POS_ZERO,
119        VEC3_ZERO,
120        filter,
121    );
122}
123
124/// (b3RecDispatch_QueryOverlapShape)
125pub fn dispatch_query_overlap_shape(
126    rdr: &mut RecReader<'_>,
127    world: &mut World,
128    origin: Pos,
129    proxy: ShapeProxy,
130    filter: QueryFilter,
131) {
132    let mut s = rdr.snap();
133    let n = s.u32() as usize;
134    let mut hits = Vec::with_capacity(n);
135    for _ in 0..n {
136        let id = make_shape_id(rdr, s.shape_id());
137        let user_return_b = s.bool();
138        hits.push(RecordedHit {
139            id,
140            user_return_b,
141            ..Default::default()
142        });
143    }
144    let _ = s.i32();
145    let _ = s.i32();
146    rdr.sync_from(&s);
147    if !rdr.ok {
148        return;
149    }
150
151    let mut ctx = ReplayCtx {
152        rdr: rdr as *mut _,
153        hits,
154        cursor: 0,
155    };
156    world_overlap_shape(world, origin, &proxy, &filter, |id| {
157        if ctx.cursor >= ctx.hits.len() {
158            unsafe { mark_diverged(&mut *ctx.rdr) };
159            return false;
160        }
161        let h = &ctx.hits[ctx.cursor];
162        ctx.cursor += 1;
163        if id.index1 != h.id.index1 || id.generation != h.id.generation {
164            unsafe { mark_diverged(&mut *ctx.rdr) };
165        }
166        h.user_return_b
167    });
168    if ctx.cursor != ctx.hits.len() {
169        rdr.diverged = true;
170    }
171    stash_simple(
172        rdr,
173        RecQueryKind::OverlapShape,
174        &ctx.hits,
175        Aabb::default(),
176        origin,
177        VEC3_ZERO,
178        filter,
179    );
180}
181
182/// (b3RecDispatch_QueryCastRay)
183pub fn dispatch_query_cast_ray(
184    rdr: &mut RecReader<'_>,
185    world: &mut World,
186    origin: Pos,
187    translation: Vec3,
188    filter: QueryFilter,
189) {
190    let hits = read_cast_hits(rdr);
191    if !rdr.ok {
192        return;
193    }
194    let mut ctx = ReplayCtx {
195        rdr: rdr as *mut _,
196        hits,
197        cursor: 0,
198    };
199    world_cast_ray(
200        world,
201        origin,
202        translation,
203        &filter,
204        |id, point, normal, fraction, mid, tri, child| {
205            if ctx.cursor >= ctx.hits.len() {
206                unsafe { mark_diverged(&mut *ctx.rdr) };
207                return 0.0;
208            }
209            let h = &ctx.hits[ctx.cursor];
210            ctx.cursor += 1;
211            if id.index1 != h.id.index1
212                || id.generation != h.id.generation
213                || pos_differs(point, h.point)
214                || vec3_differs(normal, h.normal)
215                || f32_differs(fraction, h.fraction)
216                || mid != h.user_material_id
217                || tri != h.triangle_index
218                || child != h.child_index
219            {
220                unsafe { mark_diverged(&mut *ctx.rdr) };
221            }
222            h.user_return_f
223        },
224    );
225    if ctx.cursor != ctx.hits.len() {
226        rdr.diverged = true;
227    }
228    stash_simple(
229        rdr,
230        RecQueryKind::CastRay,
231        &ctx.hits,
232        Aabb::default(),
233        origin,
234        translation,
235        filter,
236    );
237}
238
239/// (b3RecDispatch_QueryCastShape)
240pub fn dispatch_query_cast_shape(
241    rdr: &mut RecReader<'_>,
242    world: &mut World,
243    origin: Pos,
244    proxy: ShapeProxy,
245    translation: Vec3,
246    filter: QueryFilter,
247) {
248    let hits = read_cast_hits(rdr);
249    if !rdr.ok {
250        return;
251    }
252    let mut ctx = ReplayCtx {
253        rdr: rdr as *mut _,
254        hits,
255        cursor: 0,
256    };
257    world_cast_shape(
258        world,
259        origin,
260        &proxy,
261        translation,
262        &filter,
263        |id, point, normal, fraction, mid, tri, child| {
264            if ctx.cursor >= ctx.hits.len() {
265                unsafe { mark_diverged(&mut *ctx.rdr) };
266                return 0.0;
267            }
268            let h = &ctx.hits[ctx.cursor];
269            ctx.cursor += 1;
270            if id.index1 != h.id.index1
271                || id.generation != h.id.generation
272                || pos_differs(point, h.point)
273                || vec3_differs(normal, h.normal)
274                || f32_differs(fraction, h.fraction)
275                || mid != h.user_material_id
276                || tri != h.triangle_index
277                || child != h.child_index
278            {
279                unsafe { mark_diverged(&mut *ctx.rdr) };
280            }
281            h.user_return_f
282        },
283    );
284    if ctx.cursor != ctx.hits.len() {
285        rdr.diverged = true;
286    }
287    stash_simple(
288        rdr,
289        RecQueryKind::CastShape,
290        &ctx.hits,
291        Aabb::default(),
292        origin,
293        translation,
294        filter,
295    );
296}
297
298/// (b3RecDispatch_QueryCastRayClosest)
299pub fn dispatch_query_cast_ray_closest(
300    rdr: &mut RecReader<'_>,
301    world: &mut World,
302    origin: Pos,
303    translation: Vec3,
304    filter: QueryFilter,
305) {
306    let mut s = rdr.snap();
307    let rec_shape = make_shape_id(rdr, s.shape_id());
308    let rec = RayResult {
309        shape_id: rec_shape,
310        point: s.pos(),
311        normal: s.vec3(),
312        user_material_id: s.u64(),
313        fraction: s.f32(),
314        triangle_index: s.i32(),
315        child_index: s.i32(),
316        hit: s.bool(),
317        node_visits: 0,
318        leaf_visits: 0,
319    };
320    rdr.sync_from(&s);
321    if !rdr.ok {
322        return;
323    }
324
325    let got = world_cast_ray_closest(world, origin, translation, &filter);
326    if got.hit != rec.hit
327        || (got.hit
328            && (got.shape_id.index1 != rec.shape_id.index1
329                || got.shape_id.generation != rec.shape_id.generation
330                || pos_differs(got.point, rec.point)
331                || vec3_differs(got.normal, rec.normal)
332                || f32_differs(got.fraction, rec.fraction)
333                || got.user_material_id != rec.user_material_id))
334    {
335        rdr.diverged = true;
336    }
337
338    let hits = if rec.hit {
339        vec![RecordedHit {
340            id: rec.shape_id,
341            point: rec.point,
342            normal: rec.normal,
343            fraction: rec.fraction,
344            ..Default::default()
345        }]
346    } else {
347        Vec::new()
348    };
349    stash_simple(
350        rdr,
351        RecQueryKind::CastRayClosest,
352        &hits,
353        Aabb::default(),
354        origin,
355        translation,
356        filter,
357    );
358}
359
360/// (b3RecDispatch_QueryCastMover)
361pub fn dispatch_query_cast_mover(
362    rdr: &mut RecReader<'_>,
363    world: &mut World,
364    origin: Pos,
365    mover: Capsule,
366    translation: Vec3,
367    filter: QueryFilter,
368) {
369    let mut s = rdr.snap();
370    let n = s.u32() as usize;
371    let mut hits = Vec::with_capacity(n);
372    for _ in 0..n {
373        let id = make_shape_id(rdr, s.shape_id());
374        let user_return_b = s.bool();
375        hits.push(RecordedHit {
376            id,
377            user_return_b,
378            ..Default::default()
379        });
380    }
381    let rec_fraction = s.f32();
382    rdr.sync_from(&s);
383    if !rdr.ok {
384        return;
385    }
386
387    let mut ctx = ReplayCtx {
388        rdr: rdr as *mut _,
389        hits,
390        cursor: 0,
391    };
392    let mut filter_fcn = |id: ShapeId| -> bool {
393        if ctx.cursor >= ctx.hits.len() {
394            unsafe { mark_diverged(&mut *ctx.rdr) };
395            return false;
396        }
397        let h = &ctx.hits[ctx.cursor];
398        ctx.cursor += 1;
399        if id.index1 != h.id.index1 || id.generation != h.id.generation {
400            unsafe { mark_diverged(&mut *ctx.rdr) };
401        }
402        h.user_return_b
403    };
404    let got = world_cast_mover(
405        world,
406        origin,
407        &mover,
408        translation,
409        &filter,
410        Some(&mut filter_fcn),
411    );
412    if ctx.cursor != ctx.hits.len() || f32_differs(got, rec_fraction) {
413        rdr.diverged = true;
414    }
415    stash_simple(
416        rdr,
417        RecQueryKind::CastMover,
418        &ctx.hits,
419        Aabb::default(),
420        origin,
421        translation,
422        filter,
423    );
424}
425
426/// (b3RecDispatch_QueryCollideMover)
427pub fn dispatch_query_collide_mover(
428    rdr: &mut RecReader<'_>,
429    world: &mut World,
430    origin: Pos,
431    mover: Capsule,
432    filter: QueryFilter,
433) {
434    let mut s = rdr.snap();
435    let shape_count = s.u32() as usize;
436    let mut hits = Vec::new();
437    for _ in 0..shape_count {
438        let id = make_shape_id(rdr, s.shape_id());
439        let plane_count = s.i32().max(0);
440        let mut planes = Vec::with_capacity(plane_count as usize);
441        for _ in 0..plane_count {
442            planes.push(PlaneResult {
443                plane: Plane {
444                    normal: s.vec3(),
445                    offset: s.f32(),
446                },
447                point: s.vec3(),
448            });
449        }
450        let user_return_b = s.bool();
451        for p in &planes {
452            hits.push(RecordedHit {
453                id,
454                plane: *p,
455                plane_count,
456                user_return_b,
457                ..Default::default()
458            });
459        }
460    }
461    rdr.sync_from(&s);
462    if !rdr.ok {
463        return;
464    }
465
466    let total = hits.len();
467    let mut ctx = ReplayCtx {
468        rdr: rdr as *mut _,
469        hits,
470        cursor: 0,
471    };
472    world_collide_mover(world, origin, &mover, &filter, |id, planes| {
473        if ctx.cursor >= ctx.hits.len() {
474            unsafe { mark_diverged(&mut *ctx.rdr) };
475            return true;
476        }
477        let head = &ctx.hits[ctx.cursor];
478        let recorded_count = head.plane_count;
479        let ret = head.user_return_b;
480        if id.index1 != head.id.index1
481            || id.generation != head.id.generation
482            || recorded_count != planes.len() as i32
483        {
484            unsafe { mark_diverged(&mut *ctx.rdr) };
485        }
486        let n = recorded_count.min(planes.len() as i32) as usize;
487        for i in 0..n {
488            let h = &ctx.hits[ctx.cursor + i];
489            if vec3_differs(h.plane.plane.normal, planes[i].plane.normal)
490                || f32_differs(h.plane.plane.offset, planes[i].plane.offset)
491                || vec3_differs(h.plane.point, planes[i].point)
492            {
493                unsafe { mark_diverged(&mut *ctx.rdr) };
494            }
495        }
496        ctx.cursor += recorded_count as usize;
497        ret
498    });
499    if ctx.cursor != total {
500        rdr.diverged = true;
501    }
502    stash_simple(
503        rdr,
504        RecQueryKind::CollideMover,
505        &ctx.hits,
506        Aabb::default(),
507        origin,
508        VEC3_ZERO,
509        filter,
510    );
511}
512
513fn read_cast_hits(rdr: &mut RecReader<'_>) -> Vec<RecordedHit> {
514    let mut s = rdr.snap();
515    let n = s.u32() as usize;
516    let mut hits = Vec::with_capacity(n);
517    for _ in 0..n {
518        let id = make_shape_id(rdr, s.shape_id());
519        hits.push(RecordedHit {
520            id,
521            point: s.pos(),
522            normal: s.vec3(),
523            fraction: s.f32(),
524            user_material_id: s.u64(),
525            triangle_index: s.i32(),
526            child_index: s.i32(),
527            user_return_f: s.f32(),
528            ..Default::default()
529        });
530    }
531    let _ = s.i32();
532    let _ = s.i32();
533    rdr.sync_from(&s);
534    hits
535}
536
537/// Public query kind for the player frame-query store. (b3RecQueryType)
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539#[repr(i32)]
540pub enum RecQueryKind {
541    OverlapAabb = 0,
542    OverlapShape = 1,
543    CastRay = 2,
544    CastShape = 3,
545    CastRayClosest = 4,
546    CastMover = 5,
547    CollideMover = 6,
548}
549
550/// Stashed per-frame query for the player. (b3RecDrawQuery subset)
551#[derive(Clone)]
552pub struct FrameQuery {
553    pub kind: RecQueryKind,
554    pub hit_count: i32,
555    pub key: u64,
556    pub filter: QueryFilter,
557    pub origin: Pos,
558    pub translation: Vec3,
559    pub aabb: Aabb,
560}
561
562fn stash_simple(
563    rdr: &mut RecReader<'_>,
564    kind: RecQueryKind,
565    hits: &[RecordedHit],
566    aabb: Aabb,
567    origin: Pos,
568    translation: Vec3,
569    filter: QueryFilter,
570) {
571    let Some(owner) = rdr.owner else {
572        rdr.pending_query_key = 0;
573        return;
574    };
575    // SAFETY: owner points at the RecPlayer driving this reader for the duration of step_frame.
576    let player = unsafe { &mut *owner };
577    let key = rdr.pending_query_key;
578    rdr.pending_query_key = 0;
579    player.frame_queries.push(FrameQuery {
580        kind,
581        hit_count: hits.len() as i32,
582        key,
583        filter,
584        origin,
585        translation,
586        aabb,
587    });
588}