performance: add animation interpolation, async zone prefetch, and event manager flood prevention
This commit is contained in:
parent
04599db6d6
commit
5bb4e4db5c
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue