27#include <tess/core/shape.h>
28#include <tess/sim/movement.h>
29#include <tess/sim/path_agent.h>
30#include <tess/sim/path_agent_tick.h>
47enum class SwapPolicy : std::uint8_t {
55 SwapPolicy swap_policy = SwapPolicy::Forbid;
82struct JointMoveScratchAccess;
90struct JointMoveScratchState {
91 std::vector<std::uint64_t> occupant_key;
92 std::vector<std::uint32_t> occupant_agent;
93 std::vector<std::uint64_t> claimed;
94 std::vector<Coord3> desired;
95 std::vector<std::uint8_t> state;
96 std::vector<std::uint8_t> failure;
97 std::vector<std::uint32_t> cycle_walk;
98 std::vector<std::uint8_t> on_walk;
99 std::vector<std::uint8_t> walked;
100 std::vector<std::uint32_t> committed;
101 std::vector<Coord3> committed_from;
103 void reserve(std::size_t agent_count) {
104 occupant_key.
reserve(agent_count);
105 occupant_agent.reserve(agent_count);
106 claimed.reserve(agent_count);
107 desired.reserve(agent_count);
108 state.reserve(agent_count);
109 failure.reserve(agent_count);
110 cycle_walk.reserve(agent_count);
111 on_walk.reserve(agent_count);
112 walked.reserve(agent_count);
113 committed.reserve(agent_count);
114 committed_from.reserve(agent_count);
127 void reserve(std::size_t agent_count) { state_.reserve(agent_count); }
130 friend struct detail::JointMoveScratchAccess;
132 detail::JointMoveScratchState state_;
146struct JointMoveScratchAccess {
147 [[nodiscard]]
static auto state(JointMoveScratch& scratch)
noexcept
148 -> JointMoveScratchState& {
149 return scratch.state_;
151 [[nodiscard]]
static auto state(
const JointMoveScratch& scratch)
noexcept
152 ->
const JointMoveScratchState& {
153 return scratch.state_;
159enum class JointState : std::uint8_t {
166[[nodiscard]]
inline auto joint_find_occupant(
167 const JointMoveScratchState& scratch, std::uint64_t key)
noexcept
169 const auto begin = scratch.occupant_key.begin();
170 const auto end = scratch.occupant_key.end();
171 const auto it = std::lower_bound(begin, end, key);
172 if (it == end || *it != key) {
173 return static_cast<std::uint32_t
>(-1);
175 return scratch.occupant_agent[
static_cast<std::size_t
>(it - begin)];
178[[nodiscard]]
inline auto joint_claim(JointMoveScratchState& scratch,
179 std::uint64_t key) ->
bool {
181 std::lower_bound(scratch.claimed.begin(), scratch.claimed.end(), key);
182 if (it != scratch.claimed.end() && *it == key) {
185 scratch.claimed.insert(it, key);
209template <
typename World,
typename ClassOrTag,
typename OccupancyTag,
210 typename ReservationTag,
typename OnCommit>
211 requires std::invocable<OnCommit&, std::size_t, Coord3, Coord3>
212auto advance_path_agents_with_joint_movement(
213 World& world, std::span<PathAgentState> agents,
218 using Shape =
typename World::shape_type;
219 TESS_ASSERT(routes.routes.size() >= agents.size());
220 auto& scratch = detail::JointMoveScratchAccess::state(scratch_storage);
222 if (advance_options.max_steps == 0) {
225 const auto n = agents.size();
226 const auto none =
static_cast<std::uint32_t
>(-1);
228 for (std::size_t step = 0; step < advance_options.max_steps; ++step) {
229 scratch.desired.assign(n,
Coord3{});
230 scratch.state.assign(
231 n,
static_cast<std::uint8_t
>(detail::JointState::Inactive));
232 scratch.failure.assign(n,
static_cast<std::uint8_t
>(MovementStatus::Moved));
233 scratch.claimed.clear();
234 scratch.committed.clear();
235 scratch.committed_from.clear();
240 scratch.occupant_key.clear();
241 scratch.occupant_agent.clear();
242 for (std::size_t i = 0; i < n; ++i) {
243 scratch.occupant_key.push_back(tile_key<Shape>(agents[i].position).value);
244 scratch.occupant_agent.push_back(
static_cast<std::uint32_t
>(i));
248 auto& keys = scratch.occupant_key;
249 auto& vals = scratch.occupant_agent;
252 for (std::size_t i = 1; i < keys.size(); ++i) {
256 while (j > 0 && keys[j - 1] > key) {
257 keys[j] = keys[j - 1];
258 vals[j] = vals[j - 1];
267 bool any_pending =
false;
268 for (std::size_t i = 0; i < n; ++i) {
269 auto& agent = agents[i];
270 if (!agent.has_goal || agent.last_result != PathStatus::Found) {
273 const auto& route = routes.routes[i];
274 if (!detail::has_next_step(agent.path_index, route.size())) {
277 const auto to = route[agent.path_index + 1];
279 validate_movement_intent<
World, ClassOrTag, OccupancyTag,
282 scratch.desired[i] = to;
283 if (verdict.status == MovementStatus::Moved) {
284 if (detail::joint_claim(scratch, tile_key<Shape>(to).value)) {
286 static_cast<std::uint8_t
>(detail::JointState::Admitted);
289 static_cast<std::uint8_t
>(detail::JointState::Failed);
291 static_cast<std::uint8_t
>(MovementStatus::Occupied);
293 }
else if (verdict.status == MovementStatus::Occupied) {
298 if (world.template field<ReservationTag>(to)) {
300 static_cast<std::uint8_t
>(detail::JointState::Failed);
302 static_cast<std::uint8_t
>(MovementStatus::Reserved);
305 static_cast<std::uint8_t
>(detail::JointState::Pending);
310 static_cast<std::uint8_t
>(detail::JointState::Failed);
311 scratch.failure[i] =
static_cast<std::uint8_t
>(verdict.status);
316 bool changed = any_pending;
319 for (std::size_t i = 0; i < n; ++i) {
320 if (scratch.state[i] !=
321 static_cast<std::uint8_t
>(detail::JointState::Pending)) {
324 const auto key = tile_key<Shape>(scratch.desired[i]).value;
325 const auto occupant = detail::joint_find_occupant(scratch, key);
326 if (occupant == none ||
327 scratch.state[occupant] !=
328 static_cast<std::uint8_t
>(detail::JointState::Admitted)) {
331 if (detail::joint_claim(scratch, key)) {
333 static_cast<std::uint8_t
>(detail::JointState::Admitted);
337 static_cast<std::uint8_t
>(detail::JointState::Failed);
339 static_cast<std::uint8_t
>(MovementStatus::Occupied);
350 scratch.walked.assign(n, 0);
351 for (std::size_t start = 0; start < n; ++start) {
352 if (scratch.state[start] !=
353 static_cast<std::uint8_t
>(detail::JointState::Pending) ||
354 scratch.walked[start] != 0) {
357 scratch.cycle_walk.clear();
358 scratch.on_walk.assign(n, 0);
359 auto at =
static_cast<std::uint32_t
>(start);
362 static_cast<std::uint8_t
>(detail::JointState::Pending) &&
363 scratch.on_walk[at] == 0) {
364 scratch.on_walk[at] = 1;
365 scratch.walked[at] = 1;
366 scratch.cycle_walk.push_back(at);
367 at = detail::joint_find_occupant(
368 scratch, tile_key<Shape>(scratch.desired[at]).value);
371 const bool closed = at != none && scratch.on_walk[at] != 0;
375 std::size_t cycle_begin = scratch.cycle_walk.size();
376 for (std::size_t k = 0; k < scratch.cycle_walk.size(); ++k) {
377 if (scratch.cycle_walk[k] == at) {
382 const auto cycle_len = scratch.cycle_walk.size() - cycle_begin;
385 if (cycle_len >= 3) {
388 }
else if (cycle_len == 2) {
389 const auto a = scratch.cycle_walk[cycle_begin];
390 const auto b = scratch.cycle_walk[cycle_begin + 1];
391 switch (options.swap_policy) {
392 case SwapPolicy::Permit:
395 case SwapPolicy::PermitOnDeadlock:
396 admit = agents[a].blocked_retries >= options.deadlock_ticks &&
397 agents[b].blocked_retries >= options.deadlock_ticks;
399 case SwapPolicy::Forbid:
406 ++stats.swaps_denied;
410 for (std::size_t k = cycle_begin; k < scratch.cycle_walk.size(); ++k) {
411 const auto member = scratch.cycle_walk[k];
416 (void)detail::joint_claim(
417 scratch, tile_key<Shape>(scratch.desired[member]).value);
418 scratch.state[member] =
419 static_cast<std::uint8_t
>(detail::JointState::Admitted);
421 scratch.state[member] =
422 static_cast<std::uint8_t
>(detail::JointState::Failed);
423 scratch.failure[member] =
424 static_cast<std::uint8_t
>(MovementStatus::Occupied);
430 for (std::size_t i = 0; i < n; ++i) {
431 if (scratch.state[i] ==
432 static_cast<std::uint8_t
>(detail::JointState::Pending)) {
434 static_cast<std::uint8_t
>(detail::JointState::Failed);
436 static_cast<std::uint8_t
>(MovementStatus::Occupied);
442 std::size_t admitted_count = 0;
443 for (std::size_t i = 0; i < n; ++i) {
444 if (scratch.state[i] ==
445 static_cast<std::uint8_t
>(detail::JointState::Admitted)) {
446 world.template field<OccupancyTag>(agents[i].position) =
false;
450 for (std::size_t i = 0; i < n; ++i) {
451 switch (
static_cast<detail::JointState
>(scratch.state[i])) {
452 case detail::JointState::Admitted: {
453 auto& agent = agents[i];
454 const auto from = agent.position;
455 const auto to = scratch.desired[i];
456 world.template field<OccupancyTag>(to) =
true;
457 world.template field<ReservationTag>(to) =
false;
458 if (advance_options.movement_dirty_mask) {
459 world.mark_dirty(chunk_key<Shape>(chunk_coord<Shape>(from)),
460 advance_options.movement_dirty_mask,
461 Box3{from, Extent3{1, 1, 1}});
462 world.mark_dirty(chunk_key<Shape>(chunk_coord<Shape>(to)),
463 advance_options.movement_dirty_mask,
468 detail::resume_path_agent(agent);
469 scratch.committed.push_back(
static_cast<std::uint32_t
>(i));
470 scratch.committed_from.push_back(from);
471 ++stats.frame.advanced;
472 if (agent.position == agent.goal) {
473 arrive_path_agent(agent, accounting);
474 agent.last_result = PathStatus::Found;
475 ++stats.frame.arrived;
479 case detail::JointState::Failed: {
480 auto& agent = agents[i];
481 const auto status =
static_cast<MovementStatus
>(scratch.failure[i]);
482 record_movement_failure(stats.frame.movement_failures, status);
483 if (is_transient_movement_failure(status)) {
484 detail::block_path_agent(agent, status);
485 ++stats.frame.blocked_waits;
487 agent.last_result.reset();
488 agent.phase = PathAgentPhase::Unreachable;
489 fail_path_agent_flow(agent, accounting);
493 case detail::JointState::Inactive:
494 case detail::JointState::Pending:
508 for (std::size_t k = 0; k < scratch.committed.size(); ++k) {
509 const auto index =
static_cast<std::size_t
>(scratch.committed[k]);
510 on_commit(index, scratch.committed_from[k], agents[index].position);
513 if (admitted_count == 0) {
521template <
typename World,
typename ClassOrTag,
typename OccupancyTag,
522 typename ReservationTag>
523auto advance_path_agents_with_joint_movement(
524 World& world, std::span<PathAgentState> agents,
525 const PathAgentRoutes& routes, JointMoveScratch& scratch,
526 JointMoveOptions options = {}, PathAgentAdvanceOptions advance_options = {},
527 diagnostics::FlowAccounting* accounting =
nullptr) -> JointMoveStats {
528 return advance_path_agents_with_joint_movement<World, ClassOrTag,
529 OccupancyTag, ReservationTag>(
530 world, agents, routes, scratch, options, advance_options,
531 [](std::size_t, Coord3, Coord3) {}, accounting);
537template <
typename World,
typename Class, std::uint32_t MaxCost,
538 typename OccupancyTag,
typename ReservationTag>
539[[nodiscard]]
auto tick_weighted_path_agents_with_joint_movement(
540 PathAgentTickState& state, World& world, std::span<PathAgentState> agents,
541 PathRequestRuntime& runtime, JointMoveScratch& scratch,
542 PathAgentTickOptions options = {}, JointMoveOptions joint_options = {},
543 const RegionGraphT<typename World::residency_type>* graph =
nullptr,
544 JointMoveStats* joint_stats =
nullptr) -> PathAgentTickStats {
545 PathAgentTickStats stats;
546 stats.tick = advance_sim_tick(state.clock);
548 const bool repath_needed = prepare_path_agent_processing(
549 agents, options, stats, state.flow_accounting);
550 state.routes.ensure_size(agents.size());
551 if (state.pathing_dirty || repath_needed) {
553 state.pathing_dirty ? PathSubmitScope::All : PathSubmitScope::NeedsOnly;
554 stats.pathing = process_weighted_path_agents<World, Class, MaxCost>(
555 world, agents, runtime, options.cache_policy, graph, scope,
556 &state.routes, state.flow_accounting);
557 stats.processed_paths =
true;
558 state.pathing_dirty =
false;
561 auto joint = advance_path_agents_with_joint_movement<
562 World, Class, OccupancyTag, ReservationTag>(
563 world, agents, state.routes, scratch, joint_options,
564 PathAgentAdvanceOptions{options.max_steps, options.movement_dirty_mask},
565 state.flow_accounting);
566 stats.movement = joint.frame;
567 if (joint_stats !=
nullptr) {
568 *joint_stats = joint;
Configures cycle admission for one joint movement pass.
Definition joint_movement.h:54
std::uint32_t deadlock_ticks
Definition joint_movement.h:62
Caller-owned workspace for the joint movement pass.
Definition joint_movement.h:125
void reserve(std::size_t agent_count)
Pre-sizes every internal container for agent_count agents.
Definition joint_movement.h:127
Reports joint-admission outcomes alongside the standard movement stats.
Definition joint_movement.h:66
std::size_t swaps
Two-agent cycles exchanged under the active policy.
Definition joint_movement.h:73
std::size_t swaps_denied
Two-agent cycles refused by the active policy.
Definition joint_movement.h:75
std::size_t chained
Moves admitted only because their destination was vacated this tick.
Definition joint_movement.h:69
std::size_t rotations
Cycles of length >= 3 rotated one place.
Definition joint_movement.h:71
Describes an adjacent move and any versions it expects to remain current.
Definition movement.h:41
Configures bounded direct movement and the dirty bits it emits.
Definition path_agent.h:78
Summarizes path submission, results, movement, and failure outcomes.
Definition path_agent.h:50
Owns index-paired route copies retained across scoped processing passes.
Definition path_agent.h:115
Definition diagnostics.h:505