Fix severe performance issues in large maps.

So... Remember that one decision I made about avoiding BlockThingsIterator as
much as possible? Turns out that was a stupid idea. There ARE situations where
it's better to iterate sector thinglists, yes, especially for things that are
NOT part of the blockmap, but in other cases, the excess allocations of new
iterators are a reasonable price to pay for the lower perf impact in extreme
cases, such as maps that have a gazillion sectors with gazillions of things in
them (I'm looking at you, UDMF mappers).
As a compromise, at least, in situations where the thinglists are needed, I did
add a sort of micro-optimization by implementing code to check if a bounding
box is inside a sector (would be nice if this was part of GZDoom itself, tho).
This commit is contained in:
Mari the Deer 2023-12-19 11:46:29 +01:00
commit e5e6ce619c
20 changed files with 441 additions and 277 deletions

View file

@ -102,15 +102,21 @@ extend Class SWWMHandler
if ( !swwm_debugview ) return;
// prepare projection data, we're going to need this
SWWMUtility.PrepareProjData(projdata,e.ViewPos,e.ViewAngle,e.ViewPitch,e.ViewRoll,players[consoleplayer].fov);
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
foreach ( s:level.Sectors )
{
if ( (a == players[consoleplayer].Camera) && !(players[consoleplayer].cheats&CF_CHASECAM) ) continue;
if ( a.bINVISIBLE && !(a is 'DynamicLight') ) continue;
if ( (a is 'Inventory') && Inventory(a).Owner ) continue;
if ( (a is 'SWWMPickupFlash') && (a.CurState == a.FindState('Pickup')) ) continue;
if ( (a is 'SWWMShadow') || (a is 'SWWMItemOverlay') || (a is 'HeadpatTracker') || (a is 'SWWMTeleportLine') || (a is 'SWWMTeleportDest') ) continue;
if ( a.Distance3DSquared(e.Camera) > 1000000 ) continue;
DrawActor(e,a);
// don't check sectors that aren't within bounds, saves some time
if ( !BoxInSectorBounds(s,players[consoleplayer].Camera.pos.xy,1000,players[consoleplayer].Camera.CurSector.PortalGroup) )
continue;
for ( Actor a=s.thinglist; a; a=a.snext )
{
if ( (a == players[consoleplayer].Camera) && !(players[consoleplayer].cheats&CF_CHASECAM) ) continue;
if ( a.bINVISIBLE && !(a is 'DynamicLight') ) continue;
if ( (a is 'Inventory') && Inventory(a).Owner ) continue;
if ( (a is 'SWWMPickupFlash') && (a.CurState == a.FindState('Pickup')) ) continue;
if ( (a is 'SWWMShadow') || (a is 'SWWMItemOverlay') || (a is 'HeadpatTracker') || (a is 'SWWMTeleportLine') || (a is 'SWWMTeleportDest') ) continue;
if ( a.Distance3DSquared(e.Camera) > 1000000 ) continue;
DrawActor(e,a);
}
}
}
}

View file

@ -17,10 +17,42 @@ Class RampancyLogonDummy : Actor
}
}
// this is used to speed up iteration through sector thinglists within a specific area
Class SectorBounds
{
Vector4 bounds;
int portalgroup;
clearscope bool PointInSectorBounds( Vector2 p, int pg = -1 ) const
{
if ( (pg >= 0) && (level.GetPortalGroupCount() > 0) && (pg != portalgroup) )
p += level.GetDisplacement(pg,portalgroup);
if ( p.x < bounds.x ) return false;
if ( p.y < bounds.y ) return false;
if ( p.x > bounds.z ) return false;
if ( p.y > bounds.w ) return false;
return true;
}
clearscope bool BoxInSectorBounds( Vector2 p, double r, int pg = -1 ) const
{
if ( (pg >= 0) && (level.GetPortalGroupCount() > 0) && (pg != portalgroup) )
p += level.GetDisplacement(pg,portalgroup);
if ( p.x+r < bounds.x ) return false;
if ( p.y+r < bounds.y ) return false;
if ( p.x-r > bounds.z ) return false;
if ( p.y-r > bounds.w ) return false;
return true;
}
}
extend Class SWWMHandler
{
bool maphaskeys;
// weird optimization
Array<SectorBounds> sbounds;
// level end stats
override void WorldUnloaded( WorldEvent e )
{
@ -146,6 +178,51 @@ extend Class SWWMHandler
SWWMUtility.MarkAchievement("hurry",players[consoleplayer]);
}
clearscope bool PointInSectorBounds( Sector s, Vector2 p, int pg = -1 ) const
{
if ( sbounds.Size() <= 0 ) return false;
int i = s.Index();
return sbounds[i].PointInSectorBounds(p,pg);
}
clearscope bool BoxInSectorBounds( Sector s, Vector2 p, double r, int pg = -1 ) const
{
if ( sbounds.Size() <= 0 ) return false;
int i = s.Index();
return sbounds[i].BoxInSectorBounds(p,r,pg);
}
void PrecalculateSectorBounds()
{
sbounds.Resize(level.Sectors.Size());
foreach ( s:level.Sectors )
{
let sb = new("SectorBounds");
sb.portalgroup = s.portalgroup;
sb.bounds = ( 32767, 32767, -32768, -32768 );
foreach ( l:s.Lines )
{
if ( l.v1.p.x < sb.bounds.x )
sb.bounds.x = l.v1.p.x;
if ( l.v2.p.x < sb.bounds.x )
sb.bounds.x = l.v2.p.x;
if ( l.v1.p.y < sb.bounds.y )
sb.bounds.y = l.v1.p.y;
if ( l.v2.p.y < sb.bounds.y )
sb.bounds.y = l.v2.p.y;
if ( l.v1.p.x > sb.bounds.z )
sb.bounds.z = l.v1.p.x;
if ( l.v2.p.x > sb.bounds.z )
sb.bounds.z = l.v2.p.x;
if ( l.v1.p.y > sb.bounds.w )
sb.bounds.w = l.v1.p.y;
if ( l.v2.p.y > sb.bounds.w )
sb.bounds.w = l.v2.p.y;
}
sbounds[s.Index()] = sb;
}
}
private void MapStartDialogues()
{
int whichboss = WhichVanillaBossMap();
@ -264,6 +341,7 @@ extend Class SWWMHandler
// (if we do it in OnRegister the existing thinker might not have been deserialized yet)
gdat = SWWMGlobals.Get();
if ( e.IsReopen ) return;
PrecalculateSectorBounds();
MapStartDialogues();
if ( gamestate != GS_TITLELEVEL )
{

View file

@ -172,18 +172,12 @@ extend Class SWWMHandler
// copies the floatbob of overlapping identical items, so it doesn't look weird
private void CopyFloatBob( Actor a )
{
bool breakout = false;
foreach ( s:level.Sectors )
for ( Actor t=a.CurSector.thinglist; t; t=t.snext )
{
for ( Actor t=s.thinglist; t; t=t.snext )
{
if ( (t == a) || !(t is 'Inventory') || !(t.spawnpoint ~== a.spawnpoint) ) continue;
a.floatbobphase = t.floatbobphase;
a.angle = t.angle; // also copy angle
breakout = true;
break;
}
if ( breakout ) break;
if ( (t == a) || !(t is 'Inventory') || !(t.spawnpoint ~== a.spawnpoint) ) continue;
a.floatbobphase = t.floatbobphase;
a.angle = t.angle; // also copy angle
break;
}
}

View file

@ -281,42 +281,48 @@ extend Class SWWMHandler
double viewdist = SWWMStatusBar.MAPVIEWDIST;
// still about as expensive as using a BlockThingsIterator, but without the need to allocate one every tic
int thisgroup = players[consoleplayer].Camera.CurSector.portalgroup;
foreach ( s:level.Sectors ) for ( Actor a=s.thinglist; a; a=a.snext )
foreach ( s:level.Sectors )
{
Vector2 rv;
if ( s.portalgroup != thisgroup )
// don't check sectors that aren't within bounds, saves some time
if ( !BoxInSectorBounds(s,players[consoleplayer].Camera.pos.xy,viewdist,players[consoleplayer].Camera.CurSector.PortalGroup) )
continue;
for ( Actor a=s.thinglist; a; a=a.snext )
{
Vector2 relpos = players[consoleplayer].Camera.pos.xy+level.GetDisplacement(thisgroup,s.portalgroup);
rv = a.pos.xy-relpos;
Vector2 rv;
if ( s.portalgroup != thisgroup )
{
Vector2 relpos = players[consoleplayer].Camera.pos.xy+level.GetDisplacement(thisgroup,s.portalgroup);
rv = a.pos.xy-relpos;
}
else rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
double rad;
bool isproj = a.bMISSILE&&SWWMUtility.ValidProjectile(a);
if ( SWWMUtility.IsBeamProj(a) )
{
isproj = true;
rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
}
else rad = a.radius;
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
continue;
if ( a == players[consoleplayer].Camera )
continue;
if ( a is 'GhostTarget' )
continue;
if ( !a.player && !a.bSOLID && !a.bSHOOTABLE && !a.bISMONSTER && !a.bFRIENDLY && !(a is 'Inventory') && !(a is 'Chancebox') && !isproj )
continue;
if ( !level.allmap && !(deathmatch && (a is 'Inventory') && !a.bDROPPED) && !(a.IsFriend(players[consoleplayer].mo) && !(a.player && (a.player.mo != a))) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
if ( a.bKILLED || (a.Health <= 0) || a.bUnmorphed )
continue;
if ( (a is 'Inventory') && (!a.bSPECIAL || Inventory(a).Owner || (a.GetClassName() == 'aas_token')) ) // autoautosave hotfix
continue;
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
continue;
if ( isproj && !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
SWWMSimpleTracker.Track(self,a);
}
else rv = a.pos.xy-players[consoleplayer].Camera.pos.xy;
double rad;
bool isproj = a.bMISSILE&&SWWMUtility.ValidProjectile(a);
if ( SWWMUtility.IsBeamProj(a) )
{
isproj = true;
rad = SWWMUtility.IsYBeam(a)?(a.scale.y*cos(a.pitch-90)):(a.speed*cos(a.pitch));
}
else rad = a.radius;
if ( max(abs(rv.x)-a.radius,abs(rv.y)-a.radius) > viewdist )
continue;
if ( a == players[consoleplayer].Camera )
continue;
if ( a is 'GhostTarget' )
continue;
if ( !a.player && !a.bSOLID && !a.bSHOOTABLE && !a.bISMONSTER && !a.bFRIENDLY && !(a is 'Inventory') && !(a is 'Chancebox') && !isproj )
continue;
if ( !level.allmap && !(deathmatch && (a is 'Inventory') && !a.bDROPPED) && !(a.IsFriend(players[consoleplayer].mo) && !(a.player && (a.player.mo != a))) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
if ( a.bKILLED || (a.Health <= 0) || a.bUnmorphed )
continue;
if ( (a is 'Inventory') && (!a.bSPECIAL || Inventory(a).Owner || (a.GetClassName() == 'aas_token')) ) // autoautosave hotfix
continue;
if ( (a is 'Chancebox') && (a.CurState != a.SpawnState) )
continue;
if ( isproj && !level.allmap && !(a.target && a.target.IsFriend(players[consoleplayer].mo)) && !a.CheckSight(players[consoleplayer].Camera,SF_IGNOREVISIBILITY|SF_IGNOREWATERBOUNDARY) )
continue;
SWWMSimpleTracker.Track(self,a);
}
SWWMSimpleTracker trk = strackers;
SWWMSimpleTracker prev = null, next;

View file

@ -99,6 +99,10 @@ Class SWWMStaticHandler : StaticEventHandler
SWWMHandler.ClearAllShaders();
EventHandler.SendInterfaceEvent(consoleplayer,"swwmflushhud");
EventHandler.SendInterfaceEvent(consoleplayer,"swwmaprcheck");
// quick fix for old savegames
let hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
if ( hnd && (hnd.sbounds.Size() <= 0) )
hnd.PrecalculateSectorBounds();
if ( !e.IsSaveGame ) return;
// save version checker
tainted = false;