Local client load speed enhancement
by Daniel Eden · 05/15/2007 (11:56 am) · 16 comments
A few people asked for this to be posted as a resource, so here it is. I've checked this code out on local and dedicated servers, in demos and out, and it all seems to work. There may be a few issues here and there, but I haven't seen any yet. As always, use the code at your own risk. I'm not supporting it in any way. If you have problems implementing it or want questions answered or whatever, too bad.
Essentially these changes amount to little more than short-circuiting both datablock transmission and the transmission of scopealways objects so as to avoid the chokepoint imposed by networking packet sizes and the like. This will greatly decrease the amount of time required for transmitting datablocks and static objects (such as the terrain and so on) to a local client.
Implementing this resource consists of a few modifications to the codebase, but it's all pretty straightforward. Kudos go out to all the people who've contributed to it.
First, around line 1230 of game/gameConnection.cc, replace the transmitDataBlocks console method with:
In the same file, at line 1146 or thereabouts, find the following:
... and replace it with ...
Then in sim/netGhost.cc, around line 958, find this code in NetConnection::loadNextGhostAlwaysObject:
... and replace it with ...
In the same file around line 828 in the NetConnection::activateGhosting method, remove the following code:
... and replace it with:
Essentially these changes amount to little more than short-circuiting both datablock transmission and the transmission of scopealways objects so as to avoid the chokepoint imposed by networking packet sizes and the like. This will greatly decrease the amount of time required for transmitting datablocks and static objects (such as the terrain and so on) to a local client.
Implementing this resource consists of a few modifications to the codebase, but it's all pretty straightforward. Kudos go out to all the people who've contributed to it.
First, around line 1230 of game/gameConnection.cc, replace the transmitDataBlocks console method with:
ConsoleMethod( GameConnection, transmitDataBlocks, void, 3, 3, "(int sequence)")
{
// Set the datablock sequence.
object->setDataBlockSequence(dAtoi(argv[2]));
// Store a pointer to the datablock group.
SimDataBlockGroup* pGroup = Sim::getDataBlockGroup();
// Determine the size of the datablock group.
const U32 iCount = pGroup->size();
// If this is the local client...
if (GameConnection::getLocalClientConnection() == object)
{
// Set up a pointer to the datablock.
SimDataBlock* pDataBlock = 0;
// Iterate through all the datablocks...
for (U32 i = 0; i < iCount; i++)
{
// Get a pointer to the datablock in question...
pDataBlock = (SimDataBlock*)(*pGroup)[i];
// Set the client's new modified key.
object->setMaxDataBlockModifiedKey(pDataBlock->getModifiedKey());
// Set up a buffer for the datablock send.
U8 iBuffer[4096];
BitStream mStream(iBuffer, 4096);
// Pack the datablock stream.
pDataBlock->packData(&mStream);
// Set the stream position back to zero.
mStream.setPosition(0);
// Unpack the datablock stream.
pDataBlock->unpackData(&mStream);
// Call the console function to set the number of blocks to be sent.
Con::executef(3, "onDataBlockObjectReceived", Con::getIntArg(i), Con::getIntArg(iCount));
// Preload the datablock on the dummy client.
pDataBlock->preload(false, NetConnection::getErrorBuffer());
}
// Get the last datablock (if any)...
if (pDataBlock)
{
// Ensure the datablock modified key is set.
object->setDataBlockModifiedKey(object->getMaxDataBlockModifiedKey());
// Ensure that the client knows that the datablock send is done...
object->sendConnectionMessage(GameConnection::DataBlocksDone, object->getDataBlockSequence());
}
}
else
{
// Otherwise, store the current datablock modified key.
const S32 iKey = object->getDataBlockModifiedKey();
// Iterate through the datablock group...
U32 i = 0;
for (; i < iCount; i++)
{
// If the datablock's modified key has already been set, break out of the loop...
if (((SimDataBlock*)(*pGroup)[i])->getModifiedKey() > iKey)
{
break;
}
}
// If this is the last datablock in the group...
if (i == iCount)
{
// Ensure that the client knows that the datablock send is done...
object->sendConnectionMessage(GameConnection::DataBlocksDone, object->getDataBlockSequence());
// Then exit out since nothing else needs to be done.
return;
}
// Set the maximum datablock modified key value.
object->setMaxDataBlockModifiedKey(iKey);
// Get the minimum number of datablocks...
const U32 iMax = getMin(i + DataBlockQueueCount, iCount);
// Iterate through the remaining datablocks...
for (;i < iMax; i++)
{
// Get a pointer to the datablock in question...
SimDataBlock* pDataBlock = (SimDataBlock*)(*pGroup)[i];
// Post the datablock event to the client.
object->postNetEvent(new SimDataBlockEvent(pDataBlock, i, iCount, object->getDataBlockSequence()));
}
}
}In the same file, at line 1146 or thereabouts, find the following:
// object failed to load, let's see if it had any missing files
if(!ResourceManager->getMissingFileList(mMissingFileList))... and replace it with ...
// object failed to load, let's see if it had any missing files
if(isLocalConnection() || !ResourceManager->getMissingFileList(mMissingFileList))Then in sim/netGhost.cc, around line 958, find this code in NetConnection::loadNextGhostAlwaysObject:
// object failed to load, let's see if it had any missing files
if(!ResourceManager->getMissingFileList(mMissingFileList))... and replace it with ...
// object failed to load, let's see if it had any missing files
if(isLocalConnection() || !ResourceManager->getMissingFileList(mMissingFileList))In the same file around line 828 in the NetConnection::activateGhosting method, remove the following code:
sendConnectionMessage(GhostAlwaysStarting, mGhostingSequence, ghostAlwaysSet->size());
for(j = mGhostZeroUpdateIndex - 1; j >= 0; j--)
{
AssertFatal((mGhostArray[j]->flags & GhostInfo::ScopeAlways) != 0, "Non-scope always in the scope always list.")
// we may end up resending state here, but at least initial state
// will not be resent.
mGhostArray[j]->updateMask = 0;
ghostPushToZero(mGhostArray[j]);
mGhostArray[j]->flags &= ~GhostInfo::NotYetGhosted;
mGhostArray[j]->flags |= GhostInfo::ScopedEvent;
postNetEvent(new GhostAlwaysObjectEvent(mGhostArray[j]->obj, mGhostArray[j]->index));
}
sendConnectionMessage(GhostAlwaysDone, mGhostingSequence);... and replace it with:
// Send the initial ghosting connection message.
sendConnectionMessage(GhostAlwaysStarting, mGhostingSequence, ghostAlwaysSet->size());
// If this is the connection to the local client...
if (getLocalClientConnection() == this)
{
// Get a pointer to the local client.
NetConnection* pClient = NetConnection::getConnectionToServer();
// Iterate through the scope always objects...
for (j = mGhostZeroUpdateIndex - 1; j >= 0; j--)
{
AssertFatal((mGhostArray[j]->flags & GhostInfo::ScopeAlways) != 0, "NetConnection::activateGhosting: Non-scope always in the scope always list.")
// Clear the ghost update mask and flags appropriately.
mGhostArray[j]->updateMask = 0;
ghostPushToZero(mGhostArray[j]);
mGhostArray[j]->flags &= ~GhostInfo::NotYetGhosted;
mGhostArray[j]->flags |= GhostInfo::ScopedEvent;
// Set up a pointer to the new object.
NetObject* pObject = 0;
// If there's a valid ghost object...
if (mGhostArray[j]->obj)
{
// Set up a buffer for the object send.
U8 iBuffer[4096];
BitStream mStream(iBuffer, 4096);
// Pack the server object's update.
mGhostArray[j]->obj->packUpdate(this, 0xFFFFFFFF, &mStream);
// Set the stream position back to zero.
mStream.setPosition(0);
// Create a new object instance for the client.
pObject = (NetObject*)ConsoleObject::create(pClient->getNetClassGroup(), NetClassTypeObject, mGhostArray[j]->obj->getClassId(getNetClassGroup()));
// Set the client object networking flags.
pObject->mNetFlags = NetObject::IsGhost;
pObject->mNetIndex = mGhostArray[j]->index;
// Unpack the client object's update.
pObject->unpackUpdate(pClient, &mStream);
}
else
{
// Otherwise, create a new dummy netobject.
pObject = new NetObject;
}
// Execute the appropriate console callback.
Con::executef(1, "onGhostAlwaysObjectReceived");
// Set the ghost always object for the client.
pClient->setGhostAlwaysObject(pObject, mGhostArray[j]->index);
}
}
else
{
// Iterate through the scope always objects...
for (j = mGhostZeroUpdateIndex - 1; j >= 0; j--)
{
AssertFatal((mGhostArray[j]->flags & GhostInfo::ScopeAlways) != 0, "NetConnection::activateGhosting: Non-scope always in the scope always list.")
// Clear the ghost update mask and flags appropriately.
mGhostArray[j]->updateMask = 0;
ghostPushToZero(mGhostArray[j]);
mGhostArray[j]->flags &= ~GhostInfo::NotYetGhosted;
mGhostArray[j]->flags |= GhostInfo::ScopedEvent;
// Post a network event to ghost the scope always object.
postNetEvent(new GhostAlwaysObjectEvent(mGhostArray[j]->obj, mGhostArray[j]->index));
}
}
// Send the ghosting always done message.
sendConnectionMessage(GhostAlwaysDone, mGhostingSequence);About the author
#2
05/15/2007 (5:49 pm)
Yeah, it'll reduce the amount of time to load the game both singleplayer and if you're playing as the host of a multiplayer server.
#3
When I release my AFX Creator Tool, you're getting a big thank you in the credits. Any AFX user knows how many datablocks you have to load up, and this not only enhances the tool's loading time, but has sped up my development.
Great resource!
05/16/2007 (6:03 am)
I love this! I've been using this for the creation of stand-alone Torque tools for a while now. When I release my AFX Creator Tool, you're getting a big thank you in the credits. Any AFX user knows how many datablocks you have to load up, and this not only enhances the tool's loading time, but has sped up my development.
Great resource!
#4
05/16/2007 (8:40 am)
Yea the reaosn why I asked was ebcause the datablock loading time in AFX was starting to seriously annoy me :P Great job
#5
and the correlative post:
Topic: Reducing level load time
www.garagegames.com/mg/forums/result.thread.php?qt=23589
05/18/2007 (3:45 am)
Daniel, Thanks very much!and the correlative post:
Topic: Reducing level load time
www.garagegames.com/mg/forums/result.thread.php?qt=23589
#6
Thanks alot Daniel!
05/28/2007 (8:26 pm)
Excellent resource for speeding up the load time for single player games.Thanks alot Daniel!
#7
If I add that one tiny change, my mission stalls and never completes loading. Here's my console log:
Without that change, I get a long list of Could not locate texture, and then it proceeds to complete mission download.
Even without the change, the loading objects time has dropped dramatically, so this is great!
07/03/2007 (4:23 am)
This mostly works for me with TGE 1.5.2 (and several other mods applied). However I had to leave out the first chang in sim/netGhost.cc, around line 958 in NetConnection::loadNextGhostAlwaysObject, where we were supposed to add isLocalConnection().If I add that one tiny change, my mission stalls and never completes loading. Here's my console log:
*** Phase 2: Download Ghost Objects Mapping string: MissionStartPhase2Ack to index: 1 Could not locate texture: gunfight/data/shapes/Gravyard/Unnamed Client 1466 packet error: Invalid packet.. CDROP: 1466 local
Without that change, I get a long list of Could not locate texture, and then it proceeds to complete mission download.
Even without the change, the loading objects time has dropped dramatically, so this is great!
#8
08/15/2007 (11:51 pm)
Great resource. I'am working on a multiplayer game so it not help for the game itself but speed up the development :)
#9
Unfortunately, Im having the same problem as Ed Zavada when I try to load up the Stronghold mission. After taking his advice and changing the code a bit, I get the same missing textures message in the console, and for some reason I get some bad latency issues in the mission when Im near the campfire and buildings?
10/28/2007 (9:41 am)
Great resource! Sped up my load up drastically!Unfortunately, Im having the same problem as Ed Zavada when I try to load up the Stronghold mission. After taking his advice and changing the code a bit, I get the same missing textures message in the console, and for some reason I get some bad latency issues in the mission when Im near the campfire and buildings?
#10
Edit: The cycle did work on local connection but not on server connection.
01/15/2008 (8:13 am)
I had problems when i played arround with the starter.racing which make a cycle mission. It stuck on loading datablocks. While I didn't see a big difference between the original code and the code used if its not local i seperate local and put the original code in if not and the cycle does work again - dont ask me why:ConsoleMethod( GameConnection, transmitDataBlocks, void, 3, 3, "(int sequence)")
{
// If this is the local client...
if (GameConnection::getLocalClientConnection() == object)
{
// Set the datablock sequence.
object->setDataBlockSequence(dAtoi(argv[2]));
// Store a pointer to the datablock group.
SimDataBlockGroup* pGroup = Sim::getDataBlockGroup();
// Determine the size of the datablock group.
const U32 iCount = pGroup->size();
// Set up a pointer to the datablock.
SimDataBlock* pDataBlock = 0;
// Iterate through all the datablocks...
for (U32 i = 0; i < iCount; i++)
{
// Get a pointer to the datablock in question...
pDataBlock = (SimDataBlock*)(*pGroup)[i];
// Set the client's new modified key.
object->setMaxDataBlockModifiedKey(pDataBlock->getModifiedKey());
// Set up a buffer for the datablock send.
U8 iBuffer[4096];
BitStream mStream(iBuffer, 4096);
// Pack the datablock stream.
pDataBlock->packData(&mStream);
// Set the stream position back to zero.
mStream.setPosition(0);
// Unpack the datablock stream.
pDataBlock->unpackData(&mStream);
// Call the console function to set the number of blocks to be sent.
Con::executef(3, "onDataBlockObjectReceived", Con::getIntArg(i), Con::getIntArg(iCount));
// Preload the datablock on the dummy client.
pDataBlock->preload(false, NetConnection::getErrorBuffer());
}
// Get the last datablock (if any)...
if (pDataBlock)
{
// Ensure the datablock modified key is set.
object->setDataBlockModifiedKey(object->getMaxDataBlockModifiedKey());
// Ensure that the client knows that the datablock send is done...
object->sendConnectionMessage(GameConnection::DataBlocksDone, object->getDataBlockSequence());
}
}
else
{
object->setDataBlockSequence(dAtoi(argv[2]));
SimDataBlockGroup *g = Sim::getDataBlockGroup();
// find the first one we haven't sent:
U32 i, groupCount = g->size();
S32 key = object->getDataBlockModifiedKey();
for(i = 0; i < groupCount; i++)
if(( (SimDataBlock *)(*g)[i])->getModifiedKey() > key)
break;
if (i == groupCount) {
object->sendConnectionMessage(GameConnection::DataBlocksDone, object->getDataBlockSequence());
return;
}
object->setMaxDataBlockModifiedKey(key);
// Ship the rest off...
U32 max = getMin(i + DataBlockQueueCount, groupCount);
for (;i < max; i++) {
SimDataBlock *data = (SimDataBlock *)(*g)[i];
object->postNetEvent(new SimDataBlockEvent(data, i, groupCount, object->getDataBlockSequence()));
}
}
}Edit: The cycle did work on local connection but not on server connection.
#12
03/17/2008 (3:12 pm)
Modified the original post with a very small update to the original code. A codeblock was slightly misplaced that was causing the game to skip notifying clients of datablock transmission completion.
#13
I looked at this again, and realized what was wrong that was causing Invalid Packet errors. The code as you have it assumes that any failed file load is fatal and generates an Invalid Packet error. The normal torque behavior is to allow failed file loads if the server doesn't have the file either. To get that behavior with your patch, I added the following to NetConnection::loadNextGhostAlwaysObject in netGhost.cc:
... and I left out the first change in sim/netGhost.cc, around line 958 in NetConnection::loadNextGhostAlwaysObject, where we were supposed to add isLocalConnection().
Because mFilesWereDownloaded is set to the value of hadNewFiles (false), tsStatic will succeed in calling register object even if some textures were missing (see tsStatic::onAdd() for details).
My original change of just leaving out the inLocalConnection() call on line 958 had the same effect, but it sent a download request to the server and waited for the server to figure out that it didn't have the file before continuing, which was overall a lot less efficient.
03/17/2008 (8:47 pm)
@DanielI looked at this again, and realized what was wrong that was causing Invalid Packet errors. The code as you have it assumes that any failed file load is fatal and generates an Invalid Packet error. The normal torque behavior is to allow failed file loads if the server doesn't have the file either. To get that behavior with your patch, I added the following to NetConnection::loadNextGhostAlwaysObject in netGhost.cc:
while(mGhostAlwaysSaveList.size())
{
[b]if (isLocalConnection()) hadNewFiles = false;[/b]
// only check for new files if this is the first load, or if new
// files were downloaded from the server.... and I left out the first change in sim/netGhost.cc, around line 958 in NetConnection::loadNextGhostAlwaysObject, where we were supposed to add isLocalConnection().
Because mFilesWereDownloaded is set to the value of hadNewFiles (false), tsStatic will succeed in calling register object even if some textures were missing (see tsStatic::onAdd() for details).
My original change of just leaving out the inLocalConnection() call on line 958 had the same effect, but it sent a download request to the server and waited for the server to figure out that it didn't have the file before continuing, which was overall a lot less efficient.
#14
03/30/2008 (6:53 pm)
It's intended functionality to error on missing files. I test exclusively for multiplayer via dedicated server. With the exception of a client hosting a server, any file missing in such a setup is cause for an error. For my money I'd keep away from jury-rigging your way around "problems" specific to development on local clients. It won't serve you well in the long run -- at least not if you're planning on any kind of multiplayer.
#15
I understand your point, but again, the normal behavior of the unaltered engine is not to fail with a packet error in that case. That makes it break with some common 3D party content packs, such as the ta FPS Environment Pack, which has models that refer to a texture "unnamed" that doesn't exist.
You've made it more strict, which suits your test case.
I my case, I push down all files using http before the mission is ever started, so there won't be any missing files that I want to retrieve -- the torque file download, because it is bandwidth throttled, is so slow I just never use it.
In any case, this is an awesome resource. Extremely useful, and thanks for contributing it!
03/30/2008 (7:07 pm)
@Daniel:I understand your point, but again, the normal behavior of the unaltered engine is not to fail with a packet error in that case. That makes it break with some common 3D party content packs, such as the ta FPS Environment Pack, which has models that refer to a texture "unnamed" that doesn't exist.
You've made it more strict, which suits your test case.
I my case, I push down all files using http before the mission is ever started, so there won't be any missing files that I want to retrieve -- the torque file download, because it is bandwidth throttled, is so slow I just never use it.
In any case, this is an awesome resource. Extremely useful, and thanks for contributing it!
#16
08/14/2009 (10:44 am)
Just an update now that I've seen this one. I am using T3D Beta 4 and all of these fixes appear now in the engine, including Ed Zavada's line setting hadNewFiles to false.
Torque 3D Owner Tom Perry