Skip to content

Commit 4a85195

Browse files
Make cleanup of OpenGL objects explicit in GLMakie (#4699)
* pull tweaks from #4689 * pull context validation from #4689 * handle texture atlas explicitly + fix errors * explicitly clean up shaders & programs * handle GLBuffer, GLVertexArray, Postprocessors * handle Texture explicitly * fix SSAO error * test cleanup * add some sanity checks * fix tests * cleanup finalizers, free(), unsafe_free() * skip GLFW initialized assert for CI * update changelog * fix freeing of unused uniforms & cleanup unused pattern_sections * count and check failed OpenGL cleanup * try to handle GLFW CI failure * try fix CI * require context for Texture and GLBuffer in most cases * fix incorrect uniform cleanup * CI please * fix typo * try handling GLFW init errors differently * maybe just destroy the window if the context dies? * fix typos * maybe also cleanup dead screens when trying to reopen? * tweak require_context to maybe catch error earlier * switch before requiring * avoid requiring context for get_texture and thus robj cleanup * treat scene finalizer * Treat scene finalizer in texture atlas too * add missing passthrough * move object deletion test to end * warn on dead context * avoid the last few current_context() calls * fix scatter indices * put finalizers behind debug constant * don't skip observable cleanup in free * cleanup require_context * fix typo * fix test errors * clean up * fix errors * fix freeing of GLBuffer Observables * cleanup unsafe_free * cleanup called_from_finalizer & add comments to implied functions * remove try .. catch in free * slightly safer + better verify_free * some small cleanups * need to switch context * catch GC bugs early and try destroy! outside * switch context, since those can be called from anywhere * switch instead of require --------- Co-authored-by: SimonDanisch <[email protected]>
1 parent 9e906ef commit 4a85195

31 files changed

+616
-332
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- Fixed gaps in corners of `poly(Rect2(...))` stroke [#4664](https://github.com/MakieOrg/Makie.jl/pull/4664)
3030
- Fixed an issue where `reinterpret`ed arrays of line points were not handled correctly in CairoMakie [#4668](https://github.com/MakieOrg/Makie.jl/pull/4668).
3131
- Fixed various issues with `markerspace = :data`, `transform_marker = true` and `rotation` for scatter in CairoMakie (incorrect marker transformations, ignored transformations, Cairo state corruption) [#4663](https://github.com/MakieOrg/Makie.jl/pull/4663)
32+
- Refactored OpenGL cleanup to run immediately rather than on GC [#4699](https://github.com/MakieOrg/Makie.jl/pull/4699)
3233

3334
## [0.21.18] - 2024-12-12
3435

Diff for: GLMakie/src/GLAbstraction/AbstractGPUArray.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ gpu_setindex!(t) = error("gpu_setindex! not implemented for: $(typeof(t)). This
190190
max_dim(t) = error("max_dim not implemented for: $(typeof(t)). This happens, when you call setindex! on an array, without implementing the GPUArray interface")
191191

192192

193-
function (::Type{GPUArrayType})(data::Observable; kw...) where GPUArrayType <: GPUArray
194-
gpu_mem = GPUArrayType(data[]; kw...)
193+
function (::Type{GPUArrayType})(context, data::Observable; kw...) where GPUArrayType <: GPUArray
194+
gpu_mem = GPUArrayType(context, data[]; kw...)
195195
# TODO merge these and handle update tracking during construction
196196
obs2 = on(new_data -> update!(gpu_mem, new_data), data)
197197
if GPUArrayType <: TextureBuffer

Diff for: GLMakie/src/GLAbstraction/GLAbstraction.jl

+28-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,33 @@ import FixedPointNumbers: N0f8, N0f16, N0f8, Normed
1818

1919
import Base: merge, resize!, similar, length, getindex, setindex!
2020

21+
# Debug tools
22+
const GLMAKIE_DEBUG = Ref(false)
23+
24+
# implemented in GLMakie/glwindow.jl
25+
function require_context_no_error(args...) end
26+
27+
function require_context(ctx, current = ShaderAbstractions.current_context())
28+
msg = require_context_no_error(ctx, current)
29+
isnothing(msg) && return nothing
30+
error(msg)
31+
end
32+
function with_context(f, context)
33+
CTX = ShaderAbstractions.ACTIVE_OPENGL_CONTEXT
34+
old_ctx = isassigned(CTX) ? CTX[] : nothing
35+
GLAbstraction.switch_context!(context)
36+
try
37+
f()
38+
finally
39+
if isnothing(old_ctx)
40+
GLAbstraction.switch_context!()
41+
else
42+
GLAbstraction.switch_context!(old_ctx)
43+
end
44+
end
45+
end
46+
export require_context, with_context
47+
2148
include("AbstractGPUArray.jl")
2249

2350
#Methods which get overloaded by GLExtendedFunctions.jl:
@@ -41,9 +68,9 @@ import ModernGL.glGetShaderiv
4168
import ModernGL.glViewport
4269
import ModernGL.glScissor
4370

71+
include("shaderabstraction.jl")
4472
include("GLUtils.jl")
4573

46-
include("shaderabstraction.jl")
4774
include("GLTypes.jl")
4875
export GLProgram # Shader/program object
4976
export Texture # Texture object, basically a 1/2/3D OpenGL data array

Diff for: GLMakie/src/GLAbstraction/GLBuffer.jl

+29-25
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1}
77
# TODO maybe also delay upload to when render happens?
88
observers::Vector{Observables.ObserverFunction}
99

10-
function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T
10+
function GLBuffer{T}(context, ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T
11+
switch_context!(context)
1112
id = glGenBuffers()
1213
glBindBuffer(buffertype, id)
1314
# size of 0 can segfault it seems
@@ -16,9 +17,9 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1}
1617
glBindBuffer(buffertype, 0)
1718

1819
obj = new(
19-
id, (buff_length,), buffertype, usage, current_context(),
20+
id, (buff_length,), buffertype, usage, context,
2021
Observables.ObserverFunction[])
21-
finalizer(free, obj)
22+
GLMAKIE_DEBUG[] && finalizer(verify_free, obj)
2223
obj
2324
end
2425
end
@@ -34,35 +35,35 @@ end
3435
bind(buffer::GLBuffer, other_target) = glBindBuffer(buffer.buffertype, other_target)
3536

3637
function similar(x::GLBuffer{T}, buff_length::Int) where T
37-
GLBuffer{T}(Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage)
38+
return GLBuffer{T}(x.context, Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage)
3839
end
3940

4041
cardinality(::GLBuffer{T}) where {T} = cardinality(T)
4142

4243
#Function to deal with any Immutable type with Real as Subtype
4344
function GLBuffer(
44-
buffer::Union{Base.ReinterpretArray{T, 1}, DenseVector{T}};
45+
context, buffer::Union{Base.ReinterpretArray{T, 1}, DenseVector{T}};
4546
buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW
4647
) where T <: GLArrayEltypes
4748
GC.@preserve buffer begin
48-
return GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage)
49+
return GLBuffer{T}(context, pointer(buffer), length(buffer), buffertype, usage)
4950
end
5051
end
5152

5253
function GLBuffer(
53-
buffer::DenseVector{T};
54+
context, buffer::DenseVector{T};
5455
buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW
5556
) where T <: GLArrayEltypes
5657
GC.@preserve buffer begin
57-
return GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage)
58+
return GLBuffer{T}(context, pointer(buffer), length(buffer), buffertype, usage)
5859
end
5960
end
6061

6162
function GLBuffer(
62-
buffer::ShaderAbstractions.Buffer{T};
63+
context, buffer::ShaderAbstractions.Buffer{T};
6364
buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW
6465
) where T <: GLArrayEltypes
65-
b = GLBuffer(ShaderAbstractions.data(buffer); buffertype=buffertype, usage=usage)
66+
b = GLBuffer(context, ShaderAbstractions.data(buffer); buffertype=buffertype, usage=usage)
6667
au = ShaderAbstractions.updater(buffer)
6768
obsfunc = on(au.update) do (f, args)
6869
f(b, args...) # forward setindex! etc
@@ -76,36 +77,30 @@ end
7677
GLBuffer(buffer::GLBuffer) = buffer
7778
GLBuffer{T}(buffer::GLBuffer{T}) where {T} = buffer
7879

79-
function GLBuffer(
80-
buffer::AbstractVector{T};
81-
kw_args...
82-
) where T <: GLArrayEltypes
83-
GLBuffer(collect(buffer); kw_args...)
80+
function GLBuffer(context, buffer::AbstractVector{T}; kw_args...) where T <: GLArrayEltypes
81+
return GLBuffer(context, collect(buffer); kw_args...)
8482
end
8583

86-
function GLBuffer{T}(
87-
buffer::AbstractVector;
88-
kw_args...
89-
) where T <: GLArrayEltypes
90-
GLBuffer(convert(Vector{T}, buffer); kw_args...)
84+
function GLBuffer{T}(context, buffer::AbstractVector; kw_args...) where T <: GLArrayEltypes
85+
return GLBuffer(context, convert(Vector{T}, buffer); kw_args...)
9186
end
9287

9388
function GLBuffer(
94-
::Type{T}, len::Int;
89+
context, ::Type{T}, len::Int;
9590
buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW
9691
) where T <: GLArrayEltypes
97-
GLBuffer{T}(Ptr{T}(C_NULL), len, buffertype, usage)
92+
return GLBuffer{T}(context, Ptr{T}(C_NULL), len, buffertype, usage)
9893
end
9994

10095

10196
function indexbuffer(
102-
buffer::VectorTypes{T};
103-
usage::GLenum = GL_STATIC_DRAW
97+
context, buffer::VectorTypes{T}; usage::GLenum = GL_STATIC_DRAW
10498
) where T <: GLArrayEltypes
105-
GLBuffer(buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage)
99+
return GLBuffer(context, buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage)
106100
end
107101
# GPUArray interface
108102
function gpu_data(b::GLBuffer{T}) where T
103+
switch_context!(b.context)
109104
data = Vector{T}(undef, length(b))
110105
bind(b)
111106
glGetBufferSubData(b.buffertype, 0, sizeof(data), data)
@@ -116,6 +111,7 @@ end
116111

117112
# Resize buffer
118113
function gpu_resize!(buffer::GLBuffer{T}, newdims::NTuple{1, Int}) where T
114+
switch_context!(buffer.context)
119115
#TODO make this safe!
120116
newlength = newdims[1]
121117
oldlen = length(buffer)
@@ -139,13 +135,15 @@ function gpu_resize!(buffer::GLBuffer{T}, newdims::NTuple{1, Int}) where T
139135
end
140136

141137
function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::Integer) where T
138+
switch_context!(b.context)
142139
multiplicator = sizeof(T)
143140
bind(b)
144141
glBufferSubData(b.buffertype, multiplicator*(offset-1), sizeof(value), value)
145142
bind(b, 0)
146143
end
147144

148145
function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::UnitRange{Int}) where T
146+
switch_context!(b.context)
149147
multiplicator = sizeof(T)
150148
bind(b)
151149
glBufferSubData(b.buffertype, multiplicator*(first(offset)-1), sizeof(value), value)
@@ -156,6 +154,8 @@ end
156154
# copy between two buffers
157155
# could be a setindex! operation, with subarrays for buffers
158156
function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T
157+
switch_context!(a.context)
158+
@assert a.context == b.context
159159
multiplicator = sizeof(T)
160160
@assert a.id != 0 & b.id != 0
161161
glBindBuffer(GL_COPY_READ_BUFFER, a.id)
@@ -178,6 +178,7 @@ end
178178

179179
#copy inside one buffer
180180
function unsafe_copy!(buffer::GLBuffer{T}, readoffset::Int, writeoffset::Int, len::Int) where T
181+
switch_context!(buffer.context)
181182
len <= 0 && return nothing
182183
bind(buffer)
183184
ptr = Ptr{T}(glMapBuffer(buffer.buffertype, GL_READ_WRITE))
@@ -190,6 +191,7 @@ function unsafe_copy!(buffer::GLBuffer{T}, readoffset::Int, writeoffset::Int, le
190191
end
191192

192193
function unsafe_copy!(a::Vector{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T
194+
switch_context!(b.context)
193195
bind(b)
194196
ptr = Ptr{T}(glMapBuffer(b.buffertype, GL_WRITE_ONLY))
195197
for i=1:len
@@ -200,6 +202,7 @@ function unsafe_copy!(a::Vector{T}, readoffset::Int, b::GLBuffer{T}, writeoffset
200202
end
201203

202204
function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset::Int, len::Int) where T
205+
switch_context!(a.context)
203206
bind(a)
204207
ptr = Ptr{T}(glMapBuffer(a.buffertype, GL_READ_ONLY))
205208
for i in 1:len
@@ -210,6 +213,7 @@ function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset
210213
end
211214

212215
function gpu_getindex(b::GLBuffer{T}, range::UnitRange) where T
216+
switch_context!(b.context)
213217
multiplicator = sizeof(T)
214218
offset = first(range)-1
215219
value = Vector{T}(undef, length(range))

Diff for: GLMakie/src/GLAbstraction/GLExtendedFunctions.jl

+3-6
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,15 @@ function glGenFramebuffers()
145145
end
146146

147147
function glDeleteTextures(id::GLuint)
148-
arr = [id]
149-
glDeleteTextures(1, arr)
148+
glDeleteTextures(1, Ref(id))
150149
end
151150

152151
function glDeleteVertexArrays(id::GLuint)
153-
arr = [id]
154-
glDeleteVertexArrays(1, arr)
152+
glDeleteVertexArrays(1, Ref(id))
155153
end
156154

157155
function glDeleteBuffers(id::GLuint)
158-
arr = [id]
159-
glDeleteBuffers(1, arr)
156+
glDeleteBuffers(1, Ref(id))
160157
end
161158

162159
function glGetTexLevelParameteriv(target::GLenum, level, name::GLenum)

Diff for: GLMakie/src/GLAbstraction/GLInfo.jl

+8
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,11 @@ function getProgramInfo(p::GLProgram)
9797
@show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE)
9898
@show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS)
9999
end
100+
101+
const FAILED_FREE_COUNTER = Threads.Atomic{Int}(0)
102+
function verify_free(obj::T, name = T) where T
103+
if obj.id != 0
104+
FAILED_FREE_COUNTER[] += 1
105+
Core.println(Core.stderr, "Error: ", name, " has not been freed.")
106+
end
107+
end

Diff for: GLMakie/src/GLAbstraction/GLRender.jl

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function setup_clip_planes(N::Integer)
1414
end
1515
end
1616

17+
# Note: context required in renderloop, not per renderobject here
1718

1819
"""
1920
When rendering a specialised list of Renderables, we can do some optimizations

Diff for: GLMakie/src/GLAbstraction/GLShader.jl

+25-10
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ function ShaderCache(context)
103103
)
104104
end
105105

106+
function free(cache::ShaderCache)
107+
with_context(cache.context) do
108+
for (k, v) in cache.shader_cache
109+
for (k2, shader) in v
110+
free(shader)
111+
end
112+
end
113+
for program in values(cache.program_cache)
114+
free(program)
115+
end
116+
end
117+
return
118+
end
119+
106120
abstract type AbstractLazyShader end
107121

108122
struct LazyShader <: AbstractLazyShader
@@ -116,9 +130,9 @@ struct LazyShader <: AbstractLazyShader
116130
end
117131
end
118132

119-
gl_convert(shader::GLProgram, data) = shader
133+
gl_convert(::GLContext, shader::GLProgram, data) = shader
120134

121-
function compile_shader(source::ShaderSource, template_src::String)
135+
function compile_shader(context, source::ShaderSource, template_src::String)
122136
name = source.name
123137
shaderid = createshader(source.typ)
124138
glShaderSource(shaderid, template_src)
@@ -127,7 +141,7 @@ function compile_shader(source::ShaderSource, template_src::String)
127141
GLAbstraction.print_with_lines(template_src)
128142
@warn("shader $(name) didn't compile. \n$(GLAbstraction.getinfolog(shaderid))")
129143
end
130-
return Shader(name, Vector{UInt8}(template_src), source.typ, shaderid)
144+
return Shader(context, name, Vector{UInt8}(template_src), source.typ, shaderid)
131145
end
132146

133147

@@ -137,14 +151,14 @@ function get_shader!(cache::ShaderCache, src::ShaderSource, template_replacement
137151
return get!(shader_dict, template_replacement) do
138152
templated_source = mustache_replace(template_replacement, src.source)
139153
ShaderAbstractions.switch_context!(cache.context)
140-
return compile_shader(src, templated_source)
154+
return compile_shader(cache.context, src, templated_source)
141155
end::Shader
142156
end
143157

144158
function get_template!(cache::ShaderCache, src::ShaderSource, view, attributes)
145159
return get!(cache.template_cache, src.name) do
146160
templated_source, replacements = template2source(src.source, view, attributes)
147-
shader = compile_shader(src, templated_source)
161+
shader = compile_shader(cache.context, src, templated_source)
148162
template_keys = collect(keys(replacements))
149163
template_replacements = collect(values(replacements))
150164
# can't yet be in here, since we didn't even have template keys
@@ -179,7 +193,7 @@ function compile_program(shaders::Vector{Shader}, fragdatalocation)
179193
# generate the link locations
180194
nametypedict = uniform_name_type(program)
181195
uniformlocationdict = uniformlocations(nametypedict, program)
182-
GLProgram(program, shaders, nametypedict, uniformlocationdict)
196+
return GLProgram(program, shaders, nametypedict, uniformlocationdict)
183197
end
184198

185199
function get_view(kw_dict)
@@ -190,12 +204,13 @@ function get_view(kw_dict)
190204
_view
191205
end
192206

193-
gl_convert(lazyshader::AbstractLazyShader, data) = error("gl_convert shader")
194-
function gl_convert(lazyshader::LazyShader, data)
195-
gl_convert(lazyshader.shader_cache, lazyshader, data)
207+
gl_convert(::GLContext, lazyshader::AbstractLazyShader, data) = error("gl_convert shader")
208+
function gl_convert(ctx::GLContext, lazyshader::LazyShader, data)
209+
gl_convert(ctx, lazyshader.shader_cache, lazyshader, data)
196210
end
197211

198-
function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data)
212+
function gl_convert(ctx::GLContext, cache::ShaderCache, lazyshader::AbstractLazyShader, data)
213+
require_context(cache.context, ctx)
199214
kw_dict = lazyshader.kw_args
200215
paths = lazyshader.paths
201216

0 commit comments

Comments
 (0)