From ae745b040cd57e31bf5c64e34cba832f4a16fb12 Mon Sep 17 00:00:00 2001 From: Richard Higgins Date: Sun, 4 Oct 2026 00:44:44 -0700 Subject: [PATCH 1/2] Release terrain and shadow shader objects after linking --- src/polyworld/quadterrain.nim | 5 ++++ src/polyworld/shadows.nim | 5 ++++ tests/render_shader_lifetime.nim | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 tests/render_shader_lifetime.nim diff --git a/src/polyworld/quadterrain.nim b/src/polyworld/quadterrain.nim index bb387aeb..651947f6 100644 --- a/src/polyworld/quadterrain.nim +++ b/src/polyworld/quadterrain.nim @@ -817,6 +817,11 @@ proc compileProgram(vertexSource, fragmentSource: string): GLuint = glAttachShader(result, vertexShader) glAttachShader(result, fragmentShader) glLinkProgram(result) + # The linked executable owns its code; release the compilation objects. + glDetachShader(result, vertexShader) + glDetachShader(result, fragmentShader) + glDeleteShader(vertexShader) + glDeleteShader(fragmentShader) var ok: GLint glGetProgramiv(result, GL_LINK_STATUS, ok.addr) if ok == 0: diff --git a/src/polyworld/shadows.nim b/src/polyworld/shadows.nim index 73e5fa66..0badaef2 100644 --- a/src/polyworld/shadows.nim +++ b/src/polyworld/shadows.nim @@ -129,6 +129,11 @@ proc compileDepthProgram(vertexSource, fragmentSource: string): GLuint = glAttachShader(result, vertexShader) glAttachShader(result, fragmentShader) glLinkProgram(result) + # The linked executable owns its code; release the compilation objects. + glDetachShader(result, vertexShader) + glDetachShader(result, fragmentShader) + glDeleteShader(vertexShader) + glDeleteShader(fragmentShader) var ok: GLint glGetProgramiv(result, GL_LINK_STATUS, ok.addr) if ok == 0: diff --git a/tests/render_shader_lifetime.nim b/tests/render_shader_lifetime.nim new file mode 100644 index 00000000..b14776fa --- /dev/null +++ b/tests/render_shader_lifetime.nim @@ -0,0 +1,51 @@ +## Run in a GL context; use -d:testSunDepth for the shadow builder. +import opengl, vmath, windy + +when defined(testSunDepth): + include ../src/polyworld/shadows +else: + include ../src/polyworld/quadterrain + +const + vertex = """#version 330 core +void main() { + vec2 vertices[3] = vec2[3](vec2(-1, -1), vec2(3, -1), vec2(-1, 3)); + gl_Position = vec4(vertices[gl_VertexID], 0, 1); +} +""" + fragment = """#version 330 core +out vec4 color; +void main() { color = vec4(1, 0, 0, 1); } +""" + +let window = newWindow("Linked shader lifetime", ivec2(32), vsync = false) +window.makeContextCurrent() +loadExtensions() +var vao: GLuint +glGenVertexArrays(1, vao.addr) +glBindVertexArray(vao) +glViewport(0, 0, 32, 32) + +for cycle in 0 ..< 3: + let program = + when defined(testSunDepth): compileDepthProgram(vertex, fragment) + else: compileProgram(vertex, fragment) + var attached, linked: GLint + glGetProgramiv(program, GL_ATTACHED_SHADERS, attached.addr) + glGetProgramiv(program, GL_LINK_STATUS, linked.addr) + doAssert attached == 0, "Compilation objects must not stay attached" + doAssert linked == 1 + glUseProgram(program) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glDrawArrays(GL_TRIANGLES, 0, 3) + var pixel: array[4, uint8] + glReadPixels(16, 16, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel[0].addr) + doAssert pixel == [255'u8, 0, 0, 255], "Linked executable must still draw" + glUseProgram(0) + glDeleteProgram(program) + doAssert glIsProgram(program) == GL_FALSE + doAssert glGetError() == GL_NO_ERROR + +glDeleteVertexArrays(1, vao.addr) +echo "Three linked programs drew exact pixels with no attached shader objects" From 8917ef53c66c3fab9b77de80bcb70311bdaba566 Mon Sep 17 00:00:00 2001 From: Richard Higgins Date: Mon, 5 Oct 2026 17:51:53 -0700 Subject: [PATCH 2/2] Keep only linked shader object cleanup --- tests/render_shader_lifetime.nim | 51 -------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 tests/render_shader_lifetime.nim diff --git a/tests/render_shader_lifetime.nim b/tests/render_shader_lifetime.nim deleted file mode 100644 index b14776fa..00000000 --- a/tests/render_shader_lifetime.nim +++ /dev/null @@ -1,51 +0,0 @@ -## Run in a GL context; use -d:testSunDepth for the shadow builder. -import opengl, vmath, windy - -when defined(testSunDepth): - include ../src/polyworld/shadows -else: - include ../src/polyworld/quadterrain - -const - vertex = """#version 330 core -void main() { - vec2 vertices[3] = vec2[3](vec2(-1, -1), vec2(3, -1), vec2(-1, 3)); - gl_Position = vec4(vertices[gl_VertexID], 0, 1); -} -""" - fragment = """#version 330 core -out vec4 color; -void main() { color = vec4(1, 0, 0, 1); } -""" - -let window = newWindow("Linked shader lifetime", ivec2(32), vsync = false) -window.makeContextCurrent() -loadExtensions() -var vao: GLuint -glGenVertexArrays(1, vao.addr) -glBindVertexArray(vao) -glViewport(0, 0, 32, 32) - -for cycle in 0 ..< 3: - let program = - when defined(testSunDepth): compileDepthProgram(vertex, fragment) - else: compileProgram(vertex, fragment) - var attached, linked: GLint - glGetProgramiv(program, GL_ATTACHED_SHADERS, attached.addr) - glGetProgramiv(program, GL_LINK_STATUS, linked.addr) - doAssert attached == 0, "Compilation objects must not stay attached" - doAssert linked == 1 - glUseProgram(program) - glClearColor(0, 0, 0, 1) - glClear(GL_COLOR_BUFFER_BIT) - glDrawArrays(GL_TRIANGLES, 0, 3) - var pixel: array[4, uint8] - glReadPixels(16, 16, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel[0].addr) - doAssert pixel == [255'u8, 0, 0, 255], "Linked executable must still draw" - glUseProgram(0) - glDeleteProgram(program) - doAssert glIsProgram(program) == GL_FALSE - doAssert glGetError() == GL_NO_ERROR - -glDeleteVertexArrays(1, vao.addr) -echo "Three linked programs drew exact pixels with no attached shader objects"