载入中...
搜索中...
未找到
RuntimeGeneration.cpp
浏览该文件的文档.
2
3#include "procgen/PointSet.h"
4
5#include <algorithm>
6#include <atomic>
7#include <chrono>
8#include <cmath>
9#include <exception>
10#include <limits>
11#include <sstream>
12#include <unordered_set>
13
14namespace eve::procgen {
15namespace {
16
17uint64_t nextSchedulerId() {
18 static std::atomic<uint64_t> next{1};
19 const auto id = next.fetch_add(1, std::memory_order_relaxed);
20 if (id == 0) std::terminate();
21 return id;
22}
23
24float distanceToCell(float x, float z, int cellX, int cellZ, float size) {
25 const float centerX = (float(cellX) + 0.5f) * size;
26 const float centerZ = (float(cellZ) + 0.5f) * size;
27 const float dx = centerX - x;
28 const float dz = centerZ - z;
29 return std::sqrt(dx * dx + dz * dz);
30}
31
32} // namespace
33
34int ProcgenCellRequest::getLevel() const { return level_; }
35int ProcgenCellRequest::getX() const { return x_; }
36int ProcgenCellRequest::getZ() const { return z_; }
37uint32_t ProcgenCellRequest::getSeed() const { return seed_; }
38uint64_t ProcgenCellRequest::getTicket() const { return ticket_; }
39float ProcgenCellRequest::getMinX() const { return float(x_) * cellSize_; }
40float ProcgenCellRequest::getMinZ() const { return float(z_) * cellSize_; }
41float ProcgenCellRequest::getMaxX() const { return float(x_ + 1) * cellSize_; }
42float ProcgenCellRequest::getMaxZ() const { return float(z_ + 1) * cellSize_; }
43
44int ProcgenGenerationJob::getLevel() const { return level_; }
45int ProcgenGenerationJob::getX() const { return x_; }
46int ProcgenGenerationJob::getZ() const { return z_; }
47uint32_t ProcgenGenerationJob::getSeed() const { return seed_; }
48uint64_t ProcgenGenerationJob::getTicket() const { return ticket_; }
49float ProcgenGenerationJob::getMinX() const { return float(x_) * cellSize_; }
50float ProcgenGenerationJob::getMinZ() const { return float(z_) * cellSize_; }
51float ProcgenGenerationJob::getMaxX() const { return float(x_ + 1) * cellSize_; }
52float ProcgenGenerationJob::getMaxZ() const { return float(z_ + 1) * cellSize_; }
53
56
58 : worldSeed_(worldSeed ? worldSeed : 1u),
59 ownerThread_(std::this_thread::get_id()),
60 schedulerId_(nextSchedulerId()) {}
61
62bool RuntimeGeneration::isOwnerThread() const noexcept { return std::this_thread::get_id() == ownerThread_; }
63
65 levels_.clear();
66 cells_.clear();
67 generateQueue_.clear();
68 cleanupQueue_.clear();
69 sources_.clear();
70 sourceOrder_.clear();
71 refreshPlan_.reset();
72 sourceRevision_ = 0;
73 committedRefreshRevision_ = 0;
74 generatingCount_ = 0;
75 activeCellCount_ = 0;
76 rejectedOutputCount_ = 0;
77 cancelledGenerationCount_ = 0;
78}
79
80int RuntimeGeneration::addLevel(float cellSize, float generationRadius, float cleanupMultiplier) {
81 if (cellSize <= 0.f || generationRadius < 0.f || cleanupMultiplier < 1.f) return -1;
82 levels_.push_back({cellSize, generationRadius, generationRadius * cleanupMultiplier});
83 return int(levels_.size()) - 1;
84}
85
86int RuntimeGeneration::getLevelCount() const { return int(levels_.size()); }
88 return level >= 0 && level < int(levels_.size()) ? levels_[size_t(level)].cellSize : 0.f;
89}
91 return level >= 0 && level < int(levels_.size())
92 ? levels_[size_t(level)].generationRadius
93 : 0.f;
94}
96 return level >= 0 && level < int(levels_.size()) ? levels_[size_t(level)].cleanupRadius : 0.f;
97}
98
100 directionWeight_ = std::clamp(weight, 0.f, 1.f);
101}
102float RuntimeGeneration::getDirectionWeight() const { return directionWeight_; }
103void RuntimeGeneration::setMaxGenerating(int count) { maxGenerating_ = std::max(1, count); }
104int RuntimeGeneration::getMaxGenerating() const { return maxGenerating_; }
105void RuntimeGeneration::setMaxActiveCells(int count) { maxActiveCells_ = std::max(0, count); }
106int RuntimeGeneration::getMaxActiveCells() const { return maxActiveCells_; }
107void RuntimeGeneration::setMaxPointsPerCell(int count) { maxPointsPerCell_ = std::max(0, count); }
108int RuntimeGeneration::getMaxPointsPerCell() const { return maxPointsPerCell_; }
110 maxResidentPoints_ = std::max(0, count);
111}
112int RuntimeGeneration::getMaxResidentPoints() const { return maxResidentPoints_; }
114 uint64_t count = 0;
115 for (const auto& [key, cell] : cells_)
116 count += uint64_t(cell.output.getCount());
117 return int(std::min(count, uint64_t(std::numeric_limits<int>::max())));
118}
119int RuntimeGeneration::getRejectedOutputCount() const { return rejectedOutputCount_; }
121 target = std::max(0, target);
122 uint64_t projectedResident = 0;
123 for (const auto& [key, cell] : cells_)
124 if (cell.state != State::Cleanup)
125 projectedResident += uint64_t(cell.output.getCount());
126 if (projectedResident <= uint64_t(target)) return 0;
127 std::vector<CellKey> candidates;
128 for (const auto& [key, cell] : cells_)
129 if (cell.state == State::Active) candidates.push_back(key);
130 std::sort(candidates.begin(), candidates.end(), [this](const CellKey& a, const CellKey& b) {
131 const float aPriority = cells_.at(a).priority;
132 const float bPriority = cells_.at(b).priority;
133 if (aPriority != bPriority) return aPriority > bPriority;
134 if (a.level != b.level) return a.level > b.level;
135 if (a.z != b.z) return a.z > b.z;
136 return a.x > b.x;
137 });
138 int scheduled = 0;
139 for (const auto& key : candidates) {
140 if (projectedResident <= uint64_t(target)) break;
141 auto& cell = cells_.at(key);
142 projectedResident -= uint64_t(cell.output.getCount());
143 transitionCellState(cell, State::Cleanup);
144 cell.trimmed = true;
145 cell.ticket = ++nextTicket_;
146 cleanupQueue_.push_back(key);
147 ++scheduled;
148 }
149 sortQueues();
150 return scheduled;
151}
153 maxGenerationRetries_ = std::max(0, count);
154}
155int RuntimeGeneration::getMaxGenerationRetries() const { return maxGenerationRetries_; }
157 frameTimeBudgetMs_ = std::max(0.f, milliseconds);
158}
159float RuntimeGeneration::getFrameTimeBudget() const { return frameTimeBudgetMs_; }
161 frameStartedNs_ = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
162 std::chrono::steady_clock::now().time_since_epoch())
163 .count());
164}
165
167 refreshWorkBudget_ = std::max(0, candidateCells);
168 if (refreshWorkBudget_ == 0 && refreshPlan_) advanceRefreshPlan(std::numeric_limits<uint64_t>::max());
169}
170int RuntimeGeneration::getRefreshWorkBudget() const { return refreshWorkBudget_; }
171bool RuntimeGeneration::isRefreshPending() const { return refreshPlan_.has_value(); }
172uint64_t RuntimeGeneration::getCommittedRefreshRevision() const { return committedRefreshRevision_; }
174 if (!refreshPlan_) return Result<uint64_t>::success(0);
175 const uint64_t budget =
176 refreshWorkBudget_ > 0 ? uint64_t(refreshWorkBudget_) : std::numeric_limits<uint64_t>::max();
177 return Result<uint64_t>::success(advanceRefreshPlan(budget));
178}
179
180void RuntimeGeneration::updateSource(float x, float z, float directionX, float directionZ) {
181 setGenerationSource("primary", x, z, directionX, directionZ, 1.f);
182}
183
184bool RuntimeGeneration::setGenerationSource(const std::string& id, float x, float z,
185 float directionX, float directionZ,
186 float radiusScale) {
187 if (id.empty() || radiusScale <= 0.f) return false;
188 const float directionLength = std::sqrt(directionX * directionX + directionZ * directionZ);
189 Source source;
190 source.x = x;
191 source.z = z;
192 source.directionX = directionLength > 0.f ? directionX / directionLength : 0.f;
193 source.directionZ = directionLength > 0.f ? directionZ / directionLength : 0.f;
194 source.radiusScale = radiusScale;
195 if (sources_.find(id) == sources_.end()) sourceOrder_.push_back(id);
196 sources_[id] = source;
198 return true;
199}
200
201bool RuntimeGeneration::removeGenerationSource(const std::string& id) {
202 if (sources_.erase(id) == 0) return false;
203 sourceOrder_.erase(std::remove(sourceOrder_.begin(), sourceOrder_.end(), id),
204 sourceOrder_.end());
206 return true;
207}
208
210 sources_.clear();
211 sourceOrder_.clear();
213}
214
215int RuntimeGeneration::getGenerationSourceCount() const { return int(sourceOrder_.size()); }
217 return index >= 0 && index < int(sourceOrder_.size()) ? sourceOrder_[size_t(index)]
218 : std::string();
219}
220
221void RuntimeGeneration::setFrustumCulling(bool enabled, float halfAngleDegrees,
222 float behindRadius) {
223 frustumCulling_ = enabled;
224 frustumHalfAngle_ = std::clamp(halfAngleDegrees, 1.f, 180.f);
225 frustumBehindRadius_ = std::max(0.f, behindRadius);
227}
228bool RuntimeGeneration::isFrustumCullingEnabled() const { return frustumCulling_; }
229float RuntimeGeneration::getFrustumHalfAngle() const { return frustumHalfAngle_; }
230float RuntimeGeneration::getFrustumBehindRadius() const { return frustumBehindRadius_; }
231
232void RuntimeGeneration::refreshGenerationSources() { requestGenerationRefresh(); }
233
234void RuntimeGeneration::requestGenerationRefresh() {
235 ++sourceRevision_;
236 if (!refreshPlan_) startRefreshPlan();
237 const uint64_t budget =
238 refreshWorkBudget_ > 0 ? uint64_t(refreshWorkBudget_) : std::numeric_limits<uint64_t>::max();
239 advanceRefreshPlan(budget);
240}
241
242void RuntimeGeneration::startRefreshPlan() {
243 constexpr float degreesToRadians = 0.017453292519943295f;
244 RefreshPlan plan;
245 plan.revision = sourceRevision_;
246 plan.levels = levels_;
247 plan.directionWeight = directionWeight_;
248 plan.frustumHalfAngle = frustumHalfAngle_;
249 plan.coneCosine = std::cos(frustumHalfAngle_ * degreesToRadians);
250 plan.frustumBehindRadius = frustumBehindRadius_;
251 plan.frustumCulling = frustumCulling_;
252 plan.sources.reserve(sourceOrder_.size());
253 for (const auto& sourceId : sourceOrder_) plan.sources.push_back(sources_.at(sourceId));
254 refreshPlan_ = std::move(plan);
255}
256
257uint64_t RuntimeGeneration::advanceRefreshPlan(uint64_t candidateBudget) {
258 uint64_t processed = 0;
259 while (refreshPlan_) {
260 auto& plan = *refreshPlan_;
261 if (plan.sourceIndex >= plan.sources.size() || plan.levels.empty()) {
262 const uint64_t committedRevision = plan.revision;
263 commitRefreshPlan(plan);
264 committedRefreshRevision_ = committedRevision;
265 refreshPlan_.reset();
266 if (committedRefreshRevision_ != sourceRevision_) {
267 startRefreshPlan();
268 continue;
269 }
270 break;
271 }
272 if (plan.levelIndex >= plan.levels.size()) {
273 ++plan.sourceIndex;
274 plan.levelIndex = 0;
275 plan.rangeReady = false;
276 continue;
277 }
278 if (processed >= candidateBudget) break;
279
280 const auto& source = plan.sources[plan.sourceIndex];
281 const size_t candidateLevelIndex = plan.levelIndex;
282 const auto& level = plan.levels[candidateLevelIndex];
283 const float generationRadius = level.generationRadius * source.radiusScale;
284 if (!plan.rangeReady) {
285 plan.minX = int(std::floor((source.x - generationRadius) / level.cellSize));
286 plan.maxX = int(std::floor((source.x + generationRadius) / level.cellSize));
287 plan.minZ = int(std::floor((source.z - generationRadius) / level.cellSize));
288 plan.maxZ = int(std::floor((source.z + generationRadius) / level.cellSize));
289 plan.cellX = plan.minX;
290 plan.cellZ = plan.minZ;
291 plan.rangeReady = true;
292 }
293 const int cellX = plan.cellX;
294 const int cellZ = plan.cellZ;
295 if (++plan.cellX > plan.maxX) {
296 plan.cellX = plan.minX;
297 if (++plan.cellZ > plan.maxZ) {
298 ++plan.levelIndex;
299 plan.rangeReady = false;
300 }
301 }
302 ++processed;
303
304 const float centerX = (float(cellX) + 0.5f) * level.cellSize;
305 const float centerZ = (float(cellZ) + 0.5f) * level.cellSize;
306 const float dx = centerX - source.x;
307 const float dz = centerZ - source.z;
308 const float distance = std::sqrt(dx * dx + dz * dz);
309 if (distance > generationRadius) continue;
310 const bool hasDirection = source.directionX != 0.f || source.directionZ != 0.f;
311 const float forward =
312 distance > 0.f && hasDirection ? (dx * source.directionX + dz * source.directionZ) / distance : 1.f;
313 if (plan.frustumCulling && hasDirection && distance > plan.frustumBehindRadius && forward < plan.coneCosine)
314 continue;
315 const CellKey key{int(candidateLevelIndex), cellX, cellZ};
316 const float priority = distance / std::max(generationRadius, 0.0001f) - plan.directionWeight * forward;
317 const auto [found, inserted] = plan.desiredPriorities.emplace(key, priority);
318 if (!inserted) found->second = std::min(found->second, priority);
319 }
320 return processed;
321}
322
323void RuntimeGeneration::commitRefreshPlan(const RefreshPlan& plan) {
324 for (auto& [key, cell] : cells_)
325 if (cell.state != State::Cleanup) cell.priority = std::numeric_limits<float>::max();
326 for (const auto& [key, priority] : plan.desiredPriorities) {
327 const auto existing = cells_.find(key);
328 if (existing != cells_.end()) {
329 if (existing->second.state == State::Cleanup) {
330 if (existing->second.trimmed) continue;
331 existing->second.ticket = ++nextTicket_;
332 transitionCellState(existing->second, existing->second.revision > 0 ? State::Active : State::Pending);
333 existing->second.trimmed = false;
334 cleanupQueue_.erase(std::remove(cleanupQueue_.begin(), cleanupQueue_.end(), key), cleanupQueue_.end());
335 if (existing->second.state == State::Pending) generateQueue_.push_back(key);
336 }
337 existing->second.priority = std::min(existing->second.priority, priority);
338 continue;
339 }
340 Cell cell;
341 cell.priority = priority;
342 cells_.emplace(key, cell);
343 generateQueue_.push_back(key);
344 }
345 for (auto& [key, cell] : cells_) {
346 if (cell.state == State::Cleanup) continue;
347 if ((cell.state == State::Pending || cell.state == State::Generating) &&
348 plan.desiredPriorities.find(key) == plan.desiredPriorities.end()) {
349 if (cell.state == State::Generating) ++cancelledGenerationCount_;
350 transitionCellState(cell, State::Cleanup);
351 cell.trimmed = false;
352 cell.ticket = ++nextTicket_;
353 cleanupQueue_.push_back(key);
354 continue;
355 }
356 const auto& level = plan.levels[size_t(key.level)];
357 bool retained = false;
358 for (const auto& source : plan.sources) {
359 if (distanceToCell(source.x, source.z, key.x, key.z, level.cellSize) <=
360 level.cleanupRadius * source.radiusScale) {
361 retained = true;
362 break;
363 }
364 }
365 if (retained) continue;
366 transitionCellState(cell, State::Cleanup);
367 cell.trimmed = false;
368 cell.ticket = ++nextTicket_;
369 cleanupQueue_.push_back(key);
370 }
371 generateQueue_.erase(
372 std::remove_if(generateQueue_.begin(), generateQueue_.end(), [this](const CellKey& key) {
373 const auto found = cells_.find(key);
374 return found == cells_.end() || found->second.state != State::Pending;
375 }),
376 generateQueue_.end());
377 sortQueues();
378}
379
380void RuntimeGeneration::sortQueues() {
381 const auto priorityLess = [this](const CellKey& a, const CellKey& b) {
382 const float aPriority = cells_.at(a).priority;
383 const float bPriority = cells_.at(b).priority;
384 if (aPriority != bPriority) return aPriority < bPriority;
385 if (a.level != b.level) return a.level < b.level;
386 if (a.z != b.z) return a.z < b.z;
387 return a.x < b.x;
388 };
389 std::sort(generateQueue_.begin(), generateQueue_.end(), priorityLess);
390 std::sort(cleanupQueue_.begin(), cleanupQueue_.end(), [](const CellKey& a, const CellKey& b) {
391 if (a.level != b.level) return a.level < b.level;
392 if (a.z != b.z) return a.z < b.z;
393 return a.x < b.x;
394 });
395}
396
397int RuntimeGeneration::getPendingGenerateCount() const { return int(generateQueue_.size()); }
398int RuntimeGeneration::getGeneratingCount() const { return generatingCount_; }
399int RuntimeGeneration::getActiveCellCount() const { return activeCellCount_; }
400int RuntimeGeneration::getPendingCleanupCount() const { return int(cleanupQueue_.size()); }
401int RuntimeGeneration::getCancelledGenerationCount() const { return cancelledGenerationCount_; }
403 return int(std::count_if(cells_.begin(), cells_.end(), [](const auto& entry) {
404 return entry.second.state == State::Failed;
405 }));
406}
407
409 int count = 0;
410 for (auto& [key, cell] : cells_) {
411 if (cell.state != State::Failed) continue;
412 transitionCellState(cell, State::Pending);
413 cell.failures = 0;
414 cell.ticket = ++nextTicket_;
415 generateQueue_.push_back(key);
416 ++count;
417 }
418 sortQueues();
419 return count;
420}
421
423 auto issued = nextGenerationJob();
424 if (!issued.ok()) return nullptr;
425 auto job = std::move(issued).takeValue();
426 if (!job) return nullptr;
427 auto* request = new ProcgenCellRequest();
428 request->level_ = job->level_;
429 request->x_ = job->x_;
430 request->z_ = job->z_;
431 request->seed_ = job->seed_;
432 request->ticket_ = job->ticket_;
433 request->schedulerId_ = job->schedulerId_;
434 request->cellSize_ = job->cellSize_;
435 return request;
436}
437
438std::optional<ProcgenGenerationJob> RuntimeGeneration::issueGenerationJob() {
439 if (generateQueue_.empty() || getGeneratingCount() >= maxGenerating_) return std::nullopt;
440 if (maxActiveCells_ > 0 && getActiveCellCount() + getGeneratingCount() >= maxActiveCells_) return std::nullopt;
441 if (frameTimeBudgetMs_ > 0.f && frameStartedNs_ != 0) {
442 const uint64_t now = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
443 std::chrono::steady_clock::now().time_since_epoch())
444 .count());
445 if (float(now - frameStartedNs_) * 0.000001f >= frameTimeBudgetMs_) return std::nullopt;
446 }
447 while (!generateQueue_.empty()) {
448 const CellKey key = generateQueue_.front();
449 generateQueue_.pop_front();
450 const auto found = cells_.find(key);
451 if (found == cells_.end() || found->second.state != State::Pending) continue;
452 transitionCellState(found->second, State::Generating);
453 found->second.ticket = ++nextTicket_;
454 return makeGenerationJob(key);
455 }
456 return std::nullopt;
457}
458
460 if (!isOwnerThread())
462 DiagnosticCode::Conflict, "runtime-generation scheduler called from a non-owner thread", "thread"));
463 return Result<std::optional<ProcgenGenerationJob>>::success(issueGenerationJob());
464}
465
466Result<std::reference_wrapper<RuntimeGeneration::Cell>> RuntimeGeneration::validateGenerationJob(
467 const ProcgenGenerationJob& job) {
468 if (!isOwnerThread())
470 DiagnosticCode::Conflict, "runtime-generation scheduler called from a non-owner thread", "thread"));
471 if (job.schedulerId_ != schedulerId_)
473 DiagnosticCode::Conflict, "runtime-generation job belongs to another scheduler", "job.scheduler"));
474 const CellKey key{job.level_, job.x_, job.z_};
475 const auto found = cells_.find(key);
476 if (found == cells_.end() || found->second.state != State::Generating || job.seed_ != cellSeed(key) ||
477 job.ticket_ != found->second.ticket)
479 Diagnostic::error(DiagnosticCode::Conflict, "runtime-generation job ticket is stale", "job.ticket"));
480 return Result<std::reference_wrapper<Cell>>::success(found->second);
481}
482
484 auto validated = validateGenerationJob(completion.job_);
485 if (!validated.ok()) return Result<uint64_t>::failure(validated.status());
486 Cell& cell = std::move(validated).takeValue().get();
487 const int outputPoints = completion.output_.getCount();
488 if ((maxPointsPerCell_ > 0 && outputPoints > maxPointsPerCell_) ||
489 (maxResidentPoints_ > 0 && getResidentPointCount() > maxResidentPoints_ - outputPoints)) {
490 ++rejectedOutputCount_;
491 if (maxResidentPoints_ > 0 && outputPoints <= maxResidentPoints_)
492 trimToResidentPoints(maxResidentPoints_ - outputPoints);
494 DiagnosticCode::InvalidArgument, "runtime-generation completion exceeds point budget", "output"));
495 }
496 cell.output = std::move(completion.output_);
497 cell.hasDelta = false;
498 cell.failures = 0;
499 ++cell.revision;
500 transitionCellState(cell, State::Active);
501 return Result<uint64_t>::success(cell.revision);
502}
503
505 auto validated = validateGenerationJob(job);
506 if (!validated.ok()) return Result<void>::failure(validated.status());
507 Cell& cell = std::move(validated).takeValue().get();
508 ++cell.failures;
509 cell.ticket = ++nextTicket_;
510 if (cell.failures <= maxGenerationRetries_) {
511 transitionCellState(cell, State::Pending);
512 generateQueue_.push_back({job.level_, job.x_, job.z_});
513 sortQueues();
514 } else {
515 transitionCellState(cell, State::Failed);
516 }
517 return Result<void>::success();
518}
519
521 auto result = nextCleanupRequest();
522 if (!result.ok()) return nullptr;
523 auto request = std::move(result).takeValue();
524 return request ? new ProcgenCellRequest(*request) : nullptr;
525}
526
528 if (!isOwnerThread())
530 DiagnosticCode::Conflict, "runtime-generation scheduler called from a non-owner thread", "thread"));
531 while (!cleanupQueue_.empty()) {
532 const CellKey key = cleanupQueue_.front();
533 cleanupQueue_.pop_front();
534 const auto found = cells_.find(key);
535 if (found == cells_.end() || found->second.state != State::Cleanup) continue;
536 found->second.ticket = ++nextTicket_;
537 return Result<std::optional<ProcgenCellRequest>>::success(makeRequest(key));
538 }
539 return Result<std::optional<ProcgenCellRequest>>::success(std::nullopt);
540}
541
545
547 if (!isOwnerThread() || !request || request->schedulerId_ != schedulerId_) return false;
548 const CellKey key{request->level_, request->x_, request->z_};
549 const auto found = cells_.find(key);
550 if (found == cells_.end() || request->seed_ != cellSeed(key) || request->ticket_ != found->second.ticket)
551 return false;
552 return found->second.state == State::Generating || found->second.state == State::Cleanup;
553}
554
556 if (!request || !output) return false;
558 job.level_ = request->level_;
559 job.x_ = request->x_;
560 job.z_ = request->z_;
561 job.seed_ = request->seed_;
562 job.ticket_ = request->ticket_;
563 job.schedulerId_ = request->schedulerId_;
564 job.cellSize_ = request->cellSize_;
565 auto completed = completeGenerationJob(ProcgenGenerationCompletion(std::move(job), *output));
566 return completed.ok();
567}
568
570 if (!request) return false;
572 job.level_ = request->level_;
573 job.x_ = request->x_;
574 job.z_ = request->z_;
575 job.seed_ = request->seed_;
576 job.ticket_ = request->ticket_;
577 job.schedulerId_ = request->schedulerId_;
578 job.cellSize_ = request->cellSize_;
579 auto failed = failGenerationJob(job);
580 return failed.ok();
581}
582
584 std::vector<const ProcgenCellRequest*> requests{request};
585 auto completed = completeCleanupsAtomic(requests);
586 return completed.ok();
587}
588
589Result<uint64_t> RuntimeGeneration::completeCleanupsAtomic(const std::vector<const ProcgenCellRequest*>& requests) {
590 if (!isOwnerThread())
592 DiagnosticCode::Conflict, "runtime-generation scheduler called from a non-owner thread", "thread"));
593 auto validated = validateCleanups(requests);
594 if (!validated.ok()) return Result<uint64_t>::failure(validated.status());
595 eraseValidatedCleanups(requests);
596 return Result<uint64_t>::success(static_cast<uint64_t>(requests.size()));
597}
598
599Result<void> RuntimeGeneration::validateCleanups(const std::vector<const ProcgenCellRequest*>& requests) const {
600 if (requests.empty())
602 DiagnosticCode::InvalidArgument, "cleanup transaction requires at least one request", "requests"));
603
604 std::unordered_set<CellKey, CellKeyHash> uniqueKeys;
605 uniqueKeys.reserve(requests.size());
606 for (const auto* request : requests) {
607 if (!request)
609 "cleanup transaction contains a null request", "requests"));
610 const CellKey key{request->level_, request->x_, request->z_};
611 if (!uniqueKeys.insert(key).second)
613 DiagnosticCode::Conflict, "cleanup transaction contains a duplicate cell", "requests"));
614 const auto found = cells_.find(key);
615 if (request->schedulerId_ != schedulerId_ || found == cells_.end() || found->second.state != State::Cleanup ||
616 request->seed_ != cellSeed(key) || request->ticket_ != found->second.ticket)
618 "cleanup transaction contains a stale request", "requests"));
619 }
620 return Result<void>::success();
621}
622
623void RuntimeGeneration::eraseValidatedCleanups(const std::vector<const ProcgenCellRequest*>& requests) {
624 for (const auto* request : requests) cells_.erase({request->level_, request->x_, request->z_});
625}
626
627bool RuntimeGeneration::hasCell(int level, int x, int z) const {
628 const auto found = cells_.find({level, x, z});
629 return found != cells_.end() && found->second.state == State::Active;
630}
631
633 const auto found = cells_.find({level, x, z});
634 return found != cells_.end() && found->second.state == State::Active
635 ? new PointSet(found->second.output)
636 : nullptr;
637}
638
639uint64_t RuntimeGeneration::getCellRevision(int level, int x, int z) const {
640 const auto found = cells_.find({level, x, z});
641 return found == cells_.end() ? 0 : found->second.revision;
642}
643
644Result<uint64_t> RuntimeGeneration::applyCellUpdate(int level, int x, int z, uint64_t expectedRevision,
645 const PointSet& output) {
646 const auto found = cells_.find({level, x, z});
647 if (found == cells_.end() || found->second.state != State::Active)
649 Diagnostic::error(DiagnosticCode::NotFound, "active runtime-generation cell was not found", "cell"));
650 if (expectedRevision == 0 || found->second.revision != expectedRevision)
652 Diagnostic::error(DiagnosticCode::Conflict, "runtime-generation cell revision is stale", "revision"));
653
654 auto delta = diffPointSets(found->second.output, output);
655 if (!delta.ok()) return Result<uint64_t>::failure(delta.status());
656 auto staged = applyPointDelta(found->second.output, delta.value());
657 if (!staged.ok()) return Result<uint64_t>::failure(staged.status());
658
659 const int outputPoints = staged.value().getCount();
660 const std::int64_t projectedResident = std::int64_t(getResidentPointCount()) - found->second.output.getCount() +
661 outputPoints;
662 if ((maxPointsPerCell_ > 0 && outputPoints > maxPointsPerCell_) ||
663 (maxResidentPoints_ > 0 && projectedResident > maxResidentPoints_)) {
664 ++rejectedOutputCount_;
667 "runtime-generation cell update exceeds the configured point budget", "output"));
668 }
669
670 found->second.output = std::move(staged).takeValue();
671 found->second.delta = std::move(delta).takeValue();
672 found->second.hasDelta = true;
673 ++found->second.revision;
674 return Result<uint64_t>::success(found->second.revision);
675}
676
677Result<uint64_t> RuntimeGeneration::migrateCellPointIds(int level, int x, int z, uint64_t expectedRevision) {
678 const CellKey key{level, x, z};
679 const auto found = cells_.find(key);
680 if (found == cells_.end() || found->second.state != State::Active)
682 Diagnostic::error(DiagnosticCode::NotFound, "active runtime-generation cell was not found", "cell"));
683 if (expectedRevision == 0 || found->second.revision != expectedRevision)
685 Diagnostic::error(DiagnosticCode::Conflict, "runtime-generation cell revision is stale", "revision"));
686
687 PointSet staged = found->second.output;
688 const std::uint64_t identityNamespace =
689 derivePointId((std::uint64_t(worldSeed_) << 32u) | cellSeed(key), found->second.revision);
690 auto assigned = staged.assignPointIds(identityNamespace);
691 if (!assigned.ok()) return Result<uint64_t>::failure(assigned.status());
692 found->second.output = std::move(staged);
693 found->second.hasDelta = false;
694 ++found->second.revision;
695 return Result<uint64_t>::success(found->second.revision);
696}
697
699 const auto found = cells_.find({level, x, z});
700 return found != cells_.end() && found->second.state == State::Active && found->second.hasDelta
701 ? new PointDelta(found->second.delta)
702 : nullptr;
703}
704
706 std::ostringstream out;
707 out << "levels=" << levels_.size() << " cells=" << cells_.size() << " pending=" << generateQueue_.size()
708 << " generating=" << getGeneratingCount() << " active=" << getActiveCellCount()
709 << " cleanup=" << cleanupQueue_.size() << " failed=" << getFailedCellCount() << " maxActive=" << maxActiveCells_
710 << " residentPoints=" << getResidentPointCount() << " maxResidentPoints=" << maxResidentPoints_
711 << " maxPointsPerCell=" << maxPointsPerCell_ << " rejectedOutputs=" << rejectedOutputCount_
712 << " cancelledGeneration=" << cancelledGenerationCount_ << " refreshPending=" << (refreshPlan_ ? 1 : 0)
713 << " refreshRevision=" << sourceRevision_ << " committedRefreshRevision=" << committedRefreshRevision_
714 << " refreshBudget=" << refreshWorkBudget_
715 << " stagedDesired=" << (refreshPlan_ ? refreshPlan_->desiredPriorities.size() : 0);
716 return out.str();
717}
718
719size_t RuntimeGeneration::CellKeyHash::operator()(const CellKey& key) const {
720 size_t hash = size_t(uint32_t(key.level));
721 hash ^= size_t(uint32_t(key.x)) * 0x9e3779b1u + (hash << 6u) + (hash >> 2u);
722 hash ^= size_t(uint32_t(key.z)) * 0x85ebca77u + (hash << 6u) + (hash >> 2u);
723 return hash;
724}
725
726ProcgenCellRequest RuntimeGeneration::makeRequest(const CellKey& key) const {
727 ProcgenCellRequest request;
728 request.level_ = key.level;
729 request.x_ = key.x;
730 request.z_ = key.z;
731 request.seed_ = cellSeed(key);
732 const auto found = cells_.find(key);
733 request.ticket_ = found == cells_.end() ? 0 : found->second.ticket;
734 request.schedulerId_ = schedulerId_;
735 request.cellSize_ = levels_[size_t(key.level)].cellSize;
736 return request;
737}
738
739ProcgenGenerationJob RuntimeGeneration::makeGenerationJob(const CellKey& key) const {
740 ProcgenGenerationJob job;
741 job.level_ = key.level;
742 job.x_ = key.x;
743 job.z_ = key.z;
744 job.seed_ = cellSeed(key);
745 const auto found = cells_.find(key);
746 job.ticket_ = found == cells_.end() ? 0 : found->second.ticket;
747 job.schedulerId_ = schedulerId_;
748 job.cellSize_ = levels_[size_t(key.level)].cellSize;
749 return job;
750}
751
752void RuntimeGeneration::transitionCellState(Cell& cell, State nextState) {
753 if (cell.state == nextState) return;
754 if (cell.state == State::Generating) --generatingCount_;
755 if (cell.state == State::Active) --activeCellCount_;
756 cell.state = nextState;
757 if (cell.state == State::Generating) ++generatingCount_;
758 if (cell.state == State::Active) ++activeCellCount_;
759}
760
761uint32_t RuntimeGeneration::cellSeed(const CellKey& key) const {
762 return deriveSeed(worldSeed_, "cell:" + std::to_string(key.level) + ":" +
763 std::to_string(key.x) + ":" + std::to_string(key.z));
764}
765
766} // namespace eve::procgen
LogicalId target
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
std::string output
int priority
std::uint64_t sourceId
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
const GltfImportRequest & request
std::uint32_t key
bool scheduled
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
int level
double & milliseconds
Definition OnnxGpgpu.cpp:19
std::string id
Definition PlayHost.cpp:108
bool found
float dz
float dx
std::uint32_t count
Cell cell
float size
Definition TreeMesh.cpp:156
uint32_t index
const UnitySourceAsset & source
std::size_t at
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Script-friendly collection of attributed 3D samples.
Definition PointSet.h:59
Result< void > assignPointIds(std::uint64_t namespaceId)
Fill zero identities deterministically and reject pre-existing duplicates.
Definition PointSet.cpp:310
int getCount() const
Returns the count.
Definition PointSet.cpp:48
One immutable runtime-generation cell request returned to script.
float getMinZ() const
Returns the min z.
int getLevel() const
Returns the level.
float getMinX() const
Returns the min x.
float getMaxZ() const
Returns the max z.
uint32_t getSeed() const
Returns the seed.
uint64_t getTicket() const
Unique scheduler ticket used to reject stale asynchronous completions.
float getMaxX() const
Returns the max x.
Owning worker result returned to a RuntimeGeneration scheduler.
ProcgenGenerationCompletion(ProcgenGenerationJob job, PointSet output)
Package a job and generated points without touching scheduler state.
Copyable generation input issued by one RuntimeGeneration scheduler.
float getMaxZ() const
Return the captured upper world-space Z bound.
float getMaxX() const
Return the captured upper world-space X bound.
uint32_t getSeed() const
Return the deterministic seed derived from world seed, level and coordinates.
float getMinX() const
Return the captured lower world-space X bound.
float getMinZ() const
Return the captured lower world-space Z bound.
uint64_t getTicket() const
Return the ticket; valid only together with this job's private scheduler identity.
int getZ() const
Return the cell Z coordinate captured at issue time.
int getX() const
Return the cell X coordinate captured at issue time.
int getLevel() const
Return the hierarchical grid level captured at issue time.
void setMaxPointsPerCell(int count)
Set the maximum points allowed in one generated cell; zero disables the limit.
bool completeGeneration(ProcgenCellRequest *request, PointSet *output)
Compatibility-only bool projection of completeGenerationJob; request and output are borrowed.
bool isFrustumCullingEnabled() const
True when frustum culling enabled.
Result< std::optional< ProcgenCellRequest > > nextCleanupRequest()
Claim an owning cleanup ticket, or an empty optional when no cleanup is pending.
float getFrustumBehindRadius() const
Returns the frustum behind radius.
int getPendingGenerateCount() const
Returns the pending generate count.
int trimToResidentPoints(int target)
Schedule lowest-priority active cells for cleanup until at most target points remain.
bool isRequestCurrent(const ProcgenCellRequest *request) const
Test whether an issued generation or cleanup request still owns its scheduler ticket.
void beginFrame()
Start a new budget window before consuming generation requests.
float getLevelCellSize(int level) const
Returns the level cell size.
int addLevel(float cellSize, float generationRadius, float cleanupMultiplier)
Add a hierarchical grid level.
int getCancelledGenerationCount() const
Return issued generation requests invalidated before completion.
int getActiveCellCount() const
Returns the active cell count.
Result< uint64_t > migrateCellPointIds(int level, int x, int z, uint64_t expectedRevision)
Assign deterministic identities to a legacy active cell atomically.
int getLevelCount() const
Returns the level count.
float getLevelCleanupRadius(int level) const
Returns the level cleanup radius.
bool hasCell(int level, int x, int z) const
void clearGenerationSources()
Clears generation sources.
int getPendingCleanupCount() const
Returns the pending cleanup count.
float getDirectionWeight() const
Returns the direction weight.
void setFrustumCulling(bool enabled, float halfAngleDegrees, float behindRadius)
Enable view-cone scheduling.
int getGeneratingCount() const
Returns the generating count.
int getMaxGenerationRetries() const
Return retries allowed after the initial generation attempt.
uint64_t getCommittedRefreshRevision() const
Return the latest source revision atomically published into generation and cleanup queues.
bool isRefreshPending() const
Return whether a staged source snapshot still needs planning work.
uint64_t getCellRevision(int level, int x, int z) const
Returns the cell revision.
void setFrameTimeBudget(float milliseconds)
CPU issue budget in milliseconds for one frame; zero disables the limit.
bool failGeneration(ProcgenCellRequest *request)
Compatibility-only bool projection of failGenerationJob; request is borrowed.
Result< uint64_t > applyCellUpdate(int level, int x, int z, uint64_t expectedRevision, const PointSet &output)
Atomically replace an active cell through an identity-based delta.
int getRefreshWorkBudget() const
Return the per-call source-refresh candidate budget, or zero when synchronous.
Result< void > failGenerationJob(const ProcgenGenerationJob &job)
Report an issued job failure and apply the bounded retry policy.
ProcgenCellRequest * nextGenerate()
Compatibility facade returning a heap request; prefer nextGenerationJob().
int getMaxResidentPoints() const
Return the total resident-point limit, or zero when unlimited.
bool removeGenerationSource(const std::string &id)
Removes generation source.
int getGenerationSourceCount() const
Returns the generation source count.
PointSet * getCellOutput(int level, int x, int z) const
Returns the cell output.
float getLevelGenerationRadius(int level) const
Returns the level generation radius.
int getFailedCellCount() const
Return cells stopped after exhausting the generation retry policy.
int retryFailedCells()
Reset and requeue all terminally failed cells still tracked by the scheduler.
int getMaxGenerating() const
Returns the max generating.
Result< uint64_t > completeCleanupsAtomic(const std::vector< const ProcgenCellRequest * > &requests)
Atomically acknowledge several cleanup tickets owned by this scheduler.
float getFrustumHalfAngle() const
Returns the frustum half angle.
PointDelta * getCellDelta(int level, int x, int z) const
Return a caller-owned copy of the latest committed delta, or null when unavailable....
bool setGenerationSource(const std::string &id, float x, float z, float directionX, float directionZ, float radiusScale=1.f)
Add or update a named generation source, then rebuild desired cells.
Result< uint64_t > completeCleanupRequest(const ProcgenCellRequest &request)
Commit one cleanup ticket after consumers have removed its generated content.
void updateSource(float x, float z, float directionX, float directionZ)
Recompute desired cells for a generation source.
std::string getGenerationSourceId(int index) const
Returns the generation source id.
Result< uint64_t > continueGenerationRefresh()
Advance a budgeted source refresh without changing the requested sources.
bool completeCleanup(ProcgenCellRequest *request)
Compatibility-only bool projection of completeCleanupsAtomic; request is borrowed.
void setMaxGenerating(int count)
Maximum simultaneously issued generation requests.
void setMaxActiveCells(int count)
Maximum active plus in-flight cells; zero disables the resident-cell limit.
float getFrameTimeBudget() const
Returns the frame time budget.
bool isOwnerThread() const noexcept
Return whether the caller is the thread that constructed this scheduler.
void setMaxGenerationRetries(int count)
Set retries after the initial generation attempt; zero fails immediately.
Result< std::optional< ProcgenGenerationJob > > nextGenerationJob()
Issue one owning worker job, or an empty optional when no work is currently admissible.
int getMaxPointsPerCell() const
Return the per-cell point limit, or zero when unlimited.
int getRejectedOutputCount() const
Return generation or restore outputs rejected by point budgets.
std::string debugReport() const
Debug report.
void setDirectionWeight(float weight)
Weight favoring cells in front of the source, clamped to [0,1].
int getResidentPointCount() const
Return points retained by active or not-yet-acknowledged cleanup cells.
ProcgenCellRequest * nextCleanup()
Compatibility-only heap projection of nextCleanupRequest; caller owns the returned request.
void setRefreshWorkBudget(int candidateCells)
Limit source-refresh planning work performed by one call.
Result< uint64_t > completeGenerationJob(ProcgenGenerationCompletion completion)
Atomically publish an owning worker completion.
void refreshGenerationSources()
Re-evaluate generation/cleanup queues from every registered source.
int getMaxActiveCells() const
Return the resident-cell limit, or zero when unlimited.
RuntimeGeneration(uint32_t worldSeed=1)
Create a scheduler with a stable world seed.
void clear()
Remove levels, cells, queues and outputs while retaining the world seed.
void setMaxResidentPoints(int count)
Set the total point limit across active cell outputs; zero disables the limit.
std::vector< double > forward(const Policy &p, const Observation &o)
Forward.
Definition Learning.h:65
@ Cell
A cell was removed; the out-parameter holds it.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
std::uint64_t derivePointId(std::uint64_t namespaceId, std::uint64_t ordinal)
Deterministically derive a non-zero stable point identity.
Definition PointSet.cpp:469
Result< PointSet > applyPointDelta(const PointSet &base, const PointDelta &delta)
Apply a delta atomically, rejecting stale or internally inconsistent input.
uint32_t deriveSeed(uint32_t parent, const std::string &scope)
Stable label-based seed derivation; independent pipeline branches do not perturb each other.
Definition PointSet.cpp:459
Result< PointDelta > diffPointSets(const PointSet &before, const PointSet &after)
Compute an identity-based delta without mutating either snapshot.
bool enabled
Transactional identity-based change set between two point snapshots.
Definition PointDelta.h:16