Found it. Subsystems defer resource destruction because in-flight command
buffers may still reference what they are freeing, and the queues drain in
beginFrame when a frame slot comes around again. During shutdown no
further frames run, so everything queued sat there until
VkContext::shutdown dropped the queues unexecuted — deliberately, since by
then the pools those lambdas free sets from are gone.
The result was every resident resource outliving the device.
TerrainRenderer::clear defers each chunk, so at exit 19,984 vertex and
19,984 index buffers leaked, plus 3,276 from WMO groups and the rest from
M2 models and character bones — 426 MB reported object by object at
vkDestroyDevice, which is where the ninety thousand validation errors came
from.
Add VkContext::flushDeferredCleanup, which runs both frame queues
immediately, and call it from terrain, WMO, M2 and character shutdown
right after they queue their destruction and while their descriptor pools
are still alive. That ordering is the whole trick: draining any later
would free sets from pools that no longer exist, which is exactly why
VkContext::shutdown gave up and cleared the queues instead.