diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h index f18099d1..b0d0a33f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h @@ -90,6 +90,14 @@ namespace MobileGL::MG_State::GLState { // the TShader's own intermediate rather than a copy. These used to die when // RunBody() returned, which was safe only because nothing called getIntermediate() // afterwards. GlslangToSpv does exactly that, so phase B has to own them. + // + // MEMORY NOTE: this is the one thing the split makes live LONGER than it used to - + // a glslang arena per stage, megabytes for a shaderpack, now alive from the end of + // phase A until phase B has generated its SPIR-V instead of dying with the link + // body. Phase B clears this vector as soon as GlslangToSpv returns, but a deep + // phase-B backlog still holds one arena per queued program. If peak RSS ever + // becomes the binding constraint on a pack load, THIS is the field to attack (by + // bounding the backlog, or by moving GlslangToSpv back into phase A). Vector> shaders; // GL enum per entry of `in.shaders`, in the same order (GetSpirvBinaryFromProgram // walks it to pick the intermediates). diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp index 982d6432..bfc3943e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp @@ -90,7 +90,11 @@ namespace MobileGL::MG_State::GLState { } const phaseAReleaser{m_phaseA}; if (!m_phaseA) return; - const ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff; + // Non-const: the TShaders are dropped below, the moment GlslangToSpv is finished with + // them. This is safe by ownership rather than by locking - phase A is terminal and + // therefore immutable to everyone else, the GL-thread join touches only `artifacts` + // and `diagnostics`, and this node is the sole reader of the handoff. + ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff; const Uint externalIndex = m_phaseA->in.externalIndex; if (!handoff.ready || !handoff.reflection.program) { // Phase A did not reach its tail (it failed the link, or was cancelled mid-body). @@ -105,6 +109,12 @@ namespace MobileGL::MG_State::GLState { MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex); GenerateSpirv(handoff, externalIndex); + // GlslangToSpv was the only consumer of the parsed ASTs, and they are by far the + // largest thing this node keeps alive (one glslang arena per stage, megabytes for a + // shaderpack). Everything after this point works on the SPIR-V and on the TProgram's + // own self-contained reflection pool, so drop them here rather than at the end of the + // body - spirv-opt plus routing is ~87% of this node's runtime. + handoff.shaders.clear(); MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex); {