glslang/shim: ghastty_glslang_finalize_process + atexit hook

Releases glslang's process-wide state at app shutdown — the
per-thread TPoolAllocator pages that hit their high-water mark
on the first surface's shader compiles and otherwise leak until
process termination (Zig pthreads don't run C++ thread_local
destructors, so glslang's TLS pool is never cleaned up
incrementally). heaptrack attributed ~12 MB to this across
allocation paths rooted in glslang::TPoolAllocator::allocate.

Also clears the shim's SPV cache (the std::vector storage backing
each cached entry) so the cleanup is symmetric.

Wired via std::atexit in qt/src/main.cpp — runs AFTER Qt's
teardown chain has destroyed every GhosttySurface (and joined
every renderer thread), so glslang is provably quiescent and the
FinalizeProcess contract holds.

Cosmetic: the user's actual runtime memory doesn't change (the
pool was never going to grow further during a session); this is
purely about cleaner heaptrack output and not holding ~12 MB at
process exit.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-26 10:25:21 -05:00
parent 7d8cdf6adb
commit 5e21396f27
3 changed files with 53 additions and 0 deletions

View File

@ -237,3 +237,21 @@ extern "C" void ghastty_glslang_free_spirv(uint32_t* spv) {
extern "C" void ghastty_glslang_free_error(char* err) {
std::free(err);
}
extern "C" void ghastty_glslang_finalize_process(void) {
// Drop the cached SPV blobs first. The map owns the std::vector
// pages it holds; clearing returns them to the heap. Done before
// FinalizeProcess so a malicious post-finalize compile attempt
// (which would re-enter glslang on a dead process state) trips
// glslang's own checks rather than handing out stale cache hits.
{
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).
glslang::FinalizeProcess();
}

View File

@ -57,6 +57,23 @@ int ghastty_glslang_compile_vulkan(
void ghastty_glslang_free_spirv(uint32_t* spv);
void ghastty_glslang_free_error(char* err);
// Release the process-wide glslang state: the per-thread
// TPoolAllocator pages (the high-water-mark pool memory that
// otherwise leaks for the process lifetime because Zig pthreads
// don't run C++ thread_local destructors) AND the shim's
// SPV cache.
//
// Idempotent. Call ONCE from the host's shutdown path AFTER all
// renderer threads have joined — calling it while a renderer
// thread might still touch glslang::TShader / TProgram is
// undefined behavior per glslang's contract.
//
// libghostty's own renderer-thread teardown (Vulkan.threadExit)
// is what serializes this safely: by the time the host's main()
// returns from QApplication::exec(), every renderer thread has
// already run threadExit and is joined.
void ghastty_glslang_finalize_process(void);
#ifdef __cplusplus
}
#endif

View File

@ -2,6 +2,13 @@
#include <cstdlib>
#include <cstring>
// Symbol exported by libghostty's bundled glslang shim
// (pkg/glslang/override/ghastty_vk_shim.cpp). Declared locally
// rather than via an include because main.cpp would otherwise
// need to grow a glslang/SPIR-V include path it doesn't use for
// anything else.
extern "C" void ghastty_glslang_finalize_process(void);
#include <QApplication>
#include <QCoreApplication>
#include <QIcon>
@ -63,6 +70,17 @@ int main(int argc, char **argv) {
// libghostty action handlers may also touch the renderer).
defaultDisableMangoHud();
// Release glslang's process-wide state at process exit (the
// per-thread TPoolAllocator pages that otherwise hit their
// high-water mark from the first surface's shader compiles and
// never get released — ~12 MB cosmetic leak per heaptrack).
// atexit runs after main returns and after Qt's own teardown
// chain has destroyed every GhosttySurface (and joined every
// renderer thread), so glslang is guaranteed quiescent by then.
// Idempotent on the libghostty side, so a double-registration
// (or the unlikely racing return path) is harmless.
std::atexit(ghastty_glslang_finalize_process);
// CLI action fast path: skip Qt entirely. ghostty_init parses argv
// for the `+action`; ghostty_cli_try_action runs it and exits the
// process. If something fails (unknown action, multiple actions),