37#include <tess/core/shape.h>
38#include <tess/sim/joint_movement.h>
39#include <tess/sim/movement.h>
40#include <tess/sim/path_agent.h>
41#include <tess/sim/path_agent_tick.h>
56inline constexpr std::size_t kPibtMaxCandidates = 16;
64 std::uint32_t rank_value = 0;
66 std::uint32_t agent = 0;
67 std::uint32_t next_candidate = 0;
68 std::uint32_t candidate_count = 0;
70 Candidate candidates[kPibtMaxCandidates] = {};
73struct PibtPrioritiesAccess;
80struct PibtScratchState {
81 std::vector<std::uint32_t> order;
82 std::vector<PibtFrame> frames;
84 void reserve(std::size_t agent_count) {
85 order.reserve(agent_count);
86 frames.reserve(agent_count);
98template <
typename Ranking>
100 { rank(agent, coord) } -> std::convertible_to<std::uint32_t>;
138 std::span<const PathAgentState> agents;
147 std::numeric_limits<std::uint32_t>::max() / 8;
149 [[nodiscard]]
static auto clamped_distance(
Coord3 lhs,
Coord3 rhs)
noexcept
151 const std::uint64_t distance = manhattan_distance(lhs, rhs);
153 :
static_cast<std::uint32_t
>(distance);
156 [[nodiscard]]
auto operator()(std::size_t agent,
Coord3 candidate)
const
159 if (routes ==
nullptr || agent >= routes->routes.size()) {
160 return clamped_distance(candidate,
161 state.has_goal ? state.goal : state.position);
163 const std::vector<Coord3>& route = routes->routes[agent];
164 if (!state.has_goal || route.size() < 2) {
165 return clamped_distance(candidate,
166 state.has_goal ? state.goal : state.position);
170 for (std::size_t j = state.path_index; j < route.size(); ++j) {
171 const std::uint32_t attach = clamped_distance(candidate, route[j]);
172 nearest = std::min(nearest, attach);
176 const std::uint32_t remaining =
177 static_cast<std::uint32_t
>(route.size() - 1 - j);
178 best = std::min(best, attach + remaining);
198 scratch_.reserve(agent_count);
202 friend struct detail::PibtPrioritiesAccess;
204 detail::PibtScratchState scratch_;
214struct PibtPrioritiesAccess {
215 [[nodiscard]]
static auto scratch(PibtPriorities& priorities)
noexcept
216 -> PibtScratchState& {
217 return priorities.scratch_;
254template <
typename World,
typename ClassOrTag,
typename OccupancyTag,
255 typename ReservationTag,
typename Ranking,
typename OnCommit>
257 std::invocable<OnCommit&, std::size_t, Coord3, Coord3>
258auto advance_path_agents_with_pibt(
259 World& world, std::span<PathAgentState> agents,
264 using Shape =
typename World::shape_type;
265 using Class = movement::movement_class_of<ClassOrTag>;
268 std::is_same_v<typename World::residency_type, AlwaysResident>,
269 "advance_path_agents_with_pibt requires an AlwaysResidentWorld; use "
270 "another movement tier for sparse worlds.");
271 TESS_ASSERT(routes.routes.size() >= agents.size());
272 auto& scratch = detail::JointMoveScratchAccess::state(scratch_storage);
273 auto& decision = detail::PibtPrioritiesAccess::scratch(priorities);
276 if (advance_options.max_steps == 0) {
279 const auto n = agents.size();
280 const auto none =
static_cast<std::uint32_t
>(-1);
281 constexpr std::uint8_t undecided = 0;
282 constexpr std::uint8_t deciding = 1;
283 constexpr std::uint8_t decided = 2;
285 for (std::size_t pass = 0; pass < advance_options.max_steps; ++pass) {
287 if (priorities.elapsed.size() < n) {
288 priorities.elapsed.resize(n, 0);
290 for (std::size_t i = 0; i < n; ++i) {
292 agents[i].has_goal && agents[i].phase != PathAgentPhase::Unreachable;
293 auto& elapsed = priorities.elapsed[i];
294 elapsed = active ? (elapsed == std::numeric_limits<std::uint32_t>::max()
299 decision.order.resize(n);
300 for (std::size_t i = 0; i < n; ++i) {
301 decision.order[i] =
static_cast<std::uint32_t
>(i);
305 for (std::size_t i = 1; i < n; ++i) {
306 const auto value = decision.order[i];
308 while (j > 0 && priorities.elapsed[decision.order[j - 1]] <
309 priorities.elapsed[value]) {
310 decision.order[j] = decision.order[j - 1];
313 decision.order[j] = value;
317 scratch.desired.assign(n,
Coord3{});
318 scratch.state.assign(n, undecided);
319 scratch.failure.assign(n,
static_cast<std::uint8_t
>(MovementStatus::Moved));
320 scratch.claimed.clear();
321 scratch.committed.clear();
322 scratch.committed_from.clear();
323 scratch.occupant_key.clear();
324 scratch.occupant_agent.clear();
325 for (std::size_t i = 0; i < n; ++i) {
326 scratch.desired[i] = agents[i].position;
327 scratch.occupant_key.push_back(tile_key<Shape>(agents[i].position).value);
328 scratch.occupant_agent.push_back(
static_cast<std::uint32_t
>(i));
331 auto& keys = scratch.occupant_key;
332 auto& vals = scratch.occupant_agent;
333 for (std::size_t i = 1; i < keys.size(); ++i) {
337 while (j > 0 && keys[j - 1] > key) {
338 keys[j] = keys[j - 1];
339 vals[j] = vals[j - 1];
347 const auto find_occupant = [&](
Coord3 coord) -> std::uint32_t {
348 return detail::joint_find_occupant(scratch, tile_key<Shape>(coord).value);
350 const auto claim = [&](
Coord3 coord) ->
bool {
351 return detail::joint_claim(scratch, tile_key<Shape>(coord).value);
359 auto& frames = decision.frames;
376 const auto start_deciding = [&](std::size_t i) ->
bool {
377 scratch.state[i] = deciding;
378 const auto position = agents[i].position;
379 if (!agents[i].has_goal ||
380 agents[i].phase == PathAgentPhase::Unreachable) {
381 (void)claim(position);
383 static_cast<std::uint8_t
>(MovementStatus::Occupied);
384 scratch.state[i] = decided;
387 if (!detail::is_passable<World, ClassOrTag>(world, position)) {
388 (void)claim(position);
390 static_cast<std::uint8_t
>(MovementStatus::ImpassableFrom);
391 scratch.state[i] = decided;
394 frames.emplace_back();
395 auto& frame = frames.back();
396 frame.agent =
static_cast<std::uint32_t
>(i);
397 const auto index = detail::tile_index<Shape>(position);
398 model.for_each_forward(world, position, index, [&](
auto probe) {
399 if (probe.availability != TransitionAvailability::Legal ||
400 probe.cost_overflow ||
401 frame.candidate_count >= detail::kPibtMaxCandidates - 1) {
404 const auto coord = detail::tile_coord<Shape>(probe.to_index);
405 if (!detail::is_passable<World, ClassOrTag>(world, coord)) {
408 if (world.template field<ReservationTag>(coord)) {
411 if (world.template field<OccupancyTag>(coord) &&
412 find_occupant(coord) == none) {
416 frame.candidates[frame.candidate_count++] = {coord, rank(i, coord)};
418 frame.candidates[frame.candidate_count++] = {position, rank(i, position)};
421 for (std::uint32_t a = 1; a < frame.candidate_count; ++a) {
422 const auto value = frame.candidates[a];
424 while (b > 0 && frame.candidates[b - 1].rank_value > value.rank_value) {
425 frame.candidates[b] = frame.candidates[b - 1];
428 frame.candidates[b] = value;
433 const auto run_decision = [&](std::size_t root) {
434 if (!start_deciding(root)) {
437 bool child_succeeded =
false;
438 while (!frames.empty()) {
439 auto& frame = frames.back();
440 const auto i =
static_cast<std::size_t
>(frame.agent);
441 const auto position = agents[i].position;
443 frame.waiting =
false;
444 if (child_succeeded) {
445 scratch.state[i] = decided;
447 child_succeeded =
true;
454 scratch.desired[i] = position;
455 ++frame.next_candidate;
457 if (frame.next_candidate >= frame.candidate_count) {
460 (void)claim(position);
461 scratch.desired[i] = position;
462 scratch.state[i] = decided;
464 child_succeeded =
false;
467 const auto v = frame.candidates[frame.next_candidate].coord;
468 const auto occupant = find_occupant(v);
469 const bool moving = !(v == position);
476 bool is_swap =
false;
477 if (moving && occupant != none &&
478 scratch.desired[occupant] == position) {
480 switch (options.swap_policy) {
481 case SwapPolicy::Permit:
484 case SwapPolicy::PermitOnDeadlock:
486 agents[i].blocked_retries >= options.deadlock_ticks &&
487 agents[occupant].blocked_retries >= options.deadlock_ticks;
489 case SwapPolicy::Forbid:
494 ++stats.swaps_denied;
495 ++frame.next_candidate;
501 ++frame.next_candidate;
510 scratch.desired[i] = v;
511 if (moving && occupant != none &&
512 scratch.state[occupant] == undecided) {
516 frame.waiting =
true;
517 if (!start_deciding(
static_cast<std::size_t
>(occupant))) {
518 child_succeeded =
false;
522 scratch.state[i] = decided;
524 child_succeeded =
true;
528 for (
const auto i : decision.order) {
529 const auto agent_index =
static_cast<std::size_t
>(i);
530 if (scratch.state[agent_index] != undecided) {
533 if (!agents[agent_index].has_goal ||
534 agents[agent_index].phase == PathAgentPhase::Unreachable) {
535 scratch.state[agent_index] = decided;
538 run_decision(agent_index);
542 const auto advanced_before = stats.frame.advanced;
543 for (std::size_t i = 0; i < n; ++i) {
544 if (!(scratch.desired[i] == agents[i].position)) {
545 world.template field<OccupancyTag>(agents[i].position) =
false;
548 for (std::size_t i = 0; i < n; ++i) {
549 auto& agent = agents[i];
550 const auto from = agent.position;
551 const auto to = scratch.desired[i];
553 if (agent.has_goal && agent.phase != PathAgentPhase::Unreachable) {
555 scratch.failure[i] ==
556 static_cast<std::uint8_t
>(MovementStatus::Moved)
557 ? MovementStatus::Occupied
558 : static_cast<MovementStatus>(scratch.failure[i]);
559 record_movement_failure(stats.frame.movement_failures, status);
560 detail::block_path_agent(agent, status);
561 ++stats.frame.blocked_waits;
565 world.template field<OccupancyTag>(to) =
true;
566 world.template field<ReservationTag>(to) =
false;
567 if (advance_options.movement_dirty_mask) {
568 world.mark_dirty(chunk_key<Shape>(chunk_coord<Shape>(from)),
569 advance_options.movement_dirty_mask,
570 Box3{from, Extent3{1, 1, 1}});
571 world.mark_dirty(chunk_key<Shape>(chunk_coord<Shape>(to)),
572 advance_options.movement_dirty_mask,
575 const auto& route = routes.routes[i];
576 const bool on_route =
577 detail::has_next_step(agent.path_index, route.size()) &&
578 route[agent.path_index + 1] == to;
582 detail::resume_path_agent(agent);
587 agent.last_result.reset();
588 agent.phase = PathAgentPhase::Blocked;
589 agent.blocked_retries = 0;
591 scratch.committed.push_back(
static_cast<std::uint32_t
>(i));
592 scratch.committed_from.push_back(from);
593 ++stats.frame.advanced;
599 if (agent.has_goal && agent.position == agent.goal) {
600 arrive_path_agent(agent, accounting);
601 agent.last_result = PathStatus::Found;
605 priorities.elapsed[i] = 0;
606 ++stats.frame.arrived;
611 for (std::size_t k = 0; k < scratch.committed.size(); ++k) {
612 const auto index =
static_cast<std::size_t
>(scratch.committed[k]);
613 on_commit(index, scratch.committed_from[k], agents[index].position);
617 if (stats.frame.advanced == advanced_before) {
625template <
typename World,
typename ClassOrTag,
typename OccupancyTag,
626 typename ReservationTag,
typename Ranking>
627 requires PibtRanking<Ranking>
628auto advance_path_agents_with_pibt(
629 World& world, std::span<PathAgentState> agents,
630 const PathAgentRoutes& routes, PibtPriorities& priorities,
631 JointMoveScratch& scratch, Ranking&& rank, JointMoveOptions options = {},
632 PathAgentAdvanceOptions advance_options = {},
633 diagnostics::FlowAccounting* accounting =
nullptr) -> JointMoveStats {
634 return advance_path_agents_with_pibt<World, ClassOrTag, OccupancyTag,
636 world, agents, routes, priorities, scratch, std::forward<Ranking>(rank),
637 options, advance_options, [](std::size_t, Coord3, Coord3) {}, accounting);
643template <
typename World,
typename Class, std::uint32_t MaxCost,
644 typename OccupancyTag,
typename ReservationTag,
typename Ranking>
645 requires PibtRanking<Ranking>
646[[nodiscard]]
auto tick_weighted_path_agents_with_pibt(
647 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
648 PathRequestRuntime& runtime, PibtPriorities& priorities,
649 JointMoveScratch& scratch, Ranking&& rank,
650 PathAgentTickOptions options = {}, JointMoveOptions pibt_options = {},
651 const RegionGraphT<typename World::residency_type>* graph =
nullptr,
652 JointMoveStats* pibt_stats =
nullptr) -> PathAgentTickStats {
653 PathAgentTickStats stats;
654 stats.tick = advance_sim_tick(state.clock);
656 const bool repath_needed = prepare_path_agent_processing(
657 agents, options, stats, state.flow_accounting);
658 state.routes.ensure_size(agents.size());
659 if (state.pathing_dirty || repath_needed) {
661 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
662 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
663 world, agents, runtime, options.cache_policy, graph, scope,
664 &state.routes, state.flow_accounting);
665 stats.processed_paths =
true;
666 state.pathing_dirty =
false;
670 advance_path_agents_with_pibt<World, Class, OccupancyTag, ReservationTag>(
671 world, agents, state.routes, priorities, scratch,
672 std::forward<Ranking>(rank), pibt_options,
673 PathAgentAdvanceOptions{options.max_steps,
674 options.movement_dirty_mask},
675 state.flow_accounting);
676 stats.movement = moved.frame;
677 if (pibt_stats !=
nullptr) {
Definition transition_model.h:380
Requirements on a ranking callable supplied to the PIBT movement tier.
Definition pibt_movement.h:99
Supplies no special transitions beyond ordinary face adjacency.
Definition transition_provider.h:132
Configures cycle admission for one joint movement pass.
Definition joint_movement.h:54
Caller-owned workspace for the joint movement pass.
Definition joint_movement.h:125
Reports joint-admission outcomes alongside the standard movement stats.
Definition joint_movement.h:66
Configures bounded direct movement and the dirty bits it emits.
Definition path_agent.h:78
Owns index-paired route copies retained across scoped processing passes.
Definition path_agent.h:115
Stores one agent's goal, route cursor, and retry lifecycle state.
Definition path_agent.h:35
Caller-owned adaptive priorities for the PIBT movement tier.
Definition pibt_movement.h:191
std::vector< std::uint32_t > elapsed
Ticks each agent has spent unarrived; higher decides earlier.
Definition pibt_movement.h:193
void reserve(std::size_t agent_count)
Pre-sizes the containers for agent_count agents.
Definition pibt_movement.h:196
Definition pibt_movement.h:137
std::uint32_t attach_radius
Definition pibt_movement.h:143
static constexpr std::uint32_t kDetachedBase
Scores below kDetachedBase are attached; higher steer back.
Definition pibt_movement.h:146
Definition diagnostics.h:505