Просмотр исходного кода

MT optimizations in visibility

Panagiotis Christopoulos Charitos 9 лет назад
Родитель
Сommit
b8a48d2d2d
5 измененных файлов с 355 добавлено и 351 удалено
  1. 21 11
      include/anki/scene/SceneNode.h
  2. 36 40
      include/anki/scene/Sector.h
  3. 6 8
      src/scene/SceneGraph.cpp
  4. 40 47
      src/scene/Sector.cpp
  5. 252 245
      src/scene/Visibility.cpp

+ 21 - 11
include/anki/scene/SceneNode.h

@@ -9,8 +9,10 @@
 #include <anki/util/Hierarchy.h>
 #include <anki/util/Rtti.h>
 #include <anki/util/BitMask.h>
+#include <anki/util/BitSet.h>
 #include <anki/util/List.h>
 #include <anki/util/Enum.h>
+#include <anki/util/Thread.h>
 #include <anki/scene/SceneComponent.h>
 
 namespace anki
@@ -84,19 +86,23 @@ public:
 		return ErrorCode::NONE;
 	}
 
+	ANKI_USE_RESULT Error frameUpdateComplete(F32 prevUpdateTime, F32 crntTime)
+	{
+		m_sectorVisitedBitset.unsetAll();
+		return frameUpdate(prevUpdateTime, crntTime);
+	}
+
 	/// Return the last frame the node was updated. It checks all components
 	U32 getLastUpdateFrame() const;
 
 	/// Inform if a sector has visited this node.
-	void setSectorVisited(Bool visited)
+	/// @return The previous value.
+	Bool fetchSetSectorVisited(U testId, Bool visited)
 	{
-		m_flags.set(Flag::SECTOR_VISITED, visited);
-	}
-
-	/// Check if a sector has visited this node.
-	Bool getSectorVisited() const
-	{
-		return m_flags.get(Flag::SECTOR_VISITED);
+		LockGuard<SpinLock> lock(m_sectorVisitedBitsetLock);
+		Bool prevValue = m_sectorVisitedBitset.get(testId);
+		m_sectorVisitedBitset.set(testId, visited);
+		return prevValue;
 	}
 
 	/// Iterate all components
@@ -192,10 +198,9 @@ protected:
 	ResourceManager& getResourceManager();
 
 private:
-	enum class Flag
+	enum class Flag : U8
 	{
-		MARKED_FOR_DELETION = 1 << 0,
-		SECTOR_VISITED = 1 << 1
+		MARKED_FOR_DELETION = 1 << 0
 	};
 	ANKI_ENUM_ALLOW_NUMERIC_OPERATIONS(Flag, friend)
 
@@ -207,6 +212,11 @@ private:
 	String m_name; ///< A unique name
 	BitMask<Flag> m_flags;
 
+	/// A mask of bits for each test. If bit set then the node was visited by
+	/// a sector.
+	BitSet<256> m_sectorVisitedBitset = {false};
+	SpinLock m_sectorVisitedBitsetLock;
+
 	void cacheImportantComponents();
 };
 /// @}

+ 36 - 40
include/anki/scene/Sector.h

@@ -150,6 +150,33 @@ private:
 		SpatialComponent* sp);
 };
 
+/// The context for visibility tests from a single FrustumComponent (not for
+/// all of them).
+class SectorGroupVisibilityTestsContext
+{
+	friend class SectorGroup;
+
+public:
+	U getVisibleSceneNodeCount() const
+	{
+		return m_visibleNodes.getSize();
+	}
+
+	template<typename Func>
+	void iterateVisibleSceneNodes(PtrSize begin, PtrSize end, Func func)
+	{
+		ANKI_ASSERT(begin <= end);
+
+		for(U i = begin; i < end; ++i)
+		{
+			func(*m_visibleNodes[i]);
+		}
+	}
+
+private:
+	SArray<SceneNode*> m_visibleNodes;
+};
+
 /// Sector group. This is supposed to represent the whole scene
 class SectorGroup
 {
@@ -169,63 +196,32 @@ public:
 	void spatialUpdated(SpatialComponent* sp);
 	void spatialDeleted(SpatialComponent* sp);
 
-	void prepareForVisibilityTests(
-		const FrustumComponent& frc, const Renderer& r);
+	void prepareForVisibilityTests();
 
-	PtrSize getVisibleNodesCount() const
-	{
-		return m_visibleNodesCount;
-	}
-
-	template<typename Func>
-	ANKI_USE_RESULT Error iterateVisibleSceneNodes(
-		PtrSize begin, PtrSize end, Func func);
+	void findVisibleNodes(const FrustumComponent& frc,
+		U threadId,
+		SectorGroupVisibilityTestsContext& ctx) const;
 
 private:
 	SceneGraph* m_scene; ///< Keep it here to access various allocators
 	List<Sector*> m_sectors;
 	List<Portal*> m_portals;
 
-	SceneNode** m_visibleNodes = nullptr;
-	U m_visibleNodesCount = 0;
-
 	List<SpatialComponent*> m_spatialsDeferredBinning;
 	SpinLock m_mtx;
 
 	void findVisibleSectors(const FrustumComponent& frc,
-		List<Sector*>& visibleSectors,
-		U& spatialsCount,
-		const Renderer& r);
+		List<const Sector*>& visibleSectors,
+		U& spatialsCount) const;
 
 	/// Recursive method
 	void findVisibleSectorsInternal(const FrustumComponent& frc,
-		Sector& s,
-		List<Sector*>& visibleSectors,
-		U& spatialsCount,
-		const Renderer& r);
+		const Sector& s,
+		List<const Sector*>& visibleSectors,
+		U& spatialsCount) const;
 
 	void binSpatial(SpatialComponent* sp);
 };
-
-//==============================================================================
-template<typename TFunc>
-inline Error SectorGroup::iterateVisibleSceneNodes(
-	PtrSize begin, PtrSize end, TFunc func)
-{
-	Error err = ErrorCode::NONE;
-
-	SceneNode** it = m_visibleNodes + begin;
-	SceneNode** itend = m_visibleNodes + end;
-	for(; it != itend && !err; ++it)
-	{
-		SceneNode* sn = *it;
-		ANKI_ASSERT(sn->getSectorVisited() == true);
-		sn->setSectorVisited(false);
-		err = func(*sn);
-	}
-
-	return err;
-}
 /// @}
 
 } // end namespace anki

+ 6 - 8
src/scene/SceneGraph.cpp

@@ -99,20 +99,18 @@ public:
 			return comp.updateReal(node, prevTime, crntTime, updated);
 		});
 
-		if(err)
+		// Update children
+		if(!err)
 		{
-			return err;
+			err = node.visitChildren([&](SceneNode& child) -> Error {
+				return updateInternal(child, prevTime, crntTime);
+			});
 		}
 
-		// Update children
-		err = node.visitChildren([&](SceneNode& child) -> Error {
-			return updateInternal(child, prevTime, crntTime);
-		});
-
 		// Frame update
 		if(!err)
 		{
-			err = node.frameUpdate(prevTime, crntTime);
+			err = node.frameUpdateComplete(prevTime, crntTime);
 		}
 
 		return err;

+ 40 - 47
src/scene/Sector.cpp

@@ -531,9 +531,8 @@ void SectorGroup::spatialDeleted(SpatialComponent* sp)
 
 //==============================================================================
 void SectorGroup::findVisibleSectors(const FrustumComponent& frc,
-	List<Sector*>& visibleSectors,
-	U& spatialsCount,
-	const Renderer& r)
+	List<const Sector*>& visibleSectors,
+	U& spatialsCount) const
 {
 	// Find the sector the eye is in
 	Sphere eye(frc.getFrustumOrigin(), frc.getFrustum().getNear());
@@ -557,33 +556,31 @@ void SectorGroup::findVisibleSectors(const FrustumComponent& frc,
 
 	if(it == end)
 	{
-		// eye outside all sectors, find those it collides
+		// eye outside all sectors, find those the frustum collides
 
 		it = m_sectors.getBegin();
 		for(; it != end; ++it)
 		{
-			Sector& s = *(*it);
+			const Sector& s = *(*it);
 			if(frc.insideFrustum(s.getBoundingShape()))
 			{
 				findVisibleSectorsInternal(
-					frc, s, visibleSectors, spatialsCount, r);
+					frc, s, visibleSectors, spatialsCount);
 			}
 		}
 	}
 	else
 	{
 		// eye inside a sector
-		findVisibleSectorsInternal(
-			frc, *(*it), visibleSectors, spatialsCount, r);
+		findVisibleSectorsInternal(frc, *(*it), visibleSectors, spatialsCount);
 	}
 }
 
 //==============================================================================
 void SectorGroup::findVisibleSectorsInternal(const FrustumComponent& frc,
-	Sector& s,
-	List<Sector*>& visibleSectors,
-	U& spatialsCount,
-	const Renderer& r)
+	const Sector& s,
+	List<const Sector*>& visibleSectors,
+	U& spatialsCount) const
 {
 	auto alloc = m_scene->getFrameAllocator();
 
@@ -608,26 +605,21 @@ void SectorGroup::findVisibleSectorsInternal(const FrustumComponent& frc,
 	auto itend = s.m_portals.getEnd();
 	for(; itp != itend; ++itp)
 	{
-		Portal& p = *(*itp);
+		const Portal& p = *(*itp);
 
 		Aabb box;
 		p.getBoundingShape().computeAabb(box);
 
-		if(frc.insideFrustum(p.getBoundingShape())
-#if 0
-			&& r.doGpuVisibilityTest(p.getBoundingShape(), box))
-#else
-				)
-#endif
+		if(frc.insideFrustum(p.getBoundingShape()))
 		{
-			it = p.m_sectors.getBegin();
-			end = p.m_sectors.getEnd();
+			auto it = p.m_sectors.getBegin();
+			auto end = p.m_sectors.getEnd();
 			for(; it != end; ++it)
 			{
 				if(*it != &s)
 				{
 					findVisibleSectorsInternal(
-						frc, *(*it), visibleSectors, spatialsCount, r);
+						frc, *(*it), visibleSectors, spatialsCount);
 				}
 			}
 		}
@@ -635,39 +627,40 @@ void SectorGroup::findVisibleSectorsInternal(const FrustumComponent& frc,
 }
 
 //==============================================================================
-void SectorGroup::prepareForVisibilityTests(
-	const FrustumComponent& frc, const Renderer& r)
+void SectorGroup::prepareForVisibilityTests()
 {
-	auto alloc = m_scene->getFrameAllocator();
-
 	// Bin spatials
+	auto it = m_spatialsDeferredBinning.getBegin();
+	auto end = m_spatialsDeferredBinning.getEnd();
+	for(; it != end; ++it)
 	{
-		auto it = m_spatialsDeferredBinning.getBegin();
-		auto end = m_spatialsDeferredBinning.getEnd();
-		for(; it != end; ++it)
-		{
-			binSpatial(*it);
-		}
-		m_spatialsDeferredBinning.destroy(alloc);
+		binSpatial(*it);
 	}
+	m_spatialsDeferredBinning.destroy(m_scene->getFrameAllocator());
+}
+
+//==============================================================================
+void SectorGroup::findVisibleNodes(const FrustumComponent& frc,
+	U testId,
+	SectorGroupVisibilityTestsContext& ctx) const
+{
+	auto alloc = m_scene->getFrameAllocator();
 
 	// Find visible sectors
-	ListAuto<Sector*> visSectors(m_scene->getFrameAllocator());
+	ListAuto<const Sector*> visSectors(alloc);
 	U spatialsCount = 0;
-	findVisibleSectors(frc, visSectors, spatialsCount, r);
+	findVisibleSectors(frc, visSectors, spatialsCount);
 
 	if(ANKI_UNLIKELY(spatialsCount == 0))
 	{
-		m_visibleNodes = nullptr;
-		m_visibleNodesCount = 0;
 		return;
 	}
 
 	// Initiate storage of nodes
-	m_visibleNodes = reinterpret_cast<SceneNode**>(
+	SceneNode** visibleNodesMem = reinterpret_cast<SceneNode**>(
 		alloc.allocate(spatialsCount * sizeof(void*)));
 
-	SArray<SceneNode*> visibleNodes(m_visibleNodes, spatialsCount);
+	SArray<SceneNode*> visibleNodes(visibleNodesMem, spatialsCount);
 
 	// Iterate visible sectors and get the scene nodes. The array will contain
 	// duplicates
@@ -676,22 +669,22 @@ void SectorGroup::prepareForVisibilityTests(
 	auto end = visSectors.getEnd();
 	for(; it != end; ++it)
 	{
-		Sector& s = *(*it);
+		const Sector& s = *(*it);
 		for(auto itsp : s.m_spatials)
 		{
-			SpatialComponent& spc = *itsp;
-			SceneNode& node = spc.getSceneNode();
+			const SpatialComponent& spc = *itsp;
+			SceneNode& node = const_cast<SceneNode&>(spc.getSceneNode());
 
-			// Check if visited
-			if(!node.getSectorVisited())
+			// Check if alrady visited
+			if(!node.fetchSetSectorVisited(testId, true))
 			{
 				visibleNodes[nodesCount++] = &node;
-				node.setSectorVisited(true);
 			}
 		}
 	}
-
-	m_visibleNodesCount = nodesCount;
+		
+	// Update the context
+	ctx.m_visibleNodes = SArray<SceneNode*>(visibleNodesMem, nodesCount);
 }
 
 } // end namespace anki

+ 252 - 245
src/scene/Visibility.cpp

@@ -48,13 +48,14 @@ public:
 };
 
 /// Contains all the info of a single visibility test on a FrustumComponent.
-class SingleVisTest
+class SingleVisTest : public NonCopyable
 {
 public:
 	FrustumComponent* m_frc;
 	Array<VisibilityTestResults*, ThreadPool::MAX_THREADS> m_threadResults;
 	Timestamp m_timestamp = 0;
 	SpinLock m_timestampLock;
+	SectorGroupVisibilityTestsContext m_sectorsCtx;
 
 	SingleVisTest(FrustumComponent* frc)
 		: m_frc(frc)
@@ -62,20 +63,6 @@ public:
 		ANKI_ASSERT(m_frc);
 		memset(&m_threadResults[0], 0, sizeof(m_threadResults));
 	}
-
-	SingleVisTest(const SingleVisTest& b)
-	{
-		operator=(b);
-	}
-
-	/// Will not copy everything. There is no need.
-	SingleVisTest& operator=(const SingleVisTest& b)
-	{
-		m_frc = b.m_frc;
-		m_threadResults = b.m_threadResults;
-		m_timestamp = b.m_timestamp;
-		return *this;
-	}
 };
 
 /// Data common for all tasks.
@@ -85,16 +72,20 @@ public:
 	SceneGraph* m_scene = nullptr;
 	const Renderer* m_r = nullptr;
 	Barrier m_barrier;
-	List<SingleVisTest> m_pendingTests; ///< Frustums to test
-	SpinLock m_lock;
-	List<SingleVisTest> m_completedTests;
+
+	ListAuto<SingleVisTest*> m_pendingTests; ///< Frustums to test
+	ListAuto<SingleVisTest*> m_completedTests;
 	U32 m_completedTestsCount = 0;
+	U32 m_testIdCounter = 1; ///< 1 because the main thread already added one
+	SpinLock m_lock;
 
-	Atomic<U32> m_sectorPrepareRaceAtomic = {0};
 	Atomic<U32> m_listRaceAtomic = {0};
 
-	VisibilityContext(U threadCount)
-		: m_barrier(threadCount)
+	VisibilityContext(U threadCount, SceneGraph* scene)
+		: m_scene(scene)
+		, m_barrier(threadCount)
+		, m_pendingTests(m_scene->getFrameAllocator())
+		, m_completedTests(m_scene->getFrameAllocator())
 	{
 	}
 };
@@ -103,7 +94,7 @@ public:
 class VisibilityTestTask : public ThreadPool::Task
 {
 public:
-	VisibilityContext* m_ctx;
+	VisibilityContext* m_ctx = nullptr;
 
 	/// Test a frustum component
 	void test(SingleVisTest& testInfo, U32 threadId, PtrSize threadCount);
@@ -115,34 +106,45 @@ public:
 	{
 		auto alloc = m_ctx->m_scene->getFrameAllocator();
 
-		// Iterate the nodes to check against
-		while(!m_ctx->m_pendingTests.isEmpty())
+		ANKI_ASSERT(!m_ctx->m_pendingTests.isEmpty());
+		SingleVisTest* testInfo = m_ctx->m_pendingTests.getFront();
+		ANKI_ASSERT(testInfo);
+		do
 		{
-			// Try to find a test that hasn't completed
-			SingleVisTest& testInfo = m_ctx->m_pendingTests.getFront();
-
-			// The first who touches the race atomic will prepare the sectors
-			U count = m_ctx->m_sectorPrepareRaceAtomic.fetchAdd(1);
-			if((count % threadCount) == 0)
-			{
-				SectorGroup& sectors = m_ctx->m_scene->getSectorGroup();
-				sectors.prepareForVisibilityTests(*testInfo.m_frc, *m_ctx->m_r);
-			}
+			// Test
+			test(*testInfo, threadId, threadCount);
 
+			// Sync
 			m_ctx->m_barrier.wait();
-			test(testInfo, threadId, threadCount);
 
-			// The last who will finish its tests will update the lists
-			count = m_ctx->m_listRaceAtomic.fetchAdd(1);
-			if((count % threadCount) == threadCount - 1)
+			// Get something to test next
 			{
-				m_ctx->m_completedTests.pushBack(alloc, testInfo);
-				++m_ctx->m_completedTestsCount;
-				m_ctx->m_pendingTests.popFront(alloc);
-			}
+				LockGuard<SpinLock> lock(m_ctx->m_lock);
 
-			m_ctx->m_barrier.wait();
-		}
+				if(!m_ctx->m_pendingTests.isEmpty())
+				{
+					if(testInfo == m_ctx->m_pendingTests.getFront())
+					{
+						// Push it to the completed
+						m_ctx->m_completedTests.pushBack(testInfo);
+						++m_ctx->m_completedTestsCount;
+						m_ctx->m_pendingTests.popFront();
+
+						testInfo = (m_ctx->m_pendingTests.isEmpty())
+							? nullptr
+							: m_ctx->m_pendingTests.getFront();
+					}
+					else
+					{
+						testInfo = m_ctx->m_pendingTests.getFront();	
+					}
+				}
+				else
+				{
+					testInfo = nullptr;
+				}
+			}
+		} while(testInfo);
 
 		// Sort and combind the results
 		PtrSize start, end;
@@ -155,7 +157,7 @@ public:
 			{
 				ANKI_TRACE_START_EVENT(SCENE_VISIBILITY_COMBINE_RESULTS);
 
-				combineTestResults((*it), threadCount);
+				combineTestResults(*(*it), threadCount);
 				++it;
 
 				ANKI_TRACE_STOP_EVENT(SCENE_VISIBILITY_COMBINE_RESULTS);
@@ -233,247 +235,245 @@ void VisibilityTestTask::test(
 	Bool wantsReflectionProxies = testedFrc.visibilityTestsEnabled(
 		FrustumComponentVisibilityTestFlag::REFLECTION_PROXIES);
 
-#if 0
-	ANKI_LOGW("Running test code");
-
-	// Chose the test range and a few other things
-	PtrSize start, end;
-	U nodesCount = m_ctx->m_scene->getSceneNodesCount();
-	choseStartEnd(threadId, threadCount, nodesCount, start, end);
-
-	// Iterate range of nodes
-	Error err = m_ctx->m_scene->iterateSceneNodes(
-		start, end, [&](SceneNode& node) -> Error
-#else
-	SectorGroup& sectors = m_ctx->m_scene->getSectorGroup();
 	// Chose the test range and a few other things
 	PtrSize start, end;
-	U nodesCount = sectors.getVisibleNodesCount();
-	choseStartEnd(threadId, threadCount, nodesCount, start, end);
-
-	Error err = sectors.iterateVisibleSceneNodes(start,
-		end,
-		[&](SceneNode& node) -> Error
-#endif
-	{
-		// Skip if it is the same
-		if(ANKI_UNLIKELY(&testedNode == &node))
-		{
-			return ErrorCode::NONE;
-		}
-
-		// Check what components the frustum needs
-		Bool wantNode = false;
+	choseStartEnd(threadId,
+		threadCount,
+		testInfo.m_sectorsCtx.getVisibleSceneNodeCount(),
+		start,
+		end);
+
+	testInfo.m_sectorsCtx.iterateVisibleSceneNodes(
+		start, end, [&](SceneNode& node) {
+			// Skip if it is the same
+			if(ANKI_UNLIKELY(&testedNode == &node))
+			{
+				return;
+			}
 
-		RenderComponent* rc = node.tryGetComponent<RenderComponent>();
-		if(rc && wantsRenderComponents)
-		{
-			wantNode = true;
-		}
+			// Check what components the frustum needs
+			Bool wantNode = false;
 
-		if(rc && rc->getCastsShadow() && wantsShadowCasters)
-		{
-			wantNode = true;
-		}
+			RenderComponent* rc = node.tryGetComponent<RenderComponent>();
+			if(rc && wantsRenderComponents)
+			{
+				wantNode = true;
+			}
 
-		LightComponent* lc = node.tryGetComponent<LightComponent>();
-		if(lc && wantsLightComponents)
-		{
-			wantNode = true;
-		}
+			if(rc && rc->getCastsShadow() && wantsShadowCasters)
+			{
+				wantNode = true;
+			}
 
-		LensFlareComponent* lfc = node.tryGetComponent<LensFlareComponent>();
-		if(lfc && wantsFlareComponents)
-		{
-			wantNode = true;
-		}
+			LightComponent* lc = node.tryGetComponent<LightComponent>();
+			if(lc && wantsLightComponents)
+			{
+				wantNode = true;
+			}
 
-		ReflectionProbeComponent* reflc =
-			node.tryGetComponent<ReflectionProbeComponent>();
-		if(reflc && wantsReflectionProbes)
-		{
-			wantNode = true;
-		}
+			LensFlareComponent* lfc =
+				node.tryGetComponent<LensFlareComponent>();
+			if(lfc && wantsFlareComponents)
+			{
+				wantNode = true;
+			}
 
-		ReflectionProxyComponent* proxyc =
-			node.tryGetComponent<ReflectionProxyComponent>();
-		if(proxyc && wantsReflectionProxies)
-		{
-			wantNode = true;
-		}
+			ReflectionProbeComponent* reflc =
+				node.tryGetComponent<ReflectionProbeComponent>();
+			if(reflc && wantsReflectionProbes)
+			{
+				wantNode = true;
+			}
 
-		if(ANKI_UNLIKELY(!wantNode))
-		{
-			// Skip node
-			return ErrorCode::NONE;
-		}
+			ReflectionProxyComponent* proxyc =
+				node.tryGetComponent<ReflectionProxyComponent>();
+			if(proxyc && wantsReflectionProxies)
+			{
+				wantNode = true;
+			}
 
-		// Test all spatial components of that node
-		struct SpatialTemp
-		{
-			SpatialComponent* m_sp;
-			U8 m_idx;
-			Vec4 m_origin;
-		};
-		Array<SpatialTemp, ANKI_GL_MAX_SUB_DRAWCALLS> sps;
-
-		U spIdx = 0;
-		U count = 0;
-		Error err = node.iterateComponentsOfType<SpatialComponent>(
-			[&](SpatialComponent& sp) {
-				if(testedFrc.insideFrustum(sp))
-				{
-					// Inside
-					ANKI_ASSERT(spIdx < MAX_U8);
-					sps[count++] = SpatialTemp{
-						&sp, static_cast<U8>(spIdx), sp.getSpatialOrigin()};
+			if(ANKI_UNLIKELY(!wantNode))
+			{
+				// Skip node
+				return;
+			}
 
-					sp.setVisibleByCamera(true);
-				}
+			// Test all spatial components of that node
+			struct SpatialTemp
+			{
+				SpatialComponent* m_sp;
+				U8 m_idx;
+				Vec4 m_origin;
+			};
+			Array<SpatialTemp, ANKI_GL_MAX_SUB_DRAWCALLS> sps;
+
+			U spIdx = 0;
+			U count = 0;
+			Error err = node.iterateComponentsOfType<SpatialComponent>(
+				[&](SpatialComponent& sp) {
+					if(testedFrc.insideFrustum(sp))
+					{
+						// Inside
+						ANKI_ASSERT(spIdx < MAX_U8);
+						sps[count++] = SpatialTemp{
+							&sp, static_cast<U8>(spIdx), sp.getSpatialOrigin()};
 
-				++spIdx;
+						sp.setVisibleByCamera(true);
+					}
 
-				return ErrorCode::NONE;
-			});
-		(void)err;
+					++spIdx;
 
-		if(ANKI_UNLIKELY(count == 0))
-		{
-			return ErrorCode::NONE;
-		}
+					return ErrorCode::NONE;
+				});
+			(void)err;
 
-		// Sort sub-spatials
-		Vec4 origin = testedFrc.getFrustumOrigin();
-		std::sort(sps.begin(),
-			sps.begin() + count,
-			[origin](const SpatialTemp& a, const SpatialTemp& b) -> Bool {
-				const Vec4& spa = a.m_origin;
-				const Vec4& spb = b.m_origin;
+			if(ANKI_UNLIKELY(count == 0))
+			{
+				return;
+			}
 
-				F32 dist0 = origin.getDistanceSquared(spa);
-				F32 dist1 = origin.getDistanceSquared(spb);
+			// Sort sub-spatials
+			Vec4 origin = testedFrc.getFrustumOrigin();
+			std::sort(sps.begin(),
+				sps.begin() + count,
+				[origin](const SpatialTemp& a, const SpatialTemp& b) -> Bool {
+					const Vec4& spa = a.m_origin;
+					const Vec4& spb = b.m_origin;
 
-				return dist0 < dist1;
-			});
+					F32 dist0 = origin.getDistanceSquared(spa);
+					F32 dist1 = origin.getDistanceSquared(spb);
 
-		// Update the visibleNode
-		VisibleNode visibleNode;
-		visibleNode.m_node = &node;
+					return dist0 < dist1;
+				});
 
-		// Compute distance from the frustum
-		visibleNode.m_frustumDistanceSquared =
-			(sps[0].m_origin - testedFrc.getFrustumOrigin()).getLengthSquared();
+			// Update the visibleNode
+			VisibleNode visibleNode;
+			visibleNode.m_node = &node;
 
-		ANKI_ASSERT(count < MAX_U8);
-		visibleNode.m_spatialsCount = count;
-		visibleNode.m_spatialIndices = alloc.newArray<U8>(count);
+			// Compute distance from the frustum
+			visibleNode.m_frustumDistanceSquared =
+				(sps[0].m_origin - testedFrc.getFrustumOrigin())
+					.getLengthSquared();
 
-		for(U i = 0; i < count; i++)
-		{
-			visibleNode.m_spatialIndices[i] = sps[i].m_idx;
-		}
+			ANKI_ASSERT(count < MAX_U8);
+			visibleNode.m_spatialsCount = count;
+			visibleNode.m_spatialIndices = alloc.newArray<U8>(count);
 
-		if(rc)
-		{
-			if(wantsRenderComponents
-				|| (wantsShadowCasters && rc->getCastsShadow()))
+			for(U i = 0; i < count; i++)
 			{
-				visible->moveBack(alloc,
-					rc->getMaterial().getForwardShading()
-						? VisibilityGroupType::RENDERABLES_FS
-						: VisibilityGroupType::RENDERABLES_MS,
-					visibleNode);
+				visibleNode.m_spatialIndices[i] = sps[i].m_idx;
+			}
 
-				if(wantsShadowCasters)
+			if(rc)
+			{
+				if(wantsRenderComponents
+					|| (wantsShadowCasters && rc->getCastsShadow()))
 				{
-					updateTimestamp(node, testInfo);
+					visible->moveBack(alloc,
+						rc->getMaterial().getForwardShading()
+							? VisibilityGroupType::RENDERABLES_FS
+							: VisibilityGroupType::RENDERABLES_MS,
+						visibleNode);
+
+					if(wantsShadowCasters)
+					{
+						updateTimestamp(node, testInfo);
+					}
 				}
 			}
-		}
 
-		if(lc && wantsLightComponents)
-		{
-			VisibilityGroupType gt;
-			switch(lc->getLightType())
+			if(lc && wantsLightComponents)
 			{
-			case LightComponent::LightType::POINT:
-				gt = VisibilityGroupType::LIGHTS_POINT;
-				break;
-			case LightComponent::LightType::SPOT:
-				gt = VisibilityGroupType::LIGHTS_SPOT;
-				break;
-			default:
-				ANKI_ASSERT(0);
-				gt = VisibilityGroupType::TYPE_COUNT;
-			}
-
-			visible->moveBack(alloc, gt, visibleNode);
-		}
+				VisibilityGroupType gt;
+				switch(lc->getLightType())
+				{
+				case LightComponent::LightType::POINT:
+					gt = VisibilityGroupType::LIGHTS_POINT;
+					break;
+				case LightComponent::LightType::SPOT:
+					gt = VisibilityGroupType::LIGHTS_SPOT;
+					break;
+				default:
+					ANKI_ASSERT(0);
+					gt = VisibilityGroupType::TYPE_COUNT;
+				}
 
-		if(lfc && wantsFlareComponents)
-		{
-			visible->moveBack(alloc, VisibilityGroupType::FLARES, visibleNode);
-		}
+				visible->moveBack(alloc, gt, visibleNode);
+			}
 
-		if(reflc && wantsReflectionProbes)
-		{
-			visible->moveBack(
-				alloc, VisibilityGroupType::REFLECTION_PROBES, visibleNode);
-		}
+			if(lfc && wantsFlareComponents)
+			{
+				visible->moveBack(
+					alloc, VisibilityGroupType::FLARES, visibleNode);
+			}
 
-		if(proxyc && wantsReflectionProxies)
-		{
-			visible->moveBack(
-				alloc, VisibilityGroupType::REFLECTION_PROXIES, visibleNode);
-		}
+			if(reflc && wantsReflectionProbes)
+			{
+				visible->moveBack(
+					alloc, VisibilityGroupType::REFLECTION_PROBES, visibleNode);
+			}
 
-		// Add more frustums to the list
-		err = node.iterateComponentsOfType<FrustumComponent>([&](
-			FrustumComponent& frc) {
-			// Check enabled and make sure that the results are null (this
-			// can
-			// happen on multiple on circular viewing)
-			if(frc.anyVisibilityTestEnabled()
-				&& !frc.hasVisibilityTestResults())
+			if(proxyc && wantsReflectionProxies)
 			{
-				LockGuard<SpinLock> l(m_ctx->m_lock);
+				visible->moveBack(alloc,
+					VisibilityGroupType::REFLECTION_PROXIES,
+					visibleNode);
+			}
 
-				// Check if already in the list
-				Bool alreadyThere = false;
-				for(const SingleVisTest& x : m_ctx->m_pendingTests)
-				{
-					if(x.m_frc == &frc)
-					{
-						alreadyThere = true;
-						break;
-					}
-				}
+			// Add more frustums to the list
+			err = node.iterateComponentsOfType<FrustumComponent>([&](
+				FrustumComponent& frc) {
+				SingleVisTest* t = nullptr;
+				U testId = MAX_U;
 
-				if(!alreadyThere)
+				// Check enabled and make sure that the results are null (this
+				// can happen on multiple on circular viewing)
+				if(frc.anyVisibilityTestEnabled())
 				{
-					for(const SingleVisTest& x : m_ctx->m_completedTests)
+					LockGuard<SpinLock> l(m_ctx->m_lock);
+
+					// Check if already in the list
+					Bool alreadyThere = false;
+					for(const SingleVisTest* x : m_ctx->m_pendingTests)
 					{
-						if(x.m_frc == &frc)
+						if(x->m_frc == &frc)
 						{
 							alreadyThere = true;
 							break;
 						}
 					}
-				}
 
-				if(!alreadyThere)
-				{
-					m_ctx->m_pendingTests.pushBack(alloc, SingleVisTest(&frc));
-				}
-			}
+					if(!alreadyThere)
+					{
+						for(const SingleVisTest* x : m_ctx->m_completedTests)
+						{
+							if(x->m_frc == &frc)
+							{
+								alreadyThere = true;
+								break;
+							}
+						}
+					}
 
-			return ErrorCode::NONE;
-		});
-		(void)err;
+					if(!alreadyThere)
+					{
+						testId = m_ctx->m_testIdCounter++;
+						t = alloc.newInstance<SingleVisTest>(&frc);
+						m_ctx->m_pendingTests.pushBack(t);
+					}
+				};
 
-		return ErrorCode::NONE;
-	}); // end for
-	(void)err;
+				// Do that outside the lock
+				if(t)
+				{
+					m_ctx->m_scene->getSectorGroup().findVisibleNodes(frc,
+						testId,
+						t->m_sectorsCtx);
+				}
+				return ErrorCode::NONE;
+			});
+			(void)err;
+		}); // end for
 
 	ANKI_TRACE_STOP_EVENT(SCENE_VISIBILITY_TEST);
 }
@@ -612,11 +612,18 @@ Error doVisibilityTests(SceneNode& fsn, SceneGraph& scene, const Renderer& r)
 	// Do the tests in parallel
 	ThreadPool& threadPool = scene._getThreadPool();
 
-	VisibilityContext ctx(threadPool.getThreadsCount());
-	ctx.m_scene = &scene;
+	scene.getSectorGroup().prepareForVisibilityTests();
+
+	VisibilityContext ctx(threadPool.getThreadsCount(), &scene);
 	ctx.m_r = &r;
-	ctx.m_pendingTests.pushBack(scene.getFrameAllocator(),
-		SingleVisTest(&fsn.getComponent<FrustumComponent>()));
+
+	SingleVisTest* t = scene.getFrameAllocator().newInstance<SingleVisTest>(
+		&fsn.getComponent<FrustumComponent>());
+	ctx.m_pendingTests.pushBack(t);
+	scene.getSectorGroup().findVisibleNodes(
+		fsn.getComponent<FrustumComponent>(),
+		0,
+		t->m_sectorsCtx);
 
 	Array<VisibilityTestTask, ThreadPool::MAX_THREADS> tasks;
 	for(U i = 0; i < threadPool.getThreadsCount(); i++)
@@ -626,7 +633,7 @@ Error doVisibilityTests(SceneNode& fsn, SceneGraph& scene, const Renderer& r)
 	}
 
 	Error err = threadPool.waitForAllThreadsToFinish();
-	ctx.m_completedTests.destroy(scene.getFrameAllocator());
+	ANKI_ASSERT(ctx.m_pendingTests.isEmpty());
 	return err;
 }