glslang/shim: popAll the calling thread's TPoolAllocator at exit

Yesterday's atexit hook (5e21396f2) called glslang::FinalizeProcess
but heaptrack on the post-fix binary showed identical 24.44 MB
leaked — the ~12 MB rooted in glslang::TPoolAllocator::allocate
was unchanged. Tracing the heaptrack stack revealed the leak's
TLS owner is the GUI thread (not a renderer thread):

  TPoolAllocator::allocate
  ... glslang internals ...
  shadertoy.spirvFromGlsl
  shadertoy.loadFromFile (per-surface init)
  generic.Renderer.initShaders
  Surface.init
  ghostty_surface_new
  GhosttySurface::GhosttySurface
  MainWindow::newTab     ← GUI thread

ghostty_surface_new runs glslang synchronously from
MainWindow::newTab, so the pool pages accumulate on the GUI
thread's TLS. The GUI thread doesn't exit until process exit,
and FinalizeProcess only frees the SharedSymbolTables — NOT the
calling thread's pool. Pages persist to process termination.

Fix: call glslang::GetThreadPoolAllocator().popAll() inside
ghastty_glslang_finalize_process, before FinalizeProcess. popAll
is the documented release-all method on TPoolAllocator and frees
the pages back to the system allocator. Safe at atexit because
every renderer thread has joined (ThreadState.cleanup ran via
Vulkan.threadExit on each), the SPV cache was just cleared, and
FinalizeProcess doesn't reach into per-thread pool state.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-26 10:35:57 -05:00
parent 5e21396f27
commit 1c2c5760b7
1 changed files with 21 additions and 5 deletions

View File

@ -9,6 +9,7 @@
#include <unordered_map>
#include <vector>
#include <glslang/Include/PoolAlloc.h>
#include <glslang/Public/ShaderLang.h>
#include <glslang/Public/ResourceLimits.h>
#include <SPIRV/GlslangToSpv.h>
@ -248,10 +249,25 @@ extern "C" void ghastty_glslang_finalize_process(void) {
std::lock_guard<std::mutex> lg(spv_cache_mutex());
spv_cache().clear();
}
// Release glslang's process-wide state: the thread-local
// TPoolAllocator pages that accumulated to their high-water mark
// on the first surface's compiles + any per-thread bookkeeping.
// Matches the implicit InitializeProcess on first use (or the
// explicit C-API glslang_initialize_process in pkg/glslang/init.zig).
// Free this thread's TPoolAllocator pages. heaptrack pointed
// the ~12 MB glslang leak at TPoolAllocator::allocate calls
// rooted in shadertoy.spirvFromGlsl on the GUI thread (since
// ghostty_surface_new runs glslang synchronously from
// MainWindow::newTab) — that pool's pages persist until thread
// exit, but the GUI thread doesn't exit until process
// termination. glslang::FinalizeProcess only frees the
// process-wide SharedSymbolTables, NOT this pool. Call popAll()
// explicitly to release the pages back to the system allocator.
//
// Safe here because (a) we're called from atexit, every render
// thread has joined via Vulkan.threadExit (which also runs its
// own popAll-equivalent via ThreadState.cleanup); (b) the SPV
// cache was cleared above, so no compiled blob references the
// pool; (c) FinalizeProcess below won't reach into this pool
// either.
glslang::GetThreadPoolAllocator().popAll();
// Release glslang's process-wide shared state (the version-
// indexed SharedSymbolTables built at first compile).
glslang::FinalizeProcess();
}