diff --git a/libopenage/gamestate/map.cpp b/libopenage/gamestate/map.cpp index 8300ce273d..88578fabdb 100644 --- a/libopenage/gamestate/map.cpp +++ b/libopenage/gamestate/map.cpp @@ -49,7 +49,7 @@ Map::Map(const std::shared_ptr &state, auto sector = grid->get_sector(chunk_idx); auto cost_field = sector->get_cost_field(); - cost_field->set_cost(tile_idx, path_cost.second); + cost_field->set_cost(tile_idx, path_cost.second, time::TIME_ZERO); } } } diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index 7f85264d01..33c2844ace 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,6 +1,7 @@ add_sources(libopenage cost_field.cpp definitions.cpp + field_cache.cpp flow_field.cpp grid.cpp integration_field.cpp diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp index 3299084548..3ba37a6242 100644 --- a/libopenage/pathfinding/cost_field.cpp +++ b/libopenage/pathfinding/cost_field.cpp @@ -13,7 +13,8 @@ namespace openage::path { CostField::CostField(size_t size) : size{size}, - cells(this->size * this->size, COST_MIN) { + cells(this->size * this->size, COST_MIN), + valid_until{time::TIME_MIN} { log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); } @@ -33,29 +34,34 @@ cost_t CostField::get_cost(size_t idx) const { return this->cells.at(idx); } -void CostField::set_cost(const coord::tile_delta &pos, cost_t cost) { - this->cells[pos.ne + pos.se * this->size] = cost; +void CostField::set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until) { + this->set_cost(pos.ne + pos.se * this->size, cost, valid_until); } -void CostField::set_cost(size_t x, size_t y, cost_t cost) { - this->cells[x + y * this->size] = cost; -} - -void CostField::set_cost(size_t idx, cost_t cost) { - this->cells[idx] = cost; +void CostField::set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until) { + this->set_cost(x + y * this->size, cost, valid_until); } const std::vector &CostField::get_costs() const { return this->cells; } -void CostField::set_costs(std::vector &&cells) { +void CostField::set_costs(std::vector &&cells, const time::time_t &valid_until) { ENSURE(cells.size() == this->cells.size(), "cells vector has wrong size: " << cells.size() << "; expected: " << this->cells.size()); this->cells = std::move(cells); + this->valid_until = valid_until; +} + +bool CostField::is_dirty(const time::time_t &time) { + return time >= this->valid_until; +} + +void CostField::clean() { + this->valid_until = time::TIME_MAX; } } // namespace openage::path diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h index a1fc87b046..f214e30f96 100644 --- a/libopenage/pathfinding/cost_field.h +++ b/libopenage/pathfinding/cost_field.h @@ -6,6 +6,7 @@ #include #include "pathfinding/types.h" +#include "time/time.h" namespace openage { @@ -64,8 +65,9 @@ class CostField { * * @param pos Coordinates of the cell (relative to field origin). * @param cost Cost to set. + * @param changed Time at which the cost value is changed. */ - void set_cost(const coord::tile_delta &pos, cost_t cost); + void set_cost(const coord::tile_delta &pos, cost_t cost, const time::time_t &valid_until); /** * Set the cost at a specified position. @@ -73,16 +75,22 @@ class CostField { * @param x X-coordinate of the cell. * @param y Y-coordinate of the cell. * @param cost Cost to set. + * @param changed Time at which the cost value is changed. */ - void set_cost(size_t x, size_t y, cost_t cost); + void set_cost(size_t x, size_t y, cost_t cost, const time::time_t &valid_until); /** * Set the cost at a specified position. * * @param idx Index of the cell. * @param cost Cost to set. + * @param changed Time at which the cost value is changed. */ - void set_cost(size_t idx, cost_t cost); + + inline void set_cost(size_t idx, cost_t cost, const time::time_t &until) { + cells[idx] = cost; + valid_until = until; + } /** * Get the cost field values. @@ -95,8 +103,21 @@ class CostField { * Set the cost field values. * * @param cells Cost field values. + * @param changed Time at which the cost value is changed. + */ + void set_costs(std::vector &&cells, const time::time_t &changed); + + /** + * Check if the cost field is dirty at the specified time. + * + * @param time Cost field is dirty if the cost field is accessed after the time given in valid_until. */ - void set_costs(std::vector &&cells); + bool is_dirty(const time::time_t &time); + + /** + * Cleans the dirty flag by setting it to time_MAX. + */ + void clean(); private: /** @@ -104,6 +125,11 @@ class CostField { */ size_t size; + /** + * Time the cost field was last changed. + */ + time::time_t valid_until; + /** * Cost field values. */ diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp index 483261ebaf..2dfb825fa0 100644 --- a/libopenage/pathfinding/demo/demo_0.cpp +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -33,15 +33,17 @@ void path_demo_0(const util::Path &path) { // Cost field with some obstacles auto cost_field = std::make_shared(field_length); - cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE); - cost_field->set_cost(coord::tile_delta{1, 0}, 254); - cost_field->set_cost(coord::tile_delta{4, 3}, 128); - cost_field->set_cost(coord::tile_delta{5, 3}, 128); - cost_field->set_cost(coord::tile_delta{6, 3}, 128); - cost_field->set_cost(coord::tile_delta{4, 4}, 128); - cost_field->set_cost(coord::tile_delta{5, 4}, 128); - cost_field->set_cost(coord::tile_delta{6, 4}, 128); - cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE); + + const time::time_t time = time::TIME_ZERO; + cost_field->set_cost(coord::tile_delta{0, 0}, COST_IMPASSABLE, time); + cost_field->set_cost(coord::tile_delta{1, 0}, 254, time); + cost_field->set_cost(coord::tile_delta{4, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{5, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{6, 3}, 128, time); + cost_field->set_cost(coord::tile_delta{4, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{5, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{6, 4}, 128, time); + cost_field->set_cost(coord::tile_delta{1, 7}, COST_IMPASSABLE, time); log::log(INFO << "Created cost field"); // Create an integration field from the cost field diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp index 1edeba52a1..a31b9b786b 100644 --- a/libopenage/pathfinding/demo/demo_1.cpp +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -11,6 +11,8 @@ #include "pathfinding/portal.h" #include "pathfinding/sector.h" #include "util/timer.h" +#include "time/time_loop.h" +#include "time/clock.h" #include "renderer/gui/integration/public/gui_application_with_logger.h" #include "renderer/opengl/window.h" @@ -25,13 +27,17 @@ namespace openage::path::tests { void path_demo_1(const util::Path &path) { + auto time_loop = std::make_shared(); + time_loop->run(); + auto clock = time_loop->get_clock(); auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); + const time::time_t time = clock->get_time(); // Initialize the cost field for each sector. for (auto §or : grid->get_sectors()) { auto cost_field = sector->get_cost_field(); std::vector sector_cost = sectors_cost.at(sector->get_id()); - cost_field->set_costs(std::move(sector_cost)); + cost_field->set_costs(std::move(sector_cost), time); } // Initialize portals between sectors. @@ -87,16 +93,20 @@ void path_demo_1(const util::Path &path) { coord::tile start{2, 26}; coord::tile target{36, 2}; + const time::time_t request_time = clock->get_time(); + PathRequest path_request{ grid->get_id(), start, target, + request_time }; + grid->init_portal_nodes(); timer.start(); Path path_result = pathfinder->get_path(path_request); timer.stop(); - + log::log(INFO << "Pathfinding request at " << request_time); log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " us"); // Create a renderer to display the grid and path @@ -127,6 +137,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, + clock->get_time() }; timer.reset(); @@ -147,6 +158,7 @@ void path_demo_1(const util::Path &path) { grid->get_id(), start, target, + clock->get_time() }; timer.reset(); diff --git a/libopenage/pathfinding/field_cache.cpp b/libopenage/pathfinding/field_cache.cpp new file mode 100644 index 0000000000..95276b0818 --- /dev/null +++ b/libopenage/pathfinding/field_cache.cpp @@ -0,0 +1,30 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "field_cache.h" + +namespace openage::path { + +void FieldCache::add(cache_key_t cache_key, + field_cache_t cache_entry) { + this->cache[cache_key] = cache_entry; +} + +void FieldCache::evict(cache_key_t cache_key) { + this->cache.erase(cache_key); +} + +bool FieldCache::is_cached(cache_key_t cache_key) { + return this->cache.contains(cache_key); +} + +std::shared_ptr FieldCache::get_integration_field(cache_key_t cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.first; +} + +std::shared_ptr FieldCache::get_flow_field(cache_key_t cache_key) { + auto cached = this->cache.find(cache_key); + return cached->second.second; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/field_cache.h b/libopenage/pathfinding/field_cache.h new file mode 100644 index 0000000000..23374e5123 --- /dev/null +++ b/libopenage/pathfinding/field_cache.h @@ -0,0 +1,79 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "pathfinding/types.h" +#include "util/hash.h" + +namespace openage { +namespace coord { +struct tile_delta; +} // namespace coord + +namespace path { +class IntegrationField; +class FlowField; + +/** + * Cache to store already calculated flow and integration fields for the pathfinding algorithm. + */ +class FieldCache { +public: + FieldCache() = default; + ~FieldCache() = default; + + /** + * Adds a new field cache entry to the cache with a given portal and sector cache key. + */ + void add(cache_key_t cache_key, + field_cache_t cache_entry); + + /** + * Evicts a given field cache entry from the cache at the given cache key. + */ + void evict(cache_key_t cache_key); + + /** + * Checks if there is a cached entry at a specific cache key. + */ + bool is_cached(cache_key_t cache_key); + + /** + * Gets the integration field from a given cache entry. + */ + std::shared_ptr get_integration_field(cache_key_t cache_key); + + /** + * Gets the flow field from a given cache entry. + */ + std::shared_ptr get_flow_field(cache_key_t cache_key); + +private: + /** + * Hash function for the field cache. + */ + struct pair_hash { + template + std::size_t operator()(const std::pair &pair) const { + return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); + } + }; + + /** + * Cache for already computed fields. + * + * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are + * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated + * when the field is reused. + */ + std::unordered_map + cache; +}; + +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp index 265c5285cc..a71845cffb 100644 --- a/libopenage/pathfinding/integrator.cpp +++ b/libopenage/pathfinding/integrator.cpp @@ -5,6 +5,7 @@ #include "log/log.h" #include "pathfinding/cost_field.h" +#include "pathfinding/field_cache.h" #include "pathfinding/flow_field.h" #include "pathfinding/integration_field.h" #include "pathfinding/portal.h" @@ -36,24 +37,31 @@ std::shared_ptr Integrator::integrate(const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration field for portal " << portal->get_id() << " from sector " << other_sector_id); + + // retrieve cached integration field + auto cached_integration_field = this->field_cache->get_integration_field(cache_key); + if (with_los) { log::log(SPAM << "Performing LOS pass on cached field"); // Make a copy of the cached field to avoid modifying the cached field - auto integration_field = std::make_shared(*cached->second.first); + auto integration_field = std::make_shared(*cached_integration_field); // Only integrate LOS; leave the rest of the field as is integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); return integration_field; } - return cached->second.first; + return cached_integration_field; } log::log(DBG << "Integrating cost field for portal " << portal->get_id() @@ -97,17 +105,24 @@ std::shared_ptr Integrator::build(const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached flow field for portal " << portal->get_id() << " from sector " << other_sector_id); + + // retrieve cached flow field + auto cached_flow_field = this->field_cache->get_flow_field(cache_key); + if (with_los) { log::log(SPAM << "Transferring LOS flags to cached flow field"); // Make a copy of the cached flow field - auto flow_field = std::make_shared(*cached->second.second); + auto flow_field = std::make_shared(*cached_flow_field); // Transfer the LOS flags to the flow field flow_field->transfer_dynamic_flags(integration_field); @@ -115,7 +130,7 @@ std::shared_ptr Integrator::build(const std::shared_ptrsecond.second; + return cached_flow_field; } log::log(DBG << "Building flow field for portal " << portal->get_id() @@ -140,17 +155,26 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los) { + bool with_los, + bool evict_cache) { auto cache_key = std::make_pair(portal->get_id(), other_sector_id); - auto cached = this->field_cache.find(cache_key); - if (cached != this->field_cache.end()) { + if (evict_cache) { + this->field_cache->evict(cache_key); + } + else if (this->field_cache->is_cached(cache_key)) { log::log(DBG << "Using cached integration and flow fields for portal " << portal->get_id() << " from sector " << other_sector_id); + + // retrieve cached fields + auto cached_integration_field = this->field_cache->get_integration_field(cache_key); + auto cached_flow_field = this->field_cache->get_flow_field(cache_key); + + if (with_los) { log::log(SPAM << "Performing LOS pass on cached field"); // Make a copy of the cached integration field - auto integration_field = std::make_shared(*cached->second.first); + auto integration_field = std::make_shared(*cached_integration_field); // Only integrate LOS; leave the rest of the field as is integration_field->integrate_los(cost_field, other, other_sector_id, portal, target); @@ -158,7 +182,7 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ log::log(SPAM << "Transferring LOS flags to cached flow field"); // Make a copy of the cached flow field - auto flow_field = std::make_shared(*cached->second.second); + auto flow_field = std::make_shared(*cached_flow_field); // Transfer the LOS flags to the flow field flow_field->transfer_dynamic_flags(integration_field); @@ -166,11 +190,11 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ return std::make_pair(integration_field, flow_field); } - return cached->second; + return std::make_pair(cached_integration_field, cached_flow_field); } - auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los); - auto flow_field = this->build(integration_field, other, other_sector_id, portal); + auto integration_field = this->integrate(cost_field, other, other_sector_id, portal, target, with_los, evict_cache); + auto flow_field = this->build(integration_field, other, other_sector_id, portal, evict_cache); log::log(DBG << "Caching integration and flow fields for portal ID: " << portal->get_id() << ", sector ID: " << other_sector_id); @@ -182,7 +206,9 @@ Integrator::get_return_t Integrator::get(const std::shared_ptr &cost_ std::shared_ptr cached_flow_field = std::make_shared(*flow_field); cached_flow_field->reset_dynamic_flags(); - this->field_cache[cache_key] = std::make_pair(cached_integration_field, cached_flow_field); + field_cache_t field_cache = field_cache_t(cached_integration_field, cached_flow_field); + + this->field_cache->add(cache_key, field_cache); return std::make_pair(integration_field, flow_field); } diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h index c27d3c1eb1..daa0e226fd 100644 --- a/libopenage/pathfinding/integrator.h +++ b/libopenage/pathfinding/integrator.h @@ -2,11 +2,10 @@ #pragma once -#include #include -#include #include "pathfinding/types.h" +#include "pathfinding/field_cache.h" #include "util/hash.h" @@ -65,7 +64,8 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); /** * Build the flow field from an integration field. @@ -91,7 +91,8 @@ class Integrator { const std::shared_ptr &other, sector_id_t other_sector_id, const std::shared_ptr &portal, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); using get_return_t = std::pair, std::shared_ptr>; @@ -123,30 +124,15 @@ class Integrator { sector_id_t other_sector_id, const std::shared_ptr &portal, const coord::tile_delta &target, - bool with_los = true); + bool with_los = true, + bool evict_cache = false); private: - /** - * Hash function for the field cache. - */ - struct pair_hash { - template - std::size_t operator()(const std::pair &pair) const { - return util::hash_combine(std::hash{}(pair.first), std::hash{}(pair.second)); - } - }; - + /** * Cache for already computed fields. - * - * Key is the portal ID and the sector ID from which the field was entered. Fields that are cached are - * cleared of dynamic flags, i.e. wavefront or LOS flags. These have to be recalculated - * when the field is reused. */ - std::unordered_map, - get_return_t, - pair_hash> - field_cache; + std::unique_ptr field_cache; }; } // namespace path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index d1c61ac325..df1b34a599 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -6,6 +6,7 @@ #include "coord/tile.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage::path { @@ -20,6 +21,8 @@ struct PathRequest { coord::tile start; /// Target position of the path. coord::tile target; + /// Time the request was made. + const time::time_t time; }; /** diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp index 2fef36053a..0164f249e3 100644 --- a/libopenage/pathfinding/pathfinder.cpp +++ b/libopenage/pathfinding/pathfinder.cpp @@ -155,7 +155,8 @@ const Path Pathfinder::get_path(const PathRequest &request) { prev_sector_id, portal, target_delta, - with_los); + with_los, + next_sector->is_dirty(request.time)); flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); prev_integration_field = sector_fields.first; diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp index f50dc209bf..2da73690c0 100644 --- a/libopenage/pathfinding/sector.cpp +++ b/libopenage/pathfinding/sector.cpp @@ -249,4 +249,8 @@ void Sector::connect_exits() { } } +bool Sector::is_dirty(const time::time_t &time) { + return this->cost_field->is_dirty(time); +} + } // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h index a4ba94fa52..a8aad24c01 100644 --- a/libopenage/pathfinding/sector.h +++ b/libopenage/pathfinding/sector.h @@ -9,6 +9,7 @@ #include "coord/chunk.h" #include "pathfinding/portal.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage::path { @@ -103,6 +104,13 @@ class Sector { */ void connect_exits(); + /** + * Check if the cost field is dirty at the specified time. + * + * @param time Specified time to check. + */ + bool is_dirty(const time::time_t &time); + private: /** * ID of the sector. diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 5f92496ff8..2ac0960c34 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -10,6 +10,7 @@ #include "pathfinding/integration_field.h" #include "pathfinding/integrator.h" #include "pathfinding/types.h" +#include "time/time.h" namespace openage { @@ -23,7 +24,8 @@ void flow_field() { // | 1 | 1 | 1 | // | 1 | X | 1 | // | 1 | 1 | 1 | - cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); + const time::time_t time = time::TIME_ZERO; + cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}, time); // Test the different field types { diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h index 1b48a49654..3229010a91 100644 --- a/libopenage/pathfinding/types.h +++ b/libopenage/pathfinding/types.h @@ -4,6 +4,8 @@ #include #include +#include +#include namespace openage::path { @@ -109,4 +111,17 @@ using sector_id_t = size_t; */ using portal_id_t = size_t; +class FlowField; +class IntegrationField; + +/** + * Cache key for accessing the field cache using a portal id and a sector id. + */ +using cache_key_t = std::pair; + +/** + * Returnable field cache entry pair containing an integration field and a flow field. + */ +using field_cache_t = std::pair, std::shared_ptr>; + } // namespace openage::path