diff --git a/source/graphics/MiniMapTexture.cpp b/source/graphics/MiniMapTexture.cpp index 1e7ebbda7e..0bc049160d 100644 --- a/source/graphics/MiniMapTexture.cpp +++ b/source/graphics/MiniMapTexture.cpp @@ -683,7 +683,7 @@ void CMiniMapTexture::UpdateAndUploadEntities( ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { - LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, m_Simulation.GetSimContext().GetCurrentDisplayedPlayer()); + LosVisibility vis = cmpRangeManager->GetCachedLosVisibility(it->first, m_Simulation.GetSimContext().GetCurrentDisplayedPlayer()); if (vis != LosVisibility::HIDDEN) { v.a = 255; diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp index 136a5dfde9..d1b55ad74e 100644 --- a/source/simulation2/components/CCmpRangeManager.cpp +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -1884,6 +1884,41 @@ public: return GetLosVisibility(handle, player); } + LosVisibility GetCachedLosVisibility(CEntityHandle ent, player_id_t player) const override + { + entity_id_t entId = ent.GetId(); + + // Entities not with positions in the world are never visible + if (entId == INVALID_ENTITY) + return LosVisibility::HIDDEN; + + CmpPtr cmpPosition(ent); + if (!cmpPosition || !cmpPosition->IsInWorld()) + return LosVisibility::HIDDEN; + + // Gaia and observers do not have a visibility cache + if (player <= 0) + return ComputeLosVisibility(ent, player); + + // The cache of entities that changed since the last visibility update + // is still stale (this notably keeps newly created entities, e.g. + // corpses, from being hidden until the next turn). + if (m_ModifiedEntitiesSet.find(entId) != m_ModifiedEntitiesSet.end()) + return ComputeLosVisibility(ent, player); + + EntityMap::const_iterator it = m_EntityData.find(entId); + if (it == m_EntityData.end()) + return ComputeLosVisibility(ent, player); + + return static_cast(GetPlayerVisibility(it->second.visibilities, player)); + } + + LosVisibility GetCachedLosVisibility(entity_id_t ent, player_id_t player) const override + { + CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); + return GetCachedLosVisibility(handle, player); + } + LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const override { int i = (x / LOS_TILE_SIZE).ToInt_RoundToNearest(); diff --git a/source/simulation2/components/CCmpUnitRenderer.cpp b/source/simulation2/components/CCmpUnitRenderer.cpp index f37e660346..7427646810 100644 --- a/source/simulation2/components/CCmpUnitRenderer.cpp +++ b/source/simulation2/components/CCmpUnitRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -461,7 +461,7 @@ void CCmpUnitRenderer::UpdateVisibility(SUnit& unit) const else { CmpPtr cmpRangeManager(GetSystemEntity()); - unit.visibility = cmpRangeManager->GetLosVisibility(unit.entity, + unit.visibility = cmpRangeManager->GetCachedLosVisibility(unit.entity, GetSimContext().GetCurrentDisplayedPlayer()); } } diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h index 4ed58b91e5..7ab2758c19 100644 --- a/source/simulation2/components/ICmpRangeManager.h +++ b/source/simulation2/components/ICmpRangeManager.h @@ -297,6 +297,21 @@ public: virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const = 0; virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const = 0; + /** + * Returns the visibility status of the given entity as of the last per-turn + * visibility update, with respect to the given player. + * Unlike GetLosVisibility, this does not recompute the visibility of + * entities in LOS regions that changed since the last update, so it never + * calls into scripts for them. The result may thus be one turn stale; it is + * consistent with the VisibilityChanged messages. Meant for rendering and + * other non-simulation callers (see #5876 and #8327). + * Entities whose own state changed since the last update (e.g. created or + * moved entities, see RequestVisibilityUpdate) are still recomputed, as are + * queries for players without a visibility cache (gaia and observers). + */ + virtual LosVisibility GetCachedLosVisibility(CEntityHandle ent, player_id_t player) const = 0; + virtual LosVisibility GetCachedLosVisibility(entity_id_t ent, player_id_t player) const = 0; + /** * Returns the visibility status of the given position, with respect to the given player. * This respects the GetLosRevealWholeMap flag. diff --git a/source/simulation2/components/tests/test_RangeManager.h b/source/simulation2/components/tests/test_RangeManager.h index 7fc89a1875..742a0ff4f3 100644 --- a/source/simulation2/components/tests/test_RangeManager.h +++ b/source/simulation2/components/tests/test_RangeManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -290,6 +290,73 @@ public: } + void test_cached_los_visibility() + { + ComponentTestHelper test(*g_ScriptContext); + + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + + // A viewer unit of player 1, and a static entity of player 2 watched by it. + MockVisionRgm vision; + MockPositionRgm viewerPosition, watchedPosition; + test.AddMock(100, IID_Vision, vision); + test.AddMock(100, IID_Position, viewerPosition); + test.AddMock(101, IID_Position, watchedPosition); + + cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512)); + cmp->SetSharedLos(1, { 1 }); + cmp->SetSharedLos(2, { 2 }); + { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } + { CMessageCreate msg(101); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(101, -1, 2); cmp->HandleMessage(msg, false); } + + auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, int x, int z) { + pos.m_Pos = CFixedVector3D(fixed::FromInt(x), fixed::Zero(), fixed::FromInt(z)); + CMessagePositionChanged msg(ent, true, fixed::FromInt(x), fixed::FromInt(z), entity_angle_t::Zero()); + cmp->HandleMessage(msg, false); + }; + auto update = [&cmp] { + CMessageUpdate msg(fixed::FromInt(1)); + cmp->HandleMessage(msg, false); + }; + + move(100, viewerPosition, 10, 10); + move(101, watchedPosition, 20, 10); + + // Both entities changed since the last visibility update, so the + // cached query recomputes them and agrees with the uncached one. + TS_ASSERT_EQUALS(cmp->GetCachedLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + + // The per-turn update refreshes the cache. + update(); + TS_ASSERT_EQUALS(cmp->GetCachedLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + + // The viewer moves away: the watched entity leaves player 1's vision. + // Its LOS region is now dirty, so the uncached query recomputes (the + // entity is in explored-but-not-visible territory and doesn't retain + // in fog, hence hidden), while the cached query still returns the + // result of the last per-turn update. + move(100, viewerPosition, 400, 400); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(static_cast(101), 1), LosVisibility::HIDDEN); + TS_ASSERT_EQUALS(cmp->GetCachedLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + + // After the next per-turn update both agree again. + update(); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(static_cast(101), 1), LosVisibility::HIDDEN); + TS_ASSERT_EQUALS(cmp->GetCachedLosVisibility(static_cast(101), 1), LosVisibility::HIDDEN); + + // An entity whose own state changed since the last update (here: it + // moved into player 1's vision) is recomputed even by the cached + // query, so e.g. newly spawned corpses don't stay hidden until the + // next turn. + move(101, watchedPosition, 401, 400); + TS_ASSERT_EQUALS(cmp->GetLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + TS_ASSERT_EQUALS(cmp->GetCachedLosVisibility(static_cast(101), 1), LosVisibility::VISIBLE); + } + void test_IsInTargetParabolicRange() { ComponentTestHelper test(*g_ScriptContext);