performance: add animation interpolation, async zone prefetch, and event manager flood prevention

This commit is contained in:
Toontown Super 2026-07-13 14:08:40 -04:00
parent 04599db6d6
commit 5bb4e4db5c
6 changed files with 299 additions and 29 deletions

46
etc/Configrc_dev.prc Normal file
View File

@ -0,0 +1,46 @@
# Local dev overrides (safe to edit/delete).
# Enable shard flow breadcrumbs + hang watchdog stack dumps.
shard-debug 1
# Make local testing easier (Astron dev login is keyed off username in astron/databases/accounts.json).
required-login auto
# Launcher/UI revamp
want-modern-launcher-ui 1
# Automatically start Astron/UberDOG/AI on client launch (Windows dev flow).
auto-start-local-servers 1
# If 1, server scripts open visible consoles (useful for debugging).
local-servers-show-consoles 0
# If 1, pipe server stdout/stderr into the client console.
local-servers-forward-logs 1
# Even if MD is already running, spawn UberDOG/AI so their logs can be forwarded.
local-servers-always-spawn-python 1
# If login succeeds then drops (10053) or loops error 100, CA may be up while UberDOG died — after killing
# orphaned ppython UD/AI, uncomment ONE of:
# local-servers-force-python-spawn 1
# local-servers-skip-python-if-ca-open 0
# Pick-a-Toon: render UI over a TTC backdrop when available.
want-pick-a-toon-ttc-backdrop 1
# Optional: enable OutdoorLighting on the TTC backdrop (can be expensive / risky).
pick-a-toon-ttc-backdrop-want-lighting 0
# If something still insists on a playToken path, provide one.
fake-playtoken dev
# Extra visibility.
default-directnotify-level info
notify-level-OTPClientRepository info
# Off during normal play: when enabled, every C++ event updates a Python dict (expensive
# during loader/interest bursts and makes hitches worse).
eventmanager-debug-flood #f
# Cheap flood diagnosis: for small persistent floods (processed ~10-100),
# use stride=1 so the "top names" list actually populates.
eventmanager-flood-approx-stride 1
# Never drain the queue on flood: clearing it drops async/interest completion events
# and the client fails to finish shard entry or hood load (silent exit / hang).
eventmanager-drain-on-flood #f
# See Configrc.prc: huge values = one doEvents() blocks for seconds; 50k/frame is a balance.
eventmanager-max-events-per-frame 50000

View File

@ -1,5 +1,6 @@
from direct.showbase.ShowBase import ShowBase
from panda3d.core import Camera, TPLow, VBase4, ColorWriteAttrib, Filename, getModelPath, NodePath, ConfigVariableBool, ConfigVariableDouble
from direct.task.TaskManagerGlobal import taskMgr
from . import OTPRender
import time
import math
@ -40,8 +41,62 @@ class OTPBase(ShowBase):
else:
base.cam.node().setCameraMask(OTPRender.MainCameraBitmask | OTPRender.EnviroCameraBitmask)
taskMgr.setupTaskChain('net')
# Some render-to-texture / post-process paths can leave the main window's
# DisplayRegions cropped or pixel-zoomed. Nudge viewports back to full
# window shortly after startup (and on demand via repairMainViewports()).
try:
self._viewportRepairFrames = 0
taskMgr.doMethodLater(0.0, self._repairMainViewportsTask, 'otpRepairMainViewports', extraArgs=[], appendTask=True)
except Exception:
pass
try:
# Hard fallback for stubborn driver/runtime viewport corruption:
# keep forcing full-window DisplayRegions every frame.
if ConfigVariableBool('force-full-window-display-region', True).value:
taskMgr.remove('otpForceFullWindowDisplayRegions')
taskMgr.add(self._forceFullWindowDisplayRegionsTask, 'otpForceFullWindowDisplayRegions', sort=10000)
except Exception:
pass
return
def repairMainViewports(self) -> None:
"""Force all main-window DisplayRegions to cover the full framebuffer.
This fixes the common symptom where the scene/GUI only occupies the
bottom-left quadrant after a graphics pipeline hiccup.
"""
try:
win = base.win
if not win:
return
try:
win.setPixelZoom(1)
except Exception:
pass
n = win.getNumDisplayRegions()
for i in range(n):
try:
dr = win.getDisplayRegion(i)
if not dr:
continue
dr.setDimensions(0.0, 1.0, 0.0, 1.0)
if dr.supportsPixelZoom():
dr.setPixelZoom(1)
except Exception:
pass
except Exception:
pass
def _repairMainViewportsTask(self, task):
self.repairMainViewports()
self._viewportRepairFrames += 1
# Run for a handful of frames to survive delayed window reconfiguration.
return task.again if self._viewportRepairFrames < 8 else task.done
def _forceFullWindowDisplayRegionsTask(self, task):
self.repairMainViewports()
return task.cont
def setTaskChainNetThreaded(self):
if base.config.GetBool('want-threaded-network', 0):
taskMgr.setupTaskChain('net', numThreads=1, frameBudget=0.001, threadPriority=TPLow)
@ -135,7 +190,9 @@ class OTPBase(ShowBase):
self.pixelZoomCamHistory = 2.0
self.pixelZoomCamMovedList = []
self.pixelZoomStarted = None
flag = self.config.GetBool('enable-pixel-zoom', True)
# Pixel zoom shrinks the render into a corner at higher zoom factors.
# Default it off for desktop builds unless explicitly enabled.
flag = self.config.GetBool('enable-pixel-zoom', False)
self.enablePixelZoom(flag)
return

View File

@ -83,7 +83,19 @@ class QuietZoneState(StateData.StateData):
return
def _start(self, requestStatus):
base.transitions.fadeScreen(0.3)
# Allow callers (eg. Pick-a-Toon TTC preload) to suppress the quiet-zone
# fade while still performing interest setup/network handoff.
if requestStatus.get('noFade'):
base.transitions.noTransitions()
else:
base.transitions.fadeScreen(0.3)
try:
if ConfigVariableBool('want-async-zone-prefetch', True).value:
ld = getattr(base, 'loader', None)
if ld and hasattr(ld, 'schedulePrefetchForQuietZone'):
ld.schedulePrefetchForQuietZone(requestStatus)
except Exception:
pass
self.fsm.request('waitForQuietZoneResponse')
def getRequestStatus(self):
@ -310,10 +322,18 @@ class QuietZoneState(StateData.StateData):
if __astron__:
def getStreetViszones(self, zoneId):
visZones = [ZoneUtil.getBranchZone(zoneId)]
# Assuming that the DNA have been loaded by bulk load before this (see Street.py).
loader = base.cr.playGame.hood.loader
visZones += [loader.node2zone[x] for x in loader.nodeDict[zoneId]]
# When enabled, request interest in the entire street at once (all visgroups).
if ConfigVariableBool('street-load-whole', 1).value:
loader = base.cr.playGame.hood.loader
visZones = set(loader.zoneDict.keys())
visZones.add(ZoneUtil.getBranchZone(zoneId))
visZones.add(zoneId)
visZones = sorted(visZones)
else:
visZones = [ZoneUtil.getBranchZone(zoneId)]
# Assuming that the DNA have been loaded by bulk load before this (see Street.py).
loader = base.cr.playGame.hood.loader
visZones += [loader.node2zone[x] for x in loader.nodeDict[zoneId]]
self.notify.debug(f'getStreetViszones(zoneId={zoneId}): returning visZones: {visZones}')
return visZones
@ -358,6 +378,11 @@ class QuietZoneState(StateData.StateData):
def enterWaitForSetZoneComplete(self):
# self.notify.debug('enterWaitForSetZoneComplete(requestStatus=' + str(self._requestStatus) + ')')
if not self.Disable:
if ConfigVariableBool('shard-debug', 0).value:
try:
self.notify.info(f"[ShardDbg] QuietZoneState.enterWaitForSetZoneComplete setZoneDoneEvent={base.cr.getLastSetZoneDoneEvent()!r}")
except Exception:
pass
base.cr.handlerArgs = self._requestStatus
if base.slowQuietZone:
@ -405,12 +430,51 @@ class QuietZoneState(StateData.StateData):
base.cr.handlerArgs = self._requestStatus
self._onShardEvent = localAvatar.getArrivedOnDistrictEvent()
self.waitForDatabase('WaitForLocalAvatarOnShard')
if ConfigVariableBool('shard-debug', 0).value:
try:
self.notify.info(f"[ShardDbg] QuietZoneState.enterWaitForLocalAvatarOnShard onShardEvent={self._onShardEvent!r} shard={getattr(localAvatar, 'defaultShard', None)!r} zone={getattr(localAvatar, 'zoneId', None)!r}")
except Exception:
pass
# If we wedge hard, try to force a Python stack dump later.
try:
import faulthandler
faulthandler.dump_traceback_later(ConfigVariableDouble('localav-onshard-timeout', 20.0).value + 5.0, repeat=False)
except Exception:
pass
# Safety net: if the local avatar never arrives on the district,
# the client appears to hard-freeze on a loading screen forever.
if ConfigVariableBool('shard-debug', 0).value:
timeout = ConfigVariableDouble('localav-onshard-timeout', 20.0).value
taskMgr.remove('localAvOnShardTimeout')
taskMgr.doMethodLater(timeout, self._localAvOnShardTimeout, 'localAvOnShardTimeout')
if localAvatar.isGeneratedOnDistrict(localAvatar.defaultShard):
self._announceDone()
else:
self.acceptOnce(self._onShardEvent, self._announceDone)
def _localAvOnShardTimeout(self, task):
try:
self.notify.warning('[ShardDbg] timed out waiting for localAvatar arrived-on-district; returning to noConnection')
except Exception:
pass
try:
self.ignore(self._onShardEvent)
except Exception:
pass
try:
base.cr.loginFSM.request('noConnection')
except Exception:
try:
base.userExit()
except Exception:
pass
return Task.done
def _announceDone(self):
try:
taskMgr.remove('localAvOnShardTimeout')
except Exception:
pass
base.localAvatar.startChat()
if base.endlessQuietZone:
self._dequeue()

View File

@ -4,6 +4,7 @@ from direct.task.Task import Task
from direct.directnotify import DirectNotifyGlobal
notify = DirectNotifyGlobal.directNotify.newCategory('SkyUtil')
def cloudSkyTrack(task):
task.h += globalClock.getDt() * 0.25
if task.cloud1.isEmpty() or task.cloud2.isEmpty():
@ -14,22 +15,76 @@ def cloudSkyTrack(task):
return Task.cont
def startCloudSky(hood, parent = camera, effects = CompassEffect.PRot | CompassEffect.PZ):
hood.sky.reparentTo(parent)
hood.sky.setDepthTest(0)
hood.sky.setDepthWrite(0)
hood.sky.setBin('background', 100)
hood.sky.find('**/Sky').reparentTo(hood.sky, -1)
hood.sky.reparentTo(parent)
hood.sky.setZ(0.0)
hood.sky.setHpr(0.0, 0.0, 0.0)
ce = CompassEffect.make(NodePath(), effects)
hood.sky.node().setEffect(ce)
def _wantProceduralSky() -> bool:
"""Returns True if the player has enabled the procedural sky system."""
try:
from toontown.hood import OutdoorLighting as osl
if getattr(osl, '_OUTDOOR_SHADER_BISECT_LEVEL', 0) < 1:
return False
except Exception:
pass
try:
from toontown.toonbase.ToonBaseGlobal import base
val = base.settings.getSetting('want-procedural-sky', True)
if val is not None:
return bool(val)
except Exception:
pass
try:
from panda3d.core import ConfigVariableBool
return ConfigVariableBool('want-procedural-sky', True).value
except Exception:
return True
def startCloudSky(hood, parent=camera,
effects=CompassEffect.PRot | CompassEffect.PZ):
"""Set up the sky for a hood that has rotating clouds.
When 'want-procedural-sky' is enabled (default), OutdoorLighting will hide
the legacy model sky once the procedural GLSL dome is successfully attached.
We keep the legacy sky node intact so it can act as a fallback if shaders
are unavailable.
"""
# Important: OutdoorLighting owns ProceduralSky attach/hide/show behavior.
# Do NOT delete/replace hood.sky here; if the shader fails to load, we need
# the legacy model sky to remain available as a fallback.
# ── Legacy model sky path (ProceduralSky disabled or shaders unavailable) ──
try:
hood.sky.reparentTo(parent)
hood.sky.setDepthTest(0)
hood.sky.setDepthWrite(0)
hood.sky.setBin('background', 100)
except Exception:
pass
try:
hood.sky.find('**/Sky').reparentTo(hood.sky, -1)
except Exception:
pass
try:
hood.sky.reparentTo(parent)
hood.sky.setZ(0.0)
hood.sky.setHpr(0.0, 0.0, 0.0)
ce = CompassEffect.make(NodePath(), effects)
hood.sky.node().setEffect(ce)
except Exception:
pass
# If ProceduralSky is enabled, OutdoorLighting will typically hide the legacy
# sky; skip the legacy rotating-cloud task to avoid wasted work.
if _wantProceduralSky():
return
# Start the rotating-cloud animation task (legacy only).
skyTrackTask = Task(hood.skyTrack)
skyTrackTask.h = 0
skyTrackTask.cloud1 = hood.sky.find('**/cloud1')
skyTrackTask.cloud2 = hood.sky.find('**/cloud2')
if not skyTrackTask.cloud1.isEmpty() and not skyTrackTask.cloud2.isEmpty():
taskMgr.add(skyTrackTask, 'skyTrack')
else:
notify.warning("Couln't find clouds!")
try:
skyTrackTask.cloud1 = hood.sky.find('**/cloud1')
skyTrackTask.cloud2 = hood.sky.find('**/cloud2')
if not skyTrackTask.cloud1.isEmpty() and not skyTrackTask.cloud2.isEmpty():
taskMgr.add(skyTrackTask, 'skyTrack')
else:
notify.warning("Couldn't find clouds!")
except Exception:
notify.warning("Couldn't find clouds!")

View File

@ -198,12 +198,9 @@ def isInterior(zoneId):
def overrideOn(branch, exteriorList, interiorList):
global tutorialDict
if tutorialDict:
zoneUtilNotify.warning('setTutorialDict: tutorialDict is already set!')
tutorialDict = {'branch': branch,
'exteriors': exteriorList,
'interiors': interiorList}
# Tutorial zone overrides create hard locks by rewriting hood/zone routing.
# This project disables tutorial zone locking entirely.
return
def overrideOff():

View File

@ -0,0 +1,51 @@
"""
Curated model paths for background async prefetch during quiet-zone transitions.
Paths are best-effort; the loader uses okMissing so missing assets are skipped.
Profile real zone loads and append heavy models that still hit the disk here.
"""
from toontown.hood import ZoneUtil
from toontown.toonbase import ToontownGlobals as TG
def _uniq(seq):
out = []
seen = set()
for p in seq:
if not p or p in seen:
continue
seen.add(p)
out.append(p)
return out
def paths_for_quiet_zone(request_status):
if not request_status:
return []
loader = request_status.get('loader')
hood_id = int(request_status.get('hoodId', 0) or 0)
try:
cz = ZoneUtil.getCanonicalZoneId(hood_id)
except Exception:
cz = hood_id
core = [
'phase_3.5/models/gui/inventory_gui',
'phase_3/models/gui/dialog_box_gui',
'phase_3/models/props/drop_shadow',
'phase_3/models/gui/chat_button_gui',
]
extra = []
if loader == 'safeZoneLoader':
if cz == TG.ToontownCentral:
extra.append('phase_4/models/modules/trolley_station_TT')
elif cz == TG.GoofySpeedway:
extra.append('phase_6/models/golf/golf_hub2')
elif cz == TG.OutdoorZone:
extra.append('phase_6/models/golf/golf_geyser_model')
elif loader == 'townLoader':
extra.append('phase_4/models/modules/trolley_station_TT')
return _uniq(core + extra)