Skip to content

Commit

Permalink
Use multiple threads for navmesh generation
Browse files Browse the repository at this point in the history
Navmesh generation can now in parallel in multiple threads. This is
configured by g_bot_navgen_maxThreads (sgame) and cg_navgen_maxThreads
(cgame).

We didn't do this earlier because we had never tried threads in NaCl and
were also considering porting to a WASM engine without thread support.
The code for generating a little bit each frame for background
generation in the sgame is kept around in case we do end up porting to
platforms without multithreading. The cgame and /navgen admin command
don't have the ability to run only on the main thread for the moment,
but it would be really easy to add it if needed.

The progress computation for percentage of the navmesh generated has been
improved - it now closely corresponds to the actual amount of work
instead of falsely assuming that each class has equal difficulty.

Co-Authored-by: DolceTriade <vcelestialragev@gmail.com>
  • Loading branch information
slipher and DolceTriade committed Mar 15, 2024
1 parent 600fb6c commit e881a23
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 102 deletions.
43 changes: 16 additions & 27 deletions src/cgame/cg_main.cpp
Expand Up @@ -158,6 +158,9 @@ Cvar::Cvar<float> cg_motionblurMinSpeed("cg_motionblurMinSpeed", "minimum speed
Cvar::Cvar<bool> cg_spawnEffects("cg_spawnEffects", "desaturate world view when dead or spawning", Cvar::NONE, true);

static Cvar::Cvar<bool> cg_navgenOnLoad("cg_navgenOnLoad", "generate navmeshes when starting a local game", Cvar::NONE, true);
static Cvar::Cvar<int> cg_navgenMaxThreads(
"cg_navgenMaxThreads", "Maximum number of threads to use when generating navmeshes",
Cvar::NONE, 2);

// search 'fovCvar' to find usage of these (names come from config files)
// 0 means use global FOV setting
Expand Down Expand Up @@ -1075,7 +1078,7 @@ bool CG_ClientIsReady( int clientNum )
static void GenerateNavmeshes()
{
std::string mapName = Cvar::GetValue( "mapname" );
std::vector<class_t> missing;
std::bitset<PCL_NUM_CLASSES> missing;
NavgenConfig config = ReadNavgenConfig( mapName );
bool reduceTypes;
if ( !Cvar::ParseCvarValue( Cvar::GetValue( "g_bot_navmeshReduceTypes" ), reduceTypes ) )
Expand All @@ -1089,19 +1092,19 @@ static void GenerateNavmeshes()
std::string filename = NavmeshFilename( mapName, species );
if ( BG_FOpenGameOrPakPath( filename, f ) < 0 )
{
missing.push_back( species );
missing.set(species);
continue;
}
NavMeshSetHeader header;
std::string error = GetNavmeshHeader( f, config, header, mapName );
if ( !error.empty() )
{
Log::Notice( "Existing navmesh file %s can't be used: %s", filename, error );
missing.push_back( species );
missing.set(species);
}
trap_FS_FCloseFile( f );
}
if ( missing.empty() )
if ( !missing.any() )
{
return;
}
Expand All @@ -1113,30 +1116,16 @@ static void GenerateNavmeshes()
trap_UpdateScreen();

NavmeshGenerator navgen;
navgen.Init( mapName );
float classesCompleted = 0.3; // Assume that Init() is 0.3 times as much work as generating 1 species
// and assume that each species takes the same amount of time, which is actually completely wrong:
// smaller ones take much longer
float classesTotal = classesCompleted + missing.size();
for ( class_t species : missing )
{
cg.loadingText =
Str::Format( "%s — %s", message, BG_ClassModelConfig( species )->humanName );
cg.navmeshLoadingFraction = classesCompleted / classesTotal;
trap_UpdateScreen();

std::unique_ptr<NavgenTask> task = navgen.StartGeneration( species );
do
navgen.EnqueueTasks( mapName, missing );
navgen.StartBackgroundThreads( cg_navgenMaxThreads.Get() );
navgen.WaitInMainThread( []( float progress ) {
if ( progress >= cg.navmeshLoadingFraction + 0.01f )
{
float fraction = ( classesCompleted + task->FractionCompleted() ) / classesTotal;
if ( fraction - cg.navmeshLoadingFraction > 0.01 )
{
cg.navmeshLoadingFraction = fraction;
trap_UpdateScreen();
}
} while ( !navgen.Step( *task ) );
++classesCompleted;
}
cg.navmeshLoadingFraction = progress;
trap_UpdateScreen();
}
} );

cg.loadingNavmesh = false;
}

Expand Down
109 changes: 57 additions & 52 deletions src/sgame/sg_bot_nav.cpp
Expand Up @@ -49,28 +49,18 @@ Navigation Mesh Generation
===========================
*/

static Cvar::Cvar<int> g_bot_navgen_maxThreads(
"g_bot_navgen_maxThreads", "Number of background threads to use when generating navmeshes. 0 for main thread",
Cvar::NONE, 1);

// blocks the main thread!
void G_BlockingGenerateNavmesh( std::bitset<PCL_NUM_CLASSES> classes )
{
std::string mapName = Cvar::GetValue( "mapname" );
NavmeshGenerator navgen;

for ( int i = PCL_NONE; ++i < PCL_NUM_CLASSES; )
{
if ( !classes[ i ] )
{
continue;
}

navgen.Init( mapName );
std::unique_ptr<NavgenTask> task = navgen.StartGeneration( Util::enum_cast<class_t>( i ) );

while ( !navgen.Step( *task ) )
{
// ping the engine with a useless message so that it does not think the sgame VM has hung
Cvar::GetValue( "x" );
}
}
navgen.EnqueueTasks( mapName, classes );
navgen.StartBackgroundThreads( g_bot_navgen_maxThreads.Get() );
navgen.WaitInMainThread( []( float ) {} );
}

// TODO: Latch(), when supported in gamelogic
Expand All @@ -86,44 +76,29 @@ static Cvar::Cvar<int> frameToggle("g_bot_navgen_frame", "FOR INTERNAL USE", Cva
static Cvar::Cvar<bool> g_bot_autocrouch("g_bot_autocrouch", "whether bots should crouch when they detect an obstacle", Cvar::NONE, true);

static NavmeshGenerator navgen;
static std::vector<class_t> navgenQueue;
static std::unique_ptr<NavgenTask> generatingNow;
bool usingBackgroundThreads;
static std::unique_ptr<NavgenTask> generatingNow; // used if generating on the main thread
static int nextLogTime;

static void G_BotBackgroundNavgenShutdown()
{
navgen.~NavmeshGenerator();
new (&navgen) NavmeshGenerator();
navgenQueue.clear();
generatingNow.reset();
}

void G_BotBackgroundNavgen()
// Returns true if done
static bool MainThreadBackgroundNavgen()
{
if ( navgenQueue.empty() )
{
return;
}

ASSERT_EQ( navMeshLoaded, navMeshStatus_t::GENERATING );

int stopTime = Sys::Milliseconds() + msecPerFrame.Get();

navgen.Init( Cvar::GetValue( "mapname" ) );

if ( !generatingNow )
{
class_t next = navgenQueue.back();
generatingNow = navgen.StartGeneration( next );
}

if ( level.time > nextLogTime )
{
std::string percent = std::to_string( int(generatingNow->FractionCompleted() * 100) );
trap_SendServerCommand( -1, va( "print_tr %s %s %s",
QQ( N_( "Server: generating bot navigation mesh for $1$... $2$%" ) ),
BG_Class( generatingNow->species )->name, percent.c_str() ) );
nextLogTime = level.time + 10000;
generatingNow = navgen.PopTask();
if ( !generatingNow )
{
return true;
}
}

// HACK: if the game simulation time gets behind the real time, the server runs a bunch of
Expand All @@ -135,7 +110,7 @@ void G_BotBackgroundNavgen()
static int lastToggle = -12345;
if ( lastToggle == frameToggle.Get() )
{
return;
return false;
}
lastToggle = frameToggle.Get();
trap_SendConsoleCommand( "toggle g_bot_navgen_frame" );
Expand All @@ -144,14 +119,35 @@ void G_BotBackgroundNavgen()
{
if ( navgen.Step( *generatingNow ) )
{
ASSERT_EQ( generatingNow->species, navgenQueue.back() );
navgenQueue.pop_back();
generatingNow->context.DoMainThreadTasks();
generatingNow.reset();
break;
}
}

if ( navgenQueue.empty() ) // finished?
return false;
}

void G_BotBackgroundNavgen()
{
if ( navMeshLoaded != navMeshStatus_t::GENERATING )
{
return;
}

bool done;

if ( usingBackgroundThreads )
{
done = navgen.ThreadsDone();
navgen.HandleFinishedTasks();
}
else
{
done = MainThreadBackgroundNavgen();
}

if ( done )
{
G_BotBackgroundNavgenShutdown();

Expand All @@ -169,6 +165,13 @@ void G_BotBackgroundNavgen()
// Bots on spectate will now join in their think function
}
}
else if ( level.time > nextLogTime )
{
std::string percent = std::to_string( int(navgen.FractionComplete() * 100) );
trap_SendServerCommand( -1, va( "print_tr %s %s",
QQ( N_( "Server: generating bot navigation meshes... $1$%" ) ), percent.c_str() ) );
nextLogTime = level.time + 10000;
}
}

/*
Expand All @@ -195,7 +198,8 @@ void G_BotNavInit( int generateNeeded )
}

std::bitset<PCL_NUM_CLASSES> missing;
NavgenConfig config = ReadNavgenConfig( Cvar::GetValue( "mapname" ) );
std::string mapName = Cvar::GetValue( "mapname" );
NavgenConfig config = ReadNavgenConfig( mapName );

for ( class_t i : RequiredNavmeshes( g_bot_navmeshReduceTypes.Get() ) )
{
Expand Down Expand Up @@ -231,14 +235,15 @@ void G_BotNavInit( int generateNeeded )
}
else
{
ASSERT( navgenQueue.empty() );
for ( int i = PCL_NUM_CLASSES; --i > PCL_NONE; )
ASSERT( !generatingNow );
navgen.EnqueueTasks( mapName, missing );
usingBackgroundThreads = g_bot_navgen_maxThreads.Get() > 0;

if ( usingBackgroundThreads )
{
if ( missing[ i ] )
{
navgenQueue.push_back( Util::enum_cast<class_t>( i ) );
}
navgen.StartBackgroundThreads( g_bot_navgen_maxThreads.Get() );
}

navMeshLoaded = navMeshStatus_t::GENERATING;
return;
}
Expand Down

0 comments on commit e881a23

Please sign in to comment.