diff --git a/src/Game_sync.cpp b/src/Game_sync.cpp index 9477443bd..ee32adaf2 100644 --- a/src/Game_sync.cpp +++ b/src/Game_sync.cpp @@ -143,6 +143,8 @@ void Game::syncStep(Sint32 localTeam) Uint64 startTick=SDL_GetTicks64(); + map.stepGradients(); + for (int i=0; isyncStep(); diff --git a/src/map/Map.cpp b/src/map/Map.cpp index 48281a3e2..855a9c1f6 100644 --- a/src/map/Map.cpp +++ b/src/map/Map.cpp @@ -86,6 +86,42 @@ Map::Map() fertilityMaximum = 0; } + +#ifndef YOG_SERVER_ONLY +// Draining the field jobs lives here, next to clear(), so every build that links +// Map.cpp can free a Map; the scheduling and the worker are in MapStep.cpp. + +void Map::publishGradients(Uint32 now) +{ + std::unique_lock lock(gradientMutex); + while (!gradientJobs.empty() && gradientJobs.front().publishTick <= now) + { + gradientWake.wait(lock, [this] { return gradientJobs.front().done; }); + GradientJob job = gradientJobs.front(); + gradientJobs.pop_front(); + std::swap(*job.slot, job.buffer); + spareGradients.push_back(job.buffer); + } +} + +void Map::finishPendingGradients() +{ + publishGradients(0xFFFFFFFF); + if (gradientWorker.joinable()) + { + { + std::lock_guard lock(gradientMutex); + gradientWorkerQuit = true; + gradientWake.notify_all(); + } + gradientWorker.join(); + } + for (Uint16 *spare : spareGradients) + delete[] spare; + spareGradients.clear(); +} +#endif // !YOG_SERVER_ONLY + Map::~Map(void) { clear(); @@ -93,6 +129,9 @@ Map::~Map(void) void Map::clear() { +#ifndef YOG_SERVER_ONLY + finishPendingGradients(); +#endif // A failed load can own only a subset of these arrays. for (int t=0; t +#include +#include +#include +#include +#include #include #include @@ -130,6 +135,11 @@ class Map #ifndef YOG_SERVER_ONLY //! Do a step associated with map (grow resources and process bullets) void syncStep(Uint32 stepCounter); + //! Refresh the resource and area fields round robin, one per tick: publishes the + //! field due this tick, then seeds the next one from the current map and hands it to + //! the worker thread, which propagates it while the units step. A field is seeded and + //! published at fixed ticks, so its content never depends on thread timing. + void stepGradients(); #endif // !YOG_SERVER_ONLY //! Switch the Fog of War bufferResourceType void switchFogOfWar(void); @@ -627,9 +637,15 @@ class Map void updateGlobalGradient(Uint8 *gradient); //! Dijkstra on a freshly seeded field (see MapInternal.h). Seed costs must be //! between 0 and the largest terrain step (currently 42); do not pass a completed - //! field. Uses shared scratch storage: calls across all Maps must be serial and - //! non-reentrant. swimClass must be in [0, SWIM_CLASS_COUNT). + //! field. swimClass must be in [0, SWIM_CLASS_COUNT). This overload is for the main + //! thread and uses its scratch; the worker thread passes its own scratch below, so + //! two fields can propagate concurrently. void propagateGradient(Uint16 *gradient, int swimClass); + struct GradientScratch; + struct GradientScratchDeleter { void operator()(GradientScratch *scratch) const; }; + typedef std::unique_ptr GradientScratchPtr; + void propagateGradient(Uint16 *gradient, int swimClass, GradientScratch &scratch); + static GradientScratchPtr newGradientScratch(); //! Step toward the neighbour with the highest value minus step cost. strict requires //! real progress; otherwise a random sidestep to an equal cell is accepted when blocked. bool directionByGradient(Uint32 teamMask, int swimClass, int x, int y, const Uint16 *gradient, int *dx, int *dy, bool strict) const; @@ -729,6 +745,35 @@ class Map // Used to attract idle workers into clearing // areas that aren't clear Uint16 *clearAreasGradient[Team::MAX_COUNT][SWIM_CLASS_COUNT]; + + // Round-robin fields in flight. A job is seeded at tick T into its own buffer, + // propagated by the worker thread, and published (buffer swapped into the slot) + // at tick T + GRADIENT_PIPELINE_TICKS. The slot keeps its previous buffer + // readable until then; afterwards that buffer becomes a spare for a later job. + struct GradientJob + { + Uint16 **slot = NULL; + Uint16 *buffer = NULL; + int swimClass = 0; + Uint32 publishTick = 0; + bool done = false; + }; + std::deque gradientJobs; + std::vector spareGradients; + std::thread gradientWorker; + std::mutex gradientMutex; + std::condition_variable gradientWake; + bool gradientWorkerQuit = false; + //! Pick and seed the next field of the round robin into job.buffer; false if none is in use. + bool pickRoundRobinField(GradientJob &job); + void startGradientWorker(); + //! Publish every job due at or before tick now, waiting for the worker if needed. + void publishGradients(Uint32 now); + //! Publish every job in flight and stop the worker (before freeing gradients). + void finishPendingGradients(); + void seedResourcesGradient(int teamNumber, Uint8 resourceType, int swimClass, Uint16 *gradient); + void seedGuardAreasGradient(int teamNumber, int swimClass, Uint16 *gradient); + void seedClearAreasGradient(int teamNumber, int swimClass, Uint16 *gradient); public: // Used to guide explorers diff --git a/src/map/MapStep.cpp b/src/map/MapStep.cpp index 5fd91c69c..f14d003ae 100644 --- a/src/map/MapStep.cpp +++ b/src/map/MapStep.cpp @@ -12,6 +12,7 @@ #endif // !YOG_SERVER_ONLY #include +#include // growResources, syncStep, fog of war, discovery, explored area @@ -92,50 +93,128 @@ void Map::syncStep(Uint32 stepCounter) if (team < game->mapHeader.getNumberOfTeams()) updateExploredArea(team); } - - // We only update one gradient per step, round robin over the gradients in use: - bool updated=false; - while (!updated) +} + +namespace { + +// Ticks between seeding a round-robin field and publishing it. The worker has +// this long to propagate it; the units read the previous field meanwhile. Part of +// the simulation: every peer must use the same value. +constexpr Uint32 GRADIENT_PIPELINE_TICKS = 3; + +} // namespace + +void Map::startGradientWorker() +{ + if (gradientWorker.joinable()) + return; + gradientWorkerQuit = false; + gradientWorker = std::thread([this] { - int numberOfTeam=game->mapHeader.getNumberOfTeams(); - for (int t=0; t lock(gradientMutex); + for (;;) + { + // Jobs are propagated in order; the first undone one is ours. + GradientJob *job = NULL; + gradientWake.wait(lock, [this, &job] + { + if (gradientWorkerQuit) + return true; + for (GradientJob &j : gradientJobs) + if (!j.done) + { + job = &j; + return true; + } + return false; + }); + if (gradientWorkerQuit) + return; + Uint16 *buffer = job->buffer; + int swimClass = job->swimClass; + lock.unlock(); + propagateGradient(buffer, swimClass, *scratch); + lock.lock(); + // publishGradients pops only done jobs from the front and waits on this + // one, so it is still in the deque; find it again by buffer in case the + // deque reallocated while we were unlocked. + for (GradientJob &j : gradientJobs) + if (j.buffer == buffer) + j.done = true; + gradientWake.notify_all(); + } + }); +} + +void Map::stepGradients() +{ + const Uint32 now = game->stepCounter; + publishGradients(now); + if (spareGradients.empty()) + spareGradients.push_back(new Uint16[size]); + GradientJob job; + job.buffer = spareGradients.back(); + if (!pickRoundRobinField(job)) + return; + spareGradients.pop_back(); + job.publishTick = now + GRADIENT_PIPELINE_TICKS; + startGradientWorker(); + std::lock_guard lock(gradientMutex); + gradientJobs.push_back(job); + gradientWake.notify_all(); +} + +bool Map::pickRoundRobinField(GradientJob &p) +{ + const int numberOfTeam = game->mapHeader.getNumberOfTeams(); + for (int pass = 0; pass < 2; pass++) + { + for (int t = 0; t < numberOfTeam; t++) + for (int r = 0; r < MAX_RESOURCES; r++) + for (int s = 0; s < SWIM_CLASS_COUNT; s++) if (resourcesGradient[t][r][s] && !gradientUpdated[t][r][s]) { - updateResourcesGradient(t, r, s); - gradientUpdated[t][r][s]=true; - return; + gradientUpdated[t][r][s] = true; + p.slot = &resourcesGradient[t][r][s]; + p.swimClass = s; + seedResourcesGradient(t, r, s, p.buffer); + return true; } - for (int t=0; t 0; @@ -134,7 +140,6 @@ void Map::updateGuardAreasGradient(int teamNumber, int swimClass) gradient[i] = GRADIENT_UNREACHABLE; } - propagateGradient(gradient, swimClass); } void Map::updateGuardAreasGradient(int teamNumber) @@ -154,6 +159,12 @@ void Map::updateGuardAreasGradient() void Map::updateClearAreasGradient(int teamNumber, int swimClass) { Uint16 *gradient = clearAreasGradient[teamNumber][swimClass]; + seedClearAreasGradient(teamNumber, swimClass, gradient); + propagateGradient(gradient, swimClass); +} + +void Map::seedClearAreasGradient(int teamNumber, int swimClass, Uint16 *gradient) +{ assert(gradient); bool canSwim = swimClass > 0; @@ -177,7 +188,6 @@ void Map::updateClearAreasGradient(int teamNumber, int swimClass) gradient[i] = GRADIENT_UNREACHABLE; } - propagateGradient(gradient, swimClass); } void Map::updateClearAreasGradient(int teamNumber) diff --git a/src/map/gradient/MapGradientField.cpp b/src/map/gradient/MapGradientField.cpp index f39d8a270..8fa6d045b 100644 --- a/src/map/gradient/MapGradientField.cpp +++ b/src/map/gradient/MapGradientField.cpp @@ -2,6 +2,7 @@ // Copyright (C) 2026 glob2 contributors #include "Map.h" +#include #include "MapInternal.h" #include "Utilities.h" @@ -25,10 +26,31 @@ namespace // Costs above this would run into the sentinels; propagation stops there. constexpr int COST_LIMIT = GRADIENT_AT_GOAL - GRADIENT_UNREACHABLE - 1 - MAX_STEP; - // Shared scratch storage retains capacity between fields. Calls must be serial - // and non-reentrant, including calls on different Maps. Before parallelizing - // propagation, give each worker its own workspace; these are not cached fields. +} + +// Propagation scratch: the bucket queues. One per thread that propagates (the +// main thread's below, the worker's on its own stack); capacity is retained +// between fields. Not a cache: it holds no field data between calls. +struct Map::GradientScratch +{ std::vector buckets[BUCKETS]; +}; + +void Map::GradientScratchDeleter::operator()(GradientScratch *scratch) const +{ + delete scratch; +} + +Map::GradientScratchPtr Map::newGradientScratch() +{ + return GradientScratchPtr(new GradientScratch); +} + +void Map::propagateGradient(Uint16 *gradient, int swimClass) +{ + // Main-thread callers only (units, AIs, editor), which are serial. + static GradientScratch mainScratch; + propagateGradient(gradient, swimClass, mainScratch); } static_assert(WATER_STEP[Map::SWIM_CLASS_EVEN] == GRADIENT_STEP); @@ -69,22 +91,23 @@ int Map::stepCost(int dx, int dy, size_t targetIndex, int swimClass) const // (0 for GRADIENT_AT_GOAL). Seed costs must be in [0, MAX_STEP], so the initial // queue fits one bucket rotation. Reseed before reuse; a propagated field is // not a valid seed buffer. GRADIENT_FORBIDDEN cells are obstacles. -void Map::propagateGradient(Uint16 *gradient, int swimClass) +void Map::propagateGradient(Uint16 *gradient, int swimClass, GradientScratch &scratch) { + std::vector (&bk)[BUCKETS] = scratch.buckets; for (int b = 0; b < BUCKETS; b++) - buckets[b].clear(); + bk[b].clear(); size_t pending = 0; for (size_t i = 0; i < size; i++) if (gradient[i] > GRADIENT_UNREACHABLE) { int cost = GRADIENT_AT_GOAL - gradient[i]; assert(cost <= MAX_STEP); - buckets[cost % BUCKETS].push_back((int)i); + bk[cost % BUCKETS].push_back((int)i); pending++; } for (int cur = 0; pending > 0 && cur <= COST_LIMIT; cur++) { - std::vector &bucket = buckets[cur % BUCKETS]; + std::vector &bucket = bk[cur % BUCKETS]; // Relaxations may append to other buckets but never to this one // (each step is positive and less than BUCKETS), so iteration is safe. for (size_t bi = 0; bi < bucket.size(); bi++) @@ -103,8 +126,8 @@ void Map::propagateGradient(Uint16 *gradient, int swimClass) // All reverse edges enter i, so they share its two terrain costs. const int cardinalCost = cur + stepCost(1, 0, (size_t)i, swimClass); const int diagonalCost = cur + stepCost(1, 1, (size_t)i, swimClass); - auto& cardinalBucket = buckets[cardinalCost % BUCKETS]; - auto& diagonalBucket = buckets[diagonalCost % BUCKETS]; + auto& cardinalBucket = bk[cardinalCost % BUCKETS]; + auto& diagonalBucket = bk[diagonalCost % BUCKETS]; auto relax = [&](size_t n, int cost, std::vector& destination) { if (gradient[n] != GRADIENT_FORBIDDEN && cost < GRADIENT_AT_GOAL - gradient[n]) diff --git a/src/map/gradient/MapGradientGlobal.cpp b/src/map/gradient/MapGradientGlobal.cpp index c694d531f..f406857b9 100644 --- a/src/map/gradient/MapGradientGlobal.cpp +++ b/src/map/gradient/MapGradientGlobal.cpp @@ -120,6 +120,12 @@ Uint16 *Map::getResourceGradient(int teamNumber, int resourceType, int swimClass void Map::updateResourcesGradient(int teamNumber, Uint8 resourceType, int swimClass) { Uint16 *gradient=resourcesGradient[teamNumber][resourceType][swimClass]; + seedResourcesGradient(teamNumber, resourceType, swimClass, gradient); + propagateGradient(gradient, swimClass); +} + +void Map::seedResourcesGradient(int teamNumber, Uint8 resourceType, int swimClass, Uint16 *gradient) +{ assert(gradient); bool canSwim = swimClass > 0; @@ -152,5 +158,4 @@ void Map::updateResourcesGradient(int teamNumber, Uint8 resourceType, int swimCl gradient[i]=GRADIENT_FORBIDDEN; } - propagateGradient(gradient, swimClass); } diff --git a/src/map/io/MapIO.cpp b/src/map/io/MapIO.cpp index ea3b5e630..f33ab18a1 100644 --- a/src/map/io/MapIO.cpp +++ b/src/map/io/MapIO.cpp @@ -268,6 +268,9 @@ void Map::addTeam(void) void Map::removeTeam(void) { +#ifndef YOG_SERVER_ONLY + finishPendingGradients(); +#endif int numberOfTeam=game->mapHeader.getNumberOfTeams(); assert(numberOfTeam