From 5b99033d29cf4ece071c3417ca14f393538b1878 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 15:06:19 +0100 Subject: [PATCH 001/135] pull tweaks from #4689 --- GLMakie/src/GLAbstraction/GLShader.jl | 8 ++++---- GLMakie/src/GLAbstraction/GLTypes.jl | 7 ++++--- GLMakie/src/glshaders/visualize_interface.jl | 2 +- GLMakie/src/glwindow.jl | 4 +++- GLMakie/src/postprocessing.jl | 15 +++++++++------ GLMakie/src/screen.jl | 10 ++++------ 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 8e8ab99061c..47561650b4b 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -118,7 +118,7 @@ end gl_convert(shader::GLProgram, data) = shader -function compile_shader(source::ShaderSource, template_src::String) +function compile_shader(context, source::ShaderSource, template_src::String) name = source.name shaderid = createshader(source.typ) glShaderSource(shaderid, template_src) @@ -127,7 +127,7 @@ function compile_shader(source::ShaderSource, template_src::String) GLAbstraction.print_with_lines(template_src) @warn("shader $(name) didn't compile. \n$(GLAbstraction.getinfolog(shaderid))") end - return Shader(name, Vector{UInt8}(template_src), source.typ, shaderid) + return Shader(name, Vector{UInt8}(template_src), source.typ, shaderid, context) end @@ -137,14 +137,14 @@ function get_shader!(cache::ShaderCache, src::ShaderSource, template_replacement return get!(shader_dict, template_replacement) do templated_source = mustache_replace(template_replacement, src.source) ShaderAbstractions.switch_context!(cache.context) - return compile_shader(src, templated_source) + return compile_shader(cache.context, src, templated_source) end::Shader end function get_template!(cache::ShaderCache, src::ShaderSource, view, attributes) return get!(cache.template_cache, src.name) do templated_source, replacements = template2source(src.source, view, attributes) - shader = compile_shader(src, templated_source) + shader = compile_shader(cache.context, src, templated_source) template_keys = collect(keys(replacements)) template_replacements = collect(values(replacements)) # can't yet be in here, since we didn't even have template keys diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 2d661b87835..f52e412b825 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -27,9 +27,10 @@ struct Shader typ::GLenum id::GLuint context::GLContext - function Shader(name, source, typ, id) - new(Symbol(name), source, typ, id, current_context()) - end +end + +function Shader(name, source, typ, id) + new(Symbol(name), source, typ, id, current_context()) end function Shader(name, source::Vector{UInt8}, typ) diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index d9236e4f61c..d861120bb59 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -105,7 +105,7 @@ function assemble_shader(data) GLAbstraction.StandardPrerender(transp, overdraw) end - robj = RenderObject(data, shader, pre, shader.screen.glscreen) + robj = RenderObject(data, shader, pre, nothing, shader.screen.glscreen) post = if haskey(data, :instances) GLAbstraction.StandardPostrenderInstanced(pop!(data, :instances), robj.vertexarray, primitive) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index c40d6a23f93..5e2db4ba83e 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -86,7 +86,9 @@ function check_framebuffer() return enum_to_error(status) end -Makie.@noconstprop function GLFramebuffer(fb_size::NTuple{2, Int}) +Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) + ShaderAbstractions.switch_context!(context) + # Create framebuffer frambuffer_id = glGenFramebuffers() glBindFramebuffer(GL_FRAMEBUFFER, frambuffer_id) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 0195b040ae3..b1708155c52 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -57,7 +57,7 @@ function OIT_postprocessor(framebuffer, shader_cache) # opaque.a = 0 * src.a + 1 * opaque.a glBlendFuncSeparate(GL_ONE, GL_SRC_ALPHA, GL_ZERO, GL_ONE) end, - nothing + nothing, shader_cache.context ) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) @@ -79,6 +79,7 @@ end function ssao_postprocessor(framebuffer, shader_cache) + ShaderAbstractions.switch_context!(shader_cache.context) # Add missing buffers if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) @@ -133,7 +134,7 @@ function ssao_postprocessor(framebuffer, shader_cache) :bias => 0.025f0, :radius => 0.5f0 ) - pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing) + pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) @@ -150,7 +151,7 @@ function ssao_postprocessor(framebuffer, shader_cache) :inv_texel_size => lift(rcpframe, framebuffer.resolution), :blur_range => Int32(2) ) - pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing) + pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) color_id = framebuffer[:color][1] @@ -203,6 +204,8 @@ end Returns a PostProcessor that handles fxaa. """ function fxaa_postprocessor(framebuffer, shader_cache) + ShaderAbstractions.switch_context!(shader_cache.context) + # Add missing buffers if !haskey(framebuffer, :color_luma) if !haskey(framebuffer, :HDR_color) @@ -226,7 +229,7 @@ function fxaa_postprocessor(framebuffer, shader_cache) :color_texture => framebuffer[:color][2], :object_ids => framebuffer[:objectid][2] ) - pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing) + pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) # perform FXAA @@ -239,7 +242,7 @@ function fxaa_postprocessor(framebuffer, shader_cache) :color_texture => getfallback(framebuffer, :color_luma, :HDR_color)[2], :RCPFrame => lift(rcpframe, framebuffer.resolution), ) - pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing) + pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) color_id = framebuffer[:color][1] @@ -282,7 +285,7 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi data = Dict{Symbol, Any}( :color_texture => framebuffer[:color][2] ) - pass = RenderObject(data, shader, PostprocessPrerender(), nothing) + pass = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) full_render = screen -> begin diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 5d2012b1187..d2465cbcae5 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -280,7 +280,7 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) # This is important for resource tracking, and only needed for the first context ShaderAbstractions.switch_context!(window) shader_cache = GLAbstraction.ShaderCache(window) - fb = GLFramebuffer(initial_resolution) + fb = GLFramebuffer(window, initial_resolution) postprocessors = [ empty_postprocessor(), empty_postprocessor(), @@ -642,10 +642,7 @@ end function destroy!(screen::Screen) @debug("Destroy screen!") close(screen; reuse=false) - # wait for rendertask to finish - # otherwise, during rendertask clean up we may run into a destroyed window - wait(screen) - screen.rendertask = nothing + @assert screen.rendertask === nothing window = screen.glscreen GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) @@ -671,8 +668,9 @@ function Base.close(screen::Screen; reuse=true) if screen.window_open[] # otherwise we trigger an infinite loop of closing screen.window_open[] = false end - empty!(screen) + stop_renderloop!(screen; close_after_renderloop=false) + empty!(screen) if reuse && screen.reuse @debug("reusing screen!") From fed962bd34943ae8b74fcb89ba8083a72c3d05d0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 15:08:41 +0100 Subject: [PATCH 002/135] pull context validation from #4689 --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 4 ++++ GLMakie/src/GLAbstraction/GLRender.jl | 4 ++++ GLMakie/src/glwindow.jl | 9 +++++++++ GLMakie/src/postprocessing.jl | 8 ++++++++ GLMakie/src/rendering.jl | 4 ++++ 5 files changed, 29 insertions(+) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 67c6843ed10..a33ac3a9b7b 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -18,6 +18,10 @@ import FixedPointNumbers: N0f8, N0f16, N0f8, Normed import Base: merge, resize!, similar, length, getindex, setindex! +# Debug tools +require_context() = nothing # implemented in GLMakie/glwindow +export require_context + include("AbstractGPUArray.jl") #Methods which get overloaded by GLExtendedFunctions.jl: diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index 44bb5b5a59b..9131df2053d 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -21,6 +21,7 @@ When rendering a specialised list of Renderables, we can do some optimizations function render(list::Vector{RenderObject{Pre}}) where Pre isempty(list) && return nothing first(list).prerenderfunction() + require_context(first(list).context) vertexarray = first(list).vertexarray program = vertexarray.program glUseProgram(program.id) @@ -50,6 +51,7 @@ function render(list::Vector{RenderObject{Pre}}) where Pre end end renderobject.postrenderfunction() + require_context(renderobject.context) end # we need to assume, that we're done here, which is why # we need to bind VertexArray to 0. @@ -68,6 +70,7 @@ a lot of objects. """ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray) if renderobject.visible + require_context(renderobject.context) renderobject.prerenderfunction() setup_clip_planes(to_value(get(renderobject.uniforms, :num_clip_planes, 0))) program = vertexarray.program @@ -91,6 +94,7 @@ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray bind(vertexarray) renderobject.postrenderfunction() glBindVertexArray(0) + require_context(renderobject.context) end return end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 5e2db4ba83e..97b317f556b 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -148,6 +148,8 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) :stencil => depth_buffer ) + require_context(context) + return GLFramebuffer( fb_size_node, frambuffer_id, buffer_ids, buffers, @@ -206,6 +208,13 @@ function ShaderAbstractions.native_context_alive(x::GLFW.Window) GLFW.is_initialized() && !was_destroyed(x) end +# require_context(ctx, current = nothing) = nothing +function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context()) + @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." + @assert !was_destroyed(ctx) "Context $ctx must not be destroyed." + @assert ctx.handle == current.handle "Context $ctx must be current, but $current is." +end + function destroy!(nw::GLFW.Window) was_current = ShaderAbstractions.is_current_context(nw) if !was_destroyed(nw) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index b1708155c52..8a94495f098 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -102,6 +102,8 @@ function ssao_postprocessor(framebuffer, shader_cache) push!(framebuffer.render_buffer_ids, normal_occ_id) end + require_context(shader_cache.context) + # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -195,6 +197,8 @@ function ssao_postprocessor(framebuffer, shader_cache) glDisable(GL_SCISSOR_TEST) end + require_context(shader_cache.context) + PostProcessor(RenderObject[pass1, pass2], full_render, ssao_postprocessor) end @@ -219,6 +223,8 @@ function fxaa_postprocessor(framebuffer, shader_cache) end end + require_context(shader_cache.context) + # calculate luma for FXAA shader1 = LazyShader( shader_cache, @@ -263,6 +269,8 @@ function fxaa_postprocessor(framebuffer, shader_cache) GLAbstraction.render(pass2) end + require_context(shader_cache.context) + PostProcessor(RenderObject[pass1, pass2], full_render, fxaa_postprocessor) end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 4e6f9f65caf..2e514cf1de2 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -101,6 +101,8 @@ function render_frame(screen::Screen; resize_buffers=true) # transfer everything to the screen screen.postprocessors[4].render(screen) + GLAbstraction.require_context(nw) + return end @@ -115,6 +117,7 @@ end function GLAbstraction.render(filter_elem_func, screen::Screen) # Somehow errors in here get ignored silently!? try + GLAbstraction.require_context(nw) for (zindex, screenid, elem) in screen.renderlist filter_elem_func(elem)::Bool || continue @@ -126,6 +129,7 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) render(elem) end + GLAbstraction.require_context(nw) catch e @error "Error while rendering!" exception = e rethrow(e) From 442b271f10e7c9555c8e128c0b60ffb29b2d14dd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 15:58:34 +0100 Subject: [PATCH 003/135] handle texture atlas explicitly + fix errors --- GLMakie/src/GLAbstraction/GLTypes.jl | 18 ++++++++++++++---- GLMakie/src/drawing_primitives.jl | 8 ++++---- GLMakie/src/gl_backend.jl | 19 ++++++++++++++----- GLMakie/src/rendering.jl | 4 ++-- GLMakie/src/screen.jl | 3 ++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index f52e412b825..170e5fa1dca 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -21,16 +21,18 @@ cardinality(x) = length(x) cardinality(x::Number) = 1 cardinality(x::Type{T}) where {T <: Number} = 1 -struct Shader +mutable struct Shader name::Symbol source::Vector{UInt8} typ::GLenum id::GLuint context::GLContext -end -function Shader(name, source, typ, id) - new(Symbol(name), source, typ, id, current_context()) + function Shader(name, source, typ, id, context = current_context()) + obj = new(Symbol(name), source, typ, id, context) + finalizer(free, obj) + return obj + end end function Shader(name, source::Vector{UInt8}, typ) @@ -456,6 +458,14 @@ function unsafe_free(x::GLProgram) return end +function unsafe_free(x::Shader) + x.id == 0 && return + GLAbstraction.context_alive(x.context) || return + GLAbstraction.switch_context!(x.context) + glDeleteShader(x.id) + return +end + function unsafe_free(x::GLBuffer) # don't free if already freed x.id == 0 && return diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 3a0d8a58f44..adcb7d42fe6 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -431,7 +431,7 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca gl_attributes[:shape] = shape get!(gl_attributes, :distancefield) do if shape[] === Cint(DISTANCEFIELD) - return get_texture!(atlas) + return get_texture!(screen.glscreen, atlas) else return nothing end @@ -642,7 +642,7 @@ function draw_atomic(screen::Screen, scene::Scene, gl_attributes[:quad_offset] = quad_offset gl_attributes[:marker_offset] = char_offset gl_attributes[:uv_offset_width] = uv_offset_width - gl_attributes[:distancefield] = get_texture!(atlas) + gl_attributes[:distancefield] = get_texture!(screen.glscreen, atlas) gl_attributes[:visible] = plot.visible gl_attributes[:fxaa] = get(plot, :fxaa, Observable(false)) gl_attributes[:depthsorting] = get(plot, :depthsorting, false) @@ -740,7 +740,7 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= color = pop!(gl_attributes, :color) interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = interp ? :linear : :nearest - + if to_value(color) isa Colorant gl_attributes[:vertex_color] = color delete!(gl_attributes, :color_map) @@ -760,7 +760,7 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= elseif to_value(color) isa ShaderAbstractions.Sampler gl_attributes[:image] = Texture(lift(el32convert, plot, color)) delete!(gl_attributes, :color_map) - delete!(gl_attributes, :color_norm) + delete!(gl_attributes, :color_norm) elseif to_value(color) isa AbstractMatrix{<:Colorant} gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index ee4bffe3402..f06a9e48bf4 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -16,23 +16,32 @@ using .GLAbstraction const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}() -function get_texture!(atlas::Makie.TextureAtlas) - current_ctx = GLAbstraction.current_context() - if !GLAbstraction.context_alive(current_ctx) - return nothing +function cleanup_texture_atlas!(context) + to_delete = filter(atlas_ctx -> atlas_ctx[2] == context, keys(atlas_texture_cache)) + require_context(context) + for (atlas, ctx) in to_delete + tex, func = pop!(atlas_texture_cache, (atlas, ctx)) + Makie.remove_font_render_callback!(atlas, func) + GLAbstraction.free(tex) end + return +end + +function get_texture!(context, atlas::Makie.TextureAtlas) + require_context(context) # clean up dead context! filter!(atlas_texture_cache) do ((ptr, ctx), tex_func) if GLAbstraction.context_alive(ctx) return true else + @error("Cached atlas textures should be removed explicitly! $ctx") Makie.remove_font_render_callback!(atlas, tex_func[2]) return false end end - tex, func = get!(atlas_texture_cache, (pointer(atlas.data), current_ctx)) do + tex, func = get!(atlas_texture_cache, (atlas, context)) do tex = Texture( atlas.data, minfilter = :linear, diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 2e514cf1de2..fd6d0fe9d8c 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -117,7 +117,7 @@ end function GLAbstraction.render(filter_elem_func, screen::Screen) # Somehow errors in here get ignored silently!? try - GLAbstraction.require_context(nw) + GLAbstraction.require_context(screen.glscreen) for (zindex, screenid, elem) in screen.renderlist filter_elem_func(elem)::Bool || continue @@ -129,7 +129,7 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) render(elem) end - GLAbstraction.require_context(nw) + GLAbstraction.require_context(screen.glscreen) catch e @error "Error while rendering!" exception = e rethrow(e) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index d2465cbcae5..f86bc922a15 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -569,7 +569,7 @@ function destroy!(rob::RenderObject) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. GLAbstraction.switch_context!(rob.context) - tex = get_texture!(gl_texture_atlas()) + tex = get_texture!(rob.context, gl_texture_atlas()) for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) @@ -646,6 +646,7 @@ function destroy!(screen::Screen) window = screen.glscreen GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) + cleanup_texture_atlas!(window) destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) From 1310186444975b7f199e9eaa72ddad3cff93aa63 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 16:24:36 +0100 Subject: [PATCH 004/135] explicitly clean up shaders & programs --- GLMakie/src/GLAbstraction/GLShader.jl | 14 ++++++++++++++ GLMakie/src/GLAbstraction/GLTypes.jl | 10 ++++++++++ GLMakie/src/screen.jl | 5 +++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 47561650b4b..ba38dc5954a 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -103,6 +103,20 @@ function ShaderCache(context) ) end +function unsafe_free(cache::ShaderCache) + require_context(cache.context) + for (k, v) in cache.shader_cache + for (k2, shader) in v + unsafe_free(shader) + end + end + for program in values(cache.program_cache) + unsafe_free(program) + end + require_context(cache.context) + return +end + abstract type AbstractLazyShader end struct LazyShader <: AbstractLazyShader diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 170e5fa1dca..d446febb43d 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -455,6 +455,7 @@ function unsafe_free(x::GLProgram) GLAbstraction.context_alive(x.context) || return GLAbstraction.switch_context!(x.context) glDeleteProgram(x.id) + x.id = 0 return end @@ -463,6 +464,7 @@ function unsafe_free(x::Shader) GLAbstraction.context_alive(x.context) || return GLAbstraction.switch_context!(x.context) glDeleteShader(x.id) + x.id = 0 return end @@ -505,3 +507,11 @@ function unsafe_free(x::GLVertexArray) x.id = 0 return end + +function free(x::Shader) + @assert x.id == 0 "Must be freed explicitly" +end + +function free(x::GLProgram) + @assert x.id == 0 "Must be freed explicitly" +end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index f86bc922a15..72205a820ea 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -580,13 +580,13 @@ function destroy!(rob::RenderObject) # TODO, refcounting, or leaving freeing to GC... # GC is a bit tricky with active contexts, so immediate free is preferred. # I guess as long as we make it hard for users to share buffers directly, this should be fine! - GLAbstraction.free(v) + GLAbstraction.unsafe_free(v) end end for obs in rob.observables Observables.clear(obs) end - GLAbstraction.free(rob.vertexarray) + GLAbstraction.unsafe_free(rob.vertexarray) end function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) @@ -647,6 +647,7 @@ function destroy!(screen::Screen) GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) cleanup_texture_atlas!(window) + GLAbstraction.unsafe_free(screen.shader_cache) destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) From fb4bf071ea4d0c514e5c278d52c81663002510a3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 16:25:21 +0100 Subject: [PATCH 005/135] handle GLBuffer, GLVertexArray, Postprocessors --- GLMakie/src/GLAbstraction/GLTypes.jl | 12 ++++++++++-- GLMakie/src/screen.jl | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index d446febb43d..4f5527d0c58 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -497,10 +497,10 @@ function unsafe_free(x::GLVertexArray) GLAbstraction.context_alive(x.context) || return GLAbstraction.switch_context!(x.context) for (key, buffer) in x.buffers - free(buffer) + unsafe_free(buffer) end if x.indices isa GPUArray - free(x.indices) + unsafe_free(x.indices) end id = Ref(x.id) glDeleteVertexArrays(1, id) @@ -515,3 +515,11 @@ end function free(x::GLProgram) @assert x.id == 0 "Must be freed explicitly" end + +function free(x::GLBuffer) + @assert x.id == 0 "Must be freed explicitly" +end + +function free(x::GLVertexArray) + @assert x.id == 0 "Must be freed explicitly" +end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 72205a820ea..64461f14669 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -646,6 +646,7 @@ function destroy!(screen::Screen) window = screen.glscreen GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) + foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates cleanup_texture_atlas!(window) GLAbstraction.unsafe_free(screen.shader_cache) destroy!(window) From e8bf0fedfd889c01aaad29a9fc51b446b9654a7c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 16:46:55 +0100 Subject: [PATCH 006/135] handle Texture explicitly --- GLMakie/src/GLAbstraction/GLTypes.jl | 4 ++++ GLMakie/src/gl_backend.jl | 2 +- GLMakie/src/glwindow.jl | 10 ++++++++++ GLMakie/src/precompiles.jl | 1 - GLMakie/src/screen.jl | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 4f5527d0c58..3c430865fa2 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -523,3 +523,7 @@ end function free(x::GLVertexArray) @assert x.id == 0 "Must be freed explicitly" end + +function free(x::Texture) + @assert x.id == 0 "Must be freed explicitly" +end diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index f06a9e48bf4..9958d409273 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -22,7 +22,7 @@ function cleanup_texture_atlas!(context) for (atlas, ctx) in to_delete tex, func = pop!(atlas_texture_cache, (atlas, ctx)) Makie.remove_font_render_callback!(atlas, func) - GLAbstraction.free(tex) + GLAbstraction.unsafe_free(tex) end return end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 97b317f556b..971935834b1 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -157,6 +157,16 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) )::GLFramebuffer end +function destroy!(fb::GLFramebuffer) + for tex in values(fb.buffers) + GLAbstraction.unsafe_free(tex) + end + id = [fb.id] + glDeleteFramebuffers(1, id) + fb.id = 0 + return +end + function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) (w > 0 && h > 0 && (w, h) != size(fb)) || return for (name, buffer) in fb.buffers diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 32072bce182..56770ad1164 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -49,7 +49,6 @@ let screen = Screen(Scene(), config; visible=false, start_renderloop=false) close(screen) - empty!(atlas_texture_cache) closeall(; empty_shader=false) @assert isempty(SCREEN_REUSE_POOL) @assert isempty(ALL_SCREENS) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 64461f14669..94656cf9a05 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -647,6 +647,7 @@ function destroy!(screen::Screen) GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates + destroy!(screen.framebuffer) cleanup_texture_atlas!(window) GLAbstraction.unsafe_free(screen.shader_cache) destroy!(window) From d12c511de8133148de1a5b8752a694b73a144fcf Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 17:09:58 +0100 Subject: [PATCH 007/135] fix SSAO error --- GLMakie/src/postprocessing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 8a94495f098..26ea675de57 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -177,7 +177,7 @@ function ssao_postprocessor(framebuffer, shader_cache) a = viewport(scene)[] glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms - data1[:projection] = scene.camera.projection[] + data1[:projection] = Mat4f(scene.camera.projection[]) data1[:bias] = scene.ssao.bias[] data1[:radius] = scene.ssao.radius[] GLAbstraction.render(pass1) From affbfa9ddfe10ae35c6b1bc7251da8a85bcf000b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 17:10:08 +0100 Subject: [PATCH 008/135] test cleanup --- GLMakie/src/postprocessing.jl | 4 +- GLMakie/test/unit_tests.jl | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 26ea675de57..7764cb0faec 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -310,8 +310,6 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi end function destroy!(pp::PostProcessor) - while !isempty(pp.robjs) - destroy!(pop!(pp.robjs)) - end + foreach(destroy!, pp.robjs) return end diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index c14d1ba7504..fa16ea0cd5e 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -486,3 +486,81 @@ end @test robj.uniforms[:resolution][] == screen.px_per_unit[] * cam.resolution[] @test robj.uniforms[:projectionview][] == cam.projectionview[] end + +@testset "gl Object deletion" begin + # image so we have a user generated texture in the mix + # texture atlas is triggered by text + # include SSAO to make sure its cleanup works too + f,a,p = image(rand(4,4)) + screen = display(f, ssao = true, visible = false) + colorbuffer(screen) + + # verify that SSAO is active + @test screen.postprocessors[1] != GLMakie.empty_postprocessor() + + framebuffer = screen.framebuffer + framebuffer_textures = copy(screen.framebuffer.buffers) + atlas_textures = first.(values(GLMakie.atlas_texture_cache)) + shaders = vcat([[shader for shader in values(shaders)] for shaders in values(screen.shader_cache.shader_cache)]...) + programs = [program for program in values(screen.shader_cache.program_cache)] + postprocessors = copy(screen.postprocessors) + robjs = last.(screen.renderlist) + + GLMakie.destroy!(screen) + + @testset "Texture Atlas" begin + for tex in atlas_textures + @test tex.id == 0 + end + end + + @testset "Framebuffer" begin + @test framebuffer.id == 0 + for (k, tex) in framebuffer_textures + @test tex.id == 0 + end + end + + @testset "ShaderCache" begin + for shader in shaders + @test shader.id == 0 + end + for program in programs + @test program.id == 0 + end + end + + function validate_robj(robj) + for uniform in robj.uniforms + if to_value(uniform) isa GLMakie.GLAbstraction.GPUArray + @test to_value(uniform).id == 0 + end + end + @test robj.vertexarray.id == 0 + if robj.vertexarray.indices isa GLMakie.GLAbstraction.GPUArray + @test robj.vertexarray.indices.id == 0 + end + for buffer in values(robj.vertexarray.buffers) + @test buffer.id == 0 + end + @test robj.vertexarray.program.id == 0 + for shader in robj.vertexarray.program.shader + @test shader.id == 0 + end + end + + @testset "PostProcessors" begin + @test map(pp -> length(pp.robjs), postprocessors) == [2,1,2,1] + for pp in postprocessors + for robj in pp.robjs + validate_robj(robj) + end + end + end + + @testset "RenderObjects" begin + for robj in robjs + validate_robj(robj) + end + end +end \ No newline at end of file From 18fb15ffa330ca885a204c075e3d712fb57ed81c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 17:23:48 +0100 Subject: [PATCH 009/135] add some sanity checks --- GLMakie/test/unit_tests.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index fa16ea0cd5e..0e775c7dc83 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -488,6 +488,8 @@ end end @testset "gl Object deletion" begin + GLMakie.closeall() + # image so we have a user generated texture in the mix # texture atlas is triggered by text # include SSAO to make sure its cleanup works too @@ -509,6 +511,7 @@ end GLMakie.destroy!(screen) @testset "Texture Atlas" begin + @test !isempty(atlas_textures) for tex in atlas_textures @test tex.id == 0 end @@ -516,15 +519,18 @@ end @testset "Framebuffer" begin @test framebuffer.id == 0 + @test !isempty(framebuffer_textures) for (k, tex) in framebuffer_textures @test tex.id == 0 end end @testset "ShaderCache" begin + @test !isempty(shaders) for shader in shaders @test shader.id == 0 end + @test !isempty(programs) for program in programs @test program.id == 0 end @@ -559,6 +565,7 @@ end end @testset "RenderObjects" begin + @test !isempty(robjs) for robj in robjs validate_robj(robj) end From 437ac9fea2a40a897c2b475f95cc3967ea00da2c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 17:35:29 +0100 Subject: [PATCH 010/135] fix tests --- GLMakie/test/unit_tests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 0e775c7dc83..c764482d54d 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -190,7 +190,7 @@ end empty!(ax) - tex_atlas = GLMakie.get_texture!(GLMakie.gl_texture_atlas()) + tex_atlas = GLMakie.get_texture!(screen.glscreen, GLMakie.gl_texture_atlas()) for robj in robjs for (k, v) in robj.uniforms if (v isa GLMakie.GPUArray) && (v !== tex_atlas) From 662c971209c09e2914232b60cb66d3ea5c8b8f73 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 18:14:27 +0100 Subject: [PATCH 011/135] cleanup finalizers, free(), unsafe_free() --- GLMakie/src/GLAbstraction/GLBuffer.jl | 4 +- GLMakie/src/GLAbstraction/GLInfo.jl | 6 +++ GLMakie/src/GLAbstraction/GLShader.jl | 8 ++-- GLMakie/src/GLAbstraction/GLTexture.jl | 13 ++++--- GLMakie/src/GLAbstraction/GLTypes.jl | 52 +++++--------------------- GLMakie/src/gl_backend.jl | 2 +- GLMakie/src/glwindow.jl | 4 +- GLMakie/src/screen.jl | 8 ++-- 8 files changed, 36 insertions(+), 61 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index a19d789af23..8b60fc3704b 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -7,7 +7,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} # TODO maybe also delay upload to when render happens? observers::Vector{Observables.ObserverFunction} - function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T + function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum, context = current_context()) where T id = glGenBuffers() glBindBuffer(buffertype, id) # size of 0 can segfault it seems @@ -18,7 +18,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} obj = new( id, (buff_length,), buffertype, usage, current_context(), Observables.ObserverFunction[]) - finalizer(free, obj) + finalizer(verify_free, obj) obj end end diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl index c72c677807f..f6e19aedfac 100644 --- a/GLMakie/src/GLAbstraction/GLInfo.jl +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -97,3 +97,9 @@ function getProgramInfo(p::GLProgram) @show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_BUFFER_MODE) @show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS) end + +function verify_free(obj::T, name = string(T)) where T + if obj.id != 0 + Threads.@spawn println(stderr, "Error: $name has not been freed.") + end +end \ No newline at end of file diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index ba38dc5954a..b7e73850601 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -103,17 +103,15 @@ function ShaderCache(context) ) end -function unsafe_free(cache::ShaderCache) - require_context(cache.context) +function free(cache::ShaderCache) for (k, v) in cache.shader_cache for (k2, shader) in v - unsafe_free(shader) + free(shader) end end for program in values(cache.program_cache) - unsafe_free(program) + free(program) end - require_context(cache.context) return end diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 8fb657124eb..d9224ad946d 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -25,7 +25,8 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} internalformat ::GLenum, format ::GLenum, parameters ::TextureParameters{NDIM}, - size ::NTuple{NDIM, Int} + size ::NTuple{NDIM, Int}, + context = current_context() ) where {T, NDIM} tex = new( id, @@ -35,10 +36,10 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} format, parameters, size, - current_context(), + context, Observables.ObserverFunction[] ) - finalizer(free, tex) + finalizer(verify_free, tex) tex end end @@ -64,9 +65,9 @@ end bind(t::Texture, id) = glBindTexture(t.texturetype, id) ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture.context) -function unsafe_free(tb::TextureBuffer) - unsafe_free(tb.texture) - unsafe_free(tb.buffer) +function free(tb::TextureBuffer) + free(tb.texture) + free(tb.buffer) end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 3c430865fa2..cafc321db8c 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -30,7 +30,7 @@ mutable struct Shader function Shader(name, source, typ, id, context = current_context()) obj = new(Symbol(name), source, typ, id, context) - finalizer(free, obj) + finalizer(verify_free, obj) return obj end end @@ -66,7 +66,7 @@ mutable struct GLProgram context::GLContext function GLProgram(id::GLuint, shader::Vector{Shader}, nametype::Dict{Symbol,GLenum}, uniformloc::Dict{Symbol,Tuple}) obj = new(id, shader, nametype, uniformloc, current_context()) - finalizer(free, obj) + finalizer(verify_free, obj) obj end end @@ -246,7 +246,7 @@ function GLVertexArray(bufferdict::Dict, program::GLProgram) indexes = len end obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indexes) - finalizer(free, obj) + finalizer(verify_free, obj) return obj end using ShaderAbstractions: Buffer @@ -271,7 +271,7 @@ function GLVertexArray(program::GLProgram, buffers::Buffer, triangles::AbstractV glBindVertexArray(0) indices = indexbuffer(triangles) obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indices) - finalizer(free, obj) + finalizer(verify_free, obj) return obj end @@ -432,7 +432,12 @@ include("GLRenderObject.jl") #################################################################################### # freeing + function free(x) + # don't free if already freed + x.id == 0 && return + # error if the context is incorrect + require_context(x.context) try unsafe_free(x) catch e @@ -451,30 +456,19 @@ end # OpenGL has the annoying habit of reusing id's when creating a new context # We need to make sure to only free the current one function unsafe_free(x::GLProgram) - x.id == 0 && return - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) glDeleteProgram(x.id) x.id = 0 return end function unsafe_free(x::Shader) - x.id == 0 && return - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) glDeleteShader(x.id) x.id = 0 return end function unsafe_free(x::GLBuffer) - # don't free if already freed - x.id == 0 && return clean_up_observables(x) - # don't free from other context - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) id = Ref(x.id) glDeleteBuffers(1, id) x.id = 0 @@ -482,10 +476,7 @@ function unsafe_free(x::GLBuffer) end function unsafe_free(x::Texture) - x.id == 0 && return clean_up_observables(x) - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) id = Ref(x.id) glDeleteTextures(x.id) x.id = 0 @@ -493,9 +484,6 @@ function unsafe_free(x::Texture) end function unsafe_free(x::GLVertexArray) - x.id == 0 && return - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) for (key, buffer) in x.buffers unsafe_free(buffer) end @@ -506,24 +494,4 @@ function unsafe_free(x::GLVertexArray) glDeleteVertexArrays(1, id) x.id = 0 return -end - -function free(x::Shader) - @assert x.id == 0 "Must be freed explicitly" -end - -function free(x::GLProgram) - @assert x.id == 0 "Must be freed explicitly" -end - -function free(x::GLBuffer) - @assert x.id == 0 "Must be freed explicitly" -end - -function free(x::GLVertexArray) - @assert x.id == 0 "Must be freed explicitly" -end - -function free(x::Texture) - @assert x.id == 0 "Must be freed explicitly" -end +end \ No newline at end of file diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 9958d409273..f06a9e48bf4 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -22,7 +22,7 @@ function cleanup_texture_atlas!(context) for (atlas, ctx) in to_delete tex, func = pop!(atlas_texture_cache, (atlas, ctx)) Makie.remove_font_render_callback!(atlas, func) - GLAbstraction.unsafe_free(tex) + GLAbstraction.free(tex) end return end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 971935834b1..fc462e417d7 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -158,8 +158,10 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) end function destroy!(fb::GLFramebuffer) + # context required via free(tex) + @assert !isempty(fb.buffers) for tex in values(fb.buffers) - GLAbstraction.unsafe_free(tex) + GLAbstraction.free(tex) end id = [fb.id] glDeleteFramebuffers(1, id) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 94656cf9a05..831517882a0 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -578,15 +578,15 @@ function destroy!(rob::RenderObject) # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources # TODO, refcounting, or leaving freeing to GC... - # GC is a bit tricky with active contexts, so immediate free is preferred. + # GC can cause random context switches, so immediate free is necessary. # I guess as long as we make it hard for users to share buffers directly, this should be fine! - GLAbstraction.unsafe_free(v) + GLAbstraction.free(v) end end for obs in rob.observables Observables.clear(obs) end - GLAbstraction.unsafe_free(rob.vertexarray) + GLAbstraction.free(rob.vertexarray) end function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) @@ -649,7 +649,7 @@ function destroy!(screen::Screen) foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates destroy!(screen.framebuffer) cleanup_texture_atlas!(window) - GLAbstraction.unsafe_free(screen.shader_cache) + GLAbstraction.free(screen.shader_cache) destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) From ca120a9bb5eb354023a49cfc273026b73f1134ea Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 18:42:09 +0100 Subject: [PATCH 012/135] skip GLFW initialized assert for CI --- GLMakie/src/glwindow.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index fc462e417d7..90931ebe930 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -222,7 +222,7 @@ end # require_context(ctx, current = nothing) = nothing function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context()) - @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." + # @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." @assert !was_destroyed(ctx) "Context $ctx must not be destroyed." @assert ctx.handle == current.handle "Context $ctx must be current, but $current is." end From 3eb56a1874ba1e0ceee00791a2da9014b4959fcc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 18:44:51 +0100 Subject: [PATCH 013/135] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df26f139ce..b22e38f2c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Fixed gaps in corners of `poly(Rect2(...))` stroke [#4664](https://github.com/MakieOrg/Makie.jl/pull/4664) - 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). - 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) +- Refactored OpenGL cleanup to run immediately rather than on GC [#4699](https://github.com/MakieOrg/Makie.jl/pull/4699) ## [0.21.18] - 2024-12-12 From 4722f58b31e68e6484b7f7d93b59b17f38d6f5cd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 19:25:55 +0100 Subject: [PATCH 014/135] fix freeing of unused uniforms & cleanup unused pattern_sections --- GLMakie/src/GLAbstraction/GLTypes.jl | 1 + GLMakie/src/glshaders/lines.jl | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index cafc321db8c..22d12a62515 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -408,6 +408,7 @@ function RenderObject( special = Set([:ssao, :transparency, :instances, :fxaa, :num_clip_planes]) for k in setdiff(keys(data), keys(program.nametype)) if !(k in special) + data[k] isa GPUArray && free(data[k]) delete!(data, k) end end diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 9f29d47d8b7..7032d867aa5 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -129,7 +129,6 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: color_norm = nothing thickness = 2f0 => GLBuffer pattern = nothing - pattern_sections = pattern => Texture fxaa = false # Duplicate the vertex indices on the ends of the line, as our geometry # shader in `layout(lines_adjacency)` mode requires each rendered From 39c73573fe0818b2fff761f920d5ff91f7e0ef36 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 20:12:29 +0100 Subject: [PATCH 015/135] count and check failed OpenGL cleanup --- GLMakie/src/GLAbstraction/GLInfo.jl | 3 +++ GLMakie/test/runtests.jl | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl index f6e19aedfac..7b86a0d026c 100644 --- a/GLMakie/src/GLAbstraction/GLInfo.jl +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -98,8 +98,11 @@ function getProgramInfo(p::GLProgram) @show info = glGetProgramiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS) end +const FAILED_FREE_COUNTER = Ref(0) + function verify_free(obj::T, name = string(T)) where T if obj.id != 0 + FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 Threads.@spawn println(stderr, "Error: $name has not been freed.") end end \ No newline at end of file diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 2aa9d1d5765..6ed82db02a6 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -92,7 +92,7 @@ end events(f).tick[] = tick @test events(f).tick[] == tick - + f, a, p = scatter(rand(10)); tick_record = Makie.Tick[] on(t -> push!(tick_record, t), events(f).tick) @@ -124,4 +124,6 @@ end end @test i == length(tick_record)+1 -end \ No newline at end of file +end + +@test GLMakie.GLAbstraction.FAILED_FREE_COUNTER[] == 0 \ No newline at end of file From dbbf3f7a8807bb212cabbb47c26429927e41e5ca Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 20:18:36 +0100 Subject: [PATCH 016/135] try to handle GLFW CI failure --- GLMakie/src/screen.jl | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 831517882a0..b00d194caf2 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -667,23 +667,30 @@ Closes screen and empties it. Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=true`. """ function Base.close(screen::Screen; reuse=true) - @debug("Close screen!") - set_screen_visibility!(screen, false) - if screen.window_open[] # otherwise we trigger an infinite loop of closing - screen.window_open[] = false - end + try + @debug("Close screen!") + set_screen_visibility!(screen, false) + if screen.window_open[] # otherwise we trigger an infinite loop of closing + screen.window_open[] = false + end - stop_renderloop!(screen; close_after_renderloop=false) - empty!(screen) + stop_renderloop!(screen; close_after_renderloop=false) + empty!(screen) - if reuse && screen.reuse - @debug("reusing screen!") - push!(SCREEN_REUSE_POOL, screen) + if reuse && screen.reuse + @debug("reusing screen!") + push!(SCREEN_REUSE_POOL, screen) + end + GLFW.SetWindowShouldClose(screen.glscreen, true) + GLFW.PollEvents() + # Somehow, on osx, we need to hide the screen a second time! + set_screen_visibility!(screen, false) + catch e + # TODO: CI sometimes fails to adjust visibility with a GLFW init error + # This should not stop us from cleaning up OpenGL objects! + empty!(screen) + rethrow(e) end - GLFW.SetWindowShouldClose(screen.glscreen, true) - GLFW.PollEvents() - # Somehow, on osx, we need to hide the screen a second time! - set_screen_visibility!(screen, false) return end From e94363f7c41849ada23ba9a19abcec9744ed125f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 20:39:44 +0100 Subject: [PATCH 017/135] try fix CI --- GLMakie/src/gl_backend.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index f06a9e48bf4..3647e8943c9 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -36,6 +36,8 @@ function get_texture!(context, atlas::Makie.TextureAtlas) return true else @error("Cached atlas textures should be removed explicitly! $ctx") + Base.show_backtrace(stdout, Base.catch_backtrace()) + tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed Makie.remove_font_render_callback!(atlas, tex_func[2]) return false end From a47ed68ef426d8dd41a10fac10b79920990279e5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 21:44:54 +0100 Subject: [PATCH 018/135] require context for Texture and GLBuffer in most cases --- GLMakie/src/GLAbstraction/AbstractGPUArray.jl | 4 +- GLMakie/src/GLAbstraction/GLBuffer.jl | 52 ++++++++------- GLMakie/src/GLAbstraction/GLTexture.jl | 66 +++++++++++++------ GLMakie/src/GLAbstraction/GLTypes.jl | 11 +++- GLMakie/src/GLAbstraction/GLUniforms.jl | 8 +-- GLMakie/src/drawing_primitives.jl | 36 +++++----- GLMakie/src/gl_backend.jl | 18 ++--- GLMakie/src/glshaders/image_like.jl | 4 +- GLMakie/src/glshaders/lines.jl | 4 +- GLMakie/src/glshaders/mesh.jl | 12 ++-- GLMakie/src/glshaders/particles.jl | 34 +++++----- GLMakie/src/glshaders/surface.jl | 2 +- GLMakie/src/glshaders/voxel.jl | 2 +- GLMakie/src/glwindow.jl | 10 +-- GLMakie/src/postprocessing.jl | 8 +-- 15 files changed, 153 insertions(+), 118 deletions(-) diff --git a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl index edfba611746..f03bea0c4e5 100644 --- a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl +++ b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl @@ -190,8 +190,8 @@ gpu_setindex!(t) = error("gpu_setindex! not implemented for: $(typeof(t)). This 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") -function (::Type{GPUArrayType})(data::Observable; kw...) where GPUArrayType <: GPUArray - gpu_mem = GPUArrayType(data[]; kw...) +function (::Type{GPUArrayType})(context, data::Observable; kw...) where GPUArrayType <: GPUArray + gpu_mem = GPUArrayType(context, data[]; kw...) # TODO merge these and handle update tracking during construction obs2 = on(new_data -> update!(gpu_mem, new_data), data) if GPUArrayType <: TextureBuffer diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index 8b60fc3704b..f6f55c34127 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -7,7 +7,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} # TODO maybe also delay upload to when render happens? observers::Vector{Observables.ObserverFunction} - function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum, context = current_context()) where T + function GLBuffer{T}(context, ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T id = glGenBuffers() glBindBuffer(buffertype, id) # size of 0 can segfault it seems @@ -16,7 +16,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} glBindBuffer(buffertype, 0) obj = new( - id, (buff_length,), buffertype, usage, current_context(), + id, (buff_length,), buffertype, usage, context, Observables.ObserverFunction[]) finalizer(verify_free, obj) obj @@ -34,35 +34,36 @@ end bind(buffer::GLBuffer, other_target) = glBindBuffer(buffer.buffertype, other_target) function similar(x::GLBuffer{T}, buff_length::Int) where T - GLBuffer{T}(Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage) + require_context(x.context) + return GLBuffer{T}(x.context, Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage) end cardinality(::GLBuffer{T}) where {T} = cardinality(T) #Function to deal with any Immutable type with Real as Subtype function GLBuffer( - buffer::Union{Base.ReinterpretArray{T, 1}, DenseVector{T}}; + context, buffer::Union{Base.ReinterpretArray{T, 1}, DenseVector{T}}; buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes GC.@preserve buffer begin - return GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage) + return GLBuffer{T}(context, pointer(buffer), length(buffer), buffertype, usage) end end function GLBuffer( - buffer::DenseVector{T}; + context, buffer::DenseVector{T}; buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes GC.@preserve buffer begin - return GLBuffer{T}(pointer(buffer), length(buffer), buffertype, usage) + return GLBuffer{T}(context, pointer(buffer), length(buffer), buffertype, usage) end end function GLBuffer( - buffer::ShaderAbstractions.Buffer{T}; + context, buffer::ShaderAbstractions.Buffer{T}; buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes - b = GLBuffer(ShaderAbstractions.data(buffer); buffertype=buffertype, usage=usage) + b = GLBuffer(context, ShaderAbstractions.data(buffer); buffertype=buffertype, usage=usage) au = ShaderAbstractions.updater(buffer) obsfunc = on(au.update) do (f, args) f(b, args...) # forward setindex! etc @@ -76,36 +77,30 @@ end GLBuffer(buffer::GLBuffer) = buffer GLBuffer{T}(buffer::GLBuffer{T}) where {T} = buffer -function GLBuffer( - buffer::AbstractVector{T}; - kw_args... - ) where T <: GLArrayEltypes - GLBuffer(collect(buffer); kw_args...) +function GLBuffer(context, buffer::AbstractVector{T}; kw_args...) where T <: GLArrayEltypes + return GLBuffer(context, collect(buffer); kw_args...) end -function GLBuffer{T}( - buffer::AbstractVector; - kw_args... - ) where T <: GLArrayEltypes - GLBuffer(convert(Vector{T}, buffer); kw_args...) +function GLBuffer{T}(context, buffer::AbstractVector; kw_args...) where T <: GLArrayEltypes + return GLBuffer(context, convert(Vector{T}, buffer); kw_args...) end function GLBuffer( - ::Type{T}, len::Int; + context, ::Type{T}, len::Int; buffertype::GLenum = GL_ARRAY_BUFFER, usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes - GLBuffer{T}(Ptr{T}(C_NULL), len, buffertype, usage) + return GLBuffer{T}(context, Ptr{T}(C_NULL), len, buffertype, usage) end function indexbuffer( - buffer::VectorTypes{T}; - usage::GLenum = GL_STATIC_DRAW + buffer::VectorTypes{T}; usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes - GLBuffer(buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage) + return GLBuffer(current_context(), buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage) end # GPUArray interface function gpu_data(b::GLBuffer{T}) where T + require_context(b.context) data = Vector{T}(undef, length(b)) bind(b) glGetBufferSubData(b.buffertype, 0, sizeof(data), data) @@ -116,6 +111,7 @@ end # Resize buffer function gpu_resize!(buffer::GLBuffer{T}, newdims::NTuple{1, Int}) where T + require_context(buffer.context) #TODO make this safe! newlength = newdims[1] oldlen = length(buffer) @@ -139,6 +135,7 @@ function gpu_resize!(buffer::GLBuffer{T}, newdims::NTuple{1, Int}) where T end function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::Integer) where T + require_context(b.context) multiplicator = sizeof(T) bind(b) glBufferSubData(b.buffertype, multiplicator*(offset-1), sizeof(value), value) @@ -146,6 +143,7 @@ function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::Integer) where end function gpu_setindex!(b::GLBuffer{T}, value::Vector{T}, offset::UnitRange{Int}) where T + require_context(b.context) multiplicator = sizeof(T) bind(b) glBufferSubData(b.buffertype, multiplicator*(first(offset)-1), sizeof(value), value) @@ -156,6 +154,8 @@ end # copy between two buffers # could be a setindex! operation, with subarrays for buffers function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T + require_context(a.context) + @assert a.context == b.context multiplicator = sizeof(T) @assert a.id != 0 & b.id != 0 glBindBuffer(GL_COPY_READ_BUFFER, a.id) @@ -178,6 +178,7 @@ end #copy inside one buffer function unsafe_copy!(buffer::GLBuffer{T}, readoffset::Int, writeoffset::Int, len::Int) where T + require_context(buffer.context) len <= 0 && return nothing bind(buffer) ptr = Ptr{T}(glMapBuffer(buffer.buffertype, GL_READ_WRITE)) @@ -190,6 +191,7 @@ function unsafe_copy!(buffer::GLBuffer{T}, readoffset::Int, writeoffset::Int, le end function unsafe_copy!(a::Vector{T}, readoffset::Int, b::GLBuffer{T}, writeoffset::Int, len::Int) where T + require_context(b.context) bind(b) ptr = Ptr{T}(glMapBuffer(b.buffertype, GL_WRITE_ONLY)) for i=1:len @@ -200,6 +202,7 @@ function unsafe_copy!(a::Vector{T}, readoffset::Int, b::GLBuffer{T}, writeoffset end function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset::Int, len::Int) where T + require_context(a.context) bind(a) ptr = Ptr{T}(glMapBuffer(a.buffertype, GL_READ_ONLY)) for i in 1:len @@ -210,6 +213,7 @@ function unsafe_copy!(a::GLBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset end function gpu_getindex(b::GLBuffer{T}, range::UnitRange) where T + require_context(b.context) multiplicator = sizeof(T) offset = first(range)-1 value = Vector{T}(undef, length(range)) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index d9224ad946d..756421515ef 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -19,6 +19,7 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} context ::GLContext observers ::Vector{Observables.ObserverFunction} function Texture{T, NDIM}( + context ::GLContext, id ::GLuint, texturetype ::GLenum, pixeltype ::GLenum, @@ -26,7 +27,6 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} format ::GLenum, parameters ::TextureParameters{NDIM}, size ::NTuple{NDIM, Int}, - context = current_context() ) where {T, NDIM} tex = new( id, @@ -84,13 +84,14 @@ function set_packing_alignment(a) # at some point we should specialize to array/ end Makie.@noconstprop function Texture( - data::Ptr{T}, dims::NTuple{NDim, Int}; + context, data::Ptr{T}, dims::NTuple{NDim, Int}; internalformat::GLenum = default_internalcolorformat(T), texturetype ::GLenum = default_texturetype(NDim), format ::GLenum = default_colorformat(T), mipmap = false, parameters... # rest should be texture parameters ) where {T, NDim} + require_context(context) texparams = TextureParameters(T, NDim; parameters...) id = glGenTextures() glBindTexture(texturetype, id) @@ -99,7 +100,7 @@ Makie.@noconstprop function Texture( glTexImage(texturetype, 0, internalformat, dims..., 0, format, numbertype, data) mipmap && glGenerateMipmap(texturetype) texture = Texture{T, NDim}( - id, texturetype, numbertype, internalformat, format, + context, id, texturetype, numbertype, internalformat, format, texparams, dims ) @@ -108,6 +109,7 @@ Makie.@noconstprop function Texture( end export resize_nocopy! function resize_nocopy!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} + require_context(t.context) bind(t) glTexImage(t.texturetype, 0, t.internalformat, newdims..., 0, t.format, t.pixeltype, C_NULL) t.size = newdims @@ -120,8 +122,8 @@ Constructor for empty initialization with NULL pointer instead of an array with You just need to pass the wanted color/vector type and the dimensions. To which values the texture gets initialized is driver dependent """ -Texture(::Type{T}, dims::NTuple{N, Int}; kw_args...) where {T <: GLArrayEltypes, N} = - Texture(convert(Ptr{T}, C_NULL), dims; kw_args...)::Texture{T, N} +Texture(context, ::Type{T}, dims::NTuple{N, Int}; kw_args...) where {T <: GLArrayEltypes, N} = + Texture(context, convert(Ptr{T}, C_NULL), dims; kw_args...)::Texture{T, N} """ Constructor for a normal array, with color or Abstract Arrays as elements. @@ -129,15 +131,15 @@ So Array{Real, 2} == Texture2D with 1D Colorant dimension Array{Vec1/2/3/4, 2} == Texture2D with 1/2/3/4D Colorant dimension Colors from Colors.jl should mostly work as well """ -Texture(image::Array{T, NDim}; kw_args...) where {T <: GLArrayEltypes, NDim} = - Texture(pointer(image), size(image); kw_args...)::Texture{T, NDim} +Texture(context, image::Array{T, NDim}; kw_args...) where {T <: GLArrayEltypes, NDim} = + Texture(context, pointer(image), size(image); kw_args...)::Texture{T, NDim} -Texture(image::AbstractArray{T, NDim}; kw_args...) where {T <: GLArrayEltypes, NDim} = - Texture(collect(image); kw_args...) +Texture(context, image::AbstractArray{T, NDim}; kw_args...) where {T <: GLArrayEltypes, NDim} = + Texture(context, collect(image); kw_args...) -function Texture(s::ShaderAbstractions.Sampler{T, N}; kwargs...) where {T, N} +function Texture(context, s::ShaderAbstractions.Sampler{T, N}; kwargs...) where {T, N} tex = Texture( - pointer(s.data), size(s.data), + context, pointer(s.data), size(s.data), minfilter = s.minfilter, magfilter = s.magfilter, x_repeat = s.repeat[1], y_repeat = s.repeat[min(2, N)], z_repeat = s.repeat[min(3, N)], mipmap = s.mipmap, anisotropic = s.anisotropic; kwargs... @@ -151,12 +153,13 @@ end Constructor for Array Texture """ function Texture( - data::Vector{Array{T, 2}}; + context, data::Vector{Array{T, 2}}; internalformat::GLenum = default_internalcolorformat(T), texturetype::GLenum = GL_TEXTURE_2D_ARRAY, format::GLenum = default_colorformat(T), parameters... ) where T <: GLArrayEltypes + require_context(context) texparams = TextureParameters(T, 2; parameters...) id = glGenTextures() @@ -179,7 +182,7 @@ function Texture( end texture = Texture{T, 2}( - id, texturetype, numbertype, + context, id, texturetype, numbertype, internalformat, format, texparams, tuple(maxdims...) ) @@ -190,21 +193,22 @@ end function TextureBuffer(buffer::GLBuffer{T}) where T <: GLArrayEltypes + require_context(buffer.context) texture_type = GL_TEXTURE_BUFFER id = glGenTextures() glBindTexture(texture_type, id) internalformat = default_internalcolorformat(T) glTexBuffer(texture_type, internalformat, buffer.id) tex = Texture{T, 1}( - id, texture_type, julia2glenum(T), internalformat, + buffer.context, id, texture_type, julia2glenum(T), internalformat, default_colorformat(T), TextureParameters(T, 1), size(buffer) ) - TextureBuffer(tex, buffer) + return TextureBuffer(tex, buffer) end -function TextureBuffer(buffer::Vector{T}) where T <: GLArrayEltypes - buff = GLBuffer(buffer, buffertype = GL_TEXTURE_BUFFER, usage = GL_DYNAMIC_DRAW) - TextureBuffer(buff) +function TextureBuffer(context, buffer::Vector{T}) where T <: GLArrayEltypes + buff = GLBuffer(context, buffer, buffertype = GL_TEXTURE_BUFFER, usage = GL_DYNAMIC_DRAW) + return TextureBuffer(buff) end #= @@ -254,18 +258,22 @@ end # GPUArray interface: function unsafe_copy!(a::Vector{T}, readoffset::Int, b::TextureBuffer{T}, writeoffset::Int, len::Int) where T + require_context(b.texture.context) copy!(a, readoffset, b.buffer, writeoffset, len) bind(b.texture) glTexBuffer(b.texture.texturetype, b.texture.internalformat, b.buffer.id) # update texture end function unsafe_copy!(a::TextureBuffer{T}, readoffset::Int, b::Vector{T}, writeoffset::Int, len::Int) where T + require_context(a.texture.context) copy!(a.buffer, readoffset, b, writeoffset, len) bind(a.texture) glTexBuffer(a.texture.texturetype, a.texture.internalformat, a.buffer.id) # update texture end function unsafe_copy!(a::TextureBuffer{T}, readoffset::Int, b::TextureBuffer{T}, writeoffset::Int, len::Int) where T + require_context(a.texture.context) + @assert a.texture.context == b.texture.context unsafe_copy!(a.buffer, readoffset, b.buffer, writeoffset, len) bind(a.texture) @@ -277,6 +285,7 @@ function unsafe_copy!(a::TextureBuffer{T}, readoffset::Int, b::TextureBuffer{T}, end function gpu_setindex!(t::TextureBuffer{T}, newvalue::Vector{T}, indexes::UnitRange{I}) where {T, I <: Integer} + require_context(t.texture.context) bind(t.texture) t.buffer[indexes] = newvalue # set buffer indexes glTexBuffer(t.texture.texturetype, t.texture.internalformat, t.buffer.id) # update texture @@ -284,11 +293,13 @@ function gpu_setindex!(t::TextureBuffer{T}, newvalue::Vector{T}, indexes::UnitRa end function gpu_setindex!(t::Texture{T, 1}, newvalue::Array{T, 1}, indexes::UnitRange{I}) where {T, I <: Integer} + require_context(t.context) bind(t) texsubimage(t, newvalue, indexes) bind(t, 0) end function gpu_setindex!(t::Texture{T, N}, newvalue::Array{T, N}, indexes::Union{UnitRange,Integer}...) where {T, N} + require_context(t.context) bind(t) texsubimage(t, newvalue, indexes...) bind(t, 0) @@ -296,6 +307,8 @@ end function gpu_setindex!(target::Texture{T, 2}, source::Texture{T, 2}, fbo=glGenFramebuffers()) where T + require_context(target.context) + @assert target.context == source.context glBindFramebuffer(GL_FRAMEBUFFER, fbo) glFramebufferTexture2D( GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, @@ -326,12 +339,14 @@ end =# # Implementing the GPUArray interface function gpu_data(t::Texture{T, ND}) where {T, ND} + require_context(t.context) result = Array{T, ND}(undef, size(t)) unsafe_copy!(result, t) return result end function unsafe_copy!(dest::Array{T, N}, source::Texture{T, N}) where {T,N} + require_context(t.context) bind(source) glGetTexImage(source.texturetype, 0, source.format, source.pixeltype, dest) bind(source, 0) @@ -343,16 +358,18 @@ gpu_getindex(t::TextureBuffer{T}, i::UnitRange{Int64}) where {T} = t.buffer[i] similar(t::Texture{T, NDim}, newdims::Int...) where {T, NDim} = similar(t, newdims) function similar(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T + require_context(t.texture.context) buff = similar(t.buffer, newdims...) return TextureBuffer(buff) end function similar(t::Texture{T, NDim}, newdims::NTuple{NDim, Int}) where {T, NDim} + require_context(t.context) id = glGenTextures() glBindTexture(t.texturetype, id) glTexImage(t.texturetype, 0, t.internalformat, newdims..., 0, t.format, t.pixeltype, C_NULL) return Texture{T, NDim}( - id, + t.context, id, t.texturetype, t.pixeltype, t.internalformat, @@ -363,6 +380,7 @@ function similar(t::Texture{T, NDim}, newdims::NTuple{NDim, Int}) where {T, NDim end # Resize Texture function gpu_resize!(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T + require_context(t.texture.context) resize!(t.buffer, newdims) bind(t.texture) glTexBuffer(t.texture.texturetype, t.texture.internalformat, t.buffer.id) #update data in texture @@ -372,6 +390,7 @@ function gpu_resize!(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T end # Resize Texture function gpu_resize!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} + require_context(t.context) # dangerous code right here...Better write a few tests for this newtex = similar(t, newdims) old_size = size(t) @@ -383,11 +402,13 @@ function gpu_resize!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} end function texsubimage(t::Texture{T, 1}, newvalue::Array{T}, xrange::UnitRange, level=0) where {T} + require_context(t.context) glTexSubImage1D( t.texturetype, level, first(xrange)-1, length(xrange), t.format, t.pixeltype, newvalue ) end function texsubimage(t::Texture{T, 2}, newvalue::Array{T}, xrange::UnitRange, yrange::UnitRange, level=0) where T + require_context(t.context) glTexSubImage2D( t.texturetype, level, first(xrange)-1, first(yrange)-1, length(xrange), length(yrange), @@ -395,6 +416,7 @@ function texsubimage(t::Texture{T, 2}, newvalue::Array{T}, xrange::UnitRange, yr ) end function texsubimage(t::Texture{T, 3}, newvalue::Array{T}, xrange::UnitRange, yrange::UnitRange, zrange::UnitRange, level=0) where {T} + require_context(t.context) glTexSubImage3D( t.texturetype, level, first(xrange)-1, first(yrange)-1, first(zrange)-1, length(xrange), length(yrange), length(zrange), @@ -402,7 +424,10 @@ function texsubimage(t::Texture{T, 3}, newvalue::Array{T}, xrange::UnitRange, yr ) end -Base.iterate(t::TextureBuffer{T}) where {T} = iterate(t.buffer) +function Base.iterate(t::TextureBuffer{T}) where {T} + require_context(t.context) + iterate(t.buffer) +end function Base.iterate(t::TextureBuffer{T}, state::Tuple{Ptr{T}, Int}) where T v_idx = iterate(t.buffer, state) if v_idx === nothing @@ -558,6 +583,7 @@ function texparameter(t::Texture, key::GLenum, val::Float32) glTexParameterf(t.texturetype, key, val) end function set_parameters(t::Texture, parameters::Vector{Tuple{GLenum, Any}}) + require_context(t.context) bind(t) for elem in parameters texparameter(t, elem...) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 22d12a62515..dd96db186e2 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -178,7 +178,7 @@ mutable struct GLVertexArray{T} indices::T context::GLContext function GLVertexArray{T}(program, id, bufferlength, buffers, indices) where T - va = new(program, id, bufferlength, buffers, indices, current_context()) + va = new(program, id, bufferlength, buffers, indices, program.context) return va end end @@ -251,6 +251,7 @@ function GLVertexArray(bufferdict::Dict, program::GLProgram) end using ShaderAbstractions: Buffer function GLVertexArray(program::GLProgram, buffers::Buffer, triangles::AbstractVector{<: GLTriangleFace}) + ShaderAbstractions.switch_context!(program.context) # get the size of the first array, to assert later, that all have the same size id = glGenVertexArrays() glBindVertexArray(id) @@ -258,7 +259,7 @@ function GLVertexArray(program::GLProgram, buffers::Buffer, triangles::AbstractV array = getproperty(buffers, property_name) attribute = string(property_name) # TODO: use glVertexAttribDivisor to allow multiples of the longest buffer - buffer = GLBuffer(array) + buffer = GLBuffer(program.context, array) bind(buffer) attribLocation = get_attribute_location(program.id, attribute) if attribLocation == -1 @@ -372,7 +373,11 @@ function RenderObject( # glconvert is designed to convert everything to a fitting opengl datatype, but sometimes # the conversion is not unique. (E.g. Array -> Texture, TextureBuffer, GLBuffer, ...) # In these cases an explicit conversion target is required - data[k] = gl_convert(targets[k], v) + if targets[k] isa GPUArray + data[k] = gl_convert(context, targets[k], v) + else + data[k] = gl_convert(targets[k], v) + end else k in (:indices, :visible, :ssao, :label, :cycle) && continue diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index 934cd8c5485..d1b9aeff8be 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -198,7 +198,7 @@ gl_convert(x::T) where {T <: AbstractMesh} = gl_convert(x) gl_convert(x::T) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) gl_convert(x::Observable{T}) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) -gl_convert(s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(s) +gl_convert(s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(current_context(), s) gl_convert(s::Nothing) = s @@ -246,7 +246,7 @@ gl_convert(::Type{<: GPUArray}, a::StaticVector) = gl_convert(a) gl_convert(x::Vector) = x function gl_convert(T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} - T(convert(AbstractArray{gl_promote(X), N}, a); kw_args...) + T(current_context(), convert(AbstractArray{gl_promote(X), N}, a); kw_args...) end gl_convert(::Type{<: GLBuffer}, x::GLBuffer; kw_args...) = x @@ -254,14 +254,14 @@ gl_convert(::Type{Texture}, x::Texture) = x gl_convert(::Type{<: GPUArray}, x::GPUArray) = x function gl_convert(::Type{T}, a::Vector{Array{X, 2}}; kw_args...) where {T <: Texture, X} - T(a; kw_args...) + T(current_context(), a; kw_args...) end gl_convert(::Type{<: GPUArray}, a::Observable{<: StaticVector}) = gl_convert(a) function gl_convert(::Type{T}, a::Observable{<: AbstractArray{X, N}}; kw_args...) where {T <: GPUArray, X, N} TGL = gl_promote(X) s = (X == TGL) ? a : lift(x-> convert(Array{TGL, N}, x), a) - T(s; kw_args...) + T(current_context(), s; kw_args...) end gl_convert(f::Function, a) = f(a) diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index adcb7d42fe6..7151f18cd6e 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -199,7 +199,7 @@ function handle_intensities!(screen, attributes, plot) end attributes[:intensity] = color[].color_scaled interp = color[].color_mapping_type[] === Makie.continuous ? :linear : :nearest - attributes[:color_map] = Texture(color[].colormap; minfilter=interp) + attributes[:color_map] = Texture(screen.glscreen, color[].colormap; minfilter=interp) attributes[:color_norm] = color[].colorrange_scaled attributes[:nan_color] = color[].nan_color attributes[:highclip] = Makie.highclip(color[]) @@ -685,8 +685,8 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) end xpos = lift(first, plot, xypos) ypos = lift(last, plot, xypos) - gl_attributes[:position_x] = Texture(xpos, minfilter = :nearest) - gl_attributes[:position_y] = Texture(ypos, minfilter = :nearest) + gl_attributes[:position_x] = Texture(screen.glscreen, xpos, minfilter = :nearest) + gl_attributes[:position_y] = Texture(screen.glscreen, ypos, minfilter = :nearest) # number of planes used to render the heatmap gl_attributes[:instances] = lift(plot, xpos, ypos) do x, y (length(x)-1) * (length(y)-1) @@ -697,7 +697,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) if intensity isa ShaderAbstractions.Sampler gl_attributes[:intensity] = to_value(intensity) else - gl_attributes[:intensity] = Texture(el32convert(intensity); minfilter=interp) + gl_attributes[:intensity] = Texture(screen.glscreen, el32convert(intensity); minfilter=interp) end return draw_heatmap(screen, gl_attributes) @@ -720,9 +720,9 @@ function draw_image(screen::Screen, scene::Scene, plot::Union{Heatmap, Image}) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest if haskey(gl_attributes, :intensity) - gl_attributes[:image] = Texture(pop!(gl_attributes, :intensity); minfilter=interp) + gl_attributes[:image] = Texture(screen.glscreen, pop!(gl_attributes, :intensity); minfilter=interp) else - gl_attributes[:image] = Texture(pop!(gl_attributes, :color); minfilter=interp) + gl_attributes[:image] = Texture(screen.glscreen, pop!(gl_attributes, :color); minfilter=interp) end gl_attributes[:picking_mode] = "#define PICKING_INDEX_FROM_UV" return draw_mesh(screen, gl_attributes) @@ -758,15 +758,15 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= end end elseif to_value(color) isa ShaderAbstractions.Sampler - gl_attributes[:image] = Texture(lift(el32convert, plot, color)) + gl_attributes[:image] = Texture(screen.glscreen, lift(el32convert, plot, color)) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa AbstractMatrix{<:Colorant} - gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) + gl_attributes[:image] = Texture(screen.glscreen, lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa AbstractMatrix{<: Number} - gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) + gl_attributes[:image] = Texture(screen.glscreen, lift(el32convert, plot, color), minfilter = interp) gl_attributes[:color] = nothing elseif to_value(color) isa AbstractVector{<: Union{Number, Colorant}} gl_attributes[:vertex_color] = lift(el32convert, plot, color) @@ -777,7 +777,7 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= if haskey(gl_attributes, :intensity) intensity = pop!(gl_attributes, :intensity) if intensity[] isa Matrix - gl_attributes[:image] = Texture(intensity, minfilter = interp) + gl_attributes[:image] = Texture(screen.glscreen, intensity, minfilter = interp) else gl_attributes[:vertex_color] = intensity end @@ -833,7 +833,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) space = plot.space interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = interp ? :linear : :nearest - gl_attributes[:image] = Texture(img; minfilter=interp) + gl_attributes[:image] = Texture(screen.glscreen, img; minfilter=interp) @assert to_value(plot[3]) isa AbstractMatrix gl_attributes[:instances] = map(z -> (size(z,1)-1) * (size(z,2)-1), plot[3]) @@ -869,7 +869,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) xpos = lift(first, plot, xypos) ypos = lift(last, plot, xypos) args = map((xpos, ypos, mat)) do arg - Texture(lift(x-> convert(Array, el32convert(x)), plot, arg); minfilter=:linear) + Texture(screen.glscreen, lift(x-> convert(Array, el32convert(x)), plot, arg); minfilter=:linear) end if isnothing(img) gl_attributes[:image] = args[3] @@ -877,7 +877,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) return draw_surface(screen, args, gl_attributes) else gl_attributes[:ranges] = to_range.(to_value.(plot[1:2])) - z_data = Texture(lift(el32convert, plot, plot[3]); minfilter=:linear) + z_data = Texture(screen.glscreen, lift(el32convert, plot, plot[3]); minfilter=:linear) if isnothing(img) gl_attributes[:image] = z_data end @@ -934,7 +934,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Volume) interp = to_value(pop!(gl_attributes, :interpolate)) interp = interp ? :linear : :nearest - Tex(x) = Texture(x; minfilter=interp) + Tex(x) = Texture(screen.glscreen, x; minfilter=interp) if haskey(gl_attributes, :intensity) intensity = pop!(gl_attributes, :intensity) return draw_volume(screen, Tex(intensity), gl_attributes) @@ -949,7 +949,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Voxels) @assert to_value(plot.converted[end]) isa Array{UInt8, 3} # voxel ids - tex = Texture(plot.converted[end], minfilter = :nearest) + tex = Texture(screen.glscreen, plot.converted[end], minfilter = :nearest) # local update buffer = Vector{UInt8}(undef, 1) @@ -1025,14 +1025,14 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Voxels) # process texture mapping uv_map = pop!(gl_attributes, :uvmap) if !isnothing(to_value(uv_map)) - gl_attributes[:uv_map] = Texture(uv_map, minfilter = :nearest) + gl_attributes[:uv_map] = Texture(screen.glscreen, uv_map, minfilter = :nearest) interp = to_value(pop!(gl_attributes, :interpolate)) interp = interp ? :linear : :nearest color = gl_attributes[:color] - gl_attributes[:color] = Texture(color, minfilter = interp) + gl_attributes[:color] = Texture(screen.glscreen, color, minfilter = interp) elseif !isnothing(to_value(gl_attributes[:color])) - gl_attributes[:color] = Texture(gl_attributes[:color], minfilter = :nearest) + gl_attributes[:color] = Texture(screen.glscreen, gl_attributes[:color], minfilter = :nearest) end # for depthsorting diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 3647e8943c9..14615e83317 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -45,15 +45,15 @@ function get_texture!(context, atlas::Makie.TextureAtlas) tex, func = get!(atlas_texture_cache, (atlas, context)) do tex = Texture( - atlas.data, - minfilter = :linear, - magfilter = :linear, - # TODO: Consider alternatives to using the builtin anisotropic - # samplers for signed distance fields; the anisotropic - # filtering should happen *after* the SDF thresholding, but - # with the builtin sampler it happens before. - anisotropic = 16f0, - mipmap = true + context, atlas.data, + minfilter = :linear, + magfilter = :linear, + # TODO: Consider alternatives to using the builtin anisotropic + # samplers for signed distance fields; the anisotropic + # filtering should happen *after* the SDF thresholding, but + # with the builtin sampler it happens before. + anisotropic = 16f0, + mipmap = true ) # update the texture, whenever a new font is added to the atlas function callback(distance_field, rectangle) diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 21a1eed4081..df862391097 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -33,7 +33,7 @@ A matrix of Intensities will result in a contourf kind of plot """ function draw_heatmap(screen, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) - to_opengl_mesh!(data, primitive) + to_opengl_mesh!(screen.glscreen, data, primitive) pop!(data, :shading, FastShading) @gen_defaults! data begin intensity = nothing => Texture @@ -55,7 +55,7 @@ end function draw_volume(screen, main::VolumeTypes, data::Dict) geom = Rect3f(Vec3f(0), Vec3f(1)) - to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + to_opengl_mesh!(screen.glscreen, data, const_lift(GeometryBasics.triangle_mesh, geom)) shading = pop!(data, :shading, FastShading) pop!(data, :backlight, 0f0) # We overwrite this @gen_defaults! data begin diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 7032d867aa5..81bc6ec8141 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -157,7 +157,7 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: if !isa(to_value(pattern), Vector) error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) + tex = GLAbstraction.Texture(screen.glscreen, lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) data[:pattern] = tex end data[:pattern_length] = lift(pt -> Float32(last(pt) - first(pt)), pattern) @@ -200,7 +200,7 @@ function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where if !isa(to_value(pattern), Vector) error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) + tex = GLAbstraction.Texture(screen.glscreen, lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) data[:pattern] = tex data[:pattern_length] = lift(pt -> Float32(last(pt) - first(pt)), pattern) end diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 07229a3faf3..9deeb020765 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -1,8 +1,8 @@ -function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) +function to_opengl_mesh!(context, result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) m = convert(Observable, mesh_obs) - + result[:faces] = indexbuffer(map(faces, m)) - result[:vertices] = GLBuffer(map(coordinates, m)) + result[:vertices] = GLBuffer(context, map(coordinates, m)) function to_buffer(name, target) if hasproperty(m[], name) @@ -11,9 +11,9 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) val = map(m -> getproperty(m, name), m) end if val[] isa AbstractVector - result[target] = GLBuffer(val) + result[target] = GLBuffer(context, val) elseif val[] isa AbstractMatrix - result[target] = Texture(val) + result[target] = Texture(context, val) else error("unsupported attribute: $(name)") end @@ -31,7 +31,7 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) to_buffer(:normal, :normals) end to_buffer(:attribute_id, :attribute_id) - + return result end diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 6355f267486..7a25f8c8f0f 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -1,13 +1,13 @@ using Makie: RectanglePacker -function to_meshcolor(color::TOrSignal{Vector{T}}) where T <: Colorant - TextureBuffer(color) +function to_meshcolor(context, color::TOrSignal{Vector{T}}) where T <: Colorant + TextureBuffer(context, color) end -function to_meshcolor(color::TOrSignal{Matrix{T}}) where T <: Colorant - Texture(color) +function to_meshcolor(context, color::TOrSignal{Matrix{T}}) where T <: Colorant + Texture(context, color) end -function to_meshcolor(color) +function to_meshcolor(context, color) color end @@ -41,19 +41,19 @@ is_all_equal_scale(v::Vec2f) = v[1] == v[2] # could use ≈ too is_all_equal_scale(vs::Vector{Vec2f}) = all(is_all_equal_scale, vs) -intensity_convert(intensity, verts) = intensity -function intensity_convert(intensity::VecOrSignal{T}, verts) where T +intensity_convert(cotnext, intensity, verts) = intensity +function intensity_convert(context, intensity::VecOrSignal{T}, verts) where T if length(to_value(intensity)) == length(to_value(verts)) - GLBuffer(intensity) + GLBuffer(context, intensity) else - Texture(intensity) + Texture(context, intensity) end end -function intensity_convert_tex(intensity::VecOrSignal{T}, verts) where T +function intensity_convert_tex(context, intensity::VecOrSignal{T}, verts) where T if length(to_value(intensity)) == length(to_value(verts)) - TextureBuffer(intensity) + TextureBuffer(context, intensity) else - Texture(intensity) + Texture(context, intensity) end end @@ -67,7 +67,7 @@ function draw_mesh_particle(screen, p, data) rot = get!(data, :rotation, Vec4f(0, 0, 0, 1)) rot = vec2quaternion(rot) delete!(data, :rotation) - to_opengl_mesh!(data, p[1]) + to_opengl_mesh!(screen.glscreen, data, p[1]) @gen_defaults! data begin position = p[2] => TextureBuffer scale = Vec3f(1) => TextureBuffer @@ -96,12 +96,12 @@ function draw_mesh_particle(screen, p, data) end shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm + data[:color] = to_meshcolor(screen.glscreen, get!(data, :color, nothing)) @gen_defaults! data begin color_map = nothing => Texture color_norm = nothing intensity = nothing image = nothing => Texture - color = nothing => to_meshcolor vertex_color = Vec4f(1) matcap = nothing => Texture fetch_pixel = false @@ -126,7 +126,7 @@ function draw_mesh_particle(screen, p, data) ) end if !isnothing(Makie.to_value(intensity)) - data[:intensity] = intensity_convert_tex(intensity, position) + data[:intensity] = intensity_convert_tex(screen.glscreen, intensity, position) data[:len] = const_lift(length, position) end return assemble_shader(data) @@ -184,7 +184,7 @@ function draw_scatter( rpack = RectanglePacker(Rect2(0, 0, maxdims...)) uv_coordinates = [push!(rpack, rect).area for rect in rectangles] max_xy = mapreduce(maximum, (a,b)-> max.(a, b), uv_coordinates) - texture_atlas = Texture(eltype(images[1]), (max_xy...,)) + texture_atlas = Texture(screen.glscreen, eltype(images[1]), (max_xy...,)) for (area, img) in zip(uv_coordinates, images) texture_atlas[area] = img #transfer to texture atlas end @@ -281,7 +281,7 @@ function draw_scatter(screen, (marker, position), data) # Exception for intensity, to make it possible to handle intensity with a # different length compared to position. Intensities will be interpolated in that case - data[:intensity] = intensity_convert(intensity, position) + data[:intensity] = intensity_convert(screen.glscreen, intensity, position) data[:len] = const_lift(length, position) return assemble_shader(data) diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index a868b48bd39..384c3e246ae 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -118,7 +118,7 @@ end function draw_surface(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) - to_opengl_mesh!(data, primitive) + to_opengl_mesh!(screen.glscreen, data, primitive) shading = pop!(data, :shading, FastShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin scale = nothing diff --git a/GLMakie/src/glshaders/voxel.jl b/GLMakie/src/glshaders/voxel.jl index f0a3c679405..58f9b7d383b 100644 --- a/GLMakie/src/glshaders/voxel.jl +++ b/GLMakie/src/glshaders/voxel.jl @@ -1,7 +1,7 @@ @nospecialize function draw_voxels(screen, main::VolumeTypes, data::Dict) geom = Rect2f(Point2f(0), Vec2f(1.0)) - to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + to_opengl_mesh!(screen.glscreen, data, const_lift(GeometryBasics.triangle_mesh, geom)) shading = pop!(data, :shading, FastShading) @gen_defaults! data begin voxel_id = main => Texture diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 90931ebe930..30a6a88483b 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -96,25 +96,25 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) # Buffers we always need # Holds the image that eventually gets displayed color_buffer = Texture( - RGBA{N0f8}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + context, RGBA{N0f8}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) # Holds a (plot id, element id) for point picking objectid_buffer = Texture( - Vec{2, GLuint}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + context, Vec{2, GLuint}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) # holds depth and stencil values depth_buffer = Texture( - Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), fb_size, + context, Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge, internalformat = GL_DEPTH24_STENCIL8, format = GL_DEPTH_STENCIL ) # Order Independent Transparency HDR_color_buffer = Texture( - RGBA{Float16}, fb_size, minfilter = :linear, x_repeat = :clamp_to_edge + context, RGBA{Float16}, fb_size, minfilter = :linear, x_repeat = :clamp_to_edge ) OIT_weight_buffer = Texture( - N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge + context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) attach_framebuffer(color_buffer, GL_COLOR_ATTACHMENT0) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 7764cb0faec..a5d4e149e6e 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -84,7 +84,7 @@ function ssao_postprocessor(framebuffer, shader_cache) if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) position_buffer = Texture( - Vec3f, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + shader_cache.context, Vec3f, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge ) pos_id = attach_colorbuffer!(framebuffer, :position, position_buffer) push!(framebuffer.render_buffer_ids, pos_id) @@ -93,7 +93,7 @@ function ssao_postprocessor(framebuffer, shader_cache) if !haskey(framebuffer, :HDR_color) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) normal_occlusion_buffer = Texture( - Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge + shader_cache.context, Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge ) normal_occ_id = attach_colorbuffer!(framebuffer, :normal_occlusion, normal_occlusion_buffer) else @@ -128,7 +128,7 @@ function ssao_postprocessor(framebuffer, shader_cache) :normal_occlusion_buffer => getfallback(framebuffer, :normal_occlusion, :HDR_color)[2], :kernel => kernel, :noise => Texture( - [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], + shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], minfilter = :nearest, x_repeat = :repeat ), :noise_scale => map(s -> Vec2f(s ./ 4.0), framebuffer.resolution), @@ -215,7 +215,7 @@ function fxaa_postprocessor(framebuffer, shader_cache) if !haskey(framebuffer, :HDR_color) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) color_luma_buffer = Texture( - RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge + shader_cache.context, RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge ) luma_id = attach_colorbuffer!(framebuffer, :color_luma, color_luma_buffer) else From 4b745442c04e555e548ab93b437a60d3121206e1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 22:02:42 +0100 Subject: [PATCH 019/135] fix incorrect uniform cleanup --- GLMakie/src/GLAbstraction/GLTypes.jl | 2 +- ReferenceTests/src/tests/text.jl | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index dd96db186e2..fa14b088c66 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -413,7 +413,7 @@ function RenderObject( special = Set([:ssao, :transparency, :instances, :fxaa, :num_clip_planes]) for k in setdiff(keys(data), keys(program.nametype)) if !(k in special) - data[k] isa GPUArray && free(data[k]) + !haskey(buffers, k) && (data[k] isa GPUArray) && free(data[k]) delete!(data, k) end end diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index 3c09c2a0435..dec9f9019a7 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -220,7 +220,7 @@ end meshscatter(f[2, 1], [p1, p2]; markersize=0.3, color=[:purple, :yellow]) text!(p1; text="A", align=(:center, :center), glowwidth=10.0, glowcolor=:white, color=:black, fontsize=40, overdraw=true) text!(p2; text="B", align=(:center, :center), glowwidth=20.0, glowcolor=(:black, 0.6), color=:white, fontsize=40, overdraw=true) - + f end @@ -295,13 +295,13 @@ end @reference_test "latex (axis, scene, bbox)" begin f = Figure(size = (500, 300)) - text(f[1, 1], 1, 1, text = L"\frac{\sqrt{x + y}}{\sqrt{x + y}}", fontsize = 50, + text(f[1, 1], 1, 1, text = L"\frac{\sqrt{x + y}}{\sqrt{x + y}}", fontsize = 50, rotation = pi/4, align = (:center, :center)) - + s = LScene(f[1, 2], scenekw = (camera = campixel!,), show_axis = false) text!(s, L"\sqrt{2}", position = (100, 50), rotation = pi/2, fontsize = 20, markerspace = :data) - + t = text!(s, L"\int_0^5x^2+2ab", position = Point2f(50, 150), rotation = 0.0, fontsize = 20, markerspace = :data) wireframe!(s, boundingbox(t, :data), color=:black) From c096311d13a27edc2594feba182f593dc73a9976 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 22:40:17 +0100 Subject: [PATCH 020/135] CI please --- GLMakie/src/gl_backend.jl | 2 ++ GLMakie/src/screen.jl | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 14615e83317..eabbe033030 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -36,7 +36,9 @@ function get_texture!(context, atlas::Makie.TextureAtlas) return true else @error("Cached atlas textures should be removed explicitly! $ctx") + println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(x) ? " destroyed" : "") Base.show_backtrace(stdout, Base.catch_backtrace()) + flush(stdout) tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed Makie.remove_font_render_callback!(atlas, tex_func[2]) return false diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index b00d194caf2..446db2472f4 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -706,6 +706,12 @@ function closeall(; empty_shader=true) screen = pop!(ALL_SCREENS) destroy!(screen) end + + if !isempty(atlas_texture_cache) + @warn "texture atlas cleanup incomplete: $atlas_texture_cache" + empty!(atlas_texture_cache) + end + empty!(SINGLETON_SCREEN) empty!(SCREEN_REUSE_POOL) return From 4a1d2780b84d685284ee1ff1597e68d44d1d9981 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 22:47:41 +0100 Subject: [PATCH 021/135] fix typo --- GLMakie/src/gl_backend.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index eabbe033030..050ba9799f5 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -36,7 +36,7 @@ function get_texture!(context, atlas::Makie.TextureAtlas) return true else @error("Cached atlas textures should be removed explicitly! $ctx") - println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(x) ? " destroyed" : "") + println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(ctx) ? " destroyed" : "") Base.show_backtrace(stdout, Base.catch_backtrace()) flush(stdout) tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed From e63845046fd5b37478a714144f3035e79c8b7642 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 23:08:57 +0100 Subject: [PATCH 022/135] try handling GLFW init errors differently --- GLMakie/src/gl_backend.jl | 1 + GLMakie/src/screen.jl | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 050ba9799f5..3d3033d3c33 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -35,6 +35,7 @@ function get_texture!(context, atlas::Makie.TextureAtlas) if GLAbstraction.context_alive(ctx) return true else + flush(stdout) @error("Cached atlas textures should be removed explicitly! $ctx") println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(ctx) ? " destroyed" : "") Base.show_backtrace(stdout, Base.catch_backtrace()) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 446db2472f4..049a6ff1c94 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -667,30 +667,30 @@ Closes screen and empties it. Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=true`. """ function Base.close(screen::Screen; reuse=true) + @debug("Close screen!") + # TODO: CI sometimes fails to adjust visibility with a GLFW init error + # This should not stop us from cleaning up OpenGL objects! try - @debug("Close screen!") set_screen_visibility!(screen, false) if screen.window_open[] # otherwise we trigger an infinite loop of closing screen.window_open[] = false end + catch e + @error exception = e + end - stop_renderloop!(screen; close_after_renderloop=false) - empty!(screen) + stop_renderloop!(screen; close_after_renderloop=false) + empty!(screen) - if reuse && screen.reuse - @debug("reusing screen!") - push!(SCREEN_REUSE_POOL, screen) - end - GLFW.SetWindowShouldClose(screen.glscreen, true) - GLFW.PollEvents() - # Somehow, on osx, we need to hide the screen a second time! - set_screen_visibility!(screen, false) - catch e - # TODO: CI sometimes fails to adjust visibility with a GLFW init error - # This should not stop us from cleaning up OpenGL objects! - empty!(screen) - rethrow(e) + if reuse && screen.reuse + @debug("reusing screen!") + push!(SCREEN_REUSE_POOL, screen) end + + GLFW.SetWindowShouldClose(screen.glscreen, true) + GLFW.PollEvents() + # Somehow, on osx, we need to hide the screen a second time! + set_screen_visibility!(screen, false) return end From 3b46514d260e0c97b4d448d9334d8643e07a85a8 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 23:37:07 +0100 Subject: [PATCH 023/135] maybe just destroy the window if the context dies? --- GLMakie/src/GLAbstraction/GLTypes.jl | 5 +++-- GLMakie/src/gl_backend.jl | 1 + GLMakie/src/screen.jl | 29 ++++++++++++++++------------ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index fa14b088c66..c66f884d159 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -442,8 +442,9 @@ include("GLRenderObject.jl") function free(x) # don't free if already freed x.id == 0 && return - # error if the context is incorrect - require_context(x.context) + # warn if the context is incorrect + # require_context(x.context) + context_alive(x.context) || @warn "Context died before OpenGL objects were freed." try unsafe_free(x) catch e diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 3d3033d3c33..9bfb79be888 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -47,6 +47,7 @@ function get_texture!(context, atlas::Makie.TextureAtlas) end tex, func = get!(atlas_texture_cache, (atlas, context)) do + require_context(context) tex = Texture( context, atlas.data, minfilter = :linear, diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 049a6ff1c94..210bb25ab99 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -641,22 +641,27 @@ end function destroy!(screen::Screen) @debug("Destroy screen!") - close(screen; reuse=false) - @assert screen.rendertask === nothing window = screen.glscreen - GLFW.SetWindowRefreshCallback(window, nothing) - GLFW.SetWindowContentScaleCallback(window, nothing) + if context_alive(window) + close(screen; reuse=false) + GLFW.SetWindowRefreshCallback(window, nothing) + GLFW.SetWindowContentScaleCallback(window, nothing) + else + stop_renderloop!(screen; close_after_renderloop=false) + empty!(screen) + end + @assert screen.rendertask === nothing foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates destroy!(screen.framebuffer) cleanup_texture_atlas!(window) GLAbstraction.free(screen.shader_cache) - destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) if screen in SINGLETON_SCREEN empty!(SINGLETON_SCREEN) end + destroy!(window) return end @@ -668,15 +673,15 @@ Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=t """ function Base.close(screen::Screen; reuse=true) @debug("Close screen!") + if !context_alive(screen.glscene) + destroy!(screen) + end + # TODO: CI sometimes fails to adjust visibility with a GLFW init error # This should not stop us from cleaning up OpenGL objects! - try - set_screen_visibility!(screen, false) - if screen.window_open[] # otherwise we trigger an infinite loop of closing - screen.window_open[] = false - end - catch e - @error exception = e + set_screen_visibility!(screen, false) + if screen.window_open[] # otherwise we trigger an infinite loop of closing + screen.window_open[] = false end stop_renderloop!(screen; close_after_renderloop=false) From c7285ca77a6701ac35a691faaca29c19f79e8578 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 23:40:40 +0100 Subject: [PATCH 024/135] fix typos --- GLMakie/src/screen.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 210bb25ab99..bf6866fa3af 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -642,7 +642,7 @@ end function destroy!(screen::Screen) @debug("Destroy screen!") window = screen.glscreen - if context_alive(window) + if GLAbstraction.context_alive(window) close(screen; reuse=false) GLFW.SetWindowRefreshCallback(window, nothing) GLFW.SetWindowContentScaleCallback(window, nothing) @@ -673,7 +673,7 @@ Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=t """ function Base.close(screen::Screen; reuse=true) @debug("Close screen!") - if !context_alive(screen.glscene) + if !GLAbstraction.context_alive(screen.glscreen) destroy!(screen) end From 02962057c57f1dceace19c8254f4fc43ab142819 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 00:15:51 +0100 Subject: [PATCH 025/135] maybe also cleanup dead screens when trying to reopen? --- GLMakie/src/screen.jl | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index bf6866fa3af..031bdf1bed6 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -319,6 +319,7 @@ function reopen!(screen::Screen) @debug("reopening screen") gl = screen.glscreen @assert !was_destroyed(gl) + @assert GLAbstraction.context_alive(gl) if GLFW.WindowShouldClose(gl) GLFW.SetWindowShouldClose(gl, false) end @@ -330,30 +331,39 @@ function reopen!(screen::Screen) end function screen_from_pool(debugging; window=nothing) - screen = if isempty(SCREEN_REUSE_POOL) - @debug("create empty screen for pool") - empty_screen(debugging; window) - else - @debug("get old screen from pool") - pop!(SCREEN_REUSE_POOL) + while !isempty(SCREEN_REUSE_POOL) + screen = pop!(SCREEN_REUSE_POOL) + if GLAbstraction.context_alive(screen.glscreen) + @debug("get old screen from pool") + return reopen!(screen) + else + destroy!(screen) + end end - return reopen!(screen) + + @debug("create empty screen for pool") + return reopen!(empty_screen(debugging; window)) end const SINGLETON_SCREEN = Screen[] function singleton_screen(debugging::Bool) if !isempty(SINGLETON_SCREEN) - @debug("reusing singleton screen") - screen = SINGLETON_SCREEN[1] - stop_renderloop!(screen; close_after_renderloop=false) - empty!(screen) - else - @debug("new singleton screen") - # reuse=false, because we "manually" re-use the singleton screen! - screen = empty_screen(debugging; reuse=false) - push!(SINGLETON_SCREEN, screen) + if GLAbstraction.context_alive(SINGLETON_SCREEN[1].glscreen) + @debug("reusing singleton screen") + screen = SINGLETON_SCREEN[1] + stop_renderloop!(screen; close_after_renderloop=false) + empty!(screen) + return reopen!(screen) + else + destroy!(pop!(SINGLETON_SCREEN)) + end end + + @debug("new singleton screen") + # reuse=false, because we "manually" re-use the singleton screen! + screen = empty_screen(debugging; reuse=false) + push!(SINGLETON_SCREEN, screen) return reopen!(screen) end From b819240e297bf9631d6b3c2c6d331a1892264a76 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 00:42:15 +0100 Subject: [PATCH 026/135] tweak require_context to maybe catch error earlier --- GLMakie/src/GLAbstraction/GLTexture.jl | 1 + GLMakie/src/GLAbstraction/GLTypes.jl | 2 ++ GLMakie/src/gl_backend.jl | 2 +- GLMakie/src/glwindow.jl | 7 +++---- GLMakie/src/postprocessing.jl | 6 ++---- GLMakie/src/rendering.jl | 1 + 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 756421515ef..b061a2a4257 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -39,6 +39,7 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} context, Observables.ObserverFunction[] ) + GLAbstraction.require_context(context) finalizer(verify_free, tex) tex end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index c66f884d159..6d9ff365f3f 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -346,6 +346,7 @@ function RenderObject( pre::Pre, post, context=current_context() ) where Pre + require_context(context) switch_context!(context) # This is a lazy workaround for disabling updates of `requires_update` when @@ -374,6 +375,7 @@ function RenderObject( # the conversion is not unique. (E.g. Array -> Texture, TextureBuffer, GLBuffer, ...) # In these cases an explicit conversion target is required if targets[k] isa GPUArray + GLAbstraction.require_context(nw) data[k] = gl_convert(context, targets[k], v) else data[k] = gl_convert(targets[k], v) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 9bfb79be888..9149fbe827a 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -18,7 +18,7 @@ const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}() function cleanup_texture_atlas!(context) to_delete = filter(atlas_ctx -> atlas_ctx[2] == context, keys(atlas_texture_cache)) - require_context(context) + # require_context(context) for (atlas, ctx) in to_delete tex, func = pop!(atlas_texture_cache, (atlas, ctx)) Makie.remove_font_render_callback!(atlas, func) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 30a6a88483b..aaa399589ea 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -87,6 +87,7 @@ function check_framebuffer() end Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) + require_context(context) ShaderAbstractions.switch_context!(context) # Create framebuffer @@ -148,8 +149,6 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) :stencil => depth_buffer ) - require_context(context) - return GLFramebuffer( fb_size_node, frambuffer_id, buffer_ids, buffers, @@ -221,8 +220,8 @@ function ShaderAbstractions.native_context_alive(x::GLFW.Window) end # require_context(ctx, current = nothing) = nothing -function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context()) - # @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." +function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context(); warn = false) + @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." @assert !was_destroyed(ctx) "Context $ctx must not be destroyed." @assert ctx.handle == current.handle "Context $ctx must be current, but $current is." end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index a5d4e149e6e..8746a08389d 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -79,6 +79,7 @@ end function ssao_postprocessor(framebuffer, shader_cache) + require_context(shader_cache.context) ShaderAbstractions.switch_context!(shader_cache.context) # Add missing buffers if !haskey(framebuffer, :position) @@ -102,8 +103,6 @@ function ssao_postprocessor(framebuffer, shader_cache) push!(framebuffer.render_buffer_ids, normal_occ_id) end - require_context(shader_cache.context) - # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -208,6 +207,7 @@ end Returns a PostProcessor that handles fxaa. """ function fxaa_postprocessor(framebuffer, shader_cache) + require_context(shader_cache.context) ShaderAbstractions.switch_context!(shader_cache.context) # Add missing buffers @@ -223,8 +223,6 @@ function fxaa_postprocessor(framebuffer, shader_cache) end end - require_context(shader_cache.context) - # calculate luma for FXAA shader1 = LazyShader( shader_cache, diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index fd6d0fe9d8c..a0448341937 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -28,6 +28,7 @@ Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) nw = to_native(screen) + GLAbstraction.require_context(nw) ShaderAbstractions.switch_context!(nw) function sortby(x) From 6c64137e652a256533d2a30e09d0e813fa34e872 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 00:50:51 +0100 Subject: [PATCH 027/135] switch before requiring --- GLMakie/src/GLAbstraction/GLTypes.jl | 2 +- GLMakie/src/glwindow.jl | 2 +- GLMakie/src/postprocessing.jl | 4 ++-- GLMakie/src/rendering.jl | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 6d9ff365f3f..1641f90a37b 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -346,8 +346,8 @@ function RenderObject( pre::Pre, post, context=current_context() ) where Pre - require_context(context) switch_context!(context) + require_context(context) # This is a lazy workaround for disabling updates of `requires_update` when # not rendering on demand. A cleaner implementation should probably go diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index aaa399589ea..1c006e8aa5d 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -87,8 +87,8 @@ function check_framebuffer() end Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) - require_context(context) ShaderAbstractions.switch_context!(context) + require_context(context) # Create framebuffer frambuffer_id = glGenFramebuffers() diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 8746a08389d..fded72f1cf5 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -79,8 +79,8 @@ end function ssao_postprocessor(framebuffer, shader_cache) - require_context(shader_cache.context) ShaderAbstractions.switch_context!(shader_cache.context) + require_context(shader_cache.context) # Add missing buffers if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) @@ -207,8 +207,8 @@ end Returns a PostProcessor that handles fxaa. """ function fxaa_postprocessor(framebuffer, shader_cache) - require_context(shader_cache.context) ShaderAbstractions.switch_context!(shader_cache.context) + require_context(shader_cache.context) # Add missing buffers if !haskey(framebuffer, :color_luma) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index a0448341937..ee717ea950e 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -28,8 +28,8 @@ Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) nw = to_native(screen) - GLAbstraction.require_context(nw) ShaderAbstractions.switch_context!(nw) + GLAbstraction.require_context(nw) function sortby(x) robj = x[3] From 07e6d2ee996ff7587e1c4c27ee80e832ad834cef Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 01:02:37 +0100 Subject: [PATCH 028/135] avoid requiring context for get_texture and thus robj cleanup --- GLMakie/src/gl_backend.jl | 12 ++++++------ GLMakie/src/screen.jl | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 9149fbe827a..499a4fe7e37 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -28,8 +28,6 @@ function cleanup_texture_atlas!(context) end function get_texture!(context, atlas::Makie.TextureAtlas) - require_context(context) - # clean up dead context! filter!(atlas_texture_cache) do ((ptr, ctx), tex_func) if GLAbstraction.context_alive(ctx) @@ -46,7 +44,9 @@ function get_texture!(context, atlas::Makie.TextureAtlas) end end - tex, func = get!(atlas_texture_cache, (atlas, context)) do + if haskey(atlas_texture_cache, (atlas, context)) + return atlas_texture_cache[(atlas, context)][1] + else require_context(context) tex = Texture( context, atlas.data, @@ -59,7 +59,7 @@ function get_texture!(context, atlas::Makie.TextureAtlas) anisotropic = 16f0, mipmap = true ) - # update the texture, whenever a new font is added to the atlas + function callback(distance_field, rectangle) ctx = tex.context if GLAbstraction.context_alive(ctx) @@ -70,9 +70,9 @@ function get_texture!(context, atlas::Makie.TextureAtlas) end end Makie.font_render_callback!(callback, atlas) - return (tex, callback) + atlas_texture_cache[(atlas, context)] = (tex, callback) + return tex end - return tex end include("glwindow.jl") diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 031bdf1bed6..4434ddb7cde 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -579,7 +579,12 @@ function destroy!(rob::RenderObject) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. GLAbstraction.switch_context!(rob.context) - tex = get_texture!(rob.context, gl_texture_atlas()) + tex = try + get_texture!(rob.context, gl_texture_atlas()) + catch e + @error exception = e + nothing + end for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) From 6f6eb3216d7de3e8cacbb2b831f235b9837a893a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 15:11:33 +0100 Subject: [PATCH 029/135] treat scene finalizer --- GLMakie/src/GLAbstraction/GLShader.jl | 6 +++--- GLMakie/src/GLAbstraction/GLTexture.jl | 6 +++--- GLMakie/src/GLAbstraction/GLTypes.jl | 24 ++++++++++++++++-------- GLMakie/src/screen.jl | 22 ++++++++++++---------- src/scenes.jl | 16 +++++++++------- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index b7e73850601..5400cc5864c 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -103,14 +103,14 @@ function ShaderCache(context) ) end -function free(cache::ShaderCache) +function free(cache::ShaderCache, called_from_finalizer = false) for (k, v) in cache.shader_cache for (k2, shader) in v - free(shader) + free(shader, called_from_finalizer) end end for program in values(cache.program_cache) - free(program) + free(program, called_from_finalizer) end return end diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index b061a2a4257..3f232696f24 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -66,9 +66,9 @@ end bind(t::Texture, id) = glBindTexture(t.texturetype, id) ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture.context) -function free(tb::TextureBuffer) - free(tb.texture) - free(tb.buffer) +function free(tb::TextureBuffer, called_from_finalizer = false) + free(tb.texture, called_from_finalizer) + free(tb.buffer, called_from_finalizer) end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 1641f90a37b..4d2cfc59f85 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -441,18 +441,26 @@ include("GLRenderObject.jl") #################################################################################### # freeing -function free(x) +# This may be called from the scene finalizer in which case no errors, no printing allowed +function free(x::T, called_from_finalizer = false) where {T} # don't free if already freed x.id == 0 && return - # warn if the context is incorrect - # require_context(x.context) - context_alive(x.context) || @warn "Context died before OpenGL objects were freed." - try + if called_from_finalizer + if !context_alive(x.context) + Threads.@spawn println(stderr, "Warning: free(::$T) called with dead context from scene finalizer.") + return + end + try + unsafe_free(x) + catch e + Threads.@spawn Base.showerror(stderr, e) + end + else + # context must be valid + require_context(x.context) unsafe_free(x) - catch e - isa(e, ContextNotAvailable) && return # if context got destroyed no need to worry! - rethrow(e) end + return end function clean_up_observables(x::T) where T diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 4434ddb7cde..d8dbe8f009e 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -536,12 +536,13 @@ function Makie.insertplots!(screen::Screen, scene::Scene) end end -function Base.delete!(screen::Screen, scene::Scene) +# Note: called from scene finalizer, must not error +function Base.delete!(screen::Screen, scene::Scene, called_from_finalizer::Bool = true) for child in scene.children - delete!(screen, child) + delete!(screen, child, called_from_finalizer) end for plot in scene.plots - delete!(screen, scene, plot) + delete!(screen, scene, plot, called_from_finalizer) end filter!(x -> x !== screen, scene.current_screens) if haskey(screen.screen2scene, WeakRef(scene)) @@ -575,7 +576,7 @@ function Base.delete!(screen::Screen, scene::Scene) return end -function destroy!(rob::RenderObject) +function destroy!(rob::RenderObject, called_from_finalizer = false) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. GLAbstraction.switch_context!(rob.context) @@ -595,16 +596,17 @@ function destroy!(rob::RenderObject) # TODO, refcounting, or leaving freeing to GC... # GC can cause random context switches, so immediate free is necessary. # I guess as long as we make it hard for users to share buffers directly, this should be fine! - GLAbstraction.free(v) + GLAbstraction.free(v, called_from_finalizer) end end for obs in rob.observables Observables.clear(obs) end - GLAbstraction.free(rob.vertexarray) + GLAbstraction.free(rob.vertexarray, called_from_finalizer) end -function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) +# Note: called from scene finalizer, must not error +function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot, called_from_finalizer::Bool = true) if !isempty(plot.plots) # this plot consists of children, so we flatten it and delete the children instead for cplot in Makie.collect_atomic_plots(plot) @@ -615,7 +617,7 @@ function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) # TODO, is it? renderobject = get(screen.cache, objectid(plot), nothing) if !isnothing(renderobject) - destroy!(renderobject) + destroy!(renderobject, called_from_finalizer) filter!(x-> x[3] !== renderobject, screen.renderlist) delete!(screen.cache2plot, renderobject.id) end @@ -631,12 +633,12 @@ function Base.empty!(screen::Screen) @assert !was_destroyed(screen.glscreen) for plot in collect(values(screen.cache2plot)) - delete!(screen, Makie.rootparent(plot), plot) + delete!(screen, Makie.rootparent(plot), plot, false) end if !isnothing(screen.scene) Makie.disconnect_screen(screen.scene, screen) - delete!(screen, screen.scene) + delete!(screen, screen.scene, false) screen.scene = nothing end diff --git a/src/scenes.jl b/src/scenes.jl index b98e3db675c..42a98d6a947 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -140,7 +140,7 @@ mutable struct Scene <: AbstractScene scene.isclosed = true end end - finalizer(free, scene) + finalizer(x -> free(x, true), scene) return scene end end @@ -435,24 +435,24 @@ function delete_scene!(scene::Scene) return nothing end -function free(scene::Scene) +function free(scene::Scene, called_from_finalizer = false) empty!(scene; free=true) for field in [:backgroundcolor, :viewport, :visible] Observables.clear(getfield(scene, field)) end for screen in copy(scene.current_screens) - delete!(screen, scene) + delete!(screen, scene, called_from_finalizer) end empty!(scene.current_screens) scene.parent = nothing return end -function Base.empty!(scene::Scene; free=false) +function Base.empty!(scene::Scene, called_from_finalizer = false; free=false) foreach(empty!, copy(scene.children)) # clear plots of this scene for plot in copy(scene.plots) - delete!(scene, plot) + delete!(scene, plot, called_from_finalizer) end # clear all child scenes @@ -493,10 +493,12 @@ function Base.push!(scene::Scene, @nospecialize(plot::Plot)) end end +Base.delete!(screen::MakieScreen, scene::Scene, p::AbstractPlot, ::Bool) = delete!(screen, scene, p) function Base.delete!(screen::MakieScreen, ::Scene, ::AbstractPlot) @debug "Deleting plots not implemented for backend: $(typeof(screen))" end +Base.delete!(screen::MakieScreen, scene::Scene, ::Bool) = delete!(screen, scene) function Base.delete!(screen::MakieScreen, ::Scene) # This may not be necessary for every backed @debug "Deleting scenes not implemented for backend: $(typeof(screen))" @@ -513,7 +515,7 @@ function free(plot::AbstractPlot) return end -function Base.delete!(scene::Scene, plot::AbstractPlot) +function Base.delete!(scene::Scene, plot::AbstractPlot, called_from_finalizer = false) filter!(x -> x !== plot, scene.plots) # TODO, if we want to delete a subplot of a plot, # It won't be in scene.plots directly, but will still be deleted @@ -525,7 +527,7 @@ function Base.delete!(scene::Scene, plot::AbstractPlot) # error("$(typeof(plot)) not in scene!") # end for screen in scene.current_screens - delete!(screen, scene, plot) + delete!(screen, scene, plot, called_from_finalizer) end free(plot) end From 54f1c1bf6707c6ad5a6c7d4e136a8f52912ea769 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 15:32:02 +0100 Subject: [PATCH 030/135] Treat scene finalizer in texture atlas too --- GLMakie/src/gl_backend.jl | 22 +++++++++++++--------- GLMakie/src/screen.jl | 7 +------ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 499a4fe7e37..d2a674043f2 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -16,28 +16,30 @@ using .GLAbstraction const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}() -function cleanup_texture_atlas!(context) +function cleanup_texture_atlas!(context, called_from_finalizer = false) to_delete = filter(atlas_ctx -> atlas_ctx[2] == context, keys(atlas_texture_cache)) - # require_context(context) + called_from_finalizer || require_context(context) for (atlas, ctx) in to_delete tex, func = pop!(atlas_texture_cache, (atlas, ctx)) Makie.remove_font_render_callback!(atlas, func) - GLAbstraction.free(tex) + GLAbstraction.free(tex, called_from_finalizer) end return end -function get_texture!(context, atlas::Makie.TextureAtlas) +function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer = false) # clean up dead context! filter!(atlas_texture_cache) do ((ptr, ctx), tex_func) if GLAbstraction.context_alive(ctx) return true else - flush(stdout) - @error("Cached atlas textures should be removed explicitly! $ctx") - println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(ctx) ? " destroyed" : "") - Base.show_backtrace(stdout, Base.catch_backtrace()) - flush(stdout) + if !called_from_finalizer + @error("Cached atlas textures should be removed explicitly! $ctx") + println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(ctx) ? " destroyed" : "") + Base.show_backtrace(stderr, Base.catch_backtrace()) + else + Threads.@spawn println(stderr, "Cached atlas textures did not get cleaned up for context ", ctx) + end tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed Makie.remove_font_render_callback!(atlas, tex_func[2]) return false @@ -46,6 +48,8 @@ function get_texture!(context, atlas::Makie.TextureAtlas) if haskey(atlas_texture_cache, (atlas, context)) return atlas_texture_cache[(atlas, context)][1] + elseif called_from_finalizer + return nothing else require_context(context) tex = Texture( diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index d8dbe8f009e..f1ed3eef24a 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -580,12 +580,7 @@ function destroy!(rob::RenderObject, called_from_finalizer = false) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. GLAbstraction.switch_context!(rob.context) - tex = try - get_texture!(rob.context, gl_texture_atlas()) - catch e - @error exception = e - nothing - end + tex = get_texture!(rob.context, gl_texture_atlas(), called_from_finalizer) for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) From 73f78c2d4590675998872970cd70083ed632fe78 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 15:45:02 +0100 Subject: [PATCH 031/135] add missing passthrough --- src/scenes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scenes.jl b/src/scenes.jl index 42a98d6a947..9c02361c7dd 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -436,7 +436,7 @@ function delete_scene!(scene::Scene) end function free(scene::Scene, called_from_finalizer = false) - empty!(scene; free=true) + empty!(scene, called_from_finalizer; free=true) for field in [:backgroundcolor, :viewport, :visible] Observables.clear(getfield(scene, field)) end From c3f2131f68059f0c77f5c7e7a5371642c8b68868 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 16:11:13 +0100 Subject: [PATCH 032/135] move object deletion test to end --- GLMakie/test/runtests.jl | 89 +++++++++++++++++++++++++++++++++++++- GLMakie/test/unit_tests.jl | 85 ------------------------------------ 2 files changed, 88 insertions(+), 86 deletions(-) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 6ed82db02a6..bdf540cbbe9 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -126,4 +126,91 @@ end @test i == length(tick_record)+1 end -@test GLMakie.GLAbstraction.FAILED_FREE_COUNTER[] == 0 \ No newline at end of file + +@testset "gl object deletion" begin + GLMakie.closeall() + + # image so we have a user generated texture in the mix + # texture atlas is triggered by text + # include SSAO to make sure its cleanup works too + f,a,p = image(rand(4,4)) + screen = display(f, ssao = true, visible = false) + colorbuffer(screen) + + # verify that SSAO is active + @test screen.postprocessors[1] != GLMakie.empty_postprocessor() + + framebuffer = screen.framebuffer + framebuffer_textures = copy(screen.framebuffer.buffers) + atlas_textures = first.(values(GLMakie.atlas_texture_cache)) + shaders = vcat([[shader for shader in values(shaders)] for shaders in values(screen.shader_cache.shader_cache)]...) + programs = [program for program in values(screen.shader_cache.program_cache)] + postprocessors = copy(screen.postprocessors) + robjs = last.(screen.renderlist) + + GLMakie.destroy!(screen) + + @testset "Texture Atlas" begin + @test !isempty(atlas_textures) + for tex in atlas_textures + @test tex.id == 0 + end + end + + @testset "Framebuffer" begin + @test framebuffer.id == 0 + @test !isempty(framebuffer_textures) + for (k, tex) in framebuffer_textures + @test tex.id == 0 + end + end + + @testset "ShaderCache" begin + @test !isempty(shaders) + for shader in shaders + @test shader.id == 0 + end + @test !isempty(programs) + for program in programs + @test program.id == 0 + end + end + + function validate_robj(robj) + for uniform in robj.uniforms + if to_value(uniform) isa GLMakie.GLAbstraction.GPUArray + @test to_value(uniform).id == 0 + end + end + @test robj.vertexarray.id == 0 + if robj.vertexarray.indices isa GLMakie.GLAbstraction.GPUArray + @test robj.vertexarray.indices.id == 0 + end + for buffer in values(robj.vertexarray.buffers) + @test buffer.id == 0 + end + @test robj.vertexarray.program.id == 0 + for shader in robj.vertexarray.program.shader + @test shader.id == 0 + end + end + + @testset "PostProcessors" begin + @test map(pp -> length(pp.robjs), postprocessors) == [2,1,2,1] + for pp in postprocessors + for robj in pp.robjs + validate_robj(robj) + end + end + end + + @testset "RenderObjects" begin + @test !isempty(robjs) + for robj in robjs + validate_robj(robj) + end + end + + # Check that no finalizers triggered on un-freed objects throughout all tests + @test GLMakie.GLAbstraction.FAILED_FREE_COUNTER[] == 0 +end diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index c764482d54d..306503f6e74 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -485,89 +485,4 @@ end @test robj.uniforms[:resolution][] == screen.px_per_unit[] * cam.resolution[] @test robj.uniforms[:projectionview][] == cam.projectionview[] -end - -@testset "gl Object deletion" begin - GLMakie.closeall() - - # image so we have a user generated texture in the mix - # texture atlas is triggered by text - # include SSAO to make sure its cleanup works too - f,a,p = image(rand(4,4)) - screen = display(f, ssao = true, visible = false) - colorbuffer(screen) - - # verify that SSAO is active - @test screen.postprocessors[1] != GLMakie.empty_postprocessor() - - framebuffer = screen.framebuffer - framebuffer_textures = copy(screen.framebuffer.buffers) - atlas_textures = first.(values(GLMakie.atlas_texture_cache)) - shaders = vcat([[shader for shader in values(shaders)] for shaders in values(screen.shader_cache.shader_cache)]...) - programs = [program for program in values(screen.shader_cache.program_cache)] - postprocessors = copy(screen.postprocessors) - robjs = last.(screen.renderlist) - - GLMakie.destroy!(screen) - - @testset "Texture Atlas" begin - @test !isempty(atlas_textures) - for tex in atlas_textures - @test tex.id == 0 - end - end - - @testset "Framebuffer" begin - @test framebuffer.id == 0 - @test !isempty(framebuffer_textures) - for (k, tex) in framebuffer_textures - @test tex.id == 0 - end - end - - @testset "ShaderCache" begin - @test !isempty(shaders) - for shader in shaders - @test shader.id == 0 - end - @test !isempty(programs) - for program in programs - @test program.id == 0 - end - end - - function validate_robj(robj) - for uniform in robj.uniforms - if to_value(uniform) isa GLMakie.GLAbstraction.GPUArray - @test to_value(uniform).id == 0 - end - end - @test robj.vertexarray.id == 0 - if robj.vertexarray.indices isa GLMakie.GLAbstraction.GPUArray - @test robj.vertexarray.indices.id == 0 - end - for buffer in values(robj.vertexarray.buffers) - @test buffer.id == 0 - end - @test robj.vertexarray.program.id == 0 - for shader in robj.vertexarray.program.shader - @test shader.id == 0 - end - end - - @testset "PostProcessors" begin - @test map(pp -> length(pp.robjs), postprocessors) == [2,1,2,1] - for pp in postprocessors - for robj in pp.robjs - validate_robj(robj) - end - end - end - - @testset "RenderObjects" begin - @test !isempty(robjs) - for robj in robjs - validate_robj(robj) - end - end end \ No newline at end of file From 86d4b0bf9863f803d06c19e162640d298e2fed9f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 16:16:22 +0100 Subject: [PATCH 033/135] warn on dead context --- GLMakie/src/GLAbstraction/GLTypes.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 4d2cfc59f85..89b74d0ba49 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -448,6 +448,7 @@ function free(x::T, called_from_finalizer = false) where {T} if called_from_finalizer if !context_alive(x.context) Threads.@spawn println(stderr, "Warning: free(::$T) called with dead context from scene finalizer.") + x.id = 0 return end try @@ -456,6 +457,11 @@ function free(x::T, called_from_finalizer = false) where {T} Threads.@spawn Base.showerror(stderr, e) end else + if !context_alive(x.context) + @warn "free(::$T) called with dead context." + x.id = 0 + return + end # context must be valid require_context(x.context) unsafe_free(x) From e54c2e23e8eee9199d05132d5a2c233c42dec641 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 18:44:31 +0100 Subject: [PATCH 034/135] avoid the last few current_context() calls --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 +- GLMakie/src/GLAbstraction/GLBuffer.jl | 4 +- GLMakie/src/GLAbstraction/GLShader.jl | 15 +++-- GLMakie/src/GLAbstraction/GLTypes.jl | 27 ++++---- GLMakie/src/GLAbstraction/GLUniforms.jl | 67 ++++++++++---------- GLMakie/src/GLAbstraction/GLUtils.jl | 7 +- GLMakie/src/glshaders/mesh.jl | 2 +- GLMakie/src/glshaders/particles.jl | 2 +- GLMakie/src/glshaders/visualize_interface.jl | 30 +++++---- 9 files changed, 79 insertions(+), 77 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index a33ac3a9b7b..4fd40232bee 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -45,9 +45,9 @@ import ModernGL.glGetShaderiv import ModernGL.glViewport import ModernGL.glScissor +include("shaderabstraction.jl") include("GLUtils.jl") -include("shaderabstraction.jl") include("GLTypes.jl") export GLProgram # Shader/program object export Texture # Texture object, basically a 1/2/3D OpenGL data array diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index f6f55c34127..8c67fbcc5db 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -94,9 +94,9 @@ end function indexbuffer( - buffer::VectorTypes{T}; usage::GLenum = GL_STATIC_DRAW + context, buffer::VectorTypes{T}; usage::GLenum = GL_STATIC_DRAW ) where T <: GLArrayEltypes - return GLBuffer(current_context(), buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage) + return GLBuffer(context, buffer, buffertype = GL_ELEMENT_ARRAY_BUFFER, usage=usage) end # GPUArray interface function gpu_data(b::GLBuffer{T}) where T diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 5400cc5864c..13da407dc31 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -128,7 +128,7 @@ struct LazyShader <: AbstractLazyShader end end -gl_convert(shader::GLProgram, data) = shader +gl_convert(::GLContext, shader::GLProgram, data) = shader function compile_shader(context, source::ShaderSource, template_src::String) name = source.name @@ -139,7 +139,7 @@ function compile_shader(context, source::ShaderSource, template_src::String) GLAbstraction.print_with_lines(template_src) @warn("shader $(name) didn't compile. \n$(GLAbstraction.getinfolog(shaderid))") end - return Shader(name, Vector{UInt8}(template_src), source.typ, shaderid, context) + return Shader(context, name, Vector{UInt8}(template_src), source.typ, shaderid) end @@ -191,7 +191,7 @@ function compile_program(shaders::Vector{Shader}, fragdatalocation) # generate the link locations nametypedict = uniform_name_type(program) uniformlocationdict = uniformlocations(nametypedict, program) - GLProgram(program, shaders, nametypedict, uniformlocationdict) + return GLProgram(program, shaders, nametypedict, uniformlocationdict) end function get_view(kw_dict) @@ -202,12 +202,13 @@ function get_view(kw_dict) _view end -gl_convert(lazyshader::AbstractLazyShader, data) = error("gl_convert shader") -function gl_convert(lazyshader::LazyShader, data) - gl_convert(lazyshader.shader_cache, lazyshader, data) +gl_convert(::GLContext, lazyshader::AbstractLazyShader, data) = error("gl_convert shader") +function gl_convert(ctx::GLContext, lazyshader::LazyShader, data) + gl_convert(ctx, lazyshader.shader_cache, lazyshader, data) end -function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) +function gl_convert(ctx::GLContext, cache::ShaderCache, lazyshader::AbstractLazyShader, data) + require_context(cache.context, ctx) kw_dict = lazyshader.kw_args paths = lazyshader.paths diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 89b74d0ba49..e60c2b57a54 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -28,15 +28,15 @@ mutable struct Shader id::GLuint context::GLContext - function Shader(name, source, typ, id, context = current_context()) + function Shader(context, name, source, typ, id) obj = new(Symbol(name), source, typ, id, context) finalizer(verify_free, obj) return obj end end -function Shader(name, source::Vector{UInt8}, typ) - compile_shader(source, typ, name) +function Shader(context, name, source::Vector{UInt8}, typ) + return compile_shader(context, source, typ, name) end name(s::Shader) = s.name @@ -64,8 +64,8 @@ mutable struct GLProgram nametype::Dict{Symbol,GLenum} uniformloc::Dict{Symbol,Tuple} context::GLContext - function GLProgram(id::GLuint, shader::Vector{Shader}, nametype::Dict{Symbol,GLenum}, uniformloc::Dict{Symbol,Tuple}) - obj = new(id, shader, nametype, uniformloc, current_context()) + function GLProgram(id::GLuint, shader::Vector{Shader}, nametype::Dict{Symbol,GLenum}, uniformloc::Dict{Symbol,Tuple}, context = first(shader).context) + obj = new(id, shader, nametype, uniformloc, context) finalizer(verify_free, obj) obj end @@ -344,7 +344,7 @@ end function RenderObject( data::Dict{Symbol,Any}, program, pre::Pre, post, - context=current_context() + context ) where Pre switch_context!(context) require_context(context) @@ -374,25 +374,20 @@ function RenderObject( # glconvert is designed to convert everything to a fitting opengl datatype, but sometimes # the conversion is not unique. (E.g. Array -> Texture, TextureBuffer, GLBuffer, ...) # In these cases an explicit conversion target is required - if targets[k] isa GPUArray - GLAbstraction.require_context(nw) - data[k] = gl_convert(context, targets[k], v) - else - data[k] = gl_convert(targets[k], v) - end + data[k] = gl_convert(context, targets[k], v) else k in (:indices, :visible, :ssao, :label, :cycle) && continue # structs are decomposed into fields # $k.$fieldname -> v.$fieldname if isa_gl_struct(v) - merge!(data, gl_convert_struct(v, k)) + merge!(data, gl_convert_struct(context, v, k)) delete!(data, k) # try direct conversion - elseif applicable(gl_convert, v) + elseif applicable(gl_convert, context, v) try - data[k] = gl_convert(v) + data[k] = gl_convert(context, v) catch e @error "gl_convert for key `$k` failed" rethrow(e) @@ -407,7 +402,7 @@ function RenderObject( end buffers = filter(((key, value),) -> isa(value, GLBuffer) || key === :indices, data) - program = gl_convert(to_value(program), data) # "compile" lazyshader + program = gl_convert(context, to_value(program), data) # "compile" lazyshader vertexarray = GLVertexArray(Dict(buffers), program) # remove all uniforms not occurring in shader diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index d1b9aeff8be..afa5a612ff1 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -190,16 +190,17 @@ gl_promote(x::Type{T}) where {T <: BGR} = BGR{gl_promote(eltype(T))} gl_promote(x::Type{Vec{N, T}}) where {N, T} = Vec{N, gl_promote(T)} gl_promote(x::Type{Point{N, T}}) where {N, T} = Point{N, gl_promote(T)} -gl_convert(x::AbstractVector{Vec3f}) = x +# Note: GLContext is currently just Any +gl_convert(::GLContext, x::AbstractVector{Vec3f}) = x -gl_convert(x::T) where {T <: Number} = gl_promote(T)(x) -gl_convert(x::T) where {T <: Colorant} = gl_promote(T)(x) -gl_convert(x::T) where {T <: AbstractMesh} = gl_convert(x) -gl_convert(x::T) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) -gl_convert(x::Observable{T}) where {T <: GeometryBasics.Mesh} = gl_promote(T)(x) +gl_convert(::GLContext, x::T) where {T <: Number} = gl_promote(T)(x) +gl_convert(::GLContext, x::T) where {T <: Colorant} = gl_promote(T)(x) +gl_convert(ctx::GLContext, x::T) where {T <: AbstractMesh} = gl_convert(ctx, x) +gl_convert(::GLContext, x::T) where {T <: GeometryBasics.Mesh} = gl_promote(T)(ctx, x) +gl_convert(::GLContext, x::Observable{T}) where {T <: GeometryBasics.Mesh} = gl_promote(T)(ctx, x) -gl_convert(s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(current_context(), s) -gl_convert(s::Nothing) = s +gl_convert(ctx::GLContext, s::Vector{Matrix{T}}) where {T<:Colorant} = Texture(ctx, s) +gl_convert(::GLContext, s::Nothing) = s isa_gl_struct(x::Observable) = isa_gl_struct(to_value(x)) @@ -214,19 +215,19 @@ function isa_gl_struct(x::T) where T fnames = fieldnames(T) !isempty(fnames) && all(name -> isconcretetype(fieldtype(T, name)) && isbits(getfield(x, name)), fnames) end -function gl_convert_struct(obs::Observable{T}, uniform_name::Symbol) where T +function gl_convert_struct(ctx::GLContext, obs::Observable{T}, uniform_name::Symbol) where T if isa_gl_struct(obs) return Dict{Symbol, Any}(map(fieldnames(T)) do name - Symbol("$uniform_name.$name") => map(x -> gl_convert(getfield(x, name)), obs) + Symbol("$uniform_name.$name") => map(x -> gl_convert(ctx, getfield(x, name)), obs) end) else error("can't convert $obs to a OpenGL type. Make sure all fields are of a concrete type and isbits(FieldType)-->true") end end -function gl_convert_struct(x::T, uniform_name::Symbol) where T +function gl_convert_struct(ctx::GLContext, x::T, uniform_name::Symbol) where T if isa_gl_struct(x) return Dict{Symbol, Any}(map(fieldnames(T)) do name - Symbol("$uniform_name.$name") => gl_convert(getfield(x, name)) + Symbol("$uniform_name.$name") => gl_convert(ctx, getfield(x, name)) end) else error("can't convert $x to a OpenGL type. Make sure all fields are of a concrete type and isbits(FieldType)-->true") @@ -235,33 +236,33 @@ end # native types don't need convert! -gl_convert(a::T) where {T <: NATIVE_TYPES} = a -gl_convert(s::Observable{T}) where {T <: NATIVE_TYPES} = s -gl_convert(s::Observable{T}) where T = const_lift(gl_convert, s) -gl_convert(x::StaticVector{N, T}) where {N, T} = map(gl_promote(T), x) -gl_convert(x::Mat{N, M, T}) where {N, M, T} = Mat{N, M, gl_promote(T)}(x) -gl_convert(a::AbstractVector{<: AbstractFace}) = indexbuffer(s) -gl_convert(t::Type{T}, a::T; kw_args...) where T <: NATIVE_TYPES = a -gl_convert(::Type{<: GPUArray}, a::StaticVector) = gl_convert(a) -gl_convert(x::Vector) = x - -function gl_convert(T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} - T(current_context(), convert(AbstractArray{gl_promote(X), N}, a); kw_args...) +gl_convert(::GLContext, a::T) where {T <: NATIVE_TYPES} = a +gl_convert(::GLContext, s::Observable{T}) where {T <: NATIVE_TYPES} = s +gl_convert(ctx::GLContext, s::Observable{T}) where T = const_lift(x -> gl_convert(ctx, x), s) +gl_convert(::GLContext, x::StaticVector{N, T}) where {N, T} = map(gl_promote(T), x) +gl_convert(::GLContext, x::Mat{N, M, T}) where {N, M, T} = Mat{N, M, gl_promote(T)}(x) +gl_convert(::GLContext, a::AbstractVector{<: AbstractFace}) = indexbuffer(ctx, s) +gl_convert(::GLContext, t::Type{T}, a::T; kw_args...) where T <: NATIVE_TYPES = a +gl_convert(ctx::GLContext, ::Type{<: GPUArray}, a::StaticVector) = gl_convert(ctx, a) +gl_convert(::GLContext, x::Vector) = x + +function gl_convert(ctx::GLContext, T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} + return T(ctx, convert(AbstractArray{gl_promote(X), N}, a); kw_args...) end -gl_convert(::Type{<: GLBuffer}, x::GLBuffer; kw_args...) = x -gl_convert(::Type{Texture}, x::Texture) = x -gl_convert(::Type{<: GPUArray}, x::GPUArray) = x +gl_convert(::GLContext, ::Type{<: GLBuffer}, x::GLBuffer; kw_args...) = x +gl_convert(::GLContext, ::Type{Texture}, x::Texture) = x +gl_convert(::GLContext, ::Type{<: GPUArray}, x::GPUArray) = x -function gl_convert(::Type{T}, a::Vector{Array{X, 2}}; kw_args...) where {T <: Texture, X} - T(current_context(), a; kw_args...) +function gl_convert(ctx::GLContext, ::Type{T}, a::Vector{Array{X, 2}}; kw_args...) where {T <: Texture, X} + return T(ctx, a; kw_args...) end -gl_convert(::Type{<: GPUArray}, a::Observable{<: StaticVector}) = gl_convert(a) +gl_convert(ctx::GLContext, ::Type{<: GPUArray}, a::Observable{<: StaticVector}) = gl_convert(ctx, a) -function gl_convert(::Type{T}, a::Observable{<: AbstractArray{X, N}}; kw_args...) where {T <: GPUArray, X, N} +function gl_convert(ctx::GLContext, ::Type{T}, a::Observable{<: AbstractArray{X, N}}; kw_args...) where {T <: GPUArray, X, N} TGL = gl_promote(X) s = (X == TGL) ? a : lift(x-> convert(Array{TGL, N}, x), a) - T(current_context(), s; kw_args...) + return T(ctx, s; kw_args...) end -gl_convert(f::Function, a) = f(a) +gl_convert(ctx::GLContext, f::Function, a) = f(ctx, a) diff --git a/GLMakie/src/GLAbstraction/GLUtils.jl b/GLMakie/src/GLAbstraction/GLUtils.jl index 5fe35eda5c7..254b7ed1335 100644 --- a/GLMakie/src/GLAbstraction/GLUtils.jl +++ b/GLMakie/src/GLAbstraction/GLUtils.jl @@ -8,13 +8,16 @@ end print_with_lines(text::AbstractString) = print_with_lines(stdout, text) +# Using nothing as a stand-in for GLContext = Any +@assert GLContext === Any "matches_target() needs to be updates to check the correct type" + """ Needed to match the lazy gl_convert exceptions. `Target`: targeted OpenGL type `x`: the variable that gets matched """ -matches_target(::Type{Target}, x::T) where {Target, T} = applicable(gl_convert, Target, x) || T <: Target # it can be either converted to Target, or it's already the target -matches_target(::Type{Target}, x::Observable{T}) where {Target, T} = applicable(gl_convert, Target, x) || T <: Target +matches_target(::Type{Target}, x::T) where {Target, T} = applicable(gl_convert, nothing, Target, x) || T <: Target # it can be either converted to Target, or it's already the target +matches_target(::Type{Target}, x::Observable{T}) where {Target, T} = applicable(gl_convert, nothing, Target, x) || T <: Target matches_target(::Function, x) = true matches_target(::Function, x::Nothing) = false diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 9deeb020765..3cd14d5f7a3 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -1,7 +1,7 @@ function to_opengl_mesh!(context, result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) m = convert(Observable, mesh_obs) - result[:faces] = indexbuffer(map(faces, m)) + result[:faces] = indexbuffer(context, map(faces, m)) result[:vertices] = GLBuffer(context, map(coordinates, m)) function to_buffer(name, target) diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 7a25f8c8f0f..5032180508b 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -25,7 +25,7 @@ vec2quaternion(rotation::VectorTypes) = const_lift(x-> vec2quaternion.(x), rotat vec2quaternion(rotation::Observable) = lift(vec2quaternion, rotation) vec2quaternion(rotation::Makie.Quaternion)= Vec4f(rotation.data) vec2quaternion(rotation)= vec2quaternion(to_rotation(rotation)) -GLAbstraction.gl_convert(rotation::Makie.Quaternion)= Vec4f(rotation.data) +GLAbstraction.gl_convert(::GLAbstraction.GLContext, rotation::Makie.Quaternion)= Vec4f(rotation.data) to_pointsize(x::Number) = Float32(x) to_pointsize(x) = Float32(x[1]) struct PointSizeRender diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index d861120bb59..9cdc5ebbd29 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -46,7 +46,7 @@ Base.iterate(g::Grid, i=1) = i <= length(g) ? (g[i], i + 1) : nothing GLAbstraction.isa_gl_struct(x::Grid) = true GLAbstraction.toglsltype_string(t::Grid{N,T}) where {N,T} = "uniform Grid$(N)D" -function GLAbstraction.gl_convert_struct(g::Grid{N,T}, uniform_name::Symbol) where {N,T} +function GLAbstraction.gl_convert_struct(::GLAbstraction.GLContext, g::Grid{N,T}, uniform_name::Symbol) where {N,T} return Dict{Symbol,Any}( Symbol("$uniform_name.start") => Vec{N,Float32}(minimum.(g.dims)), Symbol("$uniform_name.stop") => Vec{N,Float32}(maximum.(g.dims)), @@ -54,7 +54,7 @@ function GLAbstraction.gl_convert_struct(g::Grid{N,T}, uniform_name::Symbol) whe Symbol("$uniform_name.dims") => Vec{N,Cint}(map(length, g.dims)) ) end -function GLAbstraction.gl_convert_struct(g::Grid{1,T}, uniform_name::Symbol) where T +function GLAbstraction.gl_convert_struct(::GLAbstraction.GLContext, g::Grid{1,T}, uniform_name::Symbol) where T x = g.dims[1] return Dict{Symbol,Any}( Symbol("$uniform_name.start") => Float32(minimum(x)), @@ -84,8 +84,8 @@ struct GLVisualizeShader <: AbstractLazyShader end end -function GLAbstraction.gl_convert(shader::GLVisualizeShader, data) - GLAbstraction.gl_convert(shader.screen.shader_cache, shader, data) +function GLAbstraction.gl_convert(ctx::GLAbstraction.GLContext, shader::GLVisualizeShader, data) + GLAbstraction.gl_convert(ctx, shader.screen.shader_cache, shader, data) end function assemble_shader(data) @@ -124,26 +124,28 @@ end """ Converts index arrays to the OpenGL equivalent. """ -to_index_buffer(x::GLBuffer) = x -to_index_buffer(x::TOrSignal{Int}) = x -to_index_buffer(x::VecOrSignal{UnitRange{Int}}) = x -to_index_buffer(x::TOrSignal{UnitRange{Int}}) = x +to_index_buffer(::GLAbstraction.GLContext, x::GLBuffer) = x +to_index_buffer(::GLAbstraction.GLContext, x::TOrSignal{Int}) = x +to_index_buffer(::GLAbstraction.GLContext, x::VecOrSignal{UnitRange{Int}}) = x +to_index_buffer(::GLAbstraction.GLContext, x::TOrSignal{UnitRange{Int}}) = x """ For integers, we transform it to 0 based indices """ -to_index_buffer(x::AbstractVector{I}) where {I <: Integer} = indexbuffer(Cuint.(x .- 1)) -function to_index_buffer(x::Observable{<: AbstractVector{I}}) where I <: Integer - indexbuffer(lift(x -> Cuint.(x .- 1), x)) +function to_index_buffer(ctx::GLAbstraction.GLContext, x::AbstractVector{I}) where {I <: Integer} + return indexbuffer(ctx, Cuint.(x .- 1)) +end +function to_index_buffer(ctx::GLAbstraction.GLContext, x::Observable{<: AbstractVector{I}}) where I <: Integer + return indexbuffer(ctx, lift(x -> Cuint.(x .- 1), x)) end """ If already GLuint, we assume its 0 based (bad heuristic, should better be solved with some Index type) """ -function to_index_buffer(x::VectorTypes{I}) where I <: Union{GLuint,LineFace{GLIndex}} - indexbuffer(x) +function to_index_buffer(ctx::GLAbstraction.GLContext, x::VectorTypes{I}) where I <: Union{GLuint,LineFace{GLIndex}} + indexbuffer(ctx, x) end -to_index_buffer(x) = error( +to_index_buffer(ctx, x) = error( "Not a valid index type: $(typeof(x)). Please choose from Int, Vector{UnitRange{Int}}, Vector{Int} or a signal of either of them" ) From c31bac891dbad6bba23f2a58b820bcfd49ba638b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 19:47:20 +0100 Subject: [PATCH 035/135] fix scatter indices --- GLMakie/src/glshaders/particles.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 5032180508b..36fd98e5131 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -224,7 +224,7 @@ function draw_scatter(screen, (marker, position), data) p4d[3] / p4d[4] end UInt32.(sortperm(depth_vals, rev = true) .- 1) - end |> indexbuffer + end end @gen_defaults! data begin From 6e4adda859861ad740714d7c596cc21ecb7ee4e0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 23 Dec 2024 17:55:27 +0100 Subject: [PATCH 036/135] start reworking render pipeline --- GLMakie/src/postprocessing.jl | 267 ++++++++++++++++++++-------------- GLMakie/src/rendering.jl | 9 +- GLMakie/src/screen.jl | 37 ++--- 3 files changed, 175 insertions(+), 138 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index fded72f1cf5..16fd19e6392 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -18,27 +18,50 @@ end rcpframe(x) = 1f0 ./ Vec2f(x[1], x[2]) -struct PostProcessor{F} - robjs::Vector{RenderObject} - render::F - constructor::Any +abstract type AbstractRenderStep end +prepare_step(screen, glscene, ::AbstractRenderStep) = nothing +run_step(screen, glscene, ::AbstractRenderStep) = nothing + +struct RenderPipeline + steps::Vector{AbstractRenderStep} +end + +function render_frame(screen, glscene, pipeline::RenderPipeline) + for step in pipeline.steps + prepare_step(screen, glscene, step) + end + for step in pipeline.steps + run_step(screen, glscene, step) + end + return end -function empty_postprocessor(args...; kwargs...) - PostProcessor(RenderObject[], screen -> nothing, empty_postprocessor) +# TODO: temporary, we should get to the point where this is not needed +struct EmptyRenderStep <: AbstractRenderStep end + +# Vaguely leaning on Vulkan Terminology +struct RenderPass{Name} <: AbstractRenderStep + framebuffer::GLFramebuffer + passes::Vector{RenderObject} + renames::Dict{Symbol, Symbol} # TODO: temporary until GLFramebuffer not shared +end +function RenderPass{Name}(framebuffer::GLFramebuffer, passes::Vector{RenderObject}) where {Name} + return RenderPass{Name}(framebuffer, passes, Dict{Symbol, Symbol}()) end +function RenderPass{:OIT}(screen) + @debug "Creating OIT postprocessor" + + framebuffer = screen.framebuffer -function OIT_postprocessor(framebuffer, shader_cache) # Based on https://jcgt.org/published/0002/02/09/, see #1390 # OIT setup shader = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/OIT_blend.frag") ) data = Dict{Symbol, Any}( - # :opaque_color => framebuffer[:color][2], :sum_color => framebuffer[:HDR_color][2], :prod_alpha => framebuffer[:OIT_weight][2], ) @@ -61,26 +84,29 @@ function OIT_postprocessor(framebuffer, shader_cache) ) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - color_id = framebuffer[:color][1] - full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - - # Blend transparent onto opaque - glDrawBuffer(color_id) - glViewport(0, 0, w, h) - GLAbstraction.render(pass) - end + return RenderPass{:OIT}(framebuffer, RenderObject[pass]) +end - PostProcessor(RenderObject[pass], full_render, OIT_postprocessor) +function run_step(screen, glscene, step::RenderPass{:OIT}) + # Blend transparent onto opaque + wh = size(screen.framebuffer) + glViewport(0, 0, wh[1], wh[2]) + glDrawBuffer(step.framebuffer[:color][1]) + GLAbstraction.render(step.passes[1]) + return end +function RenderPass{:SSAO}(screen) + @debug "Creating SSAO postprocessor" + framebuffer = screen.framebuffer + renames = Dict{Symbol, Symbol}() function ssao_postprocessor(framebuffer, shader_cache) ShaderAbstractions.switch_context!(shader_cache.context) require_context(shader_cache.context) + # Add missing buffers if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) @@ -97,8 +123,10 @@ function ssao_postprocessor(framebuffer, shader_cache) shader_cache.context, Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge ) normal_occ_id = attach_colorbuffer!(framebuffer, :normal_occlusion, normal_occlusion_buffer) + renames[:normal_occlusion] = :normal_occlusion else normal_occ_id = framebuffer[:HDR_color][1] + renames[:normal_occlusion] = :HDR_color end push!(framebuffer.render_buffer_ids, normal_occ_id) end @@ -115,7 +143,7 @@ function ssao_postprocessor(framebuffer, shader_cache) # compute occlusion shader1 = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO.frag"), view = Dict( @@ -141,7 +169,7 @@ function ssao_postprocessor(framebuffer, shader_cache) # blur occlusion and combine with color shader2 = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO_blur.frag") ) @@ -154,61 +182,56 @@ function ssao_postprocessor(framebuffer, shader_cache) ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - color_id = framebuffer[:color][1] - - full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - - # Setup rendering - # SSAO - calculate occlusion - glDrawBuffer(normal_occ_id) # occlusion buffer - glViewport(0, 0, w, h) - glEnable(GL_SCISSOR_TEST) - ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) - - for (screenid, scene) in screen.screens - # Select the area of one leaf scene - # This should be per scene because projection may vary between - # scenes. It should be a leaf scene to avoid repeatedly shading - # the same region (though this is not guaranteed...) - isempty(scene.children) || continue - a = viewport(scene)[] - glScissor(ppu(minimum(a))..., ppu(widths(a))...) - # update uniforms - data1[:projection] = Mat4f(scene.camera.projection[]) - data1[:bias] = scene.ssao.bias[] - data1[:radius] = scene.ssao.radius[] - GLAbstraction.render(pass1) - end - # SSAO - blur occlusion and apply to color - glDrawBuffer(color_id) # color buffer - for (screenid, scene) in screen.screens - # Select the area of one leaf scene - isempty(scene.children) || continue - a = viewport(scene)[] - glScissor(ppu(minimum(a))..., ppu(widths(a))...) - # update uniforms - data2[:blur_range] = scene.ssao.blur - GLAbstraction.render(pass2) - end - glDisable(GL_SCISSOR_TEST) + return RenderPass{:SSAO}(framebuffer, [pass1, pass2], renames) +end + +function run_step(screen, glscene, step::RenderPass{:SSAO}) + wh = size(screen.framebuffer) + + glViewport(0, 0, wh[1], wh[2]) + glDrawBuffer(step.framebuffer[step.renames[:normal_occ_id]][1]) # occlusion buffer + glEnable(GL_SCISSOR_TEST) + ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) + + data1 = step.passes[1].uniforms + for (screenid, scene) in screen.screens + # Select the area of one leaf scene + # This should be per scene because projection may vary between + # scenes. It should be a leaf scene to avoid repeatedly shading + # the same region (though this is not guaranteed...) + isempty(scene.children) || continue + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) + # update uniforms + data1[:projection] = scene.camera.projection[] + data1[:bias] = scene.ssao.bias[] + data1[:radius] = scene.ssao.radius[] + GLAbstraction.render(step.passes[1]) end - require_context(shader_cache.context) + # SSAO - blur occlusion and apply to color + glDrawBuffer(step.framebuffer[:color][1]) # color buffer + data2 = step.passes[2].uniforms + for (screenid, scene) in screen.screens + # Select the area of one leaf scene + isempty(scene.children) || continue + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) + # update uniforms + data2[:blur_range] = scene.ssao.blur + GLAbstraction.render(step.passes[2]) + end + glDisable(GL_SCISSOR_TEST) - PostProcessor(RenderObject[pass1, pass2], full_render, ssao_postprocessor) + return end -""" - fxaa_postprocessor(framebuffer, shader_cache) -Returns a PostProcessor that handles fxaa. -""" -function fxaa_postprocessor(framebuffer, shader_cache) - ShaderAbstractions.switch_context!(shader_cache.context) - require_context(shader_cache.context) +function RenderPass{:FXAA}(screen) + @debug "Creating FXAA postprocessor" + framebuffer = screen.framebuffer + renames = Dict{Symbol, Symbol}() # Add missing buffers if !haskey(framebuffer, :color_luma) @@ -218,14 +241,16 @@ function fxaa_postprocessor(framebuffer, shader_cache) shader_cache.context, RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge ) luma_id = attach_colorbuffer!(framebuffer, :color_luma, color_luma_buffer) + renames[:color_luma] = :color_luma else luma_id = framebuffer[:HDR_color][1] + renames[:color_luma] = :HDR_color end end # calculate luma for FXAA shader1 = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/postprocess.frag") ) @@ -238,7 +263,7 @@ function fxaa_postprocessor(framebuffer, shader_cache) # perform FXAA shader2 = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/fxaa.frag") ) @@ -249,42 +274,42 @@ function fxaa_postprocessor(framebuffer, shader_cache) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - color_id = framebuffer[:color][1] - full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - - # FXAA - calculate LUMA - glDrawBuffer(luma_id) - glViewport(0, 0, w, h) - # necessary with negative SSAO bias... - glClearColor(1, 1, 1, 1) - glClear(GL_COLOR_BUFFER_BIT) - GLAbstraction.render(pass1) - - # FXAA - perform anti-aliasing - glDrawBuffer(color_id) # color buffer - GLAbstraction.render(pass2) - end + return RenderPass{:FXAA}(framebuffer, RenderObject[pass1, pass2], renames) +end - require_context(shader_cache.context) +function run_step(screen, glscene, step::RenderPass{:FXAA}) + # TODO: make scissor explicit? + wh = size(screen.framebuffer) + glViewport(0, 0, wh[1], wh[2]) + + # FXAA - calculate LUMA + glDrawBuffer(step.framebuffer[step.renames[:color_luma]][1]) + # necessary with negative SSAO bias... + glClearColor(1, 1, 1, 1) + glClear(GL_COLOR_BUFFER_BIT) + GLAbstraction.render(step.passes[1]) + + # FXAA - perform anti-aliasing + glDrawBuffer(step.framebuffer[:color][1]) # color buffer + GLAbstraction.render(step.passes[2]) - PostProcessor(RenderObject[pass1, pass2], full_render, fxaa_postprocessor) + return end +struct BlitToScreen <: AbstractRenderStep + framebuffer::GLFramebuffer + pass::RenderObject + screen_framebuffer_id::Int +end -""" - to_screen_postprocessor(framebuffer, shader_cache, default_id = nothing) +# TODO: replacement for CImGUI? +function BlitToScreen(screen, screen_framebuffer_id = 0) + @debug "Creating to screen postprocessor" + framebuffer = screen.framebuffer -Sets up a Postprocessor which copies the color buffer to the screen. Used as a -final step for displaying the screen. The argument `screen_fb_id` can be used -to pass in a reference to the framebuffer ID of the screen. If `nothing` is -used (the default), 0 is used. -""" -function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothing) # draw color buffer shader = LazyShader( - shader_cache, + screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/copy.frag") ) @@ -294,20 +319,36 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi pass = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - full_render = screen -> begin - # transfer everything to the screen - default_id = isnothing(screen_fb_id) ? 0 : screen_fb_id[] - # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering - glBindFramebuffer(GL_FRAMEBUFFER, default_id) - glViewport(0, 0, framebuffer_size(screen)...) - glClear(GL_COLOR_BUFFER_BIT) - GLAbstraction.render(pass) # copy postprocess - end - - PostProcessor(RenderObject[pass], full_render, to_screen_postprocessor) + return BlitToScreen(framebuffer, pass, screen_framebuffer_id) end -function destroy!(pp::PostProcessor) - foreach(destroy!, pp.robjs) +function run_step(screen, ::Nothing, step::BlitToScreen) + # TODO: Is this an observable? Can this be static? + # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering + glBindFramebuffer(GL_FRAMEBUFFER, step.screen_framebuffer_id) + glViewport(0, 0, framebuffer_size(screen)...) + # glScissor(0, 0, wh[1], wh[2]) + + # clear target + # TODO: Could be skipped if glscenes[1] clears to opaque (maybe use dedicated shader?) + # TODO: Should this be cleared if we don't own the target? + glClearColor(1,1,1,1) + glClear(GL_COLOR_BUFFER_BIT) + + # transfer everything to the screen + GLAbstraction.render(step.pass) # copy postprocess + return end + +function destroy!(step::T) where {T <: AbstractRenderStep} + @debug "Default destructor of $T" + if hasfield(T, :passes) + while !isempty(step.passes) + destroy!(pop!(step.passes)) + end + elseif hasfield(T, :pass) + destroy!(step.pass) + end + return +end \ No newline at end of file diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index ee717ea950e..ed3ea82eda6 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -68,7 +68,8 @@ function render_frame(screen::Screen; resize_buffers=true) return !Bool(robj[:transparency][]) && Bool(robj[:ssao][]) end # SSAO - screen.postprocessors[1].render(screen) + run_step(screen, nothing, screen.render_pipeline[1]) + # render no SSAO glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1]) @@ -94,13 +95,13 @@ function render_frame(screen::Screen; resize_buffers=true) end # TRANSPARENT BLEND - screen.postprocessors[2].render(screen) + run_step(screen, nothing, screen.render_pipeline[2]) # FXAA - screen.postprocessors[3].render(screen) + run_step(screen, nothing, screen.render_pipeline[3]) # transfer everything to the screen - screen.postprocessors[4].render(screen) + run_step(screen, nothing, screen.render_pipeline[4]) GLAbstraction.require_context(nw) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index f1ed3eef24a..ac859c9fc8e 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -172,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} - postprocessors::Vector{PostProcessor} + render_pipeline::Vector{AbstractRenderStep} cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, Plot} framecache::Matrix{RGB{N0f8}} @@ -198,7 +198,6 @@ mutable struct Screen{GLWindow} <: MakieScreen screen2scene::Dict{WeakRef, ScreenID}, screens::Vector{ScreenArea}, renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}}, - postprocessors::Vector{PostProcessor}, cache::Dict{UInt64, RenderObject}, cache2plot::Dict{UInt32, AbstractPlot}, reuse::Bool @@ -209,7 +208,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen, owns_glscreen, shader_cache, framebuffer, config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, - screens, renderlist, postprocessors, cache, cache2plot, + screens, renderlist, AbstractRenderStep[], cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) @@ -280,13 +279,7 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) # This is important for resource tracking, and only needed for the first context ShaderAbstractions.switch_context!(window) shader_cache = GLAbstraction.ShaderCache(window) - fb = GLFramebuffer(window, initial_resolution) - postprocessors = [ - empty_postprocessor(), - empty_postprocessor(), - empty_postprocessor(), - to_screen_postprocessor(fb, shader_cache) - ] + fb = GLFramebuffer(initial_resolution) screen = Screen( window, owns_glscreen, shader_cache, fb, @@ -295,12 +288,16 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) Dict{WeakRef, ScreenID}(), ScreenArea[], Tuple{ZIndex, ScreenID, RenderObject}[], - postprocessors, Dict{UInt64, RenderObject}(), Dict{UInt32, AbstractPlot}(), reuse, ) + push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, BlitToScreen(screen)) + if owns_glscreen GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) @@ -390,20 +387,18 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] - function replace_processor!(postprocessor, idx) - fb = screen.framebuffer - shader_cache = screen.shader_cache - post = screen.postprocessors[idx] - if post.constructor !== postprocessor - destroy!(screen.postprocessors[idx]) - screen.postprocessors[idx] = postprocessor(fb, shader_cache) + function replace_renderpass!(pass, idx) + prev = screen.render_pipeline[idx] + if typeof(prev) !== pass + destroy!(prev) + screen.render_pipeline[idx] = pass(screen) end return end - replace_processor!(config.ssao ? ssao_postprocessor : empty_postprocessor, 1) - replace_processor!(config.oit ? OIT_postprocessor : empty_postprocessor, 2) - replace_processor!(config.fxaa ? fxaa_postprocessor : empty_postprocessor, 3) + replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 1) + replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 2) + replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 3) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters From ad80f6adb8601b558c37b281af118ea2105059b0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 00:02:30 +0100 Subject: [PATCH 037/135] move gl part of Framebuffer to GLAbstraction --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 7 + GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 175 +++++++++++++++++++++ GLMakie/src/glwindow.jl | 146 +++-------------- GLMakie/src/postprocessing.jl | 63 ++++---- GLMakie/src/precompiles.jl | 2 +- GLMakie/src/rendering.jl | 10 +- GLMakie/src/screen.jl | 12 +- 7 files changed, 249 insertions(+), 166 deletions(-) create mode 100644 GLMakie/src/GLAbstraction/GLFrameBuffer.jl diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 4fd40232bee..91c14720841 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -103,4 +103,11 @@ export getUniformsInfo export getProgramInfo export getAttributesInfo +include("GLFrameBuffer.jl") +export GLRenderbuffer +export GLFramebuffer +export attach_colorbuffer, attach_depthbuffer, attach_stencilbuffer +export get_attachment, get_buffer +export check_framebuffer + end # module diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl new file mode 100644 index 00000000000..c0f21417fd4 --- /dev/null +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -0,0 +1,175 @@ +# For completion sake + +# Doesn't implement getindex, setindex etc I think? +mutable struct GLRenderbuffer + id::GLuint + size::NTuple{2, Int} + format::GLenum + context::GLContext + + function GLRenderbuffer(size, format::GLenum) + renderbuffer = GLuint[0] + glGenRenderbuffers(1, renderbuffer) + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[1]) + glRenderbufferStorage(GL_RENDERBUFFER, format, size...) + + obj = new(renderbuffer[1], size, format, current_context()) + finalizer(free, obj) + + return obj + end +end + +function bind(buffer::GLRenderbuffer) + if buffer.id == 0 + error("Binding freed GLRenderbuffer") + end + glBindRenderbuffer(GL_RENDERBUFFER, buffer.id) + return +end + +function unsafe_free(x::GLRenderbuffer) + # don't free if already freed + x.id == 0 && return + # don't free from other context + GLAbstraction.context_alive(x.context) || return + GLAbstraction.switch_context!(x.context) + id = Ref(x.id) + glDeleteRenderbuffers(1, id) + x.id = 0 + return +end + + +# TODO: Add RenderBuffer, resize!() with renderbuffer (recreate?) +mutable struct GLFramebuffer + id::GLuint + size::NTuple{2, Int} + + context::GLContext + + attachments::Dict{Symbol, GLenum} + buffers::Dict{Symbol, Texture} + counter::UInt32 # for color attachments + + function GLFramebuffer(size::NTuple{2, Int}) + # Create framebuffer + id = glGenFramebuffers() + glBindFramebuffer(GL_FRAMEBUFFER, id) + + obj = new( + id, size, current_context(), + Dict{Symbol, GLuint}(), + Dict{Symbol, Texture}(), + UInt32(0) + ) + finalizer(free, obj) + + return obj + end +end + +function bind(fb::GLFramebuffer) + if fb.id == 0 + error("Binding freed GLFramebuffer") + end + glBindFramebuffer(GL_FRAMEBUFFER, fb.id) + return +end + +function unsafe_free(x::GLFramebuffer) + # don't free if already freed + x.id == 0 && return + # don't free from other context + GLAbstraction.context_alive(x.context) || return + GLAbstraction.switch_context!(x.context) + id = Ref(x.id) + glDeleteFramebuffers(1, id) + x.id = 0 + return +end + +Base.size(fb::GLFramebuffer) = fb.size +Base.haskey(fb::GLFramebuffer, key::Symbol) = haskey(fb.buffers, key) + +function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) + (w > 0 && h > 0 && (w, h) != size(fb)) || return + for (key, buffer) in fb.buffers + resize_nocopy!(buffer, (w, h)) + end + fb.size = (w, h) + return +end + +function get_next_colorbuffer_attachment(fb::GLFramebuffer) + if fb.counter >= 15 + error("The framebuffer has exhausted its maximum number of color attachments.") + end + attachment = GL_COLOR_ATTACHMENT0 + fb.counter + fb.counter += 1 + return attachment +end + +attach_colorbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, get_next_colorbuffer_attachment(fb)) +attach_depthbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_DEPTH_ATTACHMENT) +attach_stencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_STENCIL_ATTACHMENT) + +function attach(fb::GLFramebuffer, key::Symbol, buffer, attachment::GLenum) + haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") + if in(attachment, keys(fb.buffers)) + if attachment == GL_DEPTH_ATTACHMENT + type = "depth" + elseif attachment == GL_STENCIL_ATTACHMENT + type = "stencil" + else + type = "color" + end + error("Cannot attach $key as a $type attachment as it is already attached.") + end + + bind(fb) + _attach(buffer, attachment) + fb.attachments[key] = attachment + fb.buffers[key] = buffer + return attachment +end + +function _attach(t::Texture{T, 2}, attachment::GLenum) where {T} + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, t.id, 0) +end +function _attach(buffer::RenderBuffer, attachment::GLenum) + glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, buffer) +end + +get_attachment(fb::GLFramebuffer, key::Symbol) = fb.attachments[key] +get_buffer(fb::GLFramebuffer, key::Symbol) = fb.buffers[key] + +function enum_to_error(s) + s == GL_FRAMEBUFFER_COMPLETE && return + s == GL_FRAMEBUFFER_UNDEFINED && + error("GL_FRAMEBUFFER_UNDEFINED: The specified framebuffer is the default read or draw framebuffer, but the default framebuffer does not exist.") + s == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: At least one of the framebuffer attachment points is incomplete.") + s == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: The framebuffer does not have at least one image attached to it.") + s == GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: The value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for any color attachment point(s) specified by GL_DRAW_BUFFERi.") + s == GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: GL_READ_BUFFER is not GL_NONE and the value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for the color attachment point specified by GL_READ_BUFFER.") + s == GL_FRAMEBUFFER_UNSUPPORTED && + error("GL_FRAMEBUFFER_UNSUPPORTED: The combination of internal formats of the attached images violates a driver implementation-dependent set of restrictions. Check your OpenGL driver!") + s == GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && + error("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: The value of GL_RENDERBUFFER_SAMPLES is not the same for all attached renderbuffers; +if the value of GL_TEXTURE_SAMPLES is not the same for all attached textures; or, if the attached images consist of a mix of renderbuffers and textures, + the value of GL_RENDERBUFFER_SAMPLES does not match the value of GL_TEXTURE_SAMPLES. + GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE is also returned if the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not consistent across all attached textures; + or, if the attached images include a mix of renderbuffers and textures, the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not set to GL_TRUE for all attached textures.") + s == GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS && + error("GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: Any framebuffer attachment is layered, and any populated attachment is not layered, or if all populated color attachments are not from textures of the same target.") + return error("Unknown framebuffer completion error code: $s") +end + +function check_framebuffer() + status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + return enum_to_error(status) +end \ No newline at end of file diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 1c006e8aa5d..8d0e0a1d2f5 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -10,89 +10,33 @@ end Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T(s.id), T(s.index)) Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) -mutable struct GLFramebuffer - resolution::Observable{NTuple{2, Int}} - id::GLuint - - buffer_ids::Dict{Symbol, GLuint} - buffers::Dict{Symbol, Texture} +mutable struct Framebuffer + fb::GLFramebuffer render_buffer_ids::Vector{GLuint} end # it's guaranteed, that they all have the same size -Base.size(fb::GLFramebuffer) = size(fb.buffers[:color]) -Base.haskey(fb::GLFramebuffer, key::Symbol) = haskey(fb.buffers, key) -Base.getindex(fb::GLFramebuffer, key::Symbol) = fb.buffer_ids[key] => fb.buffers[key] - -function getfallback(fb::GLFramebuffer, key::Symbol, fallback_key::Symbol) - haskey(fb, key) ? fb[key] : fb[fallback_key] -end - - -function attach_framebuffer(t::Texture{T, 2}, attachment) where T - glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, t.id, 0) -end - -# attach texture as color attachment with automatic id picking -function attach_colorbuffer!(fb::GLFramebuffer, key::Symbol, t::Texture{T, 2}) where T - if haskey(fb.buffer_ids, key) || haskey(fb.buffers, key) - error("Key $key already exists.") - end - - max_color_id = GL_COLOR_ATTACHMENT0 - for id in values(fb.buffer_ids) - if GL_COLOR_ATTACHMENT0 <= id <= GL_COLOR_ATTACHMENT15 && id > max_color_id - max_color_id = id - end - end - next_color_id = max_color_id + 0x1 - if next_color_id > GL_COLOR_ATTACHMENT15 - error("Ran out of color buffers.") - end +# forwards... for now +Base.size(fb::Framebuffer) = size(fb.fb) +Base.haskey(fb::Framebuffer, key::Symbol) = haskey(fb.fb, key) +GLAbstraction.get_attachment(fb::Framebuffer, key::Symbol) = get_attachment(fb.fb, key) +GLAbstraction.get_buffer(fb::Framebuffer, key::Symbol) = get_buffer(fb.fb, key) +GLAbstraction.bind(fb::Framebuffer) = GLAbstraction.bind(fb.fb) - glFramebufferTexture2D(GL_FRAMEBUFFER, next_color_id, GL_TEXTURE_2D, t.id, 0) - push!(fb.buffer_ids, key => next_color_id) - push!(fb.buffers, key => t) - return next_color_id +function getfallback_attachment(fb::Framebuffer, key::Symbol, fallback_key::Symbol) + haskey(fb, key) ? get_attachment(fb, key) : get_attachment(fb, fallback_key) end - -function enum_to_error(s) - s == GL_FRAMEBUFFER_COMPLETE && return - s == GL_FRAMEBUFFER_UNDEFINED && - error("GL_FRAMEBUFFER_UNDEFINED: The specified framebuffer is the default read or draw framebuffer, but the default framebuffer does not exist.") - s == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT && - error("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: At least one of the framebuffer attachment points is incomplete.") - s == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT && - error("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: The framebuffer does not have at least one image attached to it.") - s == GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER && - error("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: The value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for any color attachment point(s) specified by GL_DRAW_BUFFERi.") - s == GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER && - error("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: GL_READ_BUFFER is not GL_NONE and the value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for the color attachment point specified by GL_READ_BUFFER.") - s == GL_FRAMEBUFFER_UNSUPPORTED && - error("GL_FRAMEBUFFER_UNSUPPORTED: The combination of internal formats of the attached images violates a driver implementation-dependent set of restrictions. Check your OpenGL driver!") - s == GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && - error("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: The value of GL_RENDERBUFFER_SAMPLES is not the same for all attached renderbuffers; -if the value of GL_TEXTURE_SAMPLES is not the same for all attached textures; or, if the attached images consist of a mix of renderbuffers and textures, - the value of GL_RENDERBUFFER_SAMPLES does not match the value of GL_TEXTURE_SAMPLES. - GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE is also returned if the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not consistent across all attached textures; - or, if the attached images include a mix of renderbuffers and textures, the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not set to GL_TRUE for all attached textures.") - s == GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS && - error("GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: Any framebuffer attachment is layered, and any populated attachment is not layered, or if all populated color attachments are not from textures of the same target.") - return error("Unknown framebuffer completion error code: $s") +function getfallback_buffer(fb::Framebuffer, key::Symbol, fallback_key::Symbol) + haskey(fb, key) ? get_buffer(fb, key) : get_buffer(fb, fallback_key) end -function check_framebuffer() - status = glCheckFramebufferStatus(GL_FRAMEBUFFER) - return enum_to_error(status) -end -Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) +Makie.@noconstprop function Framebuffer(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) require_context(context) # Create framebuffer - frambuffer_id = glGenFramebuffers() - glBindFramebuffer(GL_FRAMEBUFFER, frambuffer_id) + fb = GLFramebuffer(fb_size) # Buffers we always need # Holds the image that eventually gets displayed @@ -118,64 +62,20 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) - attach_framebuffer(color_buffer, GL_COLOR_ATTACHMENT0) - attach_framebuffer(objectid_buffer, GL_COLOR_ATTACHMENT1) - attach_framebuffer(HDR_color_buffer, GL_COLOR_ATTACHMENT2) - attach_framebuffer(OIT_weight_buffer, GL_COLOR_ATTACHMENT3) - attach_framebuffer(depth_buffer, GL_DEPTH_ATTACHMENT) - attach_framebuffer(depth_buffer, GL_STENCIL_ATTACHMENT) + # attach buffers + color_attachment = attach_colorbuffer(fb, :color, color_buffer) + objectid_attachment = attach_colorbuffer(fb, :objectid, objectid_buffer) + attach_colorbuffer(fb, :HDR_color, HDR_color_buffer) + attach_colorbuffer(fb, :OIT_weight, OIT_weight_buffer) + attach_depthbuffer(fb, :depth, depth_buffer) + attach_stencilbuffer(fb, :stencil, depth_buffer) check_framebuffer() - fb_size_node = Observable(fb_size) - - # To allow adding postprocessors in various combinations we need to keep - # track of the buffer ids that are already in use. We may also want to reuse - # buffers so we give them names for easy fetching. - buffer_ids = Dict{Symbol,GLuint}( - :color => GL_COLOR_ATTACHMENT0, - :objectid => GL_COLOR_ATTACHMENT1, - :HDR_color => GL_COLOR_ATTACHMENT2, - :OIT_weight => GL_COLOR_ATTACHMENT3, - :depth => GL_DEPTH_ATTACHMENT, - :stencil => GL_STENCIL_ATTACHMENT, - ) - buffers = Dict{Symbol, Texture}( - :color => color_buffer, - :objectid => objectid_buffer, - :HDR_color => HDR_color_buffer, - :OIT_weight => OIT_weight_buffer, - :depth => depth_buffer, - :stencil => depth_buffer - ) - - return GLFramebuffer( - fb_size_node, frambuffer_id, - buffer_ids, buffers, - [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1] - )::GLFramebuffer + return Framebuffer(fb, [color_attachment, objectid_attachment]) end -function destroy!(fb::GLFramebuffer) - # context required via free(tex) - @assert !isempty(fb.buffers) - for tex in values(fb.buffers) - GLAbstraction.free(tex) - end - id = [fb.id] - glDeleteFramebuffers(1, id) - fb.id = 0 - return -end - -function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) - (w > 0 && h > 0 && (w, h) != size(fb)) || return - for (name, buffer) in fb.buffers - resize_nocopy!(buffer, (w, h)) - end - fb.resolution[] = (w, h) - return nothing -end +Base.resize!(fb::Framebuffer, w::Int, h::Int) = resize!(fb.fb, w, h) struct MonitorProperties diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 16fd19e6392..5aa72970a37 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -62,8 +62,8 @@ function RenderPass{:OIT}(screen) loadshader("postprocessing/OIT_blend.frag") ) data = Dict{Symbol, Any}( - :sum_color => framebuffer[:HDR_color][2], - :prod_alpha => framebuffer[:OIT_weight][2], + :sum_color => get_buffer(framebuffer, :HDR_color), + :transmittance => get_buffer(framebuffer, :OIT_weight), ) pass = RenderObject( data, shader, @@ -84,14 +84,14 @@ function RenderPass{:OIT}(screen) ) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - return RenderPass{:OIT}(framebuffer, RenderObject[pass]) + return RenderPass{:OIT}(framebuffer.fb, RenderObject[pass]) end function run_step(screen, glscene, step::RenderPass{:OIT}) # Blend transparent onto opaque wh = size(screen.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(step.framebuffer[:color][1]) + glDrawBuffer(get_attachment(step.framebuffer, :color)) GLAbstraction.render(step.passes[1]) return end @@ -109,23 +109,23 @@ function ssao_postprocessor(framebuffer, shader_cache) # Add missing buffers if !haskey(framebuffer, :position) - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + # GLAbstraction.bind(framebuffer) position_buffer = Texture( shader_cache.context, Vec3f, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge ) - pos_id = attach_colorbuffer!(framebuffer, :position, position_buffer) + pos_id = attach_colorbuffer(framebuffer, :position, position_buffer) push!(framebuffer.render_buffer_ids, pos_id) end if !haskey(framebuffer, :normal) if !haskey(framebuffer, :HDR_color) - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + # GLAbstraction.bind(framebuffer) normal_occlusion_buffer = Texture( shader_cache.context, Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge ) - normal_occ_id = attach_colorbuffer!(framebuffer, :normal_occlusion, normal_occlusion_buffer) + normal_occ_id = attach_colorbuffer(framebuffer, :normal_occlusion, normal_occlusion_buffer) renames[:normal_occlusion] = :normal_occlusion else - normal_occ_id = framebuffer[:HDR_color][1] + normal_occ_id = get_attachment(framebuffer, :HDR_color) renames[:normal_occlusion] = :HDR_color end push!(framebuffer.render_buffer_ids, normal_occ_id) @@ -151,14 +151,14 @@ function ssao_postprocessor(framebuffer, shader_cache) ) ) data1 = Dict{Symbol, Any}( - :position_buffer => framebuffer[:position][2], - :normal_occlusion_buffer => getfallback(framebuffer, :normal_occlusion, :HDR_color)[2], + :position_buffer => get_buffer(framebuffer, :position), + :normal_occlusion_buffer => getfallback_buffer(framebuffer, :normal_occlusion, :HDR_color), :kernel => kernel, :noise => Texture( shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], minfilter = :nearest, x_repeat = :repeat ), - :noise_scale => map(s -> Vec2f(s ./ 4.0), framebuffer.resolution), + :noise_scale => Vec2f(0.25f0 .* size(framebuffer)), :projection => Observable(Mat4f(I)), :bias => 0.025f0, :radius => 0.5f0 @@ -174,23 +174,23 @@ function ssao_postprocessor(framebuffer, shader_cache) loadshader("postprocessing/SSAO_blur.frag") ) data2 = Dict{Symbol, Any}( - :normal_occlusion => getfallback(framebuffer, :normal_occlusion, :HDR_color)[2], - :color_texture => framebuffer[:color][2], - :ids => framebuffer[:objectid][2], - :inv_texel_size => lift(rcpframe, framebuffer.resolution), + :normal_occlusion => getfallback_buffer(framebuffer, :normal_occlusion, :HDR_color), + :color_texture => get_output(framebuffer, :color), + :ids => get_buffer(framebuffer, :objectid), + :inv_texel_size => rcpframe(size(framebuffer)), :blur_range => Int32(2) ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - return RenderPass{:SSAO}(framebuffer, [pass1, pass2], renames) + return RenderPass{:SSAO}(framebuffer.fb, [pass1, pass2], renames) end function run_step(screen, glscene, step::RenderPass{:SSAO}) wh = size(screen.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(step.framebuffer[step.renames[:normal_occ_id]][1]) # occlusion buffer + glDrawBuffer(get_attachment(step.framebuffer, step.renames[:normal_occ_id])) # occlusion buffer glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) @@ -211,7 +211,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) end # SSAO - blur occlusion and apply to color - glDrawBuffer(step.framebuffer[:color][1]) # color buffer + glDrawBuffer(get_attachment(step.framebuffer, :color)) # color buffer data2 = step.passes[2].uniforms for (screenid, scene) in screen.screens # Select the area of one leaf scene @@ -236,14 +236,14 @@ function RenderPass{:FXAA}(screen) # Add missing buffers if !haskey(framebuffer, :color_luma) if !haskey(framebuffer, :HDR_color) - glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) + # GLAbstraction.bind(framebuffer) color_luma_buffer = Texture( shader_cache.context, RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge ) - luma_id = attach_colorbuffer!(framebuffer, :color_luma, color_luma_buffer) + luma_id = attach_colorbuffer(framebuffer, :color_luma, color_luma_buffer) renames[:color_luma] = :color_luma else - luma_id = framebuffer[:HDR_color][1] + luma_id = get_attachment(framebuffer, :HDR_color) renames[:color_luma] = :HDR_color end end @@ -255,8 +255,8 @@ function RenderPass{:FXAA}(screen) loadshader("postprocessing/postprocess.frag") ) data1 = Dict{Symbol, Any}( - :color_texture => framebuffer[:color][2], - :object_ids => framebuffer[:objectid][2] + :color_texture => get_buffer(framebuffer, :color), + :object_ids => get_buffer(framebuffer, :objectid) ) pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) @@ -268,13 +268,13 @@ function RenderPass{:FXAA}(screen) loadshader("postprocessing/fxaa.frag") ) data2 = Dict{Symbol, Any}( - :color_texture => getfallback(framebuffer, :color_luma, :HDR_color)[2], - :RCPFrame => lift(rcpframe, framebuffer.resolution), + :color_texture => getfallback_buffer(framebuffer, :color_luma, :HDR_color), + :RCPFrame => rcpframe(size(framebuffer)), ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - return RenderPass{:FXAA}(framebuffer, RenderObject[pass1, pass2], renames) + return RenderPass{:FXAA}(framebuffer.fb, RenderObject[pass1, pass2], renames) end function run_step(screen, glscene, step::RenderPass{:FXAA}) @@ -283,14 +283,15 @@ function run_step(screen, glscene, step::RenderPass{:FXAA}) glViewport(0, 0, wh[1], wh[2]) # FXAA - calculate LUMA - glDrawBuffer(step.framebuffer[step.renames[:color_luma]][1]) + glDrawBuffer(get_attachment(step.framebuffer, step.renames[:color_luma])) # necessary with negative SSAO bias... glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(step.passes[1]) # FXAA - perform anti-aliasing - glDrawBuffer(step.framebuffer[:color][1]) # color buffer + glDrawBuffer(get_attachment(step.framebuffer, :color)) # color buffer + step.passes[2][:RCPFrame] = rcpframe(size(step.framebuffer)) GLAbstraction.render(step.passes[2]) return @@ -314,12 +315,12 @@ function BlitToScreen(screen, screen_framebuffer_id = 0) loadshader("postprocessing/copy.frag") ) data = Dict{Symbol, Any}( - :color_texture => framebuffer[:color][2] + :color_texture => get_buffer(framebuffer, :color) ) pass = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - return BlitToScreen(framebuffer, pass, screen_framebuffer_id) + return BlitToScreen(framebuffer.fb, pass, screen_framebuffer_id) end function run_step(screen, ::Nothing, step::BlitToScreen) diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 56770ad1164..62866163ba4 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -59,7 +59,7 @@ let end precompile(Screen, (Scene, ScreenConfig)) -precompile(GLFramebuffer, (NTuple{2,Int},)) +precompile(Framebuffer, (NTuple{2,Int},)) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Float32})) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBAf})) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBf})) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index ed3ea82eda6..349dd1ce220 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -53,7 +53,7 @@ function render_frame(screen::Screen; resize_buffers=true) end # prepare stencil (for sub-scenes) - glBindFramebuffer(GL_FRAMEBUFFER, fb.id) + GLAbstraction.bind(fb) glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) glClearColor(0, 0, 0, 0) glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) @@ -72,7 +72,7 @@ function render_frame(screen::Screen; resize_buffers=true) # render no SSAO - glDrawBuffers(2, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1]) + glDrawBuffers(2, [get_attachment(fb, :color), get_attachment(fb, :objectid)]) # render all non ssao GLAbstraction.render(screen) do robj return !Bool(robj[:transparency][]) && !Bool(robj[:ssao][]) @@ -80,15 +80,15 @@ function render_frame(screen::Screen; resize_buffers=true) # TRANSPARENT RENDER # clear sums to 0 - glDrawBuffer(GL_COLOR_ATTACHMENT2) + glDrawBuffer(get_attachment(fb, :HDR_color)) glClearColor(0, 0, 0, 0) glClear(GL_COLOR_BUFFER_BIT) # clear alpha product to 1 - glDrawBuffer(GL_COLOR_ATTACHMENT3) + glDrawBuffer(get_attachment(fb, :OIT_weight)) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) # draw - glDrawBuffers(3, [GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT3]) + glDrawBuffers(3, [get_attachment(fb, :HDR_color), get_attachment(fb, :objectid), get_attachment(fb, :OIT_weight)]) # Render only transparent objects GLAbstraction.render(screen) do robj return Bool(robj[:transparency][]) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index ac859c9fc8e..f614cd52edf 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -162,7 +162,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen::GLWindow owns_glscreen::Bool shader_cache::GLAbstraction.ShaderCache - framebuffer::GLFramebuffer + framebuffer::Framebuffer config::Union{Nothing, ScreenConfig} stop_renderloop::Threads.Atomic{Bool} rendertask::Union{Task, Nothing} @@ -190,7 +190,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen::GLWindow, owns_glscreen::Bool, shader_cache::GLAbstraction.ShaderCache, - framebuffer::GLFramebuffer, + framebuffer::Framebuffer, config::Union{Nothing, ScreenConfig}, stop_renderloop::Bool, rendertask::Union{Nothing, Task}, @@ -217,7 +217,7 @@ mutable struct Screen{GLWindow} <: MakieScreen end end -framebuffer_size(screen::Screen) = screen.framebuffer.resolution[] +framebuffer_size(screen::Screen) = size(screen.framebuffer) Makie.isvisible(screen::Screen) = screen.config.visible @@ -279,7 +279,7 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) # This is important for resource tracking, and only needed for the first context ShaderAbstractions.switch_context!(window) shader_cache = GLAbstraction.ShaderCache(window) - fb = GLFramebuffer(initial_resolution) + fb = Framebuffer(window, initial_resolution) screen = Screen( window, owns_glscreen, shader_cache, fb, @@ -788,7 +788,7 @@ function depthbuffer(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) render_frame(screen, resize_buffers=false) # let it render glFinish() # block until opengl is done rendering - source = screen.framebuffer.buffers[:depth] + source = get_buffer(screen.framebuffer, :depth) depth = Matrix{Float32}(undef, size(source)) GLAbstraction.bind(source) GLAbstraction.glGetTexImage(source.texturetype, 0, GL_DEPTH_COMPONENT, GL_FLOAT, depth) @@ -801,7 +801,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma error("Screen not open!") end ShaderAbstractions.switch_context!(screen.glscreen) - ctex = screen.framebuffer.buffers[:color] + ctex = get_buffer(screen.framebuffer, :color) # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! pollevents(screen, Makie.BackendTick) From 377d3f21d2691066fa10bf672c6ebd16e28d05dc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 01:20:07 +0100 Subject: [PATCH 038/135] add show() --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index c0f21417fd4..e55bfef7068 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -172,4 +172,38 @@ end function check_framebuffer() status = glCheckFramebufferStatus(GL_FRAMEBUFFER) return enum_to_error(status) +end + +function Base.show(io::IO, fb::GLFramebuffer) + X, Y = fb.size + print(io, "$X×$Y GLFrameBuffer(:") + join(io, string.(keys(fb.buffers)), ", :") + print(io, ")") +end + +function attachment_enum_to_string(x::GLenum) + x == GL_DEPTH_ATTACHMENT && return "GL_DEPTH_ATTACHMENT" + x == GL_STENCIL_ATTACHMENT && return "GL_STENCIL_ATTACHMENT" + i = Int(x - GL_COLOR_ATTACHMENT0) + return "GL_COLOR_ATTACHMENT$i" +end + +function Base.show(io::IO, ::MIME"text/plain", fb::GLFramebuffer) + X, Y = fb.size + print(io, "$X×$Y GLFrameBuffer()") + + ks = collect(keys(fb.buffers)) + key_strings = [":$k" for k in ks] + key_pad = mapreduce(length, max, key_strings) + key_strings = rpad.(key_strings, key_pad) + + attachments = map(key -> attachment_enum_to_string(get_attachment(fb, key)), ks) + idxs = sortperm(attachments) + attachment_pad = mapreduce(length, max, attachments) + attachments = rpad.(attachments, attachment_pad) + + for i in idxs + T = typeof(get_buffer(fb, ks[i])) + print(io, "\n ", key_strings[i], " => ", attachments[i], " ::", T) + end end \ No newline at end of file From ffe5b7c468eaebfb918dd6292589169ed01e22a8 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 01:36:42 +0100 Subject: [PATCH 039/135] fix incorrect rename --- GLMakie/src/postprocessing.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 5aa72970a37..32c2ea1588f 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -62,8 +62,8 @@ function RenderPass{:OIT}(screen) loadshader("postprocessing/OIT_blend.frag") ) data = Dict{Symbol, Any}( - :sum_color => get_buffer(framebuffer, :HDR_color), - :transmittance => get_buffer(framebuffer, :OIT_weight), + :sum_color => get_buffer(framebuffer, :HDR_color), + :prod_alpha => get_buffer(framebuffer, :OIT_weight), ) pass = RenderObject( data, shader, @@ -207,6 +207,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) data1[:projection] = scene.camera.projection[] data1[:bias] = scene.ssao.bias[] data1[:radius] = scene.ssao.radius[] + datas1[:noise_scale] = Vec2f(0.25f0 .* size(step.framebuffer)) GLAbstraction.render(step.passes[1]) end @@ -220,6 +221,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data2[:blur_range] = scene.ssao.blur + data2[:inv_texel_size] = rcpframe(size(framebuffer)) GLAbstraction.render(step.passes[2]) end glDisable(GL_SCISSOR_TEST) From fa47aa3759ac24587b0ec74c5fac63f0b4746a22 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 12:06:30 +0100 Subject: [PATCH 040/135] change copy-to-screen to a Blit operation --- GLMakie/src/postprocessing.jl | 97 +++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 32c2ea1588f..3ce94a964c0 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -18,10 +18,37 @@ end rcpframe(x) = 1f0 ./ Vec2f(x[1], x[2]) + +# or maybe Task? Stage? +""" + AbstractRenderStep + +Represents a task or step that needs to run when rendering a frame. These +tasks are collected in the RenderPipeline. + +Each task may implement: +- `prepare_step(screen, glscene, step)`: Initialize the task. +- `run_step(screen, glscene, step)`: Run the task. + +Initialization is grouped together and runs before all run steps. If you need +to initialize just before your run, bundle it with the run. +""" abstract type AbstractRenderStep end prepare_step(screen, glscene, ::AbstractRenderStep) = nothing run_step(screen, glscene, ::AbstractRenderStep) = nothing +function destroy!(step::T) where {T <: AbstractRenderStep} + @debug "Default destructor of $T" + if hasfield(T, :passes) + while !isempty(step.passes) + destroy!(pop!(step.passes)) + end + elseif hasfield(T, :pass) + destroy!(step.pass) + end + return +end + struct RenderPipeline steps::Vector{AbstractRenderStep} end @@ -36,9 +63,12 @@ function render_frame(screen, glscene, pipeline::RenderPipeline) return end + # TODO: temporary, we should get to the point where this is not needed struct EmptyRenderStep <: AbstractRenderStep end + +# TODO: maybe call this a PostProcessor? # Vaguely leaning on Vulkan Terminology struct RenderPass{Name} <: AbstractRenderStep framebuffer::GLFramebuffer @@ -299,59 +329,40 @@ function run_step(screen, glscene, step::RenderPass{:FXAA}) return end + +# TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer - pass::RenderObject screen_framebuffer_id::Int -end - -# TODO: replacement for CImGUI? -function BlitToScreen(screen, screen_framebuffer_id = 0) - @debug "Creating to screen postprocessor" - framebuffer = screen.framebuffer - # draw color buffer - shader = LazyShader( - screen.shader_cache, - loadshader("postprocessing/fullscreen.vert"), - loadshader("postprocessing/copy.frag") - ) - data = Dict{Symbol, Any}( - :color_texture => get_buffer(framebuffer, :color) - ) - pass = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) - pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - - return BlitToScreen(framebuffer.fb, pass, screen_framebuffer_id) + # Screen not available yet + function BlitToScreen(screen, screen_framebuffer_id::Integer = 0) + @debug "Creating to screen postprocessor" + return new(screen.framebuffer.fb, screen_framebuffer_id) + end end function run_step(screen, ::Nothing, step::BlitToScreen) + # Set source + glBindFramebuffer(GL_READ_FRAMEBUFFER, step.framebuffer.id) + glReadBuffer(get_attachment(step.framebuffer, :color)) # for safety + # TODO: Is this an observable? Can this be static? # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering - glBindFramebuffer(GL_FRAMEBUFFER, step.screen_framebuffer_id) - glViewport(0, 0, framebuffer_size(screen)...) - # glScissor(0, 0, wh[1], wh[2]) - - # clear target - # TODO: Could be skipped if glscenes[1] clears to opaque (maybe use dedicated shader?) - # TODO: Should this be cleared if we don't own the target? - glClearColor(1,1,1,1) - glClear(GL_COLOR_BUFFER_BIT) + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, step.screen_framebuffer_id) - # transfer everything to the screen - GLAbstraction.render(step.pass) # copy postprocess + src_w, src_h = framebuffer_size(screen) + if isnothing(screen.scene) + trg_w, trg_h = src_w, src_h + else + trg_w, trg_h = widths(screen.scene.viewport[]) + end - return -end + glBlitFramebuffer( + 0, 0, src_w, src_h, + 0, 0, trg_w, trg_h, + GL_COLOR_BUFFER_BIT, GL_LINEAR + ) -function destroy!(step::T) where {T <: AbstractRenderStep} - @debug "Default destructor of $T" - if hasfield(T, :passes) - while !isempty(step.passes) - destroy!(pop!(step.passes)) - end - elseif hasfield(T, :pass) - destroy!(step.pass) - end return -end \ No newline at end of file +end From 0d3b8ed1da730a8bfca2753d8b60deb8923bf461 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 12:55:33 +0100 Subject: [PATCH 041/135] fix tests --- GLMakie/src/picking.jl | 16 ++++++++-------- GLMakie/test/unit_tests.jl | 36 ++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index cde1cd4d59f..c785166be3e 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -7,8 +7,8 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) fb = screen.framebuffer - buff = fb.buffers[:objectid] - glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + buff = get_buffer(fb, :objectid) + GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) rx, ry = minimum(rect) rw, rh = widths(rect) @@ -28,8 +28,8 @@ function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) fb = screen.framebuffer - buff = fb.buffers[:objectid] - glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + buff = get_buffer(fb, :objectid) + GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) x, y = floor.(Int, xy) w, h = size(screen.scene) @@ -78,9 +78,9 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) dx = x1 - x0; dy = y1 - y0 ShaderAbstractions.switch_context!(screen.glscreen) - glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) - buff = fb.buffers[:objectid] + buff = get_buffer(fb, :objectid) sids = zeros(SelectionID{UInt32}, dx, dy) glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, sids) @@ -119,9 +119,9 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) dx = x1 - x0; dy = y1 - y0 ShaderAbstractions.switch_context!(screen.glscreen) - glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) - buff = fb.buffers[:objectid] + buff = get_buffer(fb, :objectid) picks = zeros(SelectionID{UInt32}, dx, dy) glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, picks) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 306503f6e74..22a5fbc604f 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -11,39 +11,39 @@ end screen = display(GLMakie.Screen(visible = false), Figure()) cache = screen.shader_cache; # Postprocessing shaders - @test length(cache.shader_cache) == 5 - @test length(cache.template_cache) == 5 - @test length(cache.program_cache) == 4 + @test length(cache.shader_cache) == 4 + @test length(cache.template_cache) == 4 + @test length(cache.program_cache) == 3 # Shaders for scatter + linesegments + poly etc (axis) display(screen, scatter(1:4)) - @test length(cache.shader_cache) == 18 - @test length(cache.template_cache) == 18 - @test length(cache.program_cache) == 11 + @test length(cache.shader_cache) == 17 + @test length(cache.template_cache) == 17 + @test length(cache.program_cache) == 10 # No new shaders should be added: display(screen, scatter(1:4)) - @test length(cache.shader_cache) == 18 - @test length(cache.template_cache) == 18 - @test length(cache.program_cache) == 11 + @test length(cache.shader_cache) == 17 + @test length(cache.template_cache) == 17 + @test length(cache.program_cache) == 10 # Same for linesegments display(screen, linesegments(1:4)) - @test length(cache.shader_cache) == 18 - @test length(cache.template_cache) == 18 - @test length(cache.program_cache) == 11 + @test length(cache.shader_cache) == 17 + @test length(cache.template_cache) == 17 + @test length(cache.program_cache) == 10 # heatmap hasn't been compiled so one new program should be added display(screen, heatmap([1,2,2.5,3], [1,2,2.5,3], rand(4,4))) - @test length(cache.shader_cache) == 20 - @test length(cache.template_cache) == 20 - @test length(cache.program_cache) == 12 + @test length(cache.shader_cache) == 19 + @test length(cache.template_cache) == 19 + @test length(cache.program_cache) == 11 # For second time no new shaders should be added display(screen, heatmap([1,2,2.5,3], [1,2,2.5,3], rand(4,4))) - @test length(cache.shader_cache) == 20 - @test length(cache.template_cache) == 20 - @test length(cache.program_cache) == 12 + @test length(cache.shader_cache) == 19 + @test length(cache.template_cache) == 19 + @test length(cache.program_cache) == 11 end @testset "unit tests" begin From 7da87000e1020f672654ba48b9a7f40685156847 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 15:15:02 +0100 Subject: [PATCH 042/135] add Render step --- GLMakie/src/postprocessing.jl | 89 +++++++++++++++++++++++++++++++++++ GLMakie/src/rendering.jl | 76 +++++------------------------- GLMakie/src/screen.jl | 9 ++-- 3 files changed, 107 insertions(+), 67 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 3ce94a964c0..02939c19cf1 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -68,6 +68,92 @@ end struct EmptyRenderStep <: AbstractRenderStep end +@enum FilterOptions begin + FilterFalse = 0 + FilterTrue = 1 + FilterAny = 2 +end +compare(val::Bool, filter::FilterOptions) = (filter == FilterAny) || (val == Int(filter)) +compare(val::Integer, filter::FilterOptions) = (filter == FilterAny) || (val == Int(filter)) + +struct RenderPlots <: AbstractRenderStep + framebuffer::GLFramebuffer + targets::Vector{GLuint} + clear::Vector{Pair{Int, Vec4f}} # target index -> color + + ssao::FilterOptions + transparency::FilterOptions + fxaa::FilterOptions +end + +function RenderPlots(screen, stage) + fb = screen.framebuffer + if stage === :SSAO + return RenderPlots( + fb.fb, fb.render_buffer_ids, [3 => Vec4f(0), 4 => Vec4f(0)], + FilterTrue, FilterFalse, FilterAny) + elseif stage === :FXAA + return RenderPlots( + fb.fb, get_attachment.(Ref(fb), [:color, :objectid]), Pair{Int, Vec4f}[], + FilterFalse, FilterFalse, FilterAny) + elseif stage === :OIT + targets = get_attachment.(Ref(fb), [:HDR_color, :objectid, :OIT_weight]) + # HDR_color containing sums clears to 0 + # OIT_weight containing products clears to 1 + clear = [1 => Vec4f(0), 3 => Vec4f(1)] + return RenderPlots(fb.fb, targets, clear, FilterAny, FilterTrue, FilterAny) + else + error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") + end +end + +function id2scene(screen, id1) + # TODO maybe we should use a different data structure + for (id2, scene) in screen.screens + id1 == id2 && return true, scene + end + return false, nothing +end + +function run_step(screen, glscene, step::RenderPlots) + # Somehow errors in here get ignored silently!? + try + GLAbstraction.bind(step.framebuffer) + + for (idx, color) in step.clear + idx <= length(step.targets) || continue + glDrawBuffer(step.targets[idx]) + glClearColor(color...) + glClear(GL_COLOR_BUFFER_BIT) + end + + glDrawBuffers(length(step.targets), step.targets) + + for (zindex, screenid, elem) in screen.renderlist + should_render = elem.visible && + compare(elem[:ssao][], step.ssao) && + compare(elem[:transparency][], step.transparency) && + compare(elem[:fxaa][], step.fxaa) + should_render || continue + + found, scene = id2scene(screen, screenid) + (found && scene.visible[]) || continue + + ppu = screen.px_per_unit[] + a = viewport(scene)[] + glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) + render(elem) + end + catch e + @error "Error while rendering!" exception = e + rethrow(e) + end + return +end + + + + # TODO: maybe call this a PostProcessor? # Vaguely leaning on Vulkan Terminology struct RenderPass{Name} <: AbstractRenderStep @@ -79,6 +165,8 @@ function RenderPass{Name}(framebuffer::GLFramebuffer, passes::Vector{RenderObjec return RenderPass{Name}(framebuffer, passes, Dict{Symbol, Symbol}()) end + + function RenderPass{:OIT}(screen) @debug "Creating OIT postprocessor" @@ -330,6 +418,7 @@ function run_step(screen, glscene, step::RenderPass{:FXAA}) end + # TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 349dd1ce220..6d961889372 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -52,89 +52,37 @@ function render_frame(screen::Screen; resize_buffers=true) resize!(fb, round.(Int, ppu .* size(screen.scene))...) end - # prepare stencil (for sub-scenes) + # clear global buffers GLAbstraction.bind(fb) - glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) + glDrawBuffers(2, get_attachment.(Ref(fb), [:color, :objectid])) glClearColor(0, 0, 0, 0) glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) - glDrawBuffer(fb.render_buffer_ids[1]) + # draw backgrounds + glDrawBuffer(get_attachment(fb, :color)) setup!(screen) - glDrawBuffers(length(fb.render_buffer_ids), fb.render_buffer_ids) # render with SSAO - glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE) - GLAbstraction.render(screen) do robj - return !Bool(robj[:transparency][]) && Bool(robj[:ssao][]) - end - # SSAO run_step(screen, nothing, screen.render_pipeline[1]) + # run SSAO + run_step(screen, nothing, screen.render_pipeline[2]) + # render all plots without SSAO and transparency + run_step(screen, nothing, screen.render_pipeline[3]) - # render no SSAO - glDrawBuffers(2, [get_attachment(fb, :color), get_attachment(fb, :objectid)]) - # render all non ssao - GLAbstraction.render(screen) do robj - return !Bool(robj[:transparency][]) && !Bool(robj[:ssao][]) - end - - # TRANSPARENT RENDER - # clear sums to 0 - glDrawBuffer(get_attachment(fb, :HDR_color)) - glClearColor(0, 0, 0, 0) - glClear(GL_COLOR_BUFFER_BIT) - # clear alpha product to 1 - glDrawBuffer(get_attachment(fb, :OIT_weight)) - glClearColor(1, 1, 1, 1) - glClear(GL_COLOR_BUFFER_BIT) - # draw - glDrawBuffers(3, [get_attachment(fb, :HDR_color), get_attachment(fb, :objectid), get_attachment(fb, :OIT_weight)]) # Render only transparent objects - GLAbstraction.render(screen) do robj - return Bool(robj[:transparency][]) - end + run_step(screen, nothing, screen.render_pipeline[4]) # TRANSPARENT BLEND - run_step(screen, nothing, screen.render_pipeline[2]) + run_step(screen, nothing, screen.render_pipeline[5]) # FXAA - run_step(screen, nothing, screen.render_pipeline[3]) + run_step(screen, nothing, screen.render_pipeline[6]) # transfer everything to the screen - run_step(screen, nothing, screen.render_pipeline[4]) + run_step(screen, nothing, screen.render_pipeline[7]) GLAbstraction.require_context(nw) return end - -function id2scene(screen, id1) - # TODO maybe we should use a different data structure - for (id2, scene) in screen.screens - id1 == id2 && return true, scene - end - return false, nothing -end - -function GLAbstraction.render(filter_elem_func, screen::Screen) - # Somehow errors in here get ignored silently!? - try - GLAbstraction.require_context(screen.glscreen) - for (zindex, screenid, elem) in screen.renderlist - filter_elem_func(elem)::Bool || continue - - found, scene = id2scene(screen, screenid) - found || continue - scene.visible[] || continue - ppu = screen.px_per_unit[] - a = viewport(scene)[] - glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) - render(elem) - end - GLAbstraction.require_context(screen.glscreen) - catch e - @error "Error while rendering!" exception = e - rethrow(e) - end - return -end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index f614cd52edf..c937878924a 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -293,7 +293,10 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) reuse, ) + push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) + push!(screen.render_pipeline, RenderPlots(screen, :OIT)) push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, BlitToScreen(screen)) @@ -396,9 +399,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B return end - replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 1) - replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 2) - replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 3) + replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 2) + replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 5) + replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 6) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters From 18973b67f7ee68a734cb7cb7d8a356f61299ca18 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 15:24:10 +0100 Subject: [PATCH 043/135] fix SSAO --- GLMakie/src/glwindow.jl | 2 +- GLMakie/src/postprocessing.jl | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 8d0e0a1d2f5..2937f3889e1 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -22,7 +22,7 @@ Base.haskey(fb::Framebuffer, key::Symbol) = haskey(fb.fb, key) GLAbstraction.get_attachment(fb::Framebuffer, key::Symbol) = get_attachment(fb.fb, key) GLAbstraction.get_buffer(fb::Framebuffer, key::Symbol) = get_buffer(fb.fb, key) GLAbstraction.bind(fb::Framebuffer) = GLAbstraction.bind(fb.fb) - +GLAbstraction.attach_colorbuffer(fb::Framebuffer, key, val) = GLAbstraction.attach_colorbuffer(fb.fb, key, val) function getfallback_attachment(fb::Framebuffer, key::Symbol, fallback_key::Symbol) haskey(fb, key) ? get_attachment(fb, key) : get_attachment(fb, fallback_key) end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 02939c19cf1..9c4cd819757 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -293,7 +293,7 @@ function ssao_postprocessor(framebuffer, shader_cache) ) data2 = Dict{Symbol, Any}( :normal_occlusion => getfallback_buffer(framebuffer, :normal_occlusion, :HDR_color), - :color_texture => get_output(framebuffer, :color), + :color_texture => get_buffer(framebuffer, :color), :ids => get_buffer(framebuffer, :objectid), :inv_texel_size => rcpframe(size(framebuffer)), :blur_range => Int32(2) @@ -308,7 +308,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) wh = size(screen.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(get_attachment(step.framebuffer, step.renames[:normal_occ_id])) # occlusion buffer + glDrawBuffer(get_attachment(step.framebuffer, step.renames[:normal_occlusion])) # occlusion buffer glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) @@ -322,10 +322,10 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) a = viewport(scene)[] glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms - data1[:projection] = scene.camera.projection[] + data1[:projection] = Mat4f(scene.camera.projection[]) data1[:bias] = scene.ssao.bias[] data1[:radius] = scene.ssao.radius[] - datas1[:noise_scale] = Vec2f(0.25f0 .* size(step.framebuffer)) + data1[:noise_scale] = Vec2f(0.25f0 .* size(step.framebuffer)) GLAbstraction.render(step.passes[1]) end @@ -339,7 +339,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data2[:blur_range] = scene.ssao.blur - data2[:inv_texel_size] = rcpframe(size(framebuffer)) + data2[:inv_texel_size] = rcpframe(size(step.framebuffer)) GLAbstraction.render(step.passes[2]) end glDisable(GL_SCISSOR_TEST) From c98b311e7136ad789504705ec75a3f44b674a355 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 15:48:18 +0100 Subject: [PATCH 044/135] add SortPlots task and cleanup render_frame --- GLMakie/src/postprocessing.jl | 19 +++++++ GLMakie/src/rendering.jl | 98 ++++++++++++----------------------- GLMakie/src/screen.jl | 10 ++-- 3 files changed, 59 insertions(+), 68 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 9c4cd819757..36d46971013 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -68,6 +68,25 @@ end struct EmptyRenderStep <: AbstractRenderStep end + +struct SortPlots <: AbstractRenderStep end + +function run_step(screen, glscene, ::SortPlots) + function sortby(x) + robj = x[3] + plot = screen.cache2plot[robj.id] + # TODO, use actual boundingbox + # ~7% faster than calling zvalue2d doing the same thing? + return Makie.transformationmatrix(plot)[][3, 4] + # return Makie.zvalue2d(plot) + end + + sort!(screen.renderlist; by=sortby) + return +end + + + @enum FilterOptions begin FilterFalse = 0 FilterTrue = 1 diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 6d961889372..1a8a12d7dc8 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,21 +1,40 @@ -function setup!(screen::Screen) +function setup!(screen::Screen, resize_buffers) + # Make sure this context is active (for multi-window rendering) + nw = to_native(screen) + ShaderAbstractions.switch_context!(nw) + GLAbstraction.require_context(nw) + + # Resize framebuffer to window size + fb = screen.framebuffer + GLAbstraction.bind(fb) + if resize_buffers && !isnothing(screen.scene) + ppu = screen.px_per_unit[] + resize!(fb, round.(Int, ppu .* size(screen.scene))...) + end + + # clear objectid, depth and stencil + glDrawBuffer(get_attachment(fb, :objectid)) + glClearColor(0, 0, 0, 0) + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) + + # clear color buffer + glDrawBuffer(get_attachment(fb, :color)) + glClearColor(1, 1, 1, 1) + glClear(GL_COLOR_BUFFER_BIT) + + # draw scene backgrounds glEnable(GL_SCISSOR_TEST) if isopen(screen) && !isnothing(screen.scene) ppu = screen.px_per_unit[] - glScissor(0, 0, round.(Int, size(screen.scene) .* ppu)...) - glClearColor(1, 1, 1, 1) - glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens - if scene.visible[] + if scene.visible[] && scene.clear[] a = viewport(scene)[] rt = (round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) glViewport(rt...) - if scene.clear[] - c = scene.backgroundcolor[] - glScissor(rt...) - glClearColor(red(c), green(c), blue(c), alpha(c)) - glClear(GL_COLOR_BUFFER_BIT) - end + glScissor(rt...) + c = scene.backgroundcolor[] + glClearColor(red(c), green(c), blue(c), alpha(c)) + glClear(GL_COLOR_BUFFER_BIT) end end end @@ -27,62 +46,11 @@ end Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) - nw = to_native(screen) - ShaderAbstractions.switch_context!(nw) - GLAbstraction.require_context(nw) + setup!(screen, resize_buffers) - function sortby(x) - robj = x[3] - plot = screen.cache2plot[robj.id] - # TODO, use actual boundingbox - # ~7% faster than calling zvalue2d doing the same thing? - return Makie.transformationmatrix(plot)[][3, 4] - # return Makie.zvalue2d(plot) + for step in screen.render_pipeline + run_step(screen, nothing, step) end - sort!(screen.renderlist; by=sortby) - - # NOTE - # The transparent color buffer is reused by SSAO and FXAA. Changing the - # render order here may introduce artifacts because of that. - - fb = screen.framebuffer - if resize_buffers && !isnothing(screen.scene) - ppu = screen.px_per_unit[] - resize!(fb, round.(Int, ppu .* size(screen.scene))...) - end - - # clear global buffers - GLAbstraction.bind(fb) - glDrawBuffers(2, get_attachment.(Ref(fb), [:color, :objectid])) - glClearColor(0, 0, 0, 0) - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) - - # draw backgrounds - glDrawBuffer(get_attachment(fb, :color)) - setup!(screen) - - # render with SSAO - run_step(screen, nothing, screen.render_pipeline[1]) - # run SSAO - run_step(screen, nothing, screen.render_pipeline[2]) - - # render all plots without SSAO and transparency - run_step(screen, nothing, screen.render_pipeline[3]) - - # Render only transparent objects - run_step(screen, nothing, screen.render_pipeline[4]) - - # TRANSPARENT BLEND - run_step(screen, nothing, screen.render_pipeline[5]) - - # FXAA - run_step(screen, nothing, screen.render_pipeline[6]) - - # transfer everything to the screen - run_step(screen, nothing, screen.render_pipeline[7]) - - GLAbstraction.require_context(nw) - return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index c937878924a..b301a4d48db 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -293,6 +293,10 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) reuse, ) + # NOTE + # The transparent color buffer is reused by SSAO and FXAA. Changing the + # render order here may introduce artifacts because of that. + push!(screen.render_pipeline, SortPlots()) push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) @@ -399,9 +403,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B return end - replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 2) - replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 5) - replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 6) + replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 3) + replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 6) + replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 7) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters From ecd804b75424ba769dc41a2a73b371fd6926d386 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 21:47:28 +0100 Subject: [PATCH 045/135] use dedicated framebuffers --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 +- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 8 +- GLMakie/src/glwindow.jl | 90 ++++++++++++---- GLMakie/src/picking.jl | 8 +- GLMakie/src/postprocessing.jl | 118 ++++++++------------- GLMakie/src/precompiles.jl | 2 +- GLMakie/src/rendering.jl | 8 +- GLMakie/src/screen.jl | 20 ++-- 8 files changed, 141 insertions(+), 115 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 91c14720841..c6259048322 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -106,7 +106,7 @@ export getAttributesInfo include("GLFrameBuffer.jl") export GLRenderbuffer export GLFramebuffer -export attach_colorbuffer, attach_depthbuffer, attach_stencilbuffer +export attach_colorbuffer, attach_depthbuffer, attach_stencilbuffer, attach_depthstencilbuffer export get_attachment, get_buffer export check_framebuffer diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index e55bfef7068..6f89fb89bec 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -76,6 +76,7 @@ function bind(fb::GLFramebuffer) glBindFramebuffer(GL_FRAMEBUFFER, fb.id) return end +unbind(::GLFramebuffer) = glBindFramebuffer(GL_FRAMEBUFFER, 0) function unsafe_free(x::GLFramebuffer) # don't free if already freed @@ -113,6 +114,7 @@ end attach_colorbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, get_next_colorbuffer_attachment(fb)) attach_depthbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_DEPTH_ATTACHMENT) attach_stencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_STENCIL_ATTACHMENT) +attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_DEPTH_STENCIL_ATTACHMENT) function attach(fb::GLFramebuffer, key::Symbol, buffer, attachment::GLenum) haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") @@ -121,6 +123,8 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, attachment::GLenum) type = "depth" elseif attachment == GL_STENCIL_ATTACHMENT type = "stencil" + elseif attachment == GL_DEPTH_STENCIL_ATTACHMENT + type = "depth-stencil" else type = "color" end @@ -178,7 +182,7 @@ function Base.show(io::IO, fb::GLFramebuffer) X, Y = fb.size print(io, "$X×$Y GLFrameBuffer(:") join(io, string.(keys(fb.buffers)), ", :") - print(io, ")") + print(io, ") with id ", fb.id) end function attachment_enum_to_string(x::GLenum) @@ -190,7 +194,7 @@ end function Base.show(io::IO, ::MIME"text/plain", fb::GLFramebuffer) X, Y = fb.size - print(io, "$X×$Y GLFrameBuffer()") + print(io, "$X×$Y GLFrameBuffer() with id ", fb.id) ks = collect(keys(fb.buffers)) key_strings = [":$k" for k in ks] diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 2937f3889e1..a74e7acb404 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -10,34 +10,33 @@ end Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T(s.id), T(s.index)) Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) -mutable struct Framebuffer +mutable struct FramebufferFactory fb::GLFramebuffer + buffers::Dict{Symbol, Texture} # TODO: temp, should be unnamed collection render_buffer_ids::Vector{GLuint} + children::Vector{GLFramebuffer} # TODO: how else can we handle resizing? end # it's guaranteed, that they all have the same size -# forwards... for now -Base.size(fb::Framebuffer) = size(fb.fb) -Base.haskey(fb::Framebuffer, key::Symbol) = haskey(fb.fb, key) -GLAbstraction.get_attachment(fb::Framebuffer, key::Symbol) = get_attachment(fb.fb, key) -GLAbstraction.get_buffer(fb::Framebuffer, key::Symbol) = get_buffer(fb.fb, key) -GLAbstraction.bind(fb::Framebuffer) = GLAbstraction.bind(fb.fb) -GLAbstraction.attach_colorbuffer(fb::Framebuffer, key, val) = GLAbstraction.attach_colorbuffer(fb.fb, key, val) -function getfallback_attachment(fb::Framebuffer, key::Symbol, fallback_key::Symbol) - haskey(fb, key) ? get_attachment(fb, key) : get_attachment(fb, fallback_key) -end -function getfallback_buffer(fb::Framebuffer, key::Symbol, fallback_key::Symbol) - haskey(fb, key) ? get_buffer(fb, key) : get_buffer(fb, fallback_key) +# TODO: forwards... for now +Base.size(fb::FramebufferFactory) = size(fb.fb) +Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffers, key) +GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[key] +GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) + +function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) + foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), values(fb.buffers)) + resize!(fb.fb, w, h) + filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok? + foreach(fb -> resize!(fb, w, h), fb.children) + return end -Makie.@noconstprop function Framebuffer(context, fb_size::NTuple{2, Int}) +Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) require_context(context) - # Create framebuffer - fb = GLFramebuffer(fb_size) - # Buffers we always need # Holds the image that eventually gets displayed color_buffer = Texture( @@ -62,20 +61,65 @@ Makie.@noconstprop function Framebuffer(context, fb_size::NTuple{2, Int}) context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) + buffers = Dict{Symbol, Texture}( + :color => color_buffer, + :objectid => objectid_buffer, + :HDR_color => HDR_color_buffer, + :OIT_weight => OIT_weight_buffer, + :depth_stencil => depth_buffer, + ) + + # Create render framebuffer + fb = GLFramebuffer(fb_size) + # attach buffers color_attachment = attach_colorbuffer(fb, :color, color_buffer) objectid_attachment = attach_colorbuffer(fb, :objectid, objectid_buffer) - attach_colorbuffer(fb, :HDR_color, HDR_color_buffer) - attach_colorbuffer(fb, :OIT_weight, OIT_weight_buffer) - attach_depthbuffer(fb, :depth, depth_buffer) - attach_stencilbuffer(fb, :stencil, depth_buffer) + attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) + attach_colorbuffer(fb, :HDR_color, HDR_color_buffer) # TODO: framebuffer for RenderPlots + attach_colorbuffer(fb, :OIT_weight, OIT_weight_buffer) # TODO: framebuffer for RenderPlots + + check_framebuffer() + + return FramebufferFactory(fb, buffers, [color_attachment, objectid_attachment], GLFramebuffer[]) +end + +# TODO: temporary +function Base.push!(factory::FramebufferFactory, kv::Pair{Symbol, <: Texture}) + if haskey(factory.buffers, kv[1]) + @error("Pushed buffer $(kv[1]) already assigned.") + return + end + push!(factory.buffers, kv) + return +end + +function generate_framebuffer(factory::FramebufferFactory, names...) + filter!(fb -> fb.id != 0, factory.children) # cleanup? + + parse_arg(name::Symbol) = name => name + parse_arg(p::Pair{Symbol, Symbol}) = p + parse_arg(x::Any) = error("$x not accepted") + + fb = GLFramebuffer(size(factory.fb)) + attach_depthstencilbuffer(fb, :depth_stencil, factory.buffers[:depth_stencil]) + + for arg in names + lookup, name = parse_arg(arg) + haskey(factory.buffers, lookup) || error("Add buffers yourself for now") + haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") + in(lookup, [:depth, :stencil]) && error("Depth and stencil always exist under the same name.") + + attach_colorbuffer(fb, name, factory.buffers[lookup]) + end check_framebuffer() - return Framebuffer(fb, [color_attachment, objectid_attachment]) + push!(factory.children, fb) + + return fb end -Base.resize!(fb::Framebuffer, w::Int, h::Int) = resize!(fb.fb, w, h) struct MonitorProperties diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index c785166be3e..c8f3c70821c 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,7 +6,7 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - fb = screen.framebuffer + fb = screen.framebuffer_factory buff = get_buffer(fb, :objectid) GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) @@ -27,7 +27,7 @@ end function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - fb = screen.framebuffer + fb = screen.framebuffer_factory buff = get_buffer(fb, :objectid) GLAbstraction.bind(fb) glReadBuffer(GL_COLOR_ATTACHMENT1) @@ -70,7 +70,7 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) w, h = size(screen.scene) # unitless dimensions ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) - fb = screen.framebuffer + fb = screen.framebuffer_factory ppu = screen.px_per_unit[] w, h = size(fb) # pixel dimensions x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) @@ -111,7 +111,7 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) return Tuple{AbstractPlot, Int}[] end - fb = screen.framebuffer + fb = screen.framebuffer_factory ppu = screen.px_per_unit[] w, h = size(fb) # pixel dimensions x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 36d46971013..98f679ad629 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -106,21 +106,21 @@ struct RenderPlots <: AbstractRenderStep end function RenderPlots(screen, stage) - fb = screen.framebuffer + fb = screen.framebuffer_factory.fb if stage === :SSAO return RenderPlots( - fb.fb, fb.render_buffer_ids, [3 => Vec4f(0), 4 => Vec4f(0)], + fb, screen.framebuffer_factory.render_buffer_ids, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) elseif stage === :FXAA return RenderPlots( - fb.fb, get_attachment.(Ref(fb), [:color, :objectid]), Pair{Int, Vec4f}[], + fb, get_attachment.(Ref(fb), [:color, :objectid]), Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) elseif stage === :OIT targets = get_attachment.(Ref(fb), [:HDR_color, :objectid, :OIT_weight]) # HDR_color containing sums clears to 0 # OIT_weight containing products clears to 1 clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(fb.fb, targets, clear, FilterAny, FilterTrue, FilterAny) + return RenderPlots(fb, targets, clear, FilterAny, FilterTrue, FilterAny) else error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") end @@ -178,10 +178,6 @@ end struct RenderPass{Name} <: AbstractRenderStep framebuffer::GLFramebuffer passes::Vector{RenderObject} - renames::Dict{Symbol, Symbol} # TODO: temporary until GLFramebuffer not shared -end -function RenderPass{Name}(framebuffer::GLFramebuffer, passes::Vector{RenderObject}) where {Name} - return RenderPass{Name}(framebuffer, passes, Dict{Symbol, Symbol}()) end @@ -189,7 +185,8 @@ end function RenderPass{:OIT}(screen) @debug "Creating OIT postprocessor" - framebuffer = screen.framebuffer + factory = screen.framebuffer_factory + framebuffer = generate_framebuffer(factory, :color) # Based on https://jcgt.org/published/0002/02/09/, see #1390 # OIT setup @@ -199,8 +196,8 @@ function RenderPass{:OIT}(screen) loadshader("postprocessing/OIT_blend.frag") ) data = Dict{Symbol, Any}( - :sum_color => get_buffer(framebuffer, :HDR_color), - :prod_alpha => get_buffer(framebuffer, :OIT_weight), + :sum_color => get_buffer(factory.fb, :HDR_color), + :prod_alpha => get_buffer(factory.fb, :OIT_weight), ) pass = RenderObject( data, shader, @@ -221,12 +218,13 @@ function RenderPass{:OIT}(screen) ) pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) - return RenderPass{:OIT}(framebuffer.fb, RenderObject[pass]) + return RenderPass{:OIT}(framebuffer, RenderObject[pass]) end function run_step(screen, glscene, step::RenderPass{:OIT}) # Blend transparent onto opaque - wh = size(screen.framebuffer) + GLAbstraction.bind(step.framebuffer) + wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) glDrawBuffer(get_attachment(step.framebuffer, :color)) GLAbstraction.render(step.passes[1]) @@ -236,8 +234,8 @@ end function RenderPass{:SSAO}(screen) @debug "Creating SSAO postprocessor" - framebuffer = screen.framebuffer - renames = Dict{Symbol, Symbol}() + + factory = screen.framebuffer_factory function ssao_postprocessor(framebuffer, shader_cache) @@ -245,28 +243,18 @@ function ssao_postprocessor(framebuffer, shader_cache) require_context(shader_cache.context) # Add missing buffers - if !haskey(framebuffer, :position) - # GLAbstraction.bind(framebuffer) - position_buffer = Texture( - shader_cache.context, Vec3f, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge - ) - pos_id = attach_colorbuffer(framebuffer, :position, position_buffer) - push!(framebuffer.render_buffer_ids, pos_id) - end - if !haskey(framebuffer, :normal) - if !haskey(framebuffer, :HDR_color) - # GLAbstraction.bind(framebuffer) - normal_occlusion_buffer = Texture( - shader_cache.context, Vec4{Float16}, size(framebuffer), minfilter = :nearest, x_repeat = :clamp_to_edge - ) - normal_occ_id = attach_colorbuffer(framebuffer, :normal_occlusion, normal_occlusion_buffer) - renames[:normal_occlusion] = :normal_occlusion - else - normal_occ_id = get_attachment(framebuffer, :HDR_color) - renames[:normal_occlusion] = :HDR_color - end - push!(framebuffer.render_buffer_ids, normal_occ_id) - end + pos_tex = Texture(shader_cache.context, Vec3f, size(screen), minfilter = :nearest, x_repeat = :clamp_to_edge) + push!(factory, :position => pos_tex) + # HDR_color always exists... + + # TODO: temp - update renderbuffers and main renderbuffer + # eventually RenderPlots should have dedicated GLFramebuffers too, which + # derive their draw buffers from the abstracted render pipeline + pos_id = attach_colorbuffer(factory.fb, :position, pos_tex) + normal_id = attach_colorbuffer(factory.fb, :normal, get_buffer(factory, :HDR_color)) + push!(factory.render_buffer_ids, pos_id, normal_id) + + framebuffer = generate_framebuffer(factory, :color, :HDR_color => :normal_occlusion) # SSAO setup N_samples = 64 @@ -288,14 +276,14 @@ function ssao_postprocessor(framebuffer, shader_cache) ) ) data1 = Dict{Symbol, Any}( - :position_buffer => get_buffer(framebuffer, :position), - :normal_occlusion_buffer => getfallback_buffer(framebuffer, :normal_occlusion, :HDR_color), + :position_buffer => get_buffer(factory.fb, :position), + :normal_occlusion_buffer => get_buffer(factory.fb, :normal), :kernel => kernel, :noise => Texture( shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], minfilter = :nearest, x_repeat = :repeat ), - :noise_scale => Vec2f(0.25f0 .* size(framebuffer)), + :noise_scale => Vec2f(0.25f0 .* size(screen)), :projection => Observable(Mat4f(I)), :bias => 0.025f0, :radius => 0.5f0 @@ -311,23 +299,24 @@ function ssao_postprocessor(framebuffer, shader_cache) loadshader("postprocessing/SSAO_blur.frag") ) data2 = Dict{Symbol, Any}( - :normal_occlusion => getfallback_buffer(framebuffer, :normal_occlusion, :HDR_color), - :color_texture => get_buffer(framebuffer, :color), - :ids => get_buffer(framebuffer, :objectid), - :inv_texel_size => rcpframe(size(framebuffer)), + :normal_occlusion => get_buffer(framebuffer, :normal_occlusion), + :color_texture => get_buffer(factory.fb, :color), + :ids => get_buffer(factory.fb, :objectid), + :inv_texel_size => rcpframe(size(screen)), :blur_range => Int32(2) ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - return RenderPass{:SSAO}(framebuffer.fb, [pass1, pass2], renames) + return RenderPass{:SSAO}(framebuffer, [pass1, pass2]) end function run_step(screen, glscene, step::RenderPass{:SSAO}) - wh = size(screen.framebuffer) + GLAbstraction.bind(step.framebuffer) + wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(get_attachment(step.framebuffer, step.renames[:normal_occlusion])) # occlusion buffer + glDrawBuffer(get_attachment(step.framebuffer, :normal_occlusion)) # occlusion buffer glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) @@ -369,23 +358,8 @@ end function RenderPass{:FXAA}(screen) @debug "Creating FXAA postprocessor" - framebuffer = screen.framebuffer - renames = Dict{Symbol, Symbol}() - - # Add missing buffers - if !haskey(framebuffer, :color_luma) - if !haskey(framebuffer, :HDR_color) - # GLAbstraction.bind(framebuffer) - color_luma_buffer = Texture( - shader_cache.context, RGBA{N0f8}, size(framebuffer), minfilter=:linear, x_repeat=:clamp_to_edge - ) - luma_id = attach_colorbuffer(framebuffer, :color_luma, color_luma_buffer) - renames[:color_luma] = :color_luma - else - luma_id = get_attachment(framebuffer, :HDR_color) - renames[:color_luma] = :HDR_color - end - end + factory = screen.framebuffer_factory + framebuffer = generate_framebuffer(factory, :color, :HDR_color => :color_luma) # calculate luma for FXAA shader1 = LazyShader( @@ -394,8 +368,8 @@ function RenderPass{:FXAA}(screen) loadshader("postprocessing/postprocess.frag") ) data1 = Dict{Symbol, Any}( - :color_texture => get_buffer(framebuffer, :color), - :object_ids => get_buffer(framebuffer, :objectid) + :color_texture => get_buffer(factory.fb, :color), + :object_ids => get_buffer(factory.fb, :objectid) ) pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) @@ -407,22 +381,24 @@ function RenderPass{:FXAA}(screen) loadshader("postprocessing/fxaa.frag") ) data2 = Dict{Symbol, Any}( - :color_texture => getfallback_buffer(framebuffer, :color_luma, :HDR_color), + :color_texture => get_buffer(framebuffer, :color_luma), :RCPFrame => rcpframe(size(framebuffer)), ) pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) - return RenderPass{:FXAA}(framebuffer.fb, RenderObject[pass1, pass2], renames) + return RenderPass{:FXAA}(framebuffer, RenderObject[pass1, pass2]) end function run_step(screen, glscene, step::RenderPass{:FXAA}) + GLAbstraction.bind(step.framebuffer) + # TODO: make scissor explicit? - wh = size(screen.framebuffer) + wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) # FXAA - calculate LUMA - glDrawBuffer(get_attachment(step.framebuffer, step.renames[:color_luma])) + glDrawBuffer(get_attachment(step.framebuffer, :color_luma)) # necessary with negative SSAO bias... glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) @@ -446,7 +422,7 @@ struct BlitToScreen <: AbstractRenderStep # Screen not available yet function BlitToScreen(screen, screen_framebuffer_id::Integer = 0) @debug "Creating to screen postprocessor" - return new(screen.framebuffer.fb, screen_framebuffer_id) + return new(screen.framebuffer_factory.fb, screen_framebuffer_id) end end diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 62866163ba4..70ecc78510b 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -59,7 +59,7 @@ let end precompile(Screen, (Scene, ScreenConfig)) -precompile(Framebuffer, (NTuple{2,Int},)) +precompile(FramebufferFactory, (NTuple{2,Int},)) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Float32})) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBAf})) precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBf})) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 1a8a12d7dc8..8102a048526 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -5,13 +5,15 @@ function setup!(screen::Screen, resize_buffers) GLAbstraction.require_context(nw) # Resize framebuffer to window size - fb = screen.framebuffer - GLAbstraction.bind(fb) + fb = screen.framebuffer_factory.fb if resize_buffers && !isnothing(screen.scene) ppu = screen.px_per_unit[] - resize!(fb, round.(Int, ppu .* size(screen.scene))...) + resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) end + GLAbstraction.bind(fb) + glViewport(0, 0, size(fb)...) + # clear objectid, depth and stencil glDrawBuffer(get_attachment(fb, :objectid)) glClearColor(0, 0, 0, 0) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index b301a4d48db..4e5b8b23fa2 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -162,7 +162,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen::GLWindow owns_glscreen::Bool shader_cache::GLAbstraction.ShaderCache - framebuffer::Framebuffer + framebuffer_factory::FramebufferFactory config::Union{Nothing, ScreenConfig} stop_renderloop::Threads.Atomic{Bool} rendertask::Union{Task, Nothing} @@ -190,7 +190,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen::GLWindow, owns_glscreen::Bool, shader_cache::GLAbstraction.ShaderCache, - framebuffer::Framebuffer, + framebuffer_factory::FramebufferFactory, config::Union{Nothing, ScreenConfig}, stop_renderloop::Bool, rendertask::Union{Nothing, Task}, @@ -203,9 +203,9 @@ mutable struct Screen{GLWindow} <: MakieScreen reuse::Bool ) where {GLWindow} - s = size(framebuffer) + s = size(framebuffer_factory) screen = new{GLWindow}( - glscreen, owns_glscreen, shader_cache, framebuffer, + glscreen, owns_glscreen, shader_cache, framebuffer_factory, config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, screens, renderlist, AbstractRenderStep[], cache, cache2plot, @@ -217,7 +217,7 @@ mutable struct Screen{GLWindow} <: MakieScreen end end -framebuffer_size(screen::Screen) = size(screen.framebuffer) +framebuffer_size(screen::Screen) = size(screen.framebuffer_factory) Makie.isvisible(screen::Screen) = screen.config.visible @@ -279,7 +279,7 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) # This is important for resource tracking, and only needed for the first context ShaderAbstractions.switch_context!(window) shader_cache = GLAbstraction.ShaderCache(window) - fb = Framebuffer(window, initial_resolution) + fb = FramebufferFactory(window, initial_resolution) screen = Screen( window, owns_glscreen, shader_cache, fb, @@ -510,7 +510,7 @@ Base.wait(scene::Scene) = wait(Makie.getscreen(scene)) Base.show(io::IO, screen::Screen) = print(io, "GLMakie.Screen(...)") Base.isopen(x::Screen) = isopen(x.glscreen) -Base.size(x::Screen) = size(x.framebuffer) +Base.size(x::Screen) = size(x.framebuffer_factory) function add_scene!(screen::Screen, scene::Scene) get!(screen.screen2scene, WeakRef(scene)) do @@ -763,7 +763,7 @@ function Base.resize!(screen::Screen, w::Int, h::Int) # independently of the window scale factor. fbscale = screen.px_per_unit[] fbw, fbh = round.(Int, fbscale .* (w, h)) - resize!(screen.framebuffer, fbw, fbh) + resize!(screen.framebuffer_factory, fbw, fbh) return nothing end @@ -795,7 +795,7 @@ function depthbuffer(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) render_frame(screen, resize_buffers=false) # let it render glFinish() # block until opengl is done rendering - source = get_buffer(screen.framebuffer, :depth) + source = get_buffer(screen.framebuffer_factory, :depth) depth = Matrix{Float32}(undef, size(source)) GLAbstraction.bind(source) GLAbstraction.glGetTexImage(source.texturetype, 0, GL_DEPTH_COMPONENT, GL_FLOAT, depth) @@ -808,7 +808,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma error("Screen not open!") end ShaderAbstractions.switch_context!(screen.glscreen) - ctex = get_buffer(screen.framebuffer, :color) + ctex = get_buffer(screen.framebuffer_factory, :color) # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! pollevents(screen, Makie.BackendTick) From 1d726d282cd4eaed3e8a25e009fcbce695248a6f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 22:05:04 +0100 Subject: [PATCH 046/135] fix tests --- GLMakie/test/unit_tests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 22a5fbc604f..5b3fe8f47b7 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -326,7 +326,7 @@ end screen = display(GLMakie.Screen(visible = true, scalefactor = 2), fig) @test screen.scalefactor[] === 2f0 @test screen.px_per_unit[] === 2f0 # inherited from scale factor - @test size(screen.framebuffer) == (2W, 2H) + @test size(screen.framebuffer_factory) == (2W, 2H) @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) # check that picking works through the resized GL buffers @@ -353,7 +353,7 @@ end screen = display(GLMakie.Screen(visible = false, scalefactor = 2, px_per_unit = 1), fig) @test screen.scalefactor[] === 2f0 @test screen.px_per_unit[] === 1f0 - @test size(screen.framebuffer) == (W, H) + @test size(screen.framebuffer_factory) == (W, H) # decrease the scale factor after-the-fact screen.scalefactor[] = 1 From 70d056b22942209f41fab0df0b070faf9ddffe2a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 22:42:30 +0100 Subject: [PATCH 047/135] fix depthbuffer() --- GLMakie/src/screen.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 4e5b8b23fa2..3b2bfb82a88 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -795,7 +795,7 @@ function depthbuffer(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) render_frame(screen, resize_buffers=false) # let it render glFinish() # block until opengl is done rendering - source = get_buffer(screen.framebuffer_factory, :depth) + source = get_buffer(screen.framebuffer_factory, :depth_stencil) depth = Matrix{Float32}(undef, size(source)) GLAbstraction.bind(source) GLAbstraction.glGetTexImage(source.texturetype, 0, GL_DEPTH_COMPONENT, GL_FLOAT, depth) From 7820e69f3360cb7fa6f4d1b1f3ea0092bbc32685 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 24 Dec 2024 23:37:56 +0100 Subject: [PATCH 048/135] simplify glDrawBuffer(s) --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 1 + GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 92 ++++++++++++++-------- GLMakie/src/postprocessing.jl | 19 ++--- 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index c6259048322..f833685db7d 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -108,6 +108,7 @@ export GLRenderbuffer export GLFramebuffer export attach_colorbuffer, attach_depthbuffer, attach_stencilbuffer, attach_depthstencilbuffer export get_attachment, get_buffer +export set_draw_buffers export check_framebuffer end # module diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 6f89fb89bec..548ec8c1112 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -48,8 +48,9 @@ mutable struct GLFramebuffer context::GLContext - attachments::Dict{Symbol, GLenum} - buffers::Dict{Symbol, Texture} + name2idx::Dict{Symbol, Int} + attachments::Vector{GLenum} + buffers::Vector{Texture} counter::UInt32 # for color attachments function GLFramebuffer(size::NTuple{2, Int}) @@ -59,9 +60,7 @@ mutable struct GLFramebuffer obj = new( id, size, current_context(), - Dict{Symbol, GLuint}(), - Dict{Symbol, Texture}(), - UInt32(0) + Dict{Symbol, Int}(), GLenum[], Texture[], UInt32(0) ) finalizer(free, obj) @@ -69,14 +68,32 @@ mutable struct GLFramebuffer end end -function bind(fb::GLFramebuffer) +function bind(fb::GLFramebuffer, target = fb.id) if fb.id == 0 error("Binding freed GLFramebuffer") end - glBindFramebuffer(GL_FRAMEBUFFER, fb.id) + glBindFramebuffer(GL_FRAMEBUFFER, target) return end -unbind(::GLFramebuffer) = glBindFramebuffer(GL_FRAMEBUFFER, 0) + +""" + draw_buffers(fb::GLFrameBuffer[, N::Int]) + +Activates the first N color buffers attached to the given GLFramebuffer. If N +is not given all color attachments are activated. +""" +function set_draw_buffers(fb::GLFramebuffer, N::Integer = fb.counter) + bind(fb) + glDrawBuffers(N, fb.attachments) +end +function set_draw_buffers(fb::GLFramebuffer, key::Symbol) + bind(fb) + glDrawBuffer(get_attachment(fb, key)) +end +function set_draw_buffers(fb::GLFramebuffer, keys::Symbol...) + bind(fb) + glDrawBuffer(get_attachment.(Ref(fb), keys)) +end function unsafe_free(x::GLFramebuffer) # don't free if already freed @@ -91,11 +108,11 @@ function unsafe_free(x::GLFramebuffer) end Base.size(fb::GLFramebuffer) = fb.size -Base.haskey(fb::GLFramebuffer, key::Symbol) = haskey(fb.buffers, key) +Base.haskey(fb::GLFramebuffer, key::Symbol) = haskey(fb.name2idx, key) function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) (w > 0 && h > 0 && (w, h) != size(fb)) || return - for (key, buffer) in fb.buffers + for buffer in fb.buffers resize_nocopy!(buffer, (w, h)) end fb.size = (w, h) @@ -108,17 +125,25 @@ function get_next_colorbuffer_attachment(fb::GLFramebuffer) end attachment = GL_COLOR_ATTACHMENT0 + fb.counter fb.counter += 1 - return attachment + return (fb.counter, attachment) end -attach_colorbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, get_next_colorbuffer_attachment(fb)) -attach_depthbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_DEPTH_ATTACHMENT) -attach_stencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_STENCIL_ATTACHMENT) -attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) = attach(fb, key, buffer, GL_DEPTH_STENCIL_ATTACHMENT) +function attach_colorbuffer(fb::GLFramebuffer, key::Symbol, buffer) + return attach(fb, key, buffer, get_next_colorbuffer_attachment(fb)...) +end +function attach_depthbuffer(fb::GLFramebuffer, key::Symbol, buffer) + return attach(fb, key, buffer, length(fb.attachments) + 1, GL_DEPTH_ATTACHMENT) +end +function attach_stencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) + return attach(fb, key, buffer, length(fb.attachments) + 1, GL_STENCIL_ATTACHMENT) +end +function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) + return attach(fb, key, buffer, length(fb.attachments) + 1, GL_DEPTH_STENCIL_ATTACHMENT) +end -function attach(fb::GLFramebuffer, key::Symbol, buffer, attachment::GLenum) +function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment::GLenum) haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") - if in(attachment, keys(fb.buffers)) + if attachment in fb.attachments if attachment == GL_DEPTH_ATTACHMENT type = "depth" elseif attachment == GL_STENCIL_ATTACHMENT @@ -132,21 +157,27 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, attachment::GLenum) end bind(fb) - _attach(buffer, attachment) - fb.attachments[key] = attachment - fb.buffers[key] = buffer + gl_attach(buffer, attachment) + # keep depth/stenctil/depth_stencil at end so that we can directly use + # fb.attachments when setting drawbuffers + for (k, v) in fb.name2idx + fb.name2idx[k] = ifelse(v < idx, v, v+1) + end + fb.name2idx[key] = idx + insert!(fb.attachments, idx, attachment) + insert!(fb.buffers, idx, buffer) return attachment end -function _attach(t::Texture{T, 2}, attachment::GLenum) where {T} +function gl_attach(t::Texture{T, 2}, attachment::GLenum) where {T} glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, t.id, 0) end -function _attach(buffer::RenderBuffer, attachment::GLenum) +function gl_attach(buffer::RenderBuffer, attachment::GLenum) glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, buffer) end -get_attachment(fb::GLFramebuffer, key::Symbol) = fb.attachments[key] -get_buffer(fb::GLFramebuffer, key::Symbol) = fb.buffers[key] +get_attachment(fb::GLFramebuffer, key::Symbol) = fb.attachments[fb.name2idx[key]] +get_buffer(fb::GLFramebuffer, key::Symbol) = fb.buffers[fb.name2idx[key]] function enum_to_error(s) s == GL_FRAMEBUFFER_COMPLETE && return @@ -181,7 +212,7 @@ end function Base.show(io::IO, fb::GLFramebuffer) X, Y = fb.size print(io, "$X×$Y GLFrameBuffer(:") - join(io, string.(keys(fb.buffers)), ", :") + join(io, string.(keys(fb.name2idx)), ", :") print(io, ") with id ", fb.id) end @@ -196,18 +227,17 @@ function Base.show(io::IO, ::MIME"text/plain", fb::GLFramebuffer) X, Y = fb.size print(io, "$X×$Y GLFrameBuffer() with id ", fb.id) - ks = collect(keys(fb.buffers)) + ks = collect(keys(fb.name2idx)) + sort!(ks, by = k -> fb.name2idx[k]) key_strings = [":$k" for k in ks] key_pad = mapreduce(length, max, key_strings) key_strings = rpad.(key_strings, key_pad) - attachments = map(key -> attachment_enum_to_string(get_attachment(fb, key)), ks) - idxs = sortperm(attachments) + attachments = attachment_enum_to_string(fb.attachments) attachment_pad = mapreduce(length, max, attachments) attachments = rpad.(attachments, attachment_pad) - for i in idxs - T = typeof(get_buffer(fb, ks[i])) - print(io, "\n ", key_strings[i], " => ", attachments[i], " ::", T) + for (key, attachment, buffer) in zip(key_strings, attachments, fb.buffers) + print(io, "\n ", key, " => ", attachment, " ::", typeof(buffer)) end end \ No newline at end of file diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 98f679ad629..12d8b7c58c6 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -223,10 +223,9 @@ end function run_step(screen, glscene, step::RenderPass{:OIT}) # Blend transparent onto opaque - GLAbstraction.bind(step.framebuffer) wh = size(step.framebuffer) + set_draw_buffers(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(get_attachment(step.framebuffer, :color)) GLAbstraction.render(step.passes[1]) return end @@ -312,11 +311,10 @@ function ssao_postprocessor(framebuffer, shader_cache) end function run_step(screen, glscene, step::RenderPass{:SSAO}) - GLAbstraction.bind(step.framebuffer) - wh = size(step.framebuffer) + set_draw_buffers(step.framebuffer, :normal_occlusion) # occlusion buffer + wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) - glDrawBuffer(get_attachment(step.framebuffer, :normal_occlusion)) # occlusion buffer glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) @@ -338,7 +336,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) end # SSAO - blur occlusion and apply to color - glDrawBuffer(get_attachment(step.framebuffer, :color)) # color buffer + set_draw_buffers(step.framebuffer, :color) # color buffer data2 = step.passes[2].uniforms for (screenid, scene) in screen.screens # Select the area of one leaf scene @@ -391,21 +389,18 @@ function RenderPass{:FXAA}(screen) end function run_step(screen, glscene, step::RenderPass{:FXAA}) - GLAbstraction.bind(step.framebuffer) - + # FXAA - calculate LUMA + set_draw_buffers(step.framebuffer, :color_luma) # TODO: make scissor explicit? wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) - - # FXAA - calculate LUMA - glDrawBuffer(get_attachment(step.framebuffer, :color_luma)) # necessary with negative SSAO bias... glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(step.passes[1]) # FXAA - perform anti-aliasing - glDrawBuffer(get_attachment(step.framebuffer, :color)) # color buffer + set_draw_buffers(step.framebuffer, :color) # color buffer step.passes[2][:RCPFrame] = rcpframe(size(step.framebuffer)) GLAbstraction.render(step.passes[2]) From 84440a4a11f27e2932fc962b4ed0753ffd4e9d1d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Dec 2024 18:19:57 +0100 Subject: [PATCH 049/135] add prototype for abstracted RenderPipeline --- src/utilities/RenderPipeline.jl | 533 ++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 src/utilities/RenderPipeline.jl diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl new file mode 100644 index 00000000000..bd09765f55a --- /dev/null +++ b/src/utilities/RenderPipeline.jl @@ -0,0 +1,533 @@ +using FixedPointNumbers +Float8 = N0f8 + +_promote_type(T1, T2) = promote_type(T1, T2) +# otherwise you get Float32 here... maybe there's a better solution for this? +# maybe just introduce struct Float8 end as a stand-in? +_promote_type(::Type{Float8}, ::Type{Float16}) = Float16 +_promote_type(::Type{Float16}, ::Type{Float8}) = Float16 + + +struct Format + dims::Int + type::DataType + Format(dims = 4, type = Float8) = new(dims, type) +end + +function Format(input::Format, output::Format) + if is_compatible(input, output) + dims = max(input.dims, output.dims) + type = _promote_type(input.type, output.type) + return Format(dims, type) + end + error("Could not generate compatible Format between $input and $output") +end + +function is_compatible(f1::Format, f2::Format) + is_compatible_with(f1.type, f2.type) || is_compatible_with(f2.type, f1.type) +end + +function is_compatible_with(f::Format, requirement::Format) + return (f.dims <= requirement.dims) && is_compatible_with(f.type, requirement.type) +end + +# given, required +is_compatible_with(::Type{<: Union{N0f8, Float16, Float32}}, ::Type{N0f8}) = true +is_compatible_with(::Type{<: Union{Float16, Float32}}, ::Type{Float16}) = true +is_compatible_with(::Type{Float32}, ::Type{Float32}) = true + +is_compatible_with(::Type{<: Union{Int8, Int16, Int32}}, ::Type{Int8}) = true +is_compatible_with(::Type{<: Union{Int16, Int32}}, ::Type{Int16}) = true +is_compatible_with(::Type{Int32}, ::Type{Int32}) = true + +is_compatible_with(::Type{<: Union{UInt8, UInt16, UInt32}}, ::Type{UInt8}) = true +is_compatible_with(::Type{<: Union{UInt16, UInt32}}, ::Type{UInt16}) = true +is_compatible_with(::Type{UInt32}, ::Type{UInt32}) = true + +is_compatible_with(::Type, ::Type) = false + + +# Connections can have multiple inputs and outputs +# e.g. multiple Renders write to objectid and FXAA, SSAO, Display/pick read it +struct ConnectionT{T} + inputs::Dict{T, Symbol} # stage => output name + outputs::Dict{T, Symbol} # stage => input name + format::Format # derived from inputs & outputs formats +end + +struct Stage + name::String + # order matters for outputs + inputs::Dict{Symbol, Int} + outputs::Dict{Symbol, Int} + input_formats::Vector{Format} + output_formats::Vector{Format} + # ^ technically all of these are constants + + # v these are not + input_connections::Dict{Symbol, ConnectionT{Stage}} + output_connections::Dict{Symbol, ConnectionT{Stage}} +end + +const Connection = ConnectionT{Stage} + +function Stage(name; inputs = NTuple{0, Pair{Symbol, Format}}(), outputs = NTuple{0, Pair{Symbol, Format}}()) + stage = Stage(name, + Dict{Symbol, Int}(), Dict{Symbol, Int}(), Format[], Format[], + Dict{Symbol, Connection}(), Dict{Symbol, Connection}() + ) + foreach(enumerate(inputs)) do (i, (k, v)) + stage.inputs[k] = i + push!(stage.input_formats, v) + end + foreach(enumerate(outputs)) do (i, (k, v)) + stage.outputs[k] = i + push!(stage.output_formats, v) + end + return stage +end + +function Connection(source::Stage, input::Symbol, target::Stage, output::Symbol) + format = Format( + source.output_formats[source.outputs[input]], + target.input_formats[target.inputs[output]] + ) + return Connection(Dict(source => input), Dict(target => output), format) +end + +function Base.merge(c1::Connection, c2::Connection) + return Connection( + merge!(c1.inputs, c2.inputs), + merge!(c1.outputs, c2.outputs), + Format(c1.format, c2.format) + ) +end + +function Base.hash(stage::Stage, h::UInt) + # inputs, outputs are static after init + # connections are not, and should not be relevant to identifying a stage + return hash(stage.name, hash(stage.inputs, hash(stage.outputs, h))) +end + +struct Pipeline + stages::Vector{Stage} + connections::Vector{Connection} +end +function Pipeline() + return Pipeline(Stage[], Connection[]) +end +function Pipeline(stages::Stage...) + pipeline = Pipeline() + foreach(stage -> push!(pipeline, stage), stages) + return pipeline +end + + +# Stages are allowed to be duplicates. E.g. you could have something like this: +# render -> effect 1 -. +# |-> combine +# render -> effect 2 -' +# where render is the same (name/task, inputs, outputs) +function Base.push!(pipeline::Pipeline, stage::Stage) + isempty(stage.input_connections) || error("Pushed stage $(stage.name) must not have input connections.") + isempty(stage.output_connections) || error("Pushed stage $(stage.name) must not have output connections.") + push!(pipeline.stages, stage) + return stage # for convenience +end +function Base.push!(pipeline::Pipeline, other::Pipeline) + append!(pipeline.stages, other.stages) + append!(pipeline.connections, other.connections) + return other.stages # for convernience +end + + +# function connect!(source::Stage, output::Symbol, target::Stage, input::Symbol) +function connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) + source = pipeline.stages[src] + target = pipeline.stages[trg] + + haskey(source.outputs, output) || error("output $output does not exist in source stage") + haskey(target.inputs, input) || error("input $input does not exist in target stage") + + # create requested connection + connection = Connection(source, output, target, input) + + # if the input or output already has an edge, merge it with the create edge + # e.g. the color output of source is used for a second stage + # or the color input of target is written to by second stage + if haskey(source.output_connections, output) + old = source.output_connections[output] + # There should be exactly one matching connection, and it's probably + # near the end? + idx = findlast(c -> c === old, pipeline.connections)::Int + deleteat!(pipeline.connections, idx) + connection = merge(connection, old) + end + if haskey(target.input_connections, input) + old = target.input_connections[input] + idx = findlast(c -> c === old, pipeline.connections)::Int + deleteat!(pipeline.connections, idx) + connection = merge(connection, old) + end + + # attach connection to every input and output + for (stage, key) in connection.inputs + stage.output_connections[key] = connection + end + for (stage, key) in connection.outputs + stage.input_connections[key] = connection + end + + push!(pipeline.connections, connection) + + return connection +end + +# TODO: make it impossible to break order +# TODO: Is this even a necessary condition? +function verify(pipeline::Pipeline) + for connection in pipeline.connections + earliest_input = length(pipeline.stages) + for stage in keys(connection.inputs) + idx = findfirst(==(stage), pipeline.stages) + isnothing(idx) && error("Could not find $(stage.name) in pipeline.") + earliest_input = min(earliest_input, idx) + end + + earliest_output = length(pipeline.stages) + for stage in keys(connection.outputs) + idx = findfirst(==(stage), pipeline.stages) + isnothing(idx) && error("Could not find $(stage.name) in pipeline.") + earliest_output = min(earliest_output, idx) + end + + if earliest_input >= earliest_output + inputs = join(("$(stage.name).$key" for (stage, key) in connection.inputs), ", ") + outputs = join(("$(stage.name).$key" for (stage, key) in connection.outputs), ", ") + error("Connection ($inputs) -> ($outputs) is read before being written to. Not allowed. ($earliest_input ≥ $earliest_output)") + end + end + + return true +end + +format_complexity(c::Connection) = format_complexity(c.format) +format_complexity(f::Format) = f.dims * sizeof(f.type) + +function generate_buffers(pipeline::Pipeline) + # TODO: is this necessary? + verify(pipeline) + + # We can make this more efficient later... + stage2idx = Dict([stage => i for (i, stage) in enumerate(pipeline.stages)]) + + # Group connections that exist between stages + usage_per_transfer = [Connection[] for _ in 1:length(pipeline.stages)-1] + for connection in pipeline.connections + first = mapreduce(kv -> stage2idx[kv[1]], min, connection.inputs) + last = mapreduce(kv -> stage2idx[kv[1]]-1, max, connection.outputs) + for i in first:last + push!(usage_per_transfer[i], connection) + end + end + + buffers = Format[] + connection2idx = Dict{Connection, Int}() # into buffer + needs_buffer = Connection[] + available = Int[] + + # Let's simplify to get correct behavior first... + + for i in eachindex(usage_per_transfer) + # prepare: + # - collect connections without buffers + # - collect available buffers (not in use now or last iteration) + copyto!(resize!(available, length(buffers)), eachindex(buffers)) + empty!(needs_buffer) + for j in max(1, i-1):i + # for j in i:min(length(usage_per_transfer), i+1) # reverse + for connection in usage_per_transfer[j] + if haskey(connection2idx, connection) + idx = connection2idx[connection] + filter!(!=(idx), available) + elseif j == i + push!(needs_buffer, connection) + end + end + end + + # Handle most expensive connections first + sort!(needs_buffer, by = format_complexity, rev = true) + + for connection in needs_buffer + # search for most compatible buffer + best_match = 0 + prev_comp = 999999 + prev_delta = 999999 + conn_comp = format_complexity(connection.format) + for i in available + if buffers[i] == connection.format # exact match + best_match = i + break + elseif is_compatible(buffers[i], connection.format) + # found compatible buffer, but we only use it if + # - using it is cheaper than using the last + # - using it is cheaper than creating a new buffer + # - it is more compatible than the last when both are 0 cost + # (i.e prefer 3, Float16 over 3 Float8 for 3 Float16 target) + updated_comp = format_complexity(Format(buffers[i], connection.format)) + buffer_comp = format_complexity(buffers[i]) + delta = updated_comp - buffer_comp + is_cheaper = (delta < prev_delta) && (delta <= conn_comp) + more_compatible = (delta == prev_delta == 0) && (buffer_comp < prev_comp) + if is_cheaper || more_compatible + best_match = i + prev_comp = updated_comp + prev_delta = delta + end + end + end + + if best_match == 0 + # nothing compatible found/available, add format + push!(buffers, connection.format) + best_match = length(buffers) + elseif buffers[best_match] != connection.format + # found upgradeable format, upgrade it (or use it) + new_format = Format(buffers[best_match], connection.format) + buffers[best_match] = new_format + end + + # Link to new found or upgraded format + connection2idx[connection] = best_match + + # Can't use a buffer twice in one transfer + filter!(!=(best_match), available) + end + end + + return buffers, connection2idx +end + +# Usage: +# stages become tasks in the renderloop (mostly postprocessors) +# returned buffers become color attachments +# returned connection2idx is used to find input and output buffers +# Stage -> RenderStage, Framebuffer <-----------------------------------------------------. +# |-> inputs -> (name, connection) -> (name, idx) -> (name, buffer) -> (name, Texture) --| +# '-> outputs -> (name, connection) -> (name, idx) -> (name, buffer) -> (name, Texture) -' + + +################################################################################ +### show +################################################################################ + + +function Base.show(io::IO, format::Format) + print(io, "Format($(format.dims), $(format.type))") +end + +Base.show(io::IO, stage::Stage) = print(io, "Stage($(stage.name))") +function Base.show(io::IO, ::MIME"text/plain", stage::Stage) + print(io, "Stage($(stage.name))") + + if !isempty(stage.inputs) + print(io, "\ninputs:") + ks = keys(stage.inputs) + pad = mapreduce(k -> length(string(k)), max, ks) + for k in ks + mark = haskey(stage.input_connections, k) ? 'x' : ' ' + print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.input_formats[stage.inputs[k]]) + end + end + + if !isempty(stage.outputs) + print(io, "\noutputs:") + ks = keys(stage.outputs) + pad = mapreduce(k -> length(string(k)), max, ks) + for k in ks + mark = haskey(stage.output_connections, k) ? 'x' : ' ' + print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.output_formats[stage.outputs[k]]) + end + end + + return +end + +function _names(connection::Connection) + inputs = unique(values(connection.inputs)) + outputs = unique(values(connection.outputs)) + return unique(vcat(inputs, outputs)) +end + +function Base.show(io::IO, connection::Connection) + names = _names(connection) + print(io, "Connection(") + if length(names) == 1 + print(io, names[1]) + else + print(io, names) + end + print(io, " -> $(connection.format))") +end + +function Base.show(io::IO, ::MIME"text/plain", connection::Connection) + print(io, "Connection($(connection.format))") + + if !isempty(connection.inputs) + print(io, "\ninputs:") + elements = map(keys(connection.inputs), values(connection.inputs)) do stage, k + (stage.name, string(k), stage.output_formats[stage.outputs[k]]) + end + pad1 = mapreduce(x -> length(x[1]), max, elements) + pad2 = mapreduce(x -> length(x[2]), max, elements) + for (name, key, format) in elements + print(io, "\n ", rpad(name, pad1), " -> ", lpad(key, pad2), "::", format) + end + end + + if !isempty(connection.outputs) + print(io, "\noutputs:") + elements = map(keys(connection.outputs), values(connection.outputs)) do stage, k + (stage.name, string(k), stage.input_formats[stage.inputs[k]]) + end + pad1 = mapreduce(x -> length(x[1]), max, elements) + pad2 = mapreduce(x -> length(x[2]), max, elements) + for (name, key, format) in elements + print(io, "\n ", rpad(name, pad1), " -> ", lpad(key, pad2), "::", format) + end + end + + return +end + + +################################################################################ +### Defaults +################################################################################ + + +SortStage() = Stage("z-sort") + +# I guess we should have multiple versions of this? Because SSAO render and OIT render don't really fit? +# SSAO color objectid position normal +# simple color objectid +# OIT HDR_color objectid weight +function RenderStage() + outputs = ( + :color => Format(4, Float8), + :objectid => Format(2, UInt32), + :position => Format(3, Float16), + :normal => Format(3, Float16), + ) + Stage("render", outputs = outputs) +end +function TransparentRenderStage() + outputs = ( + :weighted_color_sum => Format(4, Float16), + :objectid => Format(2, UInt32), + :alpha_product => Format(1, Float8), + ) + Stage("transparent render", outputs = outputs) +end + +# Want a MultiSzage kinda thing +function SSAOStage() + inputs = ( + :position => Format(3, Float32), + :normal => Format(3, Float16) + ) + stage1 = Stage("SSAO occlusion"; inputs, outputs = (:occlusion => Format(1, Float8),)) + + inputs = (:occlusion => Format(1, Float8), :color => Format(4, Float8), :objectid => Format(2, UInt32)) + stage2 = Stage("SSAO blur", inputs = inputs, outputs = (:color => Format(),)) + + pipeline = Pipeline(stage1, stage2) + connect!(pipeline, 1, :occlusion, 2, :occlusion) + + return pipeline +end + +function OITStage() + inputs = (:weighted_color_sum => Format(4, Float16), :alpha_product => Format(1, Float8)) + outputs = (:color => Format(4, Float8),) + return Stage("OIT"; inputs, outputs) +end + +function FXAAStage() + inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) + outputs = (:color_luma => Format(4, Float8),) + stage1 = Stage("FXAA luma"; inputs, outputs) + + inputs = (:color_luma => Format(4, Float8),) + outputs = (:color => Format(4, Float8),) + stage2 = Stage("FXAA apply"; inputs, outputs) + + pipeline = Pipeline(stage1, stage2) + connect!(pipeline, 1, :color_luma, 2, :color_luma) + + return pipeline +end + +function DisplayStage() + inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) + return Stage("display"; inputs) +end + + +function default_SSAO_pipeline() + # matching master with SSAO enabled + pipeline = Pipeline() + + push!(pipeline, SortStage()) # 1 + push!(pipeline, RenderStage()) # 2 + push!(pipeline, SSAOStage()) # 3, 4 + push!(pipeline, RenderStage()) # 5 + push!(pipeline, TransparentRenderStage()) # 6 + push!(pipeline, OITStage()) # 7 + push!(pipeline, FXAAStage()) # 8, 9 + push!(pipeline, DisplayStage()) # 10 + + connect!(pipeline, 2, :position, 3, :position) + connect!(pipeline, 2, :normal, 3, :normal) + connect!(pipeline, 2, :color, 4, :color) + connect!(pipeline, 2, :objectid, 4, :objectid) + connect!(pipeline, 2, :objectid, 8, :objectid) + connect!(pipeline, 2, :objectid, 10, :objectid) + connect!(pipeline, 4, :color, 8, :color) + connect!(pipeline, 5, :color, 8, :color) + connect!(pipeline, 5, :objectid, 10, :objectid) # will get bundled so we don't need to repeat + connect!(pipeline, 6, :weighted_color_sum, 7, :weighted_color_sum) + connect!(pipeline, 6, :objectid, 10, :objectid) + connect!(pipeline, 6, :alpha_product, 7, :alpha_product) + connect!(pipeline, 7, :color, 8, :color) + connect!(pipeline, 9, :color, 10, :color) + + return pipeline +end + +function default_pipeline() + # matching master + pipeline = Pipeline() + + push!(pipeline, SortStage()) + push!(pipeline, RenderStage()) + push!(pipeline, RenderStage()) + push!(pipeline, TransparentRenderStage()) + push!(pipeline, OITStage()) + push!(pipeline, FXAAStage()) + push!(pipeline, DisplayStage()) + + connect!(pipeline, 2, :color, 6, :color) + connect!(pipeline, 2, :objectid, 6, :objectid) + connect!(pipeline, 2, :objectid, 8, :objectid) + connect!(pipeline, 3, :color, 6, :color) + connect!(pipeline, 3, :objectid, 8, :objectid) + connect!(pipeline, 4, :weighted_color_sum, 5, :weighted_color_sum) + connect!(pipeline, 4, :objectid, 8, :objectid) + connect!(pipeline, 4, :alpha_product, 5, :alpha_product) + connect!(pipeline, 5, :color, 6, :color) + connect!(pipeline, 7, :color, 8, :color) + + return pipeline +end From 67ee4f096c570938e93403ec22683a634dacbb92 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Dec 2024 23:15:15 +0100 Subject: [PATCH 050/135] switch to Vector of connections --- src/utilities/RenderPipeline.jl | 117 +++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index bd09765f55a..d6473f89d7d 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -8,6 +8,8 @@ _promote_type(::Type{Float8}, ::Type{Float16}) = Float16 _promote_type(::Type{Float16}, ::Type{Float8}) = Float16 +# TODO: consider adding a "reuse immediately" flag so Stage can communicate that +# it allows output = input struct Format dims::Int type::DataType @@ -50,13 +52,14 @@ is_compatible_with(::Type, ::Type) = false # Connections can have multiple inputs and outputs # e.g. multiple Renders write to objectid and FXAA, SSAO, Display/pick read it struct ConnectionT{T} - inputs::Dict{T, Symbol} # stage => output name - outputs::Dict{T, Symbol} # stage => input name + inputs::Dict{T, Int} # stage => output Index + outputs::Dict{T, Int} # stage => input Index format::Format # derived from inputs & outputs formats end struct Stage - name::String + name::Symbol + # order matters for outputs inputs::Dict{Symbol, Int} outputs::Dict{Symbol, Int} @@ -65,16 +68,17 @@ struct Stage # ^ technically all of these are constants # v these are not - input_connections::Dict{Symbol, ConnectionT{Stage}} - output_connections::Dict{Symbol, ConnectionT{Stage}} + input_connections::Vector{ConnectionT{Stage}} + output_connections::Vector{ConnectionT{Stage}} end const Connection = ConnectionT{Stage} function Stage(name; inputs = NTuple{0, Pair{Symbol, Format}}(), outputs = NTuple{0, Pair{Symbol, Format}}()) - stage = Stage(name, - Dict{Symbol, Int}(), Dict{Symbol, Int}(), Format[], Format[], - Dict{Symbol, Connection}(), Dict{Symbol, Connection}() + stage = Stage(Symbol(name), + Dict{Symbol, Int}(), Dict{Symbol, Int}(), + Format[], Format[], + Connection[], Connection[] ) foreach(enumerate(inputs)) do (i, (k, v)) stage.inputs[k] = i @@ -87,11 +91,8 @@ function Stage(name; inputs = NTuple{0, Pair{Symbol, Format}}(), outputs = NTupl return stage end -function Connection(source::Stage, input::Symbol, target::Stage, output::Symbol) - format = Format( - source.output_formats[source.outputs[input]], - target.input_formats[target.inputs[output]] - ) +function Connection(source::Stage, input::Integer, target::Stage, output::Integer) + format = Format(source.output_formats[input], target.input_formats[output]) return Connection(Dict(source => input), Dict(target => output), format) end @@ -149,33 +150,40 @@ function connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer haskey(source.outputs, output) || error("output $output does not exist in source stage") haskey(target.inputs, input) || error("input $input does not exist in target stage") + # intialize if not yet initialized + isempty(source.output_connections) && resize!(source.output_connections, length(source.output_formats)) + isempty(target.input_connections) && resize!(target.input_connections, length(target.input_formats)) + + output_idx = source.outputs[output] + input_idx = target.inputs[input] + # create requested connection - connection = Connection(source, output, target, input) + connection = Connection(source, output_idx, target, input_idx) # if the input or output already has an edge, merge it with the create edge # e.g. the color output of source is used for a second stage # or the color input of target is written to by second stage - if haskey(source.output_connections, output) - old = source.output_connections[output] + if isassigned(source.output_connections, output_idx) + old = source.output_connections[output_idx] # There should be exactly one matching connection, and it's probably # near the end? idx = findlast(c -> c === old, pipeline.connections)::Int deleteat!(pipeline.connections, idx) connection = merge(connection, old) end - if haskey(target.input_connections, input) - old = target.input_connections[input] + if isassigned(target.input_connections, input_idx) + old = target.input_connections[input_idx] idx = findlast(c -> c === old, pipeline.connections)::Int deleteat!(pipeline.connections, idx) connection = merge(connection, old) end # attach connection to every input and output - for (stage, key) in connection.inputs - stage.output_connections[key] = connection + for (stage, idx) in connection.inputs + stage.output_connections[idx] = connection end - for (stage, key) in connection.outputs - stage.input_connections[key] = connection + for (stage, idx) in connection.outputs + stage.input_connections[idx] = connection end push!(pipeline.connections, connection) @@ -202,8 +210,8 @@ function verify(pipeline::Pipeline) end if earliest_input >= earliest_output - inputs = join(("$(stage.name).$key" for (stage, key) in connection.inputs), ", ") - outputs = join(("$(stage.name).$key" for (stage, key) in connection.outputs), ", ") + inputs = join(("$(stage.name).$idx" for (stage, idx) in connection.inputs), ", ") + outputs = join(("$(stage.name).$idx" for (stage, idx) in connection.outputs), ", ") error("Connection ($inputs) -> ($outputs) is read before being written to. Not allowed. ($earliest_input ≥ $earliest_output)") end end @@ -333,21 +341,23 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) if !isempty(stage.inputs) print(io, "\ninputs:") - ks = keys(stage.inputs) + ks = collect(keys(stage.inputs)) + sort!(ks, by = k -> stage.inputs[k]) pad = mapreduce(k -> length(string(k)), max, ks) - for k in ks - mark = haskey(stage.input_connections, k) ? 'x' : ' ' - print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.input_formats[stage.inputs[k]]) + for (i, k) in enumerate(ks) + mark = isassigned(stage.input_connections, i) ? 'x' : ' ' + print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.input_formats[i]) end end if !isempty(stage.outputs) print(io, "\noutputs:") - ks = keys(stage.outputs) + ks = collect(keys(stage.outputs)) + sort!(ks, by = k -> stage.outputs[k]) pad = mapreduce(k -> length(string(k)), max, ks) - for k in ks - mark = haskey(stage.output_connections, k) ? 'x' : ' ' - print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.output_formats[stage.outputs[k]]) + for (i, k) in enumerate(ks) + mark = isassigned(stage.output_connections, i) ? 'x' : ' ' + print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.output_formats[i]) end end @@ -355,9 +365,24 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) end function _names(connection::Connection) - inputs = unique(values(connection.inputs)) - outputs = unique(values(connection.outputs)) - return unique(vcat(inputs, outputs)) + names = Set{Symbol}() + for (stage, idx) in connection.inputs + for (k, i) in stage.outputs + if idx == i + push!(names, k) + break + end + end + end + for (stage, idx) in connection.outputs + for (k, i) in stage.inputs + if idx == i + push!(names, k) + break + end + end + end + return collect(names) end function Base.show(io::IO, connection::Connection) @@ -376,8 +401,15 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) if !isempty(connection.inputs) print(io, "\ninputs:") - elements = map(keys(connection.inputs), values(connection.inputs)) do stage, k - (stage.name, string(k), stage.output_formats[stage.outputs[k]]) + elements = map(keys(connection.inputs), values(connection.inputs)) do stage, idx + key = :temp + for (k, i) in stage.outputs + if idx == i + key = k + break + end + end + return (string(stage.name), string(key), stage.output_formats[idx]) end pad1 = mapreduce(x -> length(x[1]), max, elements) pad2 = mapreduce(x -> length(x[2]), max, elements) @@ -388,8 +420,15 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) if !isempty(connection.outputs) print(io, "\noutputs:") - elements = map(keys(connection.outputs), values(connection.outputs)) do stage, k - (stage.name, string(k), stage.input_formats[stage.inputs[k]]) + elements = map(keys(connection.outputs), values(connection.outputs)) do stage, idx + key = :temp + for (k, i) in stage.inputs + if idx == i + key = k + break + end + end + return (string(stage.name), string(key), stage.input_formats[idx]) end pad1 = mapreduce(x -> length(x[1]), max, elements) pad2 = mapreduce(x -> length(x[2]), max, elements) From f490da16cffb6db1ab97534d3c828424fda775c9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Dec 2024 23:51:16 +0100 Subject: [PATCH 051/135] split up multi-step stages --- GLMakie/src/postprocessing.jl | 168 +++++++++++++++++++------------- GLMakie/src/screen.jl | 10 +- src/utilities/RenderPipeline.jl | 18 ++-- 3 files changed, 114 insertions(+), 82 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 12d8b7c58c6..b0369e6cc58 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -177,7 +177,7 @@ end # Vaguely leaning on Vulkan Terminology struct RenderPass{Name} <: AbstractRenderStep framebuffer::GLFramebuffer - passes::Vector{RenderObject} + robj::RenderObject end @@ -199,7 +199,7 @@ function RenderPass{:OIT}(screen) :sum_color => get_buffer(factory.fb, :HDR_color), :prod_alpha => get_buffer(factory.fb, :OIT_weight), ) - pass = RenderObject( + robj = RenderObject( data, shader, () -> begin glDepthMask(GL_TRUE) @@ -216,9 +216,9 @@ function RenderPass{:OIT}(screen) end, nothing, shader_cache.context ) - pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) + robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) - return RenderPass{:OIT}(framebuffer, RenderObject[pass]) + return RenderPass{:OIT}(framebuffer, robj) end function run_step(screen, glscene, step::RenderPass{:OIT}) @@ -226,34 +226,41 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) wh = size(step.framebuffer) set_draw_buffers(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) - GLAbstraction.render(step.passes[1]) + GLAbstraction.render(step.robj) return end -function RenderPass{:SSAO}(screen) +function add_ssao_buffers(screen) @debug "Creating SSAO postprocessor" factory = screen.framebuffer_factory + if length(factory.render_buffer_ids) < 4 + ShaderAbstractions.switch_context!(shader_cache.context) + require_context(shader_cache.context) + + # Add missing buffers + pos_tex = Texture(shader_cache.context, Vec3f, size(screen), minfilter = :nearest, x_repeat = :clamp_to_edge) + push!(factory, :position => pos_tex) + # HDR_color always exists... + + # TODO: temp - update renderbuffers and main renderbuffer + # eventually RenderPlots should have dedicated GLFramebuffers too, which + # derive their draw buffers from the abstracted render pipeline + pos_id = attach_colorbuffer(factory.fb, :position, pos_tex) + normal_id = attach_colorbuffer(factory.fb, :normal, get_buffer(factory, :HDR_color)) + push!(factory.render_buffer_ids, pos_id, normal_id) + end -function ssao_postprocessor(framebuffer, shader_cache) - ShaderAbstractions.switch_context!(shader_cache.context) - require_context(shader_cache.context) - - # Add missing buffers - pos_tex = Texture(shader_cache.context, Vec3f, size(screen), minfilter = :nearest, x_repeat = :clamp_to_edge) - push!(factory, :position => pos_tex) - # HDR_color always exists... + return +end - # TODO: temp - update renderbuffers and main renderbuffer - # eventually RenderPlots should have dedicated GLFramebuffers too, which - # derive their draw buffers from the abstracted render pipeline - pos_id = attach_colorbuffer(factory.fb, :position, pos_tex) - normal_id = attach_colorbuffer(factory.fb, :normal, get_buffer(factory, :HDR_color)) - push!(factory.render_buffer_ids, pos_id, normal_id) +function RenderPass{:SSAO1}(screen) + factory = screen.framebuffer_factory + framebuffer = generate_framebuffer(factory, :HDR_color => :normal_occlusion) - framebuffer = generate_framebuffer(factory, :color, :HDR_color => :normal_occlusion) + add_ssao_buffers(screen) # SSAO setup N_samples = 64 @@ -266,7 +273,7 @@ function ssao_postprocessor(framebuffer, shader_cache) end # compute occlusion - shader1 = LazyShader( + shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO.frag"), @@ -274,9 +281,9 @@ function ssao_postprocessor(framebuffer, shader_cache) "N_samples" => "$N_samples" ) ) - data1 = Dict{Symbol, Any}( - :position_buffer => get_buffer(factory.fb, :position), - :normal_occlusion_buffer => get_buffer(factory.fb, :normal), + data = Dict{Symbol, Any}( + :position_buffer => get_buffer(factory, :position), + :normal_occlusion_buffer => get_buffer(factory, :HDR_color), :kernel => kernel, :noise => Texture( shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], @@ -287,38 +294,46 @@ function ssao_postprocessor(framebuffer, shader_cache) :bias => 0.025f0, :radius => 0.5f0 ) - pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) - pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) + + return RenderPass{:SSAO1}(framebuffer, robj) +end + +function RenderPass{:SSAO2}(screen) + factory = screen.framebuffer_factory + framebuffer = generate_framebuffer(factory, :color) + add_ssao_buffers(screen) # blur occlusion and combine with color - shader2 = LazyShader( + shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO_blur.frag") ) - data2 = Dict{Symbol, Any}( - :normal_occlusion => get_buffer(framebuffer, :normal_occlusion), - :color_texture => get_buffer(factory.fb, :color), - :ids => get_buffer(factory.fb, :objectid), + data = Dict{Symbol, Any}( + :normal_occlusion => get_buffer(factory, :HDR_color), + :color_texture => get_buffer(factory, :color), + :ids => get_buffer(factory, :objectid), :inv_texel_size => rcpframe(size(screen)), :blur_range => Int32(2) ) - pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) - pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) - return RenderPass{:SSAO}(framebuffer, [pass1, pass2]) + return RenderPass{:SSAO2}(framebuffer, robj) end -function run_step(screen, glscene, step::RenderPass{:SSAO}) - set_draw_buffers(step.framebuffer, :normal_occlusion) # occlusion buffer +function run_step(screen, glscene, step::RenderPass{:SSAO1}) + set_draw_buffers(step.framebuffer) # occlusion buffer wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) - data1 = step.passes[1].uniforms + data = step.robj.uniforms for (screenid, scene) in screen.screens # Select the area of one leaf scene # This should be per scene because projection may vary between @@ -328,25 +343,30 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) a = viewport(scene)[] glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms - data1[:projection] = Mat4f(scene.camera.projection[]) - data1[:bias] = scene.ssao.bias[] - data1[:radius] = scene.ssao.radius[] - data1[:noise_scale] = Vec2f(0.25f0 .* size(step.framebuffer)) - GLAbstraction.render(step.passes[1]) + data[:projection] = Mat4f(scene.camera.projection[]) + data[:bias] = scene.ssao.bias[] + data[:radius] = scene.ssao.radius[] + data[:noise_scale] = Vec2f(0.25f0 .* size(step.framebuffer)) + GLAbstraction.render(step.robj) end + return +end + +function run_step(screen, glscene, step::RenderPass{:SSAO2}) # SSAO - blur occlusion and apply to color - set_draw_buffers(step.framebuffer, :color) # color buffer - data2 = step.passes[2].uniforms + set_draw_buffers(step.framebuffer) # color buffer + ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) + data = step.robj.uniforms for (screenid, scene) in screen.screens # Select the area of one leaf scene isempty(scene.children) || continue a = viewport(scene)[] glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms - data2[:blur_range] = scene.ssao.blur - data2[:inv_texel_size] = rcpframe(size(step.framebuffer)) - GLAbstraction.render(step.passes[2]) + data[:blur_range] = scene.ssao.blur + data[:inv_texel_size] = rcpframe(size(step.framebuffer)) + GLAbstraction.render(step.robj) end glDisable(GL_SCISSOR_TEST) @@ -354,56 +374,64 @@ function run_step(screen, glscene, step::RenderPass{:SSAO}) end -function RenderPass{:FXAA}(screen) - @debug "Creating FXAA postprocessor" +function RenderPass{:FXAA1}(screen) factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :color, :HDR_color => :color_luma) + framebuffer = generate_framebuffer(factory, :HDR_color => :color_luma) # calculate luma for FXAA - shader1 = LazyShader( + shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/postprocess.frag") ) - data1 = Dict{Symbol, Any}( - :color_texture => get_buffer(factory.fb, :color), - :object_ids => get_buffer(factory.fb, :objectid) + data = Dict{Symbol, Any}( + :color_texture => get_buffer(factory, :color), + :object_ids => get_buffer(factory, :objectid) ) - pass1 = RenderObject(data1, shader1, PostprocessPrerender(), nothing, shader_cache.context) - pass1.postrenderfunction = () -> draw_fullscreen(pass1.vertexarray.id) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) + + return RenderPass{:FXAA1}(framebuffer, robj) +end + +function RenderPass{:FXAA2}(screen) + factory = screen.framebuffer_factory + framebuffer = generate_framebuffer(factory, :color) # perform FXAA - shader2 = LazyShader( + shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/fxaa.frag") ) - data2 = Dict{Symbol, Any}( - :color_texture => get_buffer(framebuffer, :color_luma), + data = Dict{Symbol, Any}( + :color_texture => get_buffer(factory, :HDR_color), :RCPFrame => rcpframe(size(framebuffer)), ) - pass2 = RenderObject(data2, shader2, PostprocessPrerender(), nothing, shader_cache.context) - pass2.postrenderfunction = () -> draw_fullscreen(pass2.vertexarray.id) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) - return RenderPass{:FXAA}(framebuffer, RenderObject[pass1, pass2]) + return RenderPass{:FXAA2}(framebuffer, robj) end -function run_step(screen, glscene, step::RenderPass{:FXAA}) +function run_step(screen, glscene, step::RenderPass{:FXAA1}) # FXAA - calculate LUMA - set_draw_buffers(step.framebuffer, :color_luma) + set_draw_buffers(step.framebuffer) # TODO: make scissor explicit? wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) # necessary with negative SSAO bias... glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) - GLAbstraction.render(step.passes[1]) + GLAbstraction.render(step.robj) + return +end +function run_step(screen, glscene, step::RenderPass{:FXAA2}) # FXAA - perform anti-aliasing - set_draw_buffers(step.framebuffer, :color) # color buffer - step.passes[2][:RCPFrame] = rcpframe(size(step.framebuffer)) - GLAbstraction.render(step.passes[2]) - + set_draw_buffers(step.framebuffer) # color buffer + step.robj[:RCPFrame] = rcpframe(size(step.framebuffer)) + GLAbstraction.render(step.robj) return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 3b2bfb82a88..ad3953139b7 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -299,10 +299,12 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) push!(screen.render_pipeline, SortPlots()) push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) push!(screen.render_pipeline, RenderPlots(screen, :OIT)) push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, EmptyRenderStep()) + push!(screen.render_pipeline, EmptyRenderStep()) push!(screen.render_pipeline, BlitToScreen(screen)) if owns_glscreen @@ -403,9 +405,11 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B return end - replace_renderpass!(config.ssao ? RenderPass{:SSAO} : EmptyRenderStep, 3) - replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 6) - replace_renderpass!(config.fxaa ? RenderPass{:FXAA} : EmptyRenderStep, 7) + replace_renderpass!(config.ssao ? RenderPass{:SSAO1} : EmptyRenderStep, 3) + replace_renderpass!(config.ssao ? RenderPass{:SSAO2} : EmptyRenderStep, 4) + replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 7) + replace_renderpass!(config.fxaa ? RenderPass{:FXAA1} : EmptyRenderStep, 8) + replace_renderpass!(config.fxaa ? RenderPass{:FXAA2} : EmptyRenderStep, 9) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index d6473f89d7d..a6f42fd4d74 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -446,7 +446,7 @@ end ################################################################################ -SortStage() = Stage("z-sort") +SortStage() = Stage(:ZSort) # I guess we should have multiple versions of this? Because SSAO render and OIT render don't really fit? # SSAO color objectid position normal @@ -459,7 +459,7 @@ function RenderStage() :position => Format(3, Float16), :normal => Format(3, Float16), ) - Stage("render", outputs = outputs) + Stage(:Render, outputs = outputs) end function TransparentRenderStage() outputs = ( @@ -467,7 +467,7 @@ function TransparentRenderStage() :objectid => Format(2, UInt32), :alpha_product => Format(1, Float8), ) - Stage("transparent render", outputs = outputs) + Stage(:TransparentRender, outputs = outputs) end # Want a MultiSzage kinda thing @@ -476,10 +476,10 @@ function SSAOStage() :position => Format(3, Float32), :normal => Format(3, Float16) ) - stage1 = Stage("SSAO occlusion"; inputs, outputs = (:occlusion => Format(1, Float8),)) + stage1 = Stage(:SSAO1; inputs, outputs = (:occlusion => Format(1, Float8),)) inputs = (:occlusion => Format(1, Float8), :color => Format(4, Float8), :objectid => Format(2, UInt32)) - stage2 = Stage("SSAO blur", inputs = inputs, outputs = (:color => Format(),)) + stage2 = Stage(:SSAO2, inputs = inputs, outputs = (:color => Format(),)) pipeline = Pipeline(stage1, stage2) connect!(pipeline, 1, :occlusion, 2, :occlusion) @@ -490,17 +490,17 @@ end function OITStage() inputs = (:weighted_color_sum => Format(4, Float16), :alpha_product => Format(1, Float8)) outputs = (:color => Format(4, Float8),) - return Stage("OIT"; inputs, outputs) + return Stage(:OIT; inputs, outputs) end function FXAAStage() inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) outputs = (:color_luma => Format(4, Float8),) - stage1 = Stage("FXAA luma"; inputs, outputs) + stage1 = Stage(:FXAA1; inputs, outputs) inputs = (:color_luma => Format(4, Float8),) outputs = (:color => Format(4, Float8),) - stage2 = Stage("FXAA apply"; inputs, outputs) + stage2 = Stage(:FXAA2; inputs, outputs) pipeline = Pipeline(stage1, stage2) connect!(pipeline, 1, :color_luma, 2, :color_luma) @@ -510,7 +510,7 @@ end function DisplayStage() inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) - return Stage("display"; inputs) + return Stage(:Display; inputs) end From ca0af0ee32db4de6592930cab9e65d007d682023 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Dec 2024 23:54:29 +0100 Subject: [PATCH 052/135] rename Format to avoid name collision --- src/utilities/RenderPipeline.jl | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index a6f42fd4d74..554e1704acc 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -10,26 +10,26 @@ _promote_type(::Type{Float16}, ::Type{Float8}) = Float16 # TODO: consider adding a "reuse immediately" flag so Stage can communicate that # it allows output = input -struct Format +struct BufferFormat dims::Int type::DataType - Format(dims = 4, type = Float8) = new(dims, type) + BufferFormat(dims = 4, type = Float8) = new(dims, type) end -function Format(input::Format, output::Format) +function BufferFormat(input::BufferFormat, output::BufferFormat) if is_compatible(input, output) dims = max(input.dims, output.dims) type = _promote_type(input.type, output.type) - return Format(dims, type) + return BufferFormat(dims, type) end - error("Could not generate compatible Format between $input and $output") + error("Could not generate compatible BufferFormat between $input and $output") end -function is_compatible(f1::Format, f2::Format) +function is_compatible(f1::BufferFormat, f2::BufferFormat) is_compatible_with(f1.type, f2.type) || is_compatible_with(f2.type, f1.type) end -function is_compatible_with(f::Format, requirement::Format) +function is_compatible_with(f::BufferFormat, requirement::BufferFormat) return (f.dims <= requirement.dims) && is_compatible_with(f.type, requirement.type) end @@ -54,7 +54,7 @@ is_compatible_with(::Type, ::Type) = false struct ConnectionT{T} inputs::Dict{T, Int} # stage => output Index outputs::Dict{T, Int} # stage => input Index - format::Format # derived from inputs & outputs formats + format::BufferFormat # derived from inputs & outputs formats end struct Stage @@ -63,8 +63,8 @@ struct Stage # order matters for outputs inputs::Dict{Symbol, Int} outputs::Dict{Symbol, Int} - input_formats::Vector{Format} - output_formats::Vector{Format} + input_formats::Vector{BufferFormat} + output_formats::Vector{BufferFormat} # ^ technically all of these are constants # v these are not @@ -74,10 +74,10 @@ end const Connection = ConnectionT{Stage} -function Stage(name; inputs = NTuple{0, Pair{Symbol, Format}}(), outputs = NTuple{0, Pair{Symbol, Format}}()) +function Stage(name; inputs = NTuple{0, Pair{Symbol, BufferFormat}}(), outputs = NTuple{0, Pair{Symbol, BufferFormat}}()) stage = Stage(Symbol(name), Dict{Symbol, Int}(), Dict{Symbol, Int}(), - Format[], Format[], + BufferFormat[], BufferFormat[], Connection[], Connection[] ) foreach(enumerate(inputs)) do (i, (k, v)) @@ -92,7 +92,7 @@ function Stage(name; inputs = NTuple{0, Pair{Symbol, Format}}(), outputs = NTupl end function Connection(source::Stage, input::Integer, target::Stage, output::Integer) - format = Format(source.output_formats[input], target.input_formats[output]) + format = BufferFormat(source.output_formats[input], target.input_formats[output]) return Connection(Dict(source => input), Dict(target => output), format) end @@ -100,7 +100,7 @@ function Base.merge(c1::Connection, c2::Connection) return Connection( merge!(c1.inputs, c2.inputs), merge!(c1.outputs, c2.outputs), - Format(c1.format, c2.format) + BufferFormat(c1.format, c2.format) ) end @@ -220,7 +220,7 @@ function verify(pipeline::Pipeline) end format_complexity(c::Connection) = format_complexity(c.format) -format_complexity(f::Format) = f.dims * sizeof(f.type) +format_complexity(f::BufferFormat) = f.dims * sizeof(f.type) function generate_buffers(pipeline::Pipeline) # TODO: is this necessary? @@ -239,7 +239,7 @@ function generate_buffers(pipeline::Pipeline) end end - buffers = Format[] + buffers = BufferFormat[] connection2idx = Dict{Connection, Int}() # into buffer needs_buffer = Connection[] available = Int[] @@ -283,7 +283,7 @@ function generate_buffers(pipeline::Pipeline) # - using it is cheaper than creating a new buffer # - it is more compatible than the last when both are 0 cost # (i.e prefer 3, Float16 over 3 Float8 for 3 Float16 target) - updated_comp = format_complexity(Format(buffers[i], connection.format)) + updated_comp = format_complexity(BufferFormat(buffers[i], connection.format)) buffer_comp = format_complexity(buffers[i]) delta = updated_comp - buffer_comp is_cheaper = (delta < prev_delta) && (delta <= conn_comp) @@ -302,7 +302,7 @@ function generate_buffers(pipeline::Pipeline) best_match = length(buffers) elseif buffers[best_match] != connection.format # found upgradeable format, upgrade it (or use it) - new_format = Format(buffers[best_match], connection.format) + new_format = BufferFormat(buffers[best_match], connection.format) buffers[best_match] = new_format end @@ -331,8 +331,8 @@ end ################################################################################ -function Base.show(io::IO, format::Format) - print(io, "Format($(format.dims), $(format.type))") +function Base.show(io::IO, format::BufferFormat) + print(io, "BufferFormat($(format.dims), $(format.type))") end Base.show(io::IO, stage::Stage) = print(io, "Stage($(stage.name))") @@ -454,18 +454,18 @@ SortStage() = Stage(:ZSort) # OIT HDR_color objectid weight function RenderStage() outputs = ( - :color => Format(4, Float8), - :objectid => Format(2, UInt32), - :position => Format(3, Float16), - :normal => Format(3, Float16), + :color => BufferFormat(4, Float8), + :objectid => BufferFormat(2, UInt32), + :position => BufferFormat(3, Float16), + :normal => BufferFormat(3, Float16), ) Stage(:Render, outputs = outputs) end function TransparentRenderStage() outputs = ( - :weighted_color_sum => Format(4, Float16), - :objectid => Format(2, UInt32), - :alpha_product => Format(1, Float8), + :weighted_color_sum => BufferFormat(4, Float16), + :objectid => BufferFormat(2, UInt32), + :alpha_product => BufferFormat(1, Float8), ) Stage(:TransparentRender, outputs = outputs) end @@ -473,13 +473,13 @@ end # Want a MultiSzage kinda thing function SSAOStage() inputs = ( - :position => Format(3, Float32), - :normal => Format(3, Float16) + :position => BufferFormat(3, Float32), + :normal => BufferFormat(3, Float16) ) - stage1 = Stage(:SSAO1; inputs, outputs = (:occlusion => Format(1, Float8),)) + stage1 = Stage(:SSAO1; inputs, outputs = (:occlusion => BufferFormat(1, Float8),)) - inputs = (:occlusion => Format(1, Float8), :color => Format(4, Float8), :objectid => Format(2, UInt32)) - stage2 = Stage(:SSAO2, inputs = inputs, outputs = (:color => Format(),)) + inputs = (:occlusion => BufferFormat(1, Float8), :color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) + stage2 = Stage(:SSAO2, inputs = inputs, outputs = (:color => BufferFormat(),)) pipeline = Pipeline(stage1, stage2) connect!(pipeline, 1, :occlusion, 2, :occlusion) @@ -488,18 +488,18 @@ function SSAOStage() end function OITStage() - inputs = (:weighted_color_sum => Format(4, Float16), :alpha_product => Format(1, Float8)) - outputs = (:color => Format(4, Float8),) + inputs = (:weighted_color_sum => BufferFormat(4, Float16), :alpha_product => BufferFormat(1, Float8)) + outputs = (:color => BufferFormat(4, Float8),) return Stage(:OIT; inputs, outputs) end function FXAAStage() - inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) - outputs = (:color_luma => Format(4, Float8),) + inputs = (:color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) + outputs = (:color_luma => BufferFormat(4, Float8),) stage1 = Stage(:FXAA1; inputs, outputs) - inputs = (:color_luma => Format(4, Float8),) - outputs = (:color => Format(4, Float8),) + inputs = (:color_luma => BufferFormat(4, Float8),) + outputs = (:color => BufferFormat(4, Float8),) stage2 = Stage(:FXAA2; inputs, outputs) pipeline = Pipeline(stage1, stage2) @@ -509,7 +509,7 @@ function FXAAStage() end function DisplayStage() - inputs = (:color => Format(4, Float8), :objectid => Format(2, UInt32)) + inputs = (:color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) return Stage(:Display; inputs) end From 17209e7a4954e380baa6d08b88663da93c15d869 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 00:40:13 +0100 Subject: [PATCH 053/135] collect framebuffer buffers in array, prepare some utilities --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 10 +++ GLMakie/src/glwindow.jl | 78 +++++++++++++--------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 548ec8c1112..69c54fcb20b 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -176,6 +176,16 @@ function gl_attach(buffer::RenderBuffer, attachment::GLenum) glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, buffer) end +function Base.delete!(fb::GLFramebuffer, key::Symbol) + idx = fb.name2idx[key] + attachment = fb.attachments[idx] + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, 0, 0) + deleteat!(fb.attachments, idx) + deleteat!(fb.buffers, idx) + delete!(fb, key) + return fb +end + get_attachment(fb::GLFramebuffer, key::Symbol) = fb.attachments[fb.name2idx[key]] get_buffer(fb::GLFramebuffer, key::Symbol) = fb.buffers[fb.name2idx[key]] diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index a74e7acb404..4b12d1fbef2 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -12,7 +12,8 @@ Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) mutable struct FramebufferFactory fb::GLFramebuffer - buffers::Dict{Symbol, Texture} # TODO: temp, should be unnamed collection + buffer_key2idx::Dict{Symbol, Int} # TODO: temp, should be unnamed collection + buffers::Vector{Texture} render_buffer_ids::Vector{GLuint} children::Vector{GLFramebuffer} # TODO: how else can we handle resizing? end @@ -20,19 +21,10 @@ end # it's guaranteed, that they all have the same size # TODO: forwards... for now Base.size(fb::FramebufferFactory) = size(fb.fb) -Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffers, key) -GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[key] +Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffer_key2idx, key) +GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[fb.buffer_key2idx[key]] GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) -function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) - foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), values(fb.buffers)) - resize!(fb.fb, w, h) - filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok? - foreach(fb -> resize!(fb, w, h), fb.children) - return -end - - Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) require_context(context) @@ -61,13 +53,8 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) - buffers = Dict{Symbol, Texture}( - :color => color_buffer, - :objectid => objectid_buffer, - :HDR_color => HDR_color_buffer, - :OIT_weight => OIT_weight_buffer, - :depth_stencil => depth_buffer, - ) + name2idx = Dict(:color => 1, :objectid => 2, :HDR_color => 3, :OIT_weight => 4, :depth_stencil => 5) + buffers = [color_buffer, objectid_buffer, HDR_color_buffer, OIT_weight_buffer, depth_buffer] # Create render framebuffer fb = GLFramebuffer(fb_size) @@ -81,36 +68,63 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) check_framebuffer() - return FramebufferFactory(fb, buffers, [color_attachment, objectid_attachment], GLFramebuffer[]) + return FramebufferFactory(fb, name2idx, buffers, [color_attachment, objectid_attachment], GLFramebuffer[]) +end + +function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) + foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), fb.buffers) + resize!(fb.fb, w, h) + filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok? + foreach(fb -> resize!(fb, w, h), fb.children) + return +end + +function unsafe_empty!(factory::FramebufferFactory) + empty!(factory.buffer_key2idx) + empty!(factory.buffers) + empty!(factory.children) + if length(factory.children) > 2 + resize!(factory.children, 2) + delete!(factory.fb, :position) + delete!(factory.fb, :normal) + end + return factory end # TODO: temporary function Base.push!(factory::FramebufferFactory, kv::Pair{Symbol, <: Texture}) - if haskey(factory.buffers, kv[1]) + if haskey(factory.buffer_key2idx, kv[1]) @error("Pushed buffer $(kv[1]) already assigned.") return end - push!(factory.buffers, kv) + push!(factory.buffers, kv[2]) + push!(factory.buffer_key2idx, kv[1] => length(factory.buffers)) return end -function generate_framebuffer(factory::FramebufferFactory, names...) - filter!(fb -> fb.id != 0, factory.children) # cleanup? - +function generate_framebuffer(factory::FramebufferFactory, args...) parse_arg(name::Symbol) = name => name parse_arg(p::Pair{Symbol, Symbol}) = p parse_arg(x::Any) = error("$x not accepted") + return generate_framebuffer(factory, parse_arg.(args)...) +end + +function generate_framebuffer(factory::FramebufferFactory, names::Pair{Symbol, Symbol}...) + remapped = map(kv -> factory.buffer_key2idx[kv[1]] => kv[2], names) + return generate_framebuffer(factory, remapped...) +end + +function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) + filter!(fb -> fb.id != 0, factory.children) # cleanup? + fb = GLFramebuffer(size(factory.fb)) - attach_depthstencilbuffer(fb, :depth_stencil, factory.buffers[:depth_stencil]) + attach_depthstencilbuffer(fb, :depth_stencil, factory.buffers[factory.buffer_key2idx[:depth_stencil]]) - for arg in names - lookup, name = parse_arg(arg) - haskey(factory.buffers, lookup) || error("Add buffers yourself for now") + for (idx, name) in idx2name haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") - in(lookup, [:depth, :stencil]) && error("Depth and stencil always exist under the same name.") - - attach_colorbuffer(fb, name, factory.buffers[lookup]) + # in(lookup, [:depth, :stencil]) && error("Depth and stencil always exist under the same name.") + attach_colorbuffer(fb, name, factory.buffers[idx]) end check_framebuffer() From d1306c8fec8fe5dd41de6b2914db3dff9993ae43 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 14:27:22 +0100 Subject: [PATCH 054/135] integrate GLMakie with new render pipeline (prototype) --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 32 ++++++- GLMakie/src/gl_backend.jl | 1 + GLMakie/src/glwindow.jl | 59 ++++++------ GLMakie/src/postprocessing.jl | 105 ++++++--------------- GLMakie/src/render_pipeline.jl | 81 ++++++++++++++++ GLMakie/src/rendering.jl | 3 +- GLMakie/src/screen.jl | 35 +++---- src/Makie.jl | 1 + src/utilities/RenderPipeline.jl | 19 ++-- 9 files changed, 200 insertions(+), 136 deletions(-) create mode 100644 GLMakie/src/render_pipeline.jl diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 69c54fcb20b..63e2dd49695 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -141,6 +141,28 @@ function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) return attach(fb, key, buffer, length(fb.attachments) + 1, GL_DEPTH_STENCIL_ATTACHMENT) end +const ATTACHMENT_LOOKUP = Dict{Int, Symbol}( + GL_DEPTH_ATTACHMENT => :GL_DEPTH_ATTACHMENT, + GL_STENCIL_ATTACHMENT => :GL_STENCIL_ATTACHMENT, + GL_DEPTH_STENCIL_ATTACHMENT => :GL_DEPTH_STENCIL_ATTACHMENT, + GL_COLOR_ATTACHMENT0 => :GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1 => :GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2 => :GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3 => :GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4 => :GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5 => :GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6 => :GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7 => :GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8 => :GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9 => :GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10 => :GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11 => :GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12 => :GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13 => :GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14 => :GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 => :GL_COLOR_ATTACHMENT15 +) + function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment::GLenum) haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") if attachment in fb.attachments @@ -156,8 +178,14 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment error("Cannot attach $key as a $type attachment as it is already attached.") end - bind(fb) - gl_attach(buffer, attachment) + try + bind(fb) + gl_attach(buffer, attachment) + check_framebuffer() + catch e + @info "$key -> $attachment = $(get(ATTACHMENT_LOOKUP, attachment, :UNKNOWN)) failed, with framebuffer id = $(fb.id)" + rethrow(e) + end # keep depth/stenctil/depth_stencil at end so that we can directly use # fb.attachments when setting drawbuffers for (k, v) in fb.name2idx diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index d2a674043f2..1217350ebb1 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -82,6 +82,7 @@ end include("glwindow.jl") include("postprocessing.jl") include("screen.jl") +include("render_pipeline.jl") include("glshaders/visualize_interface.jl") include("glshaders/lines.jl") include("glshaders/image_like.jl") diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 4b12d1fbef2..349b226a745 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -11,19 +11,26 @@ Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T( Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) mutable struct FramebufferFactory - fb::GLFramebuffer + size::Tuple{Int, Int} buffer_key2idx::Dict{Symbol, Int} # TODO: temp, should be unnamed collection buffers::Vector{Texture} - render_buffer_ids::Vector{GLuint} + core_buffers::Dict{Symbol, Texture} # Not managed by render pipeline children::Vector{GLFramebuffer} # TODO: how else can we handle resizing? end # it's guaranteed, that they all have the same size # TODO: forwards... for now -Base.size(fb::FramebufferFactory) = size(fb.fb) +Base.size(fb::FramebufferFactory) = fb.size Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffer_key2idx, key) -GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[fb.buffer_key2idx[key]] -GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) +function GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) + if haskey(fb.core_buffers, key) + return fb.core_buffers[key] + else + return fb.buffers[fb.buffer_key2idx[key]] + end +end +GLAbstraction.get_buffer(fb::FramebufferFactory, idx::Int) = fb.buffers[idx] +# GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) @@ -53,27 +60,17 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge ) - name2idx = Dict(:color => 1, :objectid => 2, :HDR_color => 3, :OIT_weight => 4, :depth_stencil => 5) - buffers = [color_buffer, objectid_buffer, HDR_color_buffer, OIT_weight_buffer, depth_buffer] - - # Create render framebuffer - fb = GLFramebuffer(fb_size) + name2idx = Dict(:color => 1, :objectid => 2, :HDR_color => 3, :OIT_weight => 4) + buffers = [color_buffer, objectid_buffer, HDR_color_buffer, OIT_weight_buffer] - # attach buffers - color_attachment = attach_colorbuffer(fb, :color, color_buffer) - objectid_attachment = attach_colorbuffer(fb, :objectid, objectid_buffer) - attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) - attach_colorbuffer(fb, :HDR_color, HDR_color_buffer) # TODO: framebuffer for RenderPlots - attach_colorbuffer(fb, :OIT_weight, OIT_weight_buffer) # TODO: framebuffer for RenderPlots - - check_framebuffer() - - return FramebufferFactory(fb, name2idx, buffers, [color_attachment, objectid_attachment], GLFramebuffer[]) + return FramebufferFactory(fb_size, name2idx, buffers, Dict(:depth_stencil => depth_buffer), GLFramebuffer[]) end function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), fb.buffers) - resize!(fb.fb, w, h) + foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), values(fb.core_buffers)) + # resize!(fb.fb, w, h) + fb.size = (w, h) filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok? foreach(fb -> resize!(fb, w, h), fb.children) return @@ -83,11 +80,6 @@ function unsafe_empty!(factory::FramebufferFactory) empty!(factory.buffer_key2idx) empty!(factory.buffers) empty!(factory.children) - if length(factory.children) > 2 - resize!(factory.children, 2) - delete!(factory.fb, :position) - delete!(factory.fb, :normal) - end return factory end @@ -97,9 +89,14 @@ function Base.push!(factory::FramebufferFactory, kv::Pair{Symbol, <: Texture}) @error("Pushed buffer $(kv[1]) already assigned.") return end - push!(factory.buffers, kv[2]) + push!(factory, kv[2]) push!(factory.buffer_key2idx, kv[1] => length(factory.buffers)) - return + return factory +end + +function Base.push!(factory::FramebufferFactory, tex::Texture) + push!(factory.buffers, tex) + return factory end function generate_framebuffer(factory::FramebufferFactory, args...) @@ -118,8 +115,8 @@ end function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) filter!(fb -> fb.id != 0, factory.children) # cleanup? - fb = GLFramebuffer(size(factory.fb)) - attach_depthstencilbuffer(fb, :depth_stencil, factory.buffers[factory.buffer_key2idx[:depth_stencil]]) + fb = GLFramebuffer(size(factory)) + attach_depthstencilbuffer(fb, :depth_stencil, factory.core_buffers[:depth_stencil]) for (idx, name) in idx2name haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") @@ -127,8 +124,6 @@ function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, S attach_colorbuffer(fb, name, factory.buffers[idx]) end - check_framebuffer() - push!(factory.children, fb) return fb diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index b0369e6cc58..9d0750eada8 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -97,7 +97,6 @@ compare(val::Integer, filter::FilterOptions) = (filter == FilterAny) || (val == struct RenderPlots <: AbstractRenderStep framebuffer::GLFramebuffer - targets::Vector{GLuint} clear::Vector{Pair{Int, Vec4f}} # target index -> color ssao::FilterOptions @@ -105,22 +104,16 @@ struct RenderPlots <: AbstractRenderStep fxaa::FilterOptions end -function RenderPlots(screen, stage) - fb = screen.framebuffer_factory.fb +function RenderPlots(screen, framebuffer, inputs, stage) if stage === :SSAO - return RenderPlots( - fb, screen.framebuffer_factory.render_buffer_ids, [3 => Vec4f(0), 4 => Vec4f(0)], - FilterTrue, FilterFalse, FilterAny) + return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) elseif stage === :FXAA - return RenderPlots( - fb, get_attachment.(Ref(fb), [:color, :objectid]), Pair{Int, Vec4f}[], - FilterFalse, FilterFalse, FilterAny) + return RenderPlots(framebuffer, Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) elseif stage === :OIT - targets = get_attachment.(Ref(fb), [:HDR_color, :objectid, :OIT_weight]) # HDR_color containing sums clears to 0 # OIT_weight containing products clears to 1 clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(fb, targets, clear, FilterAny, FilterTrue, FilterAny) + return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) else error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") end @@ -140,13 +133,14 @@ function run_step(screen, glscene, step::RenderPlots) GLAbstraction.bind(step.framebuffer) for (idx, color) in step.clear - idx <= length(step.targets) || continue - glDrawBuffer(step.targets[idx]) + # TODO: Hacky + idx <= step.framebuffer.counter || continue + glDrawBuffer(step.framebuffer.attachments[idx]) glClearColor(color...) glClear(GL_COLOR_BUFFER_BIT) end - glDrawBuffers(length(step.targets), step.targets) + set_draw_buffers(step.framebuffer) for (zindex, screenid, elem) in screen.renderlist should_render = elem.visible && @@ -182,12 +176,9 @@ end -function RenderPass{:OIT}(screen) +function RenderPass{:OIT}(screen, framebuffer, inputs) @debug "Creating OIT postprocessor" - factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :color) - # Based on https://jcgt.org/published/0002/02/09/, see #1390 # OIT setup shader = LazyShader( @@ -195,9 +186,10 @@ function RenderPass{:OIT}(screen) loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/OIT_blend.frag") ) + # TODO: rename in shader data = Dict{Symbol, Any}( - :sum_color => get_buffer(factory.fb, :HDR_color), - :prod_alpha => get_buffer(factory.fb, :OIT_weight), + :sum_color => inputs[:weighted_color_sum], + :prod_alpha => inputs[:alpha_product], ) robj = RenderObject( data, shader, @@ -230,38 +222,7 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) return end - -function add_ssao_buffers(screen) - @debug "Creating SSAO postprocessor" - - factory = screen.framebuffer_factory - - if length(factory.render_buffer_ids) < 4 - ShaderAbstractions.switch_context!(shader_cache.context) - require_context(shader_cache.context) - - # Add missing buffers - pos_tex = Texture(shader_cache.context, Vec3f, size(screen), minfilter = :nearest, x_repeat = :clamp_to_edge) - push!(factory, :position => pos_tex) - # HDR_color always exists... - - # TODO: temp - update renderbuffers and main renderbuffer - # eventually RenderPlots should have dedicated GLFramebuffers too, which - # derive their draw buffers from the abstracted render pipeline - pos_id = attach_colorbuffer(factory.fb, :position, pos_tex) - normal_id = attach_colorbuffer(factory.fb, :normal, get_buffer(factory, :HDR_color)) - push!(factory.render_buffer_ids, pos_id, normal_id) - end - - return -end - -function RenderPass{:SSAO1}(screen) - factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :HDR_color => :normal_occlusion) - - add_ssao_buffers(screen) - +function RenderPass{:SSAO1}(screen, framebuffer, inputs) # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -282,8 +243,8 @@ function RenderPass{:SSAO1}(screen) ) ) data = Dict{Symbol, Any}( - :position_buffer => get_buffer(factory, :position), - :normal_occlusion_buffer => get_buffer(factory, :HDR_color), + :position_buffer => inputs[:position], + :normal_occlusion_buffer => inputs[:normal], :kernel => kernel, :noise => Texture( shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], @@ -300,12 +261,7 @@ function RenderPass{:SSAO1}(screen) return RenderPass{:SSAO1}(framebuffer, robj) end -function RenderPass{:SSAO2}(screen) - factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :color) - - add_ssao_buffers(screen) - +function RenderPass{:SSAO2}(screen, framebuffer, inputs) # blur occlusion and combine with color shader = LazyShader( screen.shader_cache, @@ -313,9 +269,9 @@ function RenderPass{:SSAO2}(screen) loadshader("postprocessing/SSAO_blur.frag") ) data = Dict{Symbol, Any}( - :normal_occlusion => get_buffer(factory, :HDR_color), - :color_texture => get_buffer(factory, :color), - :ids => get_buffer(factory, :objectid), + :normal_occlusion => inputs[:occlusion], + :color_texture => inputs[:color], + :ids => inputs[:objectid], :inv_texel_size => rcpframe(size(screen)), :blur_range => Int32(2) ) @@ -374,10 +330,7 @@ function run_step(screen, glscene, step::RenderPass{:SSAO2}) end -function RenderPass{:FXAA1}(screen) - factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :HDR_color => :color_luma) - +function RenderPass{:FXAA1}(screen, framebuffer, inputs) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, @@ -385,8 +338,8 @@ function RenderPass{:FXAA1}(screen) loadshader("postprocessing/postprocess.frag") ) data = Dict{Symbol, Any}( - :color_texture => get_buffer(factory, :color), - :object_ids => get_buffer(factory, :objectid) + :color_texture => inputs[:color], + :object_ids => inputs[:objectid], ) robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) @@ -394,10 +347,7 @@ function RenderPass{:FXAA1}(screen) return RenderPass{:FXAA1}(framebuffer, robj) end -function RenderPass{:FXAA2}(screen) - factory = screen.framebuffer_factory - framebuffer = generate_framebuffer(factory, :color) - +function RenderPass{:FXAA2}(screen, framebuffer, inputs) # perform FXAA shader = LazyShader( screen.shader_cache, @@ -405,7 +355,7 @@ function RenderPass{:FXAA2}(screen) loadshader("postprocessing/fxaa.frag") ) data = Dict{Symbol, Any}( - :color_texture => get_buffer(factory, :HDR_color), + :color_texture => inputs[:color_luma], :RCPFrame => rcpframe(size(framebuffer)), ) robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) @@ -440,19 +390,20 @@ end # TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer + attachment::GLuint screen_framebuffer_id::Int # Screen not available yet - function BlitToScreen(screen, screen_framebuffer_id::Integer = 0) + function BlitToScreen(framebuffer, attachment, screen_framebuffer_id::Integer = 0) @debug "Creating to screen postprocessor" - return new(screen.framebuffer_factory.fb, screen_framebuffer_id) + return new(framebuffer, attachment, screen_framebuffer_id) end end function run_step(screen, ::Nothing, step::BlitToScreen) # Set source glBindFramebuffer(GL_READ_FRAMEBUFFER, step.framebuffer.id) - glReadBuffer(get_attachment(step.framebuffer, :color)) # for safety + glReadBuffer(step.attachment) # for safety # TODO: Is this an observable? Can this be static? # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl new file mode 100644 index 00000000000..9d96821ff19 --- /dev/null +++ b/GLMakie/src/render_pipeline.jl @@ -0,0 +1,81 @@ +function create_buffer!(factory::FramebufferFactory, format::Makie.BufferFormat) + T = if format.dims == 1 + format.type + elseif format.type == Makie.Float8 + (RGB, RGBA)[format.dims-2]{format.type} + else + Vec{format.dims, format.type} + end + tex = Texture(T, size(factory), minfilter = :nearest, x_repeat = :clamp_to_edge) + push!(factory, tex) +end + +function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) + render_pipeline = screen.render_pipeline + factory = screen.framebuffer_factory + + empty!(render_pipeline) + unsafe_empty!(factory) + + # Resolve pipeline + buffers, connection2idx = Makie.generate_buffers(pipeline) + + # Add required buffers + for format in buffers + create_buffer!(factory, format) + end + + # TODO: Well looks like we do need to bundle? + # Otherwise resolving stage -> Postprocessor is going to be hard/annoying + # Well, or I just split them up I guess + for stage in pipeline.stages + # TODO: Do want vectors for these + inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key + connection = stage.input_connections[stage.inputs[key]] + return key => get_buffer(factory, connection2idx[connection]) + end) + + # TODO: Switch over Framebuffer factory to just indices + N = length(stage.output_connections) + if N == 0 + framebuffer = nothing + else + idx2name = Dict([idx => k for (k, idx) in stage.outputs]) + outputs = [connection2idx[stage.output_connections[i]] => idx2name[i] for i in 1:N] + try + framebuffer = generate_framebuffer(factory,outputs...) + catch e + rethrow(e) + end + end + + # TODO: hmm... + pass = if stage.name == :ZSort + SortPlots() + elseif stage.name == :Render + # TODO: + buffer_idx = connection2idx[stage.output_connections[stage.outputs[:objectid]]] + factory.buffer_key2idx[:objectid] = buffer_idx + RenderPlots(screen, framebuffer, inputs, N == 2 ? (:FXAA) : (:SSAO)) + elseif stage.name == :TransparentRender + RenderPlots(screen, framebuffer, inputs, :OIT) + elseif stage.name == :Display + # TODO: hacky + prev = last(render_pipeline) + framebuffer = prev.framebuffer + idx = connection2idx[stage.input_connections[stage.inputs[:color]]] + attachment = framebuffer.attachments[idx] + factory.buffer_key2idx[:color_output] = idx + BlitToScreen(framebuffer, attachment) + elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] + RenderPass{stage.name}(screen, framebuffer, inputs) + end + + # I guess stage should also have extra information for settings? Or should + # that be in scene.theme? + # Maybe just leave it there for now + push!(render_pipeline, pass) + end + + return render_pipeline +end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 8102a048526..ba067641136 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -5,7 +5,8 @@ function setup!(screen::Screen, resize_buffers) GLAbstraction.require_context(nw) # Resize framebuffer to window size - fb = screen.framebuffer_factory.fb + # TODO: Hack? + fb = screen.framebuffer_factory.children[1] if resize_buffers && !isnothing(screen.scene) ppu = screen.px_per_unit[] resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index ad3953139b7..0b565f4e828 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -296,16 +296,16 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) # NOTE # The transparent color buffer is reused by SSAO and FXAA. Changing the # render order here may introduce artifacts because of that. - push!(screen.render_pipeline, SortPlots()) - push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) - push!(screen.render_pipeline, EmptyRenderStep()) - push!(screen.render_pipeline, EmptyRenderStep()) - push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) - push!(screen.render_pipeline, RenderPlots(screen, :OIT)) - push!(screen.render_pipeline, EmptyRenderStep()) - push!(screen.render_pipeline, EmptyRenderStep()) - push!(screen.render_pipeline, EmptyRenderStep()) - push!(screen.render_pipeline, BlitToScreen(screen)) + # push!(screen.render_pipeline, SortPlots()) + # push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) + # push!(screen.render_pipeline, EmptyRenderStep()) + # push!(screen.render_pipeline, EmptyRenderStep()) + # push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) + # push!(screen.render_pipeline, RenderPlots(screen, :OIT)) + # push!(screen.render_pipeline, EmptyRenderStep()) + # push!(screen.render_pipeline, EmptyRenderStep()) + # push!(screen.render_pipeline, EmptyRenderStep()) + # push!(screen.render_pipeline, BlitToScreen(screen)) if owns_glscreen GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) @@ -405,11 +405,14 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B return end - replace_renderpass!(config.ssao ? RenderPass{:SSAO1} : EmptyRenderStep, 3) - replace_renderpass!(config.ssao ? RenderPass{:SSAO2} : EmptyRenderStep, 4) - replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 7) - replace_renderpass!(config.fxaa ? RenderPass{:FXAA1} : EmptyRenderStep, 8) - replace_renderpass!(config.fxaa ? RenderPass{:FXAA2} : EmptyRenderStep, 9) + # replace_renderpass!(config.ssao ? RenderPass{:SSAO1} : EmptyRenderStep, 3) + # replace_renderpass!(config.ssao ? RenderPass{:SSAO2} : EmptyRenderStep, 4) + # replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 7) + # replace_renderpass!(config.fxaa ? RenderPass{:FXAA1} : EmptyRenderStep, 8) + # replace_renderpass!(config.fxaa ? RenderPass{:FXAA2} : EmptyRenderStep, 9) + + gl_render_pipeline!(screen, Makie.default_pipeline()) + # TODO: replace shader programs with lighting to update N_lights & N_light_parameters @@ -812,7 +815,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma error("Screen not open!") end ShaderAbstractions.switch_context!(screen.glscreen) - ctex = get_buffer(screen.framebuffer_factory, :color) + ctex = get_buffer(screen.framebuffer_factory, :color_output) # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! pollevents(screen, Makie.BackendTick) diff --git a/src/Makie.jl b/src/Makie.jl index 5ae8e337191..4787b23e29a 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -123,6 +123,7 @@ end include("documentation/docstringextension.jl") include("utilities/quaternions.jl") include("utilities/stable-hashing.jl") +include("utilities/RenderPipeline.jl") include("bezier.jl") include("types.jl") include("utilities/Plane.jl") diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 554e1704acc..acaff7d213a 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -1,5 +1,5 @@ -using FixedPointNumbers -Float8 = N0f8 +import FixedPointNumbers +Float8 = FixedPointNumbers.N0f8 _promote_type(T1, T2) = promote_type(T1, T2) # otherwise you get Float32 here... maybe there's a better solution for this? @@ -142,21 +142,24 @@ function Base.push!(pipeline::Pipeline, other::Pipeline) end -# function connect!(source::Stage, output::Symbol, target::Stage, input::Symbol) -function connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) +function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) source = pipeline.stages[src] target = pipeline.stages[trg] haskey(source.outputs, output) || error("output $output does not exist in source stage") haskey(target.inputs, input) || error("input $input does not exist in target stage") - # intialize if not yet initialized - isempty(source.output_connections) && resize!(source.output_connections, length(source.output_formats)) - isempty(target.input_connections) && resize!(target.input_connections, length(target.input_formats)) - output_idx = source.outputs[output] input_idx = target.inputs[input] + # resize if too small (this allows later connections to be skipped) + if length(source.output_connections) < output_idx + resize!(source.output_connections, output_idx) + end + if length(target.input_connections) < input_idx + resize!(target.input_connections, input_idx) + end + # create requested connection connection = Connection(source, output_idx, target, input_idx) From 8dac598e930480e360392703e538d0ddd5a07131 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 14:29:12 +0100 Subject: [PATCH 055/135] fix SSAO --- GLMakie/assets/shader/postprocessing/SSAO.frag | 10 +++++----- GLMakie/assets/shader/postprocessing/SSAO_blur.frag | 4 ++-- GLMakie/src/glshaders/visualize_interface.jl | 4 ++-- GLMakie/src/glwindow.jl | 3 ++- GLMakie/src/postprocessing.jl | 4 ++-- GLMakie/src/render_pipeline.jl | 12 +++++++++--- GLMakie/src/screen.jl | 3 ++- 7 files changed, 24 insertions(+), 16 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/SSAO.frag b/GLMakie/assets/shader/postprocessing/SSAO.frag index d107a4913f2..3d932689bf6 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO.frag @@ -2,7 +2,7 @@ // SSAO uniform sampler2D position_buffer; -uniform sampler2D normal_occlusion_buffer; +uniform sampler2D normal_buffer; uniform sampler2D noise; uniform vec3 kernel[{{N_samples}}]; uniform vec2 noise_scale; @@ -16,13 +16,13 @@ uniform float radius; in vec2 frag_uv; // occlusion.xyz is a normal vector, occlusion.w the occlusion value -out vec4 o_normal_occlusion; +out float o_occlusion; void main(void) { vec3 view_pos = texture(position_buffer, frag_uv).xyz; - vec3 normal = texture(normal_occlusion_buffer, frag_uv).xyz; + vec3 normal = texture(normal_buffer, frag_uv).xyz; // The normal buffer gets cleared every frame. (also position, color etc) // If normal == vec3(1) then there is no geometry at this fragment. @@ -83,8 +83,8 @@ void main(void) float range_check = smoothstep(0.0, 1.0, radius / abs(view_pos.z - sample_depth)); occlusion += (sample_depth >= sample_view_offset.z + view_pos.z + bias ? 1.0 : 0.0) * range_check; } - o_normal_occlusion.w = occlusion / {{N_samples}}; + o_occlusion = occlusion / {{N_samples}}; } else { - o_normal_occlusion.w = 0.0; + o_occlusion = 0.0; } } diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag index 6021a7379e9..4691763b4ad 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -1,7 +1,7 @@ {{GLSL_VERSION}} // occlusion.w is the occlusion value -uniform sampler2D normal_occlusion; +uniform sampler2D occlusion; uniform sampler2D color_texture; uniform usampler2D ids; uniform vec2 inv_texel_size; @@ -33,7 +33,7 @@ void main(void) // shine effect. uvec2 id = texture(ids, frag_uv + offset).xy; float valid = float(id == id0); - blurred_occlusion += valid * texture(normal_occlusion, frag_uv + offset).w; + blurred_occlusion += valid * texture(occlusion, frag_uv + offset).x; weight += valid; } } diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 9cdc5ebbd29..134151db34e 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -158,7 +158,7 @@ function output_buffers(screen::Screen, transparency = false) elseif screen.config.ssao """ layout(location=2) out vec3 fragment_position; - layout(location=3) out vec3 fragment_normal_occlusion; + layout(location=3) out vec3 fragment_normal; """ else "" @@ -178,7 +178,7 @@ function output_buffer_writes(screen::Screen, transparency = false) """ fragment_color = color; fragment_position = o_view_pos; - fragment_normal_occlusion.xyz = o_view_normal; + fragment_normal.xyz = o_view_normal; """ else "fragment_color = color;" diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 349b226a745..ba41d6026f8 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -116,7 +116,6 @@ function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, S filter!(fb -> fb.id != 0, factory.children) # cleanup? fb = GLFramebuffer(size(factory)) - attach_depthstencilbuffer(fb, :depth_stencil, factory.core_buffers[:depth_stencil]) for (idx, name) in idx2name haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") @@ -124,6 +123,8 @@ function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, S attach_colorbuffer(fb, name, factory.buffers[idx]) end + attach_depthstencilbuffer(fb, :depth_stencil, factory.core_buffers[:depth_stencil]) + push!(factory.children, fb) return fb diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 9d0750eada8..a867a87dc0a 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -244,7 +244,7 @@ function RenderPass{:SSAO1}(screen, framebuffer, inputs) ) data = Dict{Symbol, Any}( :position_buffer => inputs[:position], - :normal_occlusion_buffer => inputs[:normal], + :normal_buffer => inputs[:normal], :kernel => kernel, :noise => Texture( shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], @@ -269,7 +269,7 @@ function RenderPass{:SSAO2}(screen, framebuffer, inputs) loadshader("postprocessing/SSAO_blur.frag") ) data = Dict{Symbol, Any}( - :normal_occlusion => inputs[:occlusion], + :occlusion => inputs[:occlusion], :color_texture => inputs[:color], :ids => inputs[:objectid], :inv_texel_size => rcpframe(size(screen)), diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 9d96821ff19..17cece0db5f 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -63,9 +63,15 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # TODO: hacky prev = last(render_pipeline) framebuffer = prev.framebuffer - idx = connection2idx[stage.input_connections[stage.inputs[:color]]] - attachment = framebuffer.attachments[idx] - factory.buffer_key2idx[:color_output] = idx + # TODO: Technically need to find connection from prev to this stage + # that ends up in :color + # Assuming that connection attached to a :color output: + attachment = get_attachment(framebuffer, :color) + buffer_idx = stage.inputs[:color] + factory.buffer_key2idx[:color_output] = buffer_idx + # idx = stage.input_connections[stage.inputs[:color]].inputs + # attachment = framebuffer.attachments[idx] + # factory.buffer_key2idx[:color_output] = idx BlitToScreen(framebuffer, attachment) elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] RenderPass{stage.name}(screen, framebuffer, inputs) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 0b565f4e828..2a26733fd22 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -411,7 +411,8 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B # replace_renderpass!(config.fxaa ? RenderPass{:FXAA1} : EmptyRenderStep, 8) # replace_renderpass!(config.fxaa ? RenderPass{:FXAA2} : EmptyRenderStep, 9) - gl_render_pipeline!(screen, Makie.default_pipeline()) + # gl_render_pipeline!(screen, Makie.default_pipeline()) + gl_render_pipeline!(screen, Makie.default_SSAO_pipeline()) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters From c1776792ed7ba858d4251775dd22df63f6014745 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 15:04:32 +0100 Subject: [PATCH 056/135] fix FXAA --- GLMakie/src/glwindow.jl | 35 ++++------------------------------ GLMakie/src/postprocessing.jl | 5 +++-- GLMakie/src/render_pipeline.jl | 5 ++--- 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index ba41d6026f8..f573531e945 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -36,15 +36,6 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) require_context(context) - # Buffers we always need - # Holds the image that eventually gets displayed - color_buffer = Texture( - context, RGBA{N0f8}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge - ) - # Holds a (plot id, element id) for point picking - objectid_buffer = Texture( - context, Vec{2, GLuint}, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge - ) # holds depth and stencil values depth_buffer = Texture( context, Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), fb_size, @@ -52,18 +43,11 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) internalformat = GL_DEPTH24_STENCIL8, format = GL_DEPTH_STENCIL ) - # Order Independent Transparency - HDR_color_buffer = Texture( - context, RGBA{Float16}, fb_size, minfilter = :linear, x_repeat = :clamp_to_edge - ) - OIT_weight_buffer = Texture( - context, N0f8, fb_size, minfilter = :nearest, x_repeat = :clamp_to_edge - ) - - name2idx = Dict(:color => 1, :objectid => 2, :HDR_color => 3, :OIT_weight => 4) - buffers = [color_buffer, objectid_buffer, HDR_color_buffer, OIT_weight_buffer] - return FramebufferFactory(fb_size, name2idx, buffers, Dict(:depth_stencil => depth_buffer), GLFramebuffer[]) + return FramebufferFactory( + fb_size, Dict{Symbol, Int}(), Texture[], + Dict(:depth_stencil => depth_buffer), GLFramebuffer[] + ) end function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) @@ -83,17 +67,6 @@ function unsafe_empty!(factory::FramebufferFactory) return factory end -# TODO: temporary -function Base.push!(factory::FramebufferFactory, kv::Pair{Symbol, <: Texture}) - if haskey(factory.buffer_key2idx, kv[1]) - @error("Pushed buffer $(kv[1]) already assigned.") - return - end - push!(factory, kv[2]) - push!(factory.buffer_key2idx, kv[1] => length(factory.buffers)) - return factory -end - function Base.push!(factory::FramebufferFactory, tex::Texture) push!(factory.buffers, tex) return factory diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index a867a87dc0a..b8fd748480f 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -370,9 +370,10 @@ function run_step(screen, glscene, step::RenderPass{:FXAA1}) # TODO: make scissor explicit? wh = size(step.framebuffer) glViewport(0, 0, wh[1], wh[2]) + # TODO: is this still true? # necessary with negative SSAO bias... - glClearColor(1, 1, 1, 1) - glClear(GL_COLOR_BUFFER_BIT) + # glClearColor(1, 1, 1, 1) + # glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(step.robj) return end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 17cece0db5f..4bcf951d9d4 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -6,7 +6,8 @@ function create_buffer!(factory::FramebufferFactory, format::Makie.BufferFormat) else Vec{format.dims, format.type} end - tex = Texture(T, size(factory), minfilter = :nearest, x_repeat = :clamp_to_edge) + tex = Texture(T, size(factory), minfilter = :linear, x_repeat = :clamp_to_edge) + # tex = Texture(T, size(factory), minfilter = :nearest, x_repeat = :clamp_to_edge) push!(factory, tex) end @@ -29,13 +30,11 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Otherwise resolving stage -> Postprocessor is going to be hard/annoying # Well, or I just split them up I guess for stage in pipeline.stages - # TODO: Do want vectors for these inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] return key => get_buffer(factory, connection2idx[connection]) end) - # TODO: Switch over Framebuffer factory to just indices N = length(stage.output_connections) if N == 0 framebuffer = nothing From 4b0d697be375dd1e32ec9a3b8fcbc823b9bff6e5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 17:40:17 +0100 Subject: [PATCH 057/135] fix picking, use config.ssao --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 22 ++++++++---- GLMakie/src/glwindow.jl | 37 ++++++++----------- GLMakie/src/picking.jl | 16 ++++----- GLMakie/src/render_pipeline.jl | 23 ++++++------ GLMakie/src/rendering.jl | 7 +++- GLMakie/src/screen.jl | 41 +++++----------------- 6 files changed, 65 insertions(+), 81 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 63e2dd49695..f37ca6f9549 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -204,13 +204,23 @@ function gl_attach(buffer::RenderBuffer, attachment::GLenum) glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, buffer) end -function Base.delete!(fb::GLFramebuffer, key::Symbol) - idx = fb.name2idx[key] - attachment = fb.attachments[idx] +# Need to be careful with counter here +# We have [colorbuffers..., depth/stencil] +# ^- counter = last before depth/stencil +# And also with the GLFramebuffer being ordered +# So we only allow pop! for colorbuffers, no delete!() +function pop_colorbuffer!(fb::GLFramebuffer) + attachment = fb.attachments[fb.counter] glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, 0, 0) - deleteat!(fb.attachments, idx) - deleteat!(fb.buffers, idx) - delete!(fb, key) + deleteat!(fb.attachments, fb.counter) + deleteat!(fb.buffers, fb.counter) + key = :unknown + for (k, v) in fb.name2idx + (v == fb.counter) && (key = k) + (v > fb.counter) && (fb.name2idx[k] = v-1) + end + delete!(fb.name2idx, key) + fb.counter -= 1 return fb end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index f573531e945..60e5bddf9b5 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -11,26 +11,19 @@ Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T( Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) mutable struct FramebufferFactory - size::Tuple{Int, Int} + fb::GLFramebuffer # core framebuffer (more or less for #4150) + # holding depth, stencil, objectid[, output_color] + buffer_key2idx::Dict{Symbol, Int} # TODO: temp, should be unnamed collection buffers::Vector{Texture} - core_buffers::Dict{Symbol, Texture} # Not managed by render pipeline children::Vector{GLFramebuffer} # TODO: how else can we handle resizing? end -# it's guaranteed, that they all have the same size -# TODO: forwards... for now -Base.size(fb::FramebufferFactory) = fb.size +Base.size(fb::FramebufferFactory) = size(fb.fb) Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffer_key2idx, key) -function GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) - if haskey(fb.core_buffers, key) - return fb.core_buffers[key] - else - return fb.buffers[fb.buffer_key2idx[key]] - end -end +GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[fb.buffer_key2idx[key]] GLAbstraction.get_buffer(fb::FramebufferFactory, idx::Int) = fb.buffers[idx] -# GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) +GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) ShaderAbstractions.switch_context!(context) @@ -44,18 +37,16 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) format = GL_DEPTH_STENCIL ) - return FramebufferFactory( - fb_size, Dict{Symbol, Int}(), Texture[], - Dict(:depth_stencil => depth_buffer), GLFramebuffer[] - ) + fb = GLFramebuffer(fb_size) + attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) + + return FramebufferFactory(fb, Dict{Symbol, Int}(), Texture[], GLFramebuffer[]) end function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), fb.buffers) - foreach(tex -> GLAbstraction.resize_nocopy!(tex, (w, h)), values(fb.core_buffers)) - # resize!(fb.fb, w, h) - fb.size = (w, h) - filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok? + resize!(fb.fb, w, h) + filter!(fb -> fb.id != 0, fb.children) # TODO: is this ok for cleanup? foreach(fb -> resize!(fb, w, h), fb.children) return end @@ -64,6 +55,8 @@ function unsafe_empty!(factory::FramebufferFactory) empty!(factory.buffer_key2idx) empty!(factory.buffers) empty!(factory.children) + haskey(factory.fb, :color) && GLAbstraction.pop_colorbuffer!(factory.fb) + haskey(factory.fb, :objectid) && GLAbstraction.pop_colorbuffer!(factory.fb) return factory end @@ -96,7 +89,7 @@ function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, S attach_colorbuffer(fb, name, factory.buffers[idx]) end - attach_depthstencilbuffer(fb, :depth_stencil, factory.core_buffers[:depth_stencil]) + attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) push!(factory.children, fb) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index c8f3c70821c..908e443fae7 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,10 +6,10 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - fb = screen.framebuffer_factory + fb = screen.framebuffer_factory.fb buff = get_buffer(fb, :objectid) GLAbstraction.bind(fb) - glReadBuffer(GL_COLOR_ATTACHMENT1) + glReadBuffer(get_attachment(fb, :objectid)) rx, ry = minimum(rect) rw, rh = widths(rect) w, h = size(screen.scene) @@ -27,10 +27,10 @@ end function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - fb = screen.framebuffer_factory + fb = screen.framebuffer_factory.fb buff = get_buffer(fb, :objectid) GLAbstraction.bind(fb) - glReadBuffer(GL_COLOR_ATTACHMENT1) + glReadBuffer(get_attachment(fb, :objectid)) x, y = floor.(Int, xy) w, h = size(screen.scene) ppu = screen.px_per_unit[] @@ -70,7 +70,7 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) w, h = size(screen.scene) # unitless dimensions ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) - fb = screen.framebuffer_factory + fb = screen.framebuffer_factory.fb ppu = screen.px_per_unit[] w, h = size(fb) # pixel dimensions x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) @@ -79,7 +79,7 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) ShaderAbstractions.switch_context!(screen.glscreen) GLAbstraction.bind(fb) - glReadBuffer(GL_COLOR_ATTACHMENT1) + glReadBuffer(get_attachment(fb, :objectid)) buff = get_buffer(fb, :objectid) sids = zeros(SelectionID{UInt32}, dx, dy) glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, sids) @@ -111,7 +111,7 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) return Tuple{AbstractPlot, Int}[] end - fb = screen.framebuffer_factory + fb = screen.framebuffer_factory.fb ppu = screen.px_per_unit[] w, h = size(fb) # pixel dimensions x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) @@ -120,7 +120,7 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) ShaderAbstractions.switch_context!(screen.glscreen) GLAbstraction.bind(fb) - glReadBuffer(GL_COLOR_ATTACHMENT1) + glReadBuffer(get_attachment(fb, :objectid)) buff = get_buffer(fb, :objectid) picks = zeros(SelectionID{UInt32}, dx, dy) glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, picks) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 4bcf951d9d4..482ddfb9a4a 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -26,9 +26,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) create_buffer!(factory, format) end - # TODO: Well looks like we do need to bundle? - # Otherwise resolving stage -> Postprocessor is going to be hard/annoying - # Well, or I just split them up I guess + first_render = true + for stage in pipeline.stages inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] @@ -53,9 +52,13 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) SortPlots() elseif stage.name == :Render # TODO: - buffer_idx = connection2idx[stage.output_connections[stage.outputs[:objectid]]] - factory.buffer_key2idx[:objectid] = buffer_idx - RenderPlots(screen, framebuffer, inputs, N == 2 ? (:FXAA) : (:SSAO)) + if first_render + attach_colorbuffer(factory.fb, :objectid, get_buffer(framebuffer, :objectid)) + first_render = false + RenderPlots(screen, framebuffer, inputs, :SSAO) + else + RenderPlots(screen, framebuffer, inputs, :FXAA) + end elseif stage.name == :TransparentRender RenderPlots(screen, framebuffer, inputs, :OIT) elseif stage.name == :Display @@ -66,14 +69,12 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # that ends up in :color # Assuming that connection attached to a :color output: attachment = get_attachment(framebuffer, :color) - buffer_idx = stage.inputs[:color] - factory.buffer_key2idx[:color_output] = buffer_idx - # idx = stage.input_connections[stage.inputs[:color]].inputs - # attachment = framebuffer.attachments[idx] - # factory.buffer_key2idx[:color_output] = idx + attach_colorbuffer(factory.fb, :color, get_buffer(framebuffer, :color)) BlitToScreen(framebuffer, attachment) elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] RenderPass{stage.name}(screen, framebuffer, inputs) + else + error("Unknown stage $(stage.name)") end # I guess stage should also have extra information for settings? Or should diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index ba067641136..b2f76f2d531 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,11 +1,15 @@ function setup!(screen::Screen, resize_buffers) + isempty(screen.framebuffer_factory.children) && return + # Make sure this context is active (for multi-window rendering) nw = to_native(screen) ShaderAbstractions.switch_context!(nw) GLAbstraction.require_context(nw) # Resize framebuffer to window size - # TODO: Hack? + # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and + # no earlier stage uses color or objectid + # Also assumes specific names fb = screen.framebuffer_factory.children[1] if resize_buffers && !isnothing(screen.scene) ppu = screen.px_per_unit[] @@ -42,6 +46,7 @@ function setup!(screen::Screen, resize_buffers) end end glDisable(GL_SCISSOR_TEST) + return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 2a26733fd22..37c1fae19be 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -293,20 +293,6 @@ Makie.@noconstprop function empty_screen(debugging::Bool, reuse::Bool, window) reuse, ) - # NOTE - # The transparent color buffer is reused by SSAO and FXAA. Changing the - # render order here may introduce artifacts because of that. - # push!(screen.render_pipeline, SortPlots()) - # push!(screen.render_pipeline, RenderPlots(screen, :SSAO)) - # push!(screen.render_pipeline, EmptyRenderStep()) - # push!(screen.render_pipeline, EmptyRenderStep()) - # push!(screen.render_pipeline, RenderPlots(screen, :FXAA)) - # push!(screen.render_pipeline, RenderPlots(screen, :OIT)) - # push!(screen.render_pipeline, EmptyRenderStep()) - # push!(screen.render_pipeline, EmptyRenderStep()) - # push!(screen.render_pipeline, EmptyRenderStep()) - # push!(screen.render_pipeline, BlitToScreen(screen)) - if owns_glscreen GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) @@ -396,24 +382,13 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] - function replace_renderpass!(pass, idx) - prev = screen.render_pipeline[idx] - if typeof(prev) !== pass - destroy!(prev) - screen.render_pipeline[idx] = pass(screen) - end - return - end - - # replace_renderpass!(config.ssao ? RenderPass{:SSAO1} : EmptyRenderStep, 3) - # replace_renderpass!(config.ssao ? RenderPass{:SSAO2} : EmptyRenderStep, 4) - # replace_renderpass!(config.oit ? RenderPass{:OIT} : EmptyRenderStep, 7) - # replace_renderpass!(config.fxaa ? RenderPass{:FXAA1} : EmptyRenderStep, 8) - # replace_renderpass!(config.fxaa ? RenderPass{:FXAA2} : EmptyRenderStep, 9) - - # gl_render_pipeline!(screen, Makie.default_pipeline()) - gl_render_pipeline!(screen, Makie.default_SSAO_pipeline()) + # TODO: FXAA, OIT on-off + if config.ssao + gl_render_pipeline!(screen, Makie.default_SSAO_pipeline()) + else + gl_render_pipeline!(screen, Makie.default_pipeline()) + end # TODO: replace shader programs with lighting to update N_lights & N_light_parameters @@ -803,7 +778,7 @@ function depthbuffer(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) render_frame(screen, resize_buffers=false) # let it render glFinish() # block until opengl is done rendering - source = get_buffer(screen.framebuffer_factory, :depth_stencil) + source = get_buffer(screen.framebuffer_factory.fb, :depth_stencil) depth = Matrix{Float32}(undef, size(source)) GLAbstraction.bind(source) GLAbstraction.glGetTexImage(source.texturetype, 0, GL_DEPTH_COMPONENT, GL_FLOAT, depth) @@ -816,7 +791,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma error("Screen not open!") end ShaderAbstractions.switch_context!(screen.glscreen) - ctex = get_buffer(screen.framebuffer_factory, :color_output) + ctex = get_buffer(screen.framebuffer_factory.fb, :color) # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! pollevents(screen, Makie.BackendTick) From 85720002f8884919efaa93d3aea6bf6173914009 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 27 Dec 2024 18:02:12 +0100 Subject: [PATCH 058/135] try recreating Framebuffer instead of removing attachments --- GLMakie/src/glwindow.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 60e5bddf9b5..36caf199326 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -55,8 +55,11 @@ function unsafe_empty!(factory::FramebufferFactory) empty!(factory.buffer_key2idx) empty!(factory.buffers) empty!(factory.children) - haskey(factory.fb, :color) && GLAbstraction.pop_colorbuffer!(factory.fb) - haskey(factory.fb, :objectid) && GLAbstraction.pop_colorbuffer!(factory.fb) + # haskey(factory.fb, :color) && GLAbstraction.pop_colorbuffer!(factory.fb) + # haskey(factory.fb, :objectid) && GLAbstraction.pop_colorbuffer!(factory.fb) + fb = GLFramebuffer(size(factory)) + attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) + factory.fb = fb return factory end @@ -78,7 +81,7 @@ function generate_framebuffer(factory::FramebufferFactory, names::Pair{Symbol, S return generate_framebuffer(factory, remapped...) end -function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) +Makie.@noconstprop function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) filter!(fb -> fb.id != 0, factory.children) # cleanup? fb = GLFramebuffer(size(factory)) From 8b7181111be703804abc18b6e22cb4a0d9890249 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 00:18:56 +0100 Subject: [PATCH 059/135] some cleanup & performance tweaks - reuse buffers when regenerating gl render pipeline - switch Connection from Dict to Vector - inline is_compatible and promote_type - cache pipeline --- GLMakie/src/GLAbstraction/GLRenderObject.jl | 2 +- GLMakie/src/glwindow.jl | 14 +- GLMakie/src/render_pipeline.jl | 53 +++-- src/utilities/RenderPipeline.jl | 208 ++++++++++---------- 4 files changed, 148 insertions(+), 129 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl index e7f333f3d87..b4c8142dbe3 100644 --- a/GLMakie/src/GLAbstraction/GLRenderObject.jl +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -1,5 +1,5 @@ function Base.show(io::IO, obj::RenderObject) - println(io, "RenderObject with ID: ", obj.id) + print(io, "RenderObject with ID: ", obj.id) end Base.getindex(obj::RenderObject, symbol::Symbol) = obj.uniforms[symbol] diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 36caf199326..70402da069f 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -14,14 +14,11 @@ mutable struct FramebufferFactory fb::GLFramebuffer # core framebuffer (more or less for #4150) # holding depth, stencil, objectid[, output_color] - buffer_key2idx::Dict{Symbol, Int} # TODO: temp, should be unnamed collection buffers::Vector{Texture} children::Vector{GLFramebuffer} # TODO: how else can we handle resizing? end Base.size(fb::FramebufferFactory) = size(fb.fb) -Base.haskey(fb::FramebufferFactory, key::Symbol) = haskey(fb.buffer_key2idx, key) -GLAbstraction.get_buffer(fb::FramebufferFactory, key::Symbol) = fb.buffers[fb.buffer_key2idx[key]] GLAbstraction.get_buffer(fb::FramebufferFactory, idx::Int) = fb.buffers[idx] GLAbstraction.bind(fb::FramebufferFactory) = GLAbstraction.bind(fb.fb) @@ -40,7 +37,7 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) fb = GLFramebuffer(fb_size) attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) - return FramebufferFactory(fb, Dict{Symbol, Int}(), Texture[], GLFramebuffer[]) + return FramebufferFactory(fb, Texture[], GLFramebuffer[]) end function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) @@ -52,11 +49,8 @@ function Base.resize!(fb::FramebufferFactory, w::Int, h::Int) end function unsafe_empty!(factory::FramebufferFactory) - empty!(factory.buffer_key2idx) empty!(factory.buffers) empty!(factory.children) - # haskey(factory.fb, :color) && GLAbstraction.pop_colorbuffer!(factory.fb) - # haskey(factory.fb, :objectid) && GLAbstraction.pop_colorbuffer!(factory.fb) fb = GLFramebuffer(size(factory)) attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) factory.fb = fb @@ -76,11 +70,6 @@ function generate_framebuffer(factory::FramebufferFactory, args...) return generate_framebuffer(factory, parse_arg.(args)...) end -function generate_framebuffer(factory::FramebufferFactory, names::Pair{Symbol, Symbol}...) - remapped = map(kv -> factory.buffer_key2idx[kv[1]] => kv[2], names) - return generate_framebuffer(factory, remapped...) -end - Makie.@noconstprop function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) filter!(fb -> fb.id != 0, factory.children) # cleanup? @@ -88,7 +77,6 @@ Makie.@noconstprop function generate_framebuffer(factory::FramebufferFactory, id for (idx, name) in idx2name haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") - # in(lookup, [:depth, :stencil]) && error("Depth and stencil always exist under the same name.") attach_colorbuffer(fb, name, factory.buffers[idx]) end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 482ddfb9a4a..0f0d04a0c88 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -1,30 +1,61 @@ function create_buffer!(factory::FramebufferFactory, format::Makie.BufferFormat) - T = if format.dims == 1 - format.type - elseif format.type == Makie.Float8 - (RGB, RGBA)[format.dims-2]{format.type} - else - Vec{format.dims, format.type} - end + T = format_to_type(format) tex = Texture(T, size(factory), minfilter = :linear, x_repeat = :clamp_to_edge) # tex = Texture(T, size(factory), minfilter = :nearest, x_repeat = :clamp_to_edge) push!(factory, tex) end +function format_to_type(format::Makie.BufferFormat) + return format.dims == 1 ? format.type : Vec{format.dims, format.type} +end + +function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) + # empty!(factory.buffer_key2idx) + empty!(factory.children) + + # reuse buffers that match formats (and make sure that factory.buffers + # follows the order of formats) + buffers = copy(factory.buffers) + empty!(factory.buffers) + for format in formats + T = format_to_type(format) + found = false + for (i, buffer) in enumerate(buffers) + if T == eltype(buffer) # TODO: && extra parameters match... + found = true + push!(factory.buffers, popat!(buffers, i)) + break + end + end + + if !found + tex = Texture(T, size(factory), minfilter = :linear, x_repeat = :clamp_to_edge) + push!(factory.buffers, tex) + end + end + + # Always rebuild this though, since we don't know which buffers are the + # final output buffers + fb = GLFramebuffer(size(factory)) + attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) + factory.fb = fb + + return factory +end + + function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) + # TODO: check if pipeline is different from the last one before replacing it render_pipeline = screen.render_pipeline factory = screen.framebuffer_factory empty!(render_pipeline) - unsafe_empty!(factory) # Resolve pipeline buffers, connection2idx = Makie.generate_buffers(pipeline) # Add required buffers - for format in buffers - create_buffer!(factory, format) - end + reset!(factory, buffers) first_render = true diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index acaff7d213a..7b8545e3438 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -1,59 +1,58 @@ import FixedPointNumbers Float8 = FixedPointNumbers.N0f8 -_promote_type(T1, T2) = promote_type(T1, T2) -# otherwise you get Float32 here... maybe there's a better solution for this? -# maybe just introduce struct Float8 end as a stand-in? -_promote_type(::Type{Float8}, ::Type{Float16}) = Float16 -_promote_type(::Type{Float16}, ::Type{Float8}) = Float16 - - # TODO: consider adding a "reuse immediately" flag so Stage can communicate that # it allows output = input struct BufferFormat dims::Int type::DataType - BufferFormat(dims = 4, type = Float8) = new(dims, type) + BufferFormat(dims::Integer = 4, type::DataType = Float8) = new(dims, type) end -function BufferFormat(input::BufferFormat, output::BufferFormat) - if is_compatible(input, output) - dims = max(input.dims, output.dims) - type = _promote_type(input.type, output.type) +function BufferFormat(f1::BufferFormat, f2::BufferFormat) + if is_compatible(f1, f2) + dims = max(f1.dims, f2.dims) + T1 = f1.type; T2 = f2.type + type = if T1 == T2; T1 + elseif (T1 == Float32) || (T2 == Float32); Float32 + elseif (T1 == Float16) || (T2 == Float16); Float16 + elseif (T1 == Float8) || (T2 == Float8); Float8 + elseif (T1 == Int32) || (T2 == Int32); Int32 + elseif (T1 == Int16) || (T2 == Int16); Int16 + elseif (T1 == Int8) || (T2 == Int8); Int8 + elseif (T1 == UInt32) || (T2 == UInt32); UInt32 + elseif (T1 == UInt16) || (T2 == UInt16); UInt16 + elseif (T1 == UInt8) || (T2 == UInt8); UInt8 + else error("Types $T1 and $T2 are not allowed.") + end return BufferFormat(dims, type) end error("Could not generate compatible BufferFormat between $input and $output") end -function is_compatible(f1::BufferFormat, f2::BufferFormat) - is_compatible_with(f1.type, f2.type) || is_compatible_with(f2.type, f1.type) -end -function is_compatible_with(f::BufferFormat, requirement::BufferFormat) - return (f.dims <= requirement.dims) && is_compatible_with(f.type, requirement.type) +function is_compatible(f1::BufferFormat, f2::BufferFormat) + # About 10% faster for default_SSAO_pipeline() (with no caching) than + # is_compatible(f1.type, f2.type), but still does runtime dispatch on == + T1 = f1.type + T2 = f2.type + return( + ((T1 == Float8) || (T1 == Float16) || (T1 == Float32)) && + ((T2 == Float8) || (T2 == Float16) || (T2 == Float32)) + ) || ( + ((T1 == Int8) || (T1 == Int16) || (T1 == Int32)) && + ((T2 == Int8) || (T2 == Int16) || (T2 == Int32)) + ) || ( + ((T1 == UInt8) || (T1 == UInt16) || (T1 == UInt32)) && + ((T2 == UInt8) || (T2 == UInt16) || (T2 == UInt32)) + ) end -# given, required -is_compatible_with(::Type{<: Union{N0f8, Float16, Float32}}, ::Type{N0f8}) = true -is_compatible_with(::Type{<: Union{Float16, Float32}}, ::Type{Float16}) = true -is_compatible_with(::Type{Float32}, ::Type{Float32}) = true - -is_compatible_with(::Type{<: Union{Int8, Int16, Int32}}, ::Type{Int8}) = true -is_compatible_with(::Type{<: Union{Int16, Int32}}, ::Type{Int16}) = true -is_compatible_with(::Type{Int32}, ::Type{Int32}) = true - -is_compatible_with(::Type{<: Union{UInt8, UInt16, UInt32}}, ::Type{UInt8}) = true -is_compatible_with(::Type{<: Union{UInt16, UInt32}}, ::Type{UInt16}) = true -is_compatible_with(::Type{UInt32}, ::Type{UInt32}) = true - -is_compatible_with(::Type, ::Type) = false - - # Connections can have multiple inputs and outputs # e.g. multiple Renders write to objectid and FXAA, SSAO, Display/pick read it struct ConnectionT{T} - inputs::Dict{T, Int} # stage => output Index - outputs::Dict{T, Int} # stage => input Index + inputs::Vector{Pair{T, Int}} + outputs::Vector{Pair{T, Int}} format::BufferFormat # derived from inputs & outputs formats end @@ -93,15 +92,18 @@ end function Connection(source::Stage, input::Integer, target::Stage, output::Integer) format = BufferFormat(source.output_formats[input], target.input_formats[output]) - return Connection(Dict(source => input), Dict(target => output), format) + return Connection([source => input], [target => output], format) end function Base.merge(c1::Connection, c2::Connection) - return Connection( - merge!(c1.inputs, c2.inputs), - merge!(c1.outputs, c2.outputs), - BufferFormat(c1.format, c2.format) - ) + for (stage, idx) in c2.inputs + any(kv -> kv[1] === stage, c1.inputs) || push!(c1.inputs, stage => idx) + end + for (stage, idx) in c2.outputs + any(kv -> kv[1] === stage, c1.outputs) || push!(c1.outputs, stage => idx) + end + + return Connection(c1.inputs, c1.outputs, BufferFormat(c1.format, c2.format)) end function Base.hash(stage::Stage, h::UInt) @@ -199,14 +201,14 @@ end function verify(pipeline::Pipeline) for connection in pipeline.connections earliest_input = length(pipeline.stages) - for stage in keys(connection.inputs) + for (stage, _) in connection.inputs idx = findfirst(==(stage), pipeline.stages) isnothing(idx) && error("Could not find $(stage.name) in pipeline.") earliest_input = min(earliest_input, idx) end earliest_output = length(pipeline.stages) - for stage in keys(connection.outputs) + for (stage, _) in connection.outputs idx = findfirst(==(stage), pipeline.stages) isnothing(idx) && error("Could not find $(stage.name) in pipeline.") earliest_output = min(earliest_output, idx) @@ -320,14 +322,6 @@ function generate_buffers(pipeline::Pipeline) return buffers, connection2idx end -# Usage: -# stages become tasks in the renderloop (mostly postprocessors) -# returned buffers become color attachments -# returned connection2idx is used to find input and output buffers -# Stage -> RenderStage, Framebuffer <-----------------------------------------------------. -# |-> inputs -> (name, connection) -> (name, idx) -> (name, buffer) -> (name, Texture) --| -# '-> outputs -> (name, connection) -> (name, idx) -> (name, buffer) -> (name, Texture) -' - ################################################################################ ### show @@ -404,7 +398,7 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) if !isempty(connection.inputs) print(io, "\ninputs:") - elements = map(keys(connection.inputs), values(connection.inputs)) do stage, idx + elements = map(connection.inputs) do (stage, idx) key = :temp for (k, i) in stage.outputs if idx == i @@ -423,7 +417,7 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) if !isempty(connection.outputs) print(io, "\noutputs:") - elements = map(keys(connection.outputs), values(connection.outputs)) do stage, idx + elements = map(connection.outputs) do (stage, idx) key = :temp for (k, i) in stage.inputs if idx == i @@ -517,59 +511,65 @@ function DisplayStage() end -function default_SSAO_pipeline() - # matching master with SSAO enabled - pipeline = Pipeline() - - push!(pipeline, SortStage()) # 1 - push!(pipeline, RenderStage()) # 2 - push!(pipeline, SSAOStage()) # 3, 4 - push!(pipeline, RenderStage()) # 5 - push!(pipeline, TransparentRenderStage()) # 6 - push!(pipeline, OITStage()) # 7 - push!(pipeline, FXAAStage()) # 8, 9 - push!(pipeline, DisplayStage()) # 10 - - connect!(pipeline, 2, :position, 3, :position) - connect!(pipeline, 2, :normal, 3, :normal) - connect!(pipeline, 2, :color, 4, :color) - connect!(pipeline, 2, :objectid, 4, :objectid) - connect!(pipeline, 2, :objectid, 8, :objectid) - connect!(pipeline, 2, :objectid, 10, :objectid) - connect!(pipeline, 4, :color, 8, :color) - connect!(pipeline, 5, :color, 8, :color) - connect!(pipeline, 5, :objectid, 10, :objectid) # will get bundled so we don't need to repeat - connect!(pipeline, 6, :weighted_color_sum, 7, :weighted_color_sum) - connect!(pipeline, 6, :objectid, 10, :objectid) - connect!(pipeline, 6, :alpha_product, 7, :alpha_product) - connect!(pipeline, 7, :color, 8, :color) - connect!(pipeline, 9, :color, 10, :color) +const PIPELINE_CACHE = Dict{Symbol, Pipeline}() - return pipeline +function default_SSAO_pipeline() + return get!(PIPELINE_CACHE, :default_SSAO_pipeline) do + # matching master with SSAO enabled + pipeline = Pipeline() + + push!(pipeline, SortStage()) # 1 + push!(pipeline, RenderStage()) # 2 + push!(pipeline, SSAOStage()) # 3, 4 + push!(pipeline, RenderStage()) # 5 + push!(pipeline, TransparentRenderStage()) # 6 + push!(pipeline, OITStage()) # 7 + push!(pipeline, FXAAStage()) # 8, 9 + push!(pipeline, DisplayStage()) # 10 + + connect!(pipeline, 2, :position, 3, :position) + connect!(pipeline, 2, :normal, 3, :normal) + connect!(pipeline, 2, :color, 4, :color) + connect!(pipeline, 2, :objectid, 4, :objectid) + connect!(pipeline, 2, :objectid, 8, :objectid) + connect!(pipeline, 2, :objectid, 10, :objectid) + connect!(pipeline, 4, :color, 8, :color) + connect!(pipeline, 5, :color, 8, :color) + connect!(pipeline, 5, :objectid, 10, :objectid) # will get bundled so we don't need to repeat + connect!(pipeline, 6, :weighted_color_sum, 7, :weighted_color_sum) + connect!(pipeline, 6, :objectid, 10, :objectid) + connect!(pipeline, 6, :alpha_product, 7, :alpha_product) + connect!(pipeline, 7, :color, 8, :color) + connect!(pipeline, 9, :color, 10, :color) + + return pipeline + end end function default_pipeline() - # matching master - pipeline = Pipeline() - - push!(pipeline, SortStage()) - push!(pipeline, RenderStage()) - push!(pipeline, RenderStage()) - push!(pipeline, TransparentRenderStage()) - push!(pipeline, OITStage()) - push!(pipeline, FXAAStage()) - push!(pipeline, DisplayStage()) - - connect!(pipeline, 2, :color, 6, :color) - connect!(pipeline, 2, :objectid, 6, :objectid) - connect!(pipeline, 2, :objectid, 8, :objectid) - connect!(pipeline, 3, :color, 6, :color) - connect!(pipeline, 3, :objectid, 8, :objectid) - connect!(pipeline, 4, :weighted_color_sum, 5, :weighted_color_sum) - connect!(pipeline, 4, :objectid, 8, :objectid) - connect!(pipeline, 4, :alpha_product, 5, :alpha_product) - connect!(pipeline, 5, :color, 6, :color) - connect!(pipeline, 7, :color, 8, :color) - - return pipeline + return get!(PIPELINE_CACHE, :default_pipeline) do + # matching master + pipeline = Pipeline() + + push!(pipeline, SortStage()) + push!(pipeline, RenderStage()) + push!(pipeline, RenderStage()) + push!(pipeline, TransparentRenderStage()) + push!(pipeline, OITStage()) + push!(pipeline, FXAAStage()) + push!(pipeline, DisplayStage()) + + connect!(pipeline, 2, :color, 6, :color) + connect!(pipeline, 2, :objectid, 6, :objectid) + connect!(pipeline, 2, :objectid, 8, :objectid) + connect!(pipeline, 3, :color, 6, :color) + connect!(pipeline, 3, :objectid, 8, :objectid) + connect!(pipeline, 4, :weighted_color_sum, 5, :weighted_color_sum) + connect!(pipeline, 4, :objectid, 8, :objectid) + connect!(pipeline, 4, :alpha_product, 5, :alpha_product) + connect!(pipeline, 5, :color, 6, :color) + connect!(pipeline, 7, :color, 8, :color) + + return pipeline + end end From c502d0b72f8c331ef5ea2d5277ad41d03d5968d1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 01:08:20 +0100 Subject: [PATCH 060/135] try to get some debug info out of CI --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 4 ++- GLMakie/test/glmakie_refimages.jl | 42 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index f37ca6f9549..f72c2b17370 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -267,6 +267,8 @@ end function attachment_enum_to_string(x::GLenum) x == GL_DEPTH_ATTACHMENT && return "GL_DEPTH_ATTACHMENT" x == GL_STENCIL_ATTACHMENT && return "GL_STENCIL_ATTACHMENT" + x == GL_DEPTH_STENCIL_ATTACHMENT && return "GL_DEPTH_STENCIL_ATTACHMENT" + x == GL_STENCIL_ATTACHMENT && return "GL_STENCIL_ATTACHMENT" i = Int(x - GL_COLOR_ATTACHMENT0) return "GL_COLOR_ATTACHMENT$i" end @@ -281,7 +283,7 @@ function Base.show(io::IO, ::MIME"text/plain", fb::GLFramebuffer) key_pad = mapreduce(length, max, key_strings) key_strings = rpad.(key_strings, key_pad) - attachments = attachment_enum_to_string(fb.attachments) + attachments = attachment_enum_to_string.(fb.attachments) attachment_pad = mapreduce(length, max, attachments) attachments = rpad.(attachments, attachment_pad) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index afc81dd8c17..8b65930d3ee 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -167,4 +167,46 @@ end mesh!(scene, Rect2f(0, 0, 300, 200), color = :red) scene +end + +@reference_test "Arrows 3D debug" begin + function SphericalToCartesian(r::T, θ::T, ϕ::T) where T <: AbstractArray + x = @.r * sin(θ) * cos(ϕ) + y = @.r * sin(θ) * sin(ϕ) + z = @.r * cos(θ) + Point3f.(x, y, z) + end + n = 100^2 # number of points to generate + r = ones(n); + θ = acos.(1 .- 2 .* RNG.rand(n)) + φ = 2π * RNG.rand(n) + pts = SphericalToCartesian(r, θ, φ) + f,a,p = arrows(pts, (normalize.(pts) .* 0.1f0), arrowsize=0.02, linecolor=:green, arrowcolor=:darkblue) + screen = display(f, visible = false) + + display(screen.render_pipeline) + println() + display(screen.render_pipeline[end-2].framebuffer) + display(screen.render_pipeline[end-2].robj.uniforms) + println() + display(screen.render_pipeline[end-1].framebuffer) + display(screen.render_pipeline[end-1].robj.uniforms) + println() + display(screen.framebuffer_factory.fb) + println() + display(screen.framebuffer_factory.buffers) + println() + display(screen.framebuffer_factory.children) + + cb = Makie.colorbuffer(f) + db = GLMakie.depthbuffer(screen) + ob = GLMakie.pick_native(screen, Rect2i(0,0,500,500)) + s = Scene(size = (1000, 1000)) + image!(s, -1..0, 0..1, cb) + image!(s, 0..1, 0..1, db) + image!(s, -1..0, -1..0, getfield.(ob, :id)) + image!(s, 0..1, -1..0, getfield.(ob, :index)) + @info unique(getfield.(ob, :id)) + @info extrema(getfield.(ob, :index)) + s end \ No newline at end of file From 1c1ef312f26cbe2e62426a591fb0d9ad43973654 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 02:39:18 +0100 Subject: [PATCH 061/135] fix FXAA in CI? --- GLMakie/src/render_pipeline.jl | 4 ++- GLMakie/test/glmakie_refimages.jl | 42 ------------------------------- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 0f0d04a0c88..9ad33e32dee 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -29,7 +29,9 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF end if !found - tex = Texture(T, size(factory), minfilter = :linear, x_repeat = :clamp_to_edge) + isfloattype = eltype(T) == N0f8 || eltype(T) <: AbstractFloat + interp = isfloattype ? (:linear) : (:nearest) + tex = Texture(T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) push!(factory.buffers, tex) end end diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 8b65930d3ee..afc81dd8c17 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -167,46 +167,4 @@ end mesh!(scene, Rect2f(0, 0, 300, 200), color = :red) scene -end - -@reference_test "Arrows 3D debug" begin - function SphericalToCartesian(r::T, θ::T, ϕ::T) where T <: AbstractArray - x = @.r * sin(θ) * cos(ϕ) - y = @.r * sin(θ) * sin(ϕ) - z = @.r * cos(θ) - Point3f.(x, y, z) - end - n = 100^2 # number of points to generate - r = ones(n); - θ = acos.(1 .- 2 .* RNG.rand(n)) - φ = 2π * RNG.rand(n) - pts = SphericalToCartesian(r, θ, φ) - f,a,p = arrows(pts, (normalize.(pts) .* 0.1f0), arrowsize=0.02, linecolor=:green, arrowcolor=:darkblue) - screen = display(f, visible = false) - - display(screen.render_pipeline) - println() - display(screen.render_pipeline[end-2].framebuffer) - display(screen.render_pipeline[end-2].robj.uniforms) - println() - display(screen.render_pipeline[end-1].framebuffer) - display(screen.render_pipeline[end-1].robj.uniforms) - println() - display(screen.framebuffer_factory.fb) - println() - display(screen.framebuffer_factory.buffers) - println() - display(screen.framebuffer_factory.children) - - cb = Makie.colorbuffer(f) - db = GLMakie.depthbuffer(screen) - ob = GLMakie.pick_native(screen, Rect2i(0,0,500,500)) - s = Scene(size = (1000, 1000)) - image!(s, -1..0, 0..1, cb) - image!(s, 0..1, 0..1, db) - image!(s, -1..0, -1..0, getfield.(ob, :id)) - image!(s, 0..1, -1..0, getfield.(ob, :index)) - @info unique(getfield.(ob, :id)) - @info extrema(getfield.(ob, :index)) - s end \ No newline at end of file From 055c1c4c81ae0a2eb8cba0eb2d79b019911cdfbd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 14:03:11 +0100 Subject: [PATCH 062/135] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 968e2f8aa5e..38b43fe1791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Fixed indexing error edge case in violin median code [#4682](https://github.com/MakieOrg/Makie.jl/pull/4682) +- Refactored rendering in GLMakie to go through a series of steps abstracted by a render pipeline. This allows rendering to be adjusted from outside and should simplify introducing more post-processing options in the future. [#4689](https://github.com/MakieOrg/Makie.jl/pull/4689) ## [0.22.0] - 2024-12-12 From 917662693738a82820df5b80873466096892cf5c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 14:33:28 +0100 Subject: [PATCH 063/135] cleanup GLAbstraction --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 102 +++++++-------------- GLMakie/src/GLAbstraction/GLTypes.jl | 20 ---- 2 files changed, 32 insertions(+), 90 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index f72c2b17370..3f0e43fe964 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -1,46 +1,3 @@ -# For completion sake - -# Doesn't implement getindex, setindex etc I think? -mutable struct GLRenderbuffer - id::GLuint - size::NTuple{2, Int} - format::GLenum - context::GLContext - - function GLRenderbuffer(size, format::GLenum) - renderbuffer = GLuint[0] - glGenRenderbuffers(1, renderbuffer) - glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[1]) - glRenderbufferStorage(GL_RENDERBUFFER, format, size...) - - obj = new(renderbuffer[1], size, format, current_context()) - finalizer(free, obj) - - return obj - end -end - -function bind(buffer::GLRenderbuffer) - if buffer.id == 0 - error("Binding freed GLRenderbuffer") - end - glBindRenderbuffer(GL_RENDERBUFFER, buffer.id) - return -end - -function unsafe_free(x::GLRenderbuffer) - # don't free if already freed - x.id == 0 && return - # don't free from other context - GLAbstraction.context_alive(x.context) || return - GLAbstraction.switch_context!(x.context) - id = Ref(x.id) - glDeleteRenderbuffers(1, id) - x.id = 0 - return -end - - # TODO: Add RenderBuffer, resize!() with renderbuffer (recreate?) mutable struct GLFramebuffer id::GLuint @@ -76,6 +33,10 @@ function bind(fb::GLFramebuffer, target = fb.id) return end +# This allows you to just call `set_draw_buffers(framebuffer)` with a framebuffer +# dedicated to a specific (type of) draw call, i.e. one that only contains +# attachments matching the outputs of the draw call. But it restricts how you +# can modify the framebuffer a bit (1) """ draw_buffers(fb::GLFrameBuffer[, N::Int]) @@ -123,6 +84,8 @@ function get_next_colorbuffer_attachment(fb::GLFramebuffer) if fb.counter >= 15 error("The framebuffer has exhausted its maximum number of color attachments.") end + # (1) Disallows random deletion as that can change the order of buffers. As + # a result we don't have to worry about counter pointing to an existing item here. attachment = GL_COLOR_ATTACHMENT0 + fb.counter fb.counter += 1 return (fb.counter, attachment) @@ -141,26 +104,27 @@ function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) return attach(fb, key, buffer, length(fb.attachments) + 1, GL_DEPTH_STENCIL_ATTACHMENT) end +# for error messages const ATTACHMENT_LOOKUP = Dict{Int, Symbol}( - GL_DEPTH_ATTACHMENT => :GL_DEPTH_ATTACHMENT, - GL_STENCIL_ATTACHMENT => :GL_STENCIL_ATTACHMENT, - GL_DEPTH_STENCIL_ATTACHMENT => :GL_DEPTH_STENCIL_ATTACHMENT, - GL_COLOR_ATTACHMENT0 => :GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1 => :GL_COLOR_ATTACHMENT1, - GL_COLOR_ATTACHMENT2 => :GL_COLOR_ATTACHMENT2, - GL_COLOR_ATTACHMENT3 => :GL_COLOR_ATTACHMENT3, - GL_COLOR_ATTACHMENT4 => :GL_COLOR_ATTACHMENT4, - GL_COLOR_ATTACHMENT5 => :GL_COLOR_ATTACHMENT5, - GL_COLOR_ATTACHMENT6 => :GL_COLOR_ATTACHMENT6, - GL_COLOR_ATTACHMENT7 => :GL_COLOR_ATTACHMENT7, - GL_COLOR_ATTACHMENT8 => :GL_COLOR_ATTACHMENT8, - GL_COLOR_ATTACHMENT9 => :GL_COLOR_ATTACHMENT9, - GL_COLOR_ATTACHMENT10 => :GL_COLOR_ATTACHMENT10, - GL_COLOR_ATTACHMENT11 => :GL_COLOR_ATTACHMENT11, - GL_COLOR_ATTACHMENT12 => :GL_COLOR_ATTACHMENT12, - GL_COLOR_ATTACHMENT13 => :GL_COLOR_ATTACHMENT13, - GL_COLOR_ATTACHMENT14 => :GL_COLOR_ATTACHMENT14, - GL_COLOR_ATTACHMENT15 => :GL_COLOR_ATTACHMENT15 + GL_DEPTH_ATTACHMENT => "GL_DEPTH_ATTACHMENT", + GL_STENCIL_ATTACHMENT => "GL_STENCIL_ATTACHMENT", + GL_DEPTH_STENCIL_ATTACHMENT => "GL_DEPTH_STENCIL_ATTACHMENT", + GL_COLOR_ATTACHMENT0 => "GL_COLOR_ATTACHMENT0", + GL_COLOR_ATTACHMENT1 => "GL_COLOR_ATTACHMENT1", + GL_COLOR_ATTACHMENT2 => "GL_COLOR_ATTACHMENT2", + GL_COLOR_ATTACHMENT3 => "GL_COLOR_ATTACHMENT3", + GL_COLOR_ATTACHMENT4 => "GL_COLOR_ATTACHMENT4", + GL_COLOR_ATTACHMENT5 => "GL_COLOR_ATTACHMENT5", + GL_COLOR_ATTACHMENT6 => "GL_COLOR_ATTACHMENT6", + GL_COLOR_ATTACHMENT7 => "GL_COLOR_ATTACHMENT7", + GL_COLOR_ATTACHMENT8 => "GL_COLOR_ATTACHMENT8", + GL_COLOR_ATTACHMENT9 => "GL_COLOR_ATTACHMENT9", + GL_COLOR_ATTACHMENT10 => "GL_COLOR_ATTACHMENT10", + GL_COLOR_ATTACHMENT11 => "GL_COLOR_ATTACHMENT11", + GL_COLOR_ATTACHMENT12 => "GL_COLOR_ATTACHMENT12", + GL_COLOR_ATTACHMENT13 => "GL_COLOR_ATTACHMENT13", + GL_COLOR_ATTACHMENT14 => "GL_COLOR_ATTACHMENT14", + GL_COLOR_ATTACHMENT15 => "GL_COLOR_ATTACHMENT15" ) function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment::GLenum) @@ -183,11 +147,11 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment gl_attach(buffer, attachment) check_framebuffer() catch e - @info "$key -> $attachment = $(get(ATTACHMENT_LOOKUP, attachment, :UNKNOWN)) failed, with framebuffer id = $(fb.id)" + @info "$key -> $attachment = $(get(ATTACHMENT_LOOKUP, attachment, "UNKNOWN")) failed, with framebuffer id = $(fb.id)" rethrow(e) end - # keep depth/stenctil/depth_stencil at end so that we can directly use - # fb.attachments when setting drawbuffers + # (1) requires us to keep depth/stenctil/depth_stencil at end so that the first + # fb.counter buffers are usable draw buffers. for (k, v) in fb.name2idx fb.name2idx[k] = ifelse(v < idx, v, v+1) end @@ -204,12 +168,10 @@ function gl_attach(buffer::RenderBuffer, attachment::GLenum) glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, buffer) end -# Need to be careful with counter here -# We have [colorbuffers..., depth/stencil] -# ^- counter = last before depth/stencil -# And also with the GLFramebuffer being ordered -# So we only allow pop! for colorbuffers, no delete!() +# (1) disallows random deletion because that could disrupt the order of draw buffers -> no delete!() function pop_colorbuffer!(fb::GLFramebuffer) + # (1) depth, stencil are attached after the last colorbuffer, after fb.counter. + # Need to fix their indices after deletion attachment = fb.attachments[fb.counter] glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, 0, 0) deleteat!(fb.attachments, fb.counter) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index e60c2b57a54..8ecaa9e2c98 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -108,26 +108,6 @@ function resize!(rb::RenderBuffer, newsize::AbstractArray) glRenderbufferStorage(GL_RENDERBUFFER, rb.format, newsize...) end -struct FrameBuffer{T} - id::GLuint - attachments::Vector{Any} - context::GLContext - function FrameBuffer{T}(dimensions::Observable) where T - fb = glGenFramebuffers() - glBindFramebuffer(GL_FRAMEBUFFER, fb) - new(id, attachments, current_context()) - end -end - -function resize!(fbo::FrameBuffer, newsize::AbstractArray) - if length(newsize) != 2 - error("FrameBuffer needs to be 2 dimensional. Dimension found: ", newsize) - end - for elem in fbo.attachments - resize!(elem) - end -end - ######################################################################################## # OpenGL Arrays From 35ea37b6326358e6990cee96a4a869f89c9bb415 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 17:49:33 +0100 Subject: [PATCH 064/135] cleanup, add extras to format, usability improvements --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 2 +- src/utilities/RenderPipeline.jl | 337 ++++++++++++++------- 2 files changed, 236 insertions(+), 103 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 3f0e43fe964..4f7ea0c2fb9 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -105,7 +105,7 @@ function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) end # for error messages -const ATTACHMENT_LOOKUP = Dict{Int, Symbol}( +const ATTACHMENT_LOOKUP = Dict{Int, String}( GL_DEPTH_ATTACHMENT => "GL_DEPTH_ATTACHMENT", GL_STENCIL_ATTACHMENT => "GL_STENCIL_ATTACHMENT", GL_DEPTH_STENCIL_ATTACHMENT => "GL_DEPTH_STENCIL_ATTACHMENT", diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 7b8545e3438..d7ad4dbaa40 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -1,14 +1,43 @@ -import FixedPointNumbers -Float8 = FixedPointNumbers.N0f8 +# Note: This file could easily be moved out into a mini-package. -# TODO: consider adding a "reuse immediately" flag so Stage can communicate that -# it allows output = input +import FixedPointNumbers: N0f8 + +# TODO: try `BufferFormat{T} ... type::Type{T}` for better performance? +# TODO: consider adding a "immediately reusable" flag here or to Stage so that +# Stage can communicate that it allows output = input struct BufferFormat dims::Int type::DataType - BufferFormat(dims::Integer = 4, type::DataType = Float8) = new(dims, type) + extras::Dict{Symbol, Any} +end + +""" + BufferFormat([dims = 4, type = N0f8]; extras...) + +Creates a `BufferFormat` which encodes requirements for an input or output of a +`Stage`. For example, a color output may require 3 (RGB) N0f8's (8 bit "float" +normalized to a 0..1 range). + +The `BufferFormat` may also include extra requirements such as +`x_minfilter = :nearest` or `mipmap = true`. +""" +function BufferFormat(dims::Integer = 4, type::DataType = N0f8; extras...) + return BufferFormat(dims, type, Dict{Symbol, Any}(extras)) end +""" + BufferFormat(f1::BufferFormat, f2::BufferFormat) + +Creates a new `BufferFormat` combining two given formats. For this the formats +need to be compatible, but not the same. + +Rules: +- The output size is `dims = max(f1.dims, f2.dims)` +- Types must come from the same base type (AbstractFloat, Signed, Unsigned) where N0f8 counts as a float. +- The output type is the larger one of the two +- Extra requirements must match if both formats have them. +- If only one format contains a specific extra requirement it is accepted and copied to the output. +""" function BufferFormat(f1::BufferFormat, f2::BufferFormat) if is_compatible(f1, f2) dims = max(f1.dims, f2.dims) @@ -16,18 +45,23 @@ function BufferFormat(f1::BufferFormat, f2::BufferFormat) type = if T1 == T2; T1 elseif (T1 == Float32) || (T2 == Float32); Float32 elseif (T1 == Float16) || (T2 == Float16); Float16 - elseif (T1 == Float8) || (T2 == Float8); Float8 + elseif (T1 == N0f8) || (T2 == N0f8); N0f8 elseif (T1 == Int32) || (T2 == Int32); Int32 elseif (T1 == Int16) || (T2 == Int16); Int16 elseif (T1 == Int8) || (T2 == Int8); Int8 elseif (T1 == UInt32) || (T2 == UInt32); UInt32 elseif (T1 == UInt16) || (T2 == UInt16); UInt16 elseif (T1 == UInt8) || (T2 == UInt8); UInt8 - else error("Types $T1 and $T2 are not allowed.") + else error("Failed to merge BufferFormat: Types $T1 and $T2 are not allowed.") + end + extras = copy(f1.extras) + for (k, v) in f2.extras + get!(extras, k, v) == v || error("Failed to merge BufferFormat: Extra requirement $(extras[k]) != $v.") end - return BufferFormat(dims, type) + return BufferFormat(dims, type, extras) + else + error("Failed to merge BufferFormat: $f1 and $f2 are not compatible.") end - error("Could not generate compatible BufferFormat between $input and $output") end @@ -36,9 +70,9 @@ function is_compatible(f1::BufferFormat, f2::BufferFormat) # is_compatible(f1.type, f2.type), but still does runtime dispatch on == T1 = f1.type T2 = f2.type - return( - ((T1 == Float8) || (T1 == Float16) || (T1 == Float32)) && - ((T2 == Float8) || (T2 == Float16) || (T2 == Float32)) + return ( + ((T1 == N0f8) || (T1 == Float16) || (T1 == Float32)) && + ((T2 == N0f8) || (T2 == Float16) || (T2 == Float32)) ) || ( ((T1 == Int8) || (T1 == Int16) || (T1 == Int32)) && ((T2 == Int8) || (T2 == Int16) || (T2 == Int32)) @@ -64,7 +98,7 @@ struct Stage outputs::Dict{Symbol, Int} input_formats::Vector{BufferFormat} output_formats::Vector{BufferFormat} - # ^ technically all of these are constants + # ^ technically all of these are constants (no push, pop, delete, setindex allowed) # v these are not input_connections::Vector{ConnectionT{Stage}} @@ -73,7 +107,14 @@ end const Connection = ConnectionT{Stage} -function Stage(name; inputs = NTuple{0, Pair{Symbol, BufferFormat}}(), outputs = NTuple{0, Pair{Symbol, BufferFormat}}()) +""" + Stage(name::Symbol[; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbol, BufferFormat}[]) + +Creates a new `Stage` from the given `name`, `inputs` and `outputs`. A `Stage` +represents an action taken during rendering, e.g. rendering (a subset of) render +objects, running a post processor or sorting render objects. +""" +function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbol, BufferFormat}[]) stage = Stage(Symbol(name), Dict{Symbol, Int}(), Dict{Symbol, Int}(), BufferFormat[], BufferFormat[], @@ -90,32 +131,33 @@ function Stage(name; inputs = NTuple{0, Pair{Symbol, BufferFormat}}(), outputs = return stage end +""" + Connection(source::Stage, output::Integer, target::Stage, input::Integer) + +Creates a `Connection` between the `output` of a `source` stage and the `input` +of a `target` Stage. The `output` and `input` can be either the name of that +output/input or the index. + +Note: This constructor does not update the given stages or make any checks. It +should be considered internal. Use `connect!()` instead. +""" function Connection(source::Stage, input::Integer, target::Stage, output::Integer) format = BufferFormat(source.output_formats[input], target.input_formats[output]) return Connection([source => input], [target => output], format) end -function Base.merge(c1::Connection, c2::Connection) - for (stage, idx) in c2.inputs - any(kv -> kv[1] === stage, c1.inputs) || push!(c1.inputs, stage => idx) - end - for (stage, idx) in c2.outputs - any(kv -> kv[1] === stage, c1.outputs) || push!(c1.outputs, stage => idx) - end - - return Connection(c1.inputs, c1.outputs, BufferFormat(c1.format, c2.format)) -end - -function Base.hash(stage::Stage, h::UInt) - # inputs, outputs are static after init - # connections are not, and should not be relevant to identifying a stage - return hash(stage.name, hash(stage.inputs, hash(stage.outputs, h))) -end - struct Pipeline stages::Vector{Stage} connections::Vector{Connection} end + +""" + Pipeline([stages::Stage...]) + +Creates a `Pipeline` from the given `stages` or an empty pipeline if none are +given. The pipeline represents a series of actions (stages) executed during +rendering. +""" function Pipeline() return Pipeline(Stage[], Connection[]) end @@ -140,19 +182,93 @@ end function Base.push!(pipeline::Pipeline, other::Pipeline) append!(pipeline.stages, other.stages) append!(pipeline.connections, other.connections) - return other.stages # for convernience + return other # for convenience +end + + +""" + connect!(pipeline::Pipeline, source::Union{Pipeline, Stage}, target::Union{Pipeline, Stage}) + +Connects every output in `source` to every `input` in `target` that shares the +same name. For example, if `:a, :b, :c, :d` exist in source and `:b, :d, :e` +exist in target, `:b, :d` will get connected. +""" +function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage}, trg::Union{Pipeline, Stage}) + stages(pipeline::Pipeline) = pipeline.stages + stages(stage::Stage) = [stage] + + outputs = Set(mapreduce(stage -> keys(stage.outputs), union, stages(src))) + inputs = Set(mapreduce(stage -> keys(stage.inputs), union, stages(trg))) + for key in intersect(outputs, inputs) + connect!(pipeline, src, trg, key) + end + return +end +function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage, Integer}, trg::Union{Pipeline, Stage, Integer}, key::Symbol) + return connect!(pipeline, src, key, trg, key) end +# TODO: Not sure about this... Maybe it should be first/last instead? But what +# then it wouldn't really work with e.g. SSAO, which needs color as an +# input in step 2. +""" + connect!(pipeline, source, output, target, input) + +Connects an `output::Union{Symbol, Integer}` from `source::Union{Stage, Integer}` +to an `input` of `target`. If either already has a connection the new connection +will be merged with the old. The source and target stage as well as the pipeline +will be updated appropriately. + +`source` and `target` can also be `Pipeline`s if both output and input are +`Symbol`s. In this case every stage in source with an appropriately named output +is connected to every stage in target with an appropriately named input. Use with +caution. +""" +function Observables.connect!(pipeline::Pipeline, + src::Union{Pipeline, Stage}, output::Symbol, + trg::Union{Pipeline, Stage}, input::Symbol + ) + function sources(pipeline::Pipeline, output) + return [(stage, stage.outputs[output]) for stage in pipeline.stages if haskey(stage.outputs, output)] + end + sources(stage::Stage, output) = [(stage, stage.outputs[output])] + + function targets(pipeline::Pipeline, input) + return [(stage, stage.inputs[input]) for stage in pipeline.stages if haskey(stage.inputs, input)] + end + targets(stage::Stage, input) = [(stage, stage.inputs[input])] + + for (source, output_idx) in sources(src, output) + for (target, input_idx) in targets(trg, input) + @inbounds connect!(pipeline, source, output_idx, target, input_idx) + end + end + + return +end function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) - source = pipeline.stages[src] - target = pipeline.stages[trg] + return connect!(pipeline, pipeline.stages[src], output, pipeline.stages[trg], input) +end +function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, trg::Integer, input::Integer) + return connect!(pipeline, pipeline.stages[src], output, pipeline.stages[trg], input) +end +function Observables.connect!(pipeline::Pipeline, source::Stage, output::Symbol, target::Stage, input::Symbol) haskey(source.outputs, output) || error("output $output does not exist in source stage") haskey(target.inputs, input) || error("input $input does not exist in target stage") - output_idx = source.outputs[output] input_idx = target.inputs[input] + return @inbounds connect!(pipeline, source, output_idx, target, input_idx) +end + +function Observables.connect!(pipeline::Pipeline, + source::Stage, output_idx::Integer, target::Stage, input_idx::Integer) + + @boundscheck begin + checkbounds(source.output_formats, output_idx) + checkbounds(target.input_formats, input_idx) + end # resize if too small (this allows later connections to be skipped) if length(source.output_connections) < output_idx @@ -162,6 +278,25 @@ function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, resize!(target.input_connections, input_idx) end + # Don't make a new connection if the connection already exists + # (the format must be correct if it exists) + if isassigned(source.output_connections, output_idx) && + isassigned(target.input_connections, input_idx) && + (source.output_connections[output_idx] === target.input_connections[input_idx]) + return source.output_connections[output_idx] + end + + function unsafe_merge(c1::Connection, c2::Connection) + for (stage, idx) in c2.inputs + any(kv -> kv[1] === stage, c1.inputs) || push!(c1.inputs, stage => idx) + end + for (stage, idx) in c2.outputs + any(kv -> kv[1] === stage, c1.outputs) || push!(c1.outputs, stage => idx) + end + + return Connection(c1.inputs, c1.outputs, BufferFormat(c1.format, c2.format)) + end + # create requested connection connection = Connection(source, output_idx, target, input_idx) @@ -174,13 +309,13 @@ function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, # near the end? idx = findlast(c -> c === old, pipeline.connections)::Int deleteat!(pipeline.connections, idx) - connection = merge(connection, old) + connection = unsafe_merge(connection, old) end if isassigned(target.input_connections, input_idx) old = target.input_connections[input_idx] idx = findlast(c -> c === old, pipeline.connections)::Int deleteat!(pipeline.connections, idx) - connection = merge(connection, old) + connection = unsafe_merge(connection, old) end # attach connection to every input and output @@ -196,8 +331,8 @@ function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, return connection end -# TODO: make it impossible to break order -# TODO: Is this even a necessary condition? +# This checks that no connection goes backwards, i.e. every connection is written +# to before being read. function verify(pipeline::Pipeline) for connection in pipeline.connections earliest_input = length(pipeline.stages) @@ -224,11 +359,18 @@ function verify(pipeline::Pipeline) return true end -format_complexity(c::Connection) = format_complexity(c.format) -format_complexity(f::BufferFormat) = f.dims * sizeof(f.type) +""" + generate_buffers(pipeline) +Maps the connections in the given pipeline to a vector of buffer formats and +returns them together with a connection-to-index map. This will attempt to +optimize buffers for the lowest memory overhead. I.e. it will reuse buffers for +multiple connections and upgrade them if it is cheaper than creating a new one. +""" function generate_buffers(pipeline::Pipeline) - # TODO: is this necessary? + format_complexity(c::Connection) = format_complexity(c.format) + format_complexity(f::BufferFormat) = f.dims * sizeof(f.type) + verify(pipeline) # We can make this more efficient later... @@ -450,33 +592,30 @@ SortStage() = Stage(:ZSort) # simple color objectid # OIT HDR_color objectid weight function RenderStage() - outputs = ( - :color => BufferFormat(4, Float8), + outputs = [ + :color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32), :position => BufferFormat(3, Float16), :normal => BufferFormat(3, Float16), - ) + ] Stage(:Render, outputs = outputs) end function TransparentRenderStage() - outputs = ( + outputs = [ :weighted_color_sum => BufferFormat(4, Float16), :objectid => BufferFormat(2, UInt32), - :alpha_product => BufferFormat(1, Float8), - ) + :alpha_product => BufferFormat(1, N0f8), + ] Stage(:TransparentRender, outputs = outputs) end # Want a MultiSzage kinda thing function SSAOStage() - inputs = ( - :position => BufferFormat(3, Float32), - :normal => BufferFormat(3, Float16) - ) - stage1 = Stage(:SSAO1; inputs, outputs = (:occlusion => BufferFormat(1, Float8),)) + inputs = [:position => BufferFormat(3, Float32), :normal => BufferFormat(3, Float16)] + stage1 = Stage(:SSAO1; inputs, outputs = [:occlusion => BufferFormat(1, N0f8)]) - inputs = (:occlusion => BufferFormat(1, Float8), :color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) - stage2 = Stage(:SSAO2, inputs = inputs, outputs = (:color => BufferFormat(),)) + inputs = [:occlusion => BufferFormat(1, N0f8), :color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] + stage2 = Stage(:SSAO2, inputs = inputs, outputs = [:color => BufferFormat()]) pipeline = Pipeline(stage1, stage2) connect!(pipeline, 1, :occlusion, 2, :occlusion) @@ -485,18 +624,18 @@ function SSAOStage() end function OITStage() - inputs = (:weighted_color_sum => BufferFormat(4, Float16), :alpha_product => BufferFormat(1, Float8)) - outputs = (:color => BufferFormat(4, Float8),) + inputs = [:weighted_color_sum => BufferFormat(4, Float16), :alpha_product => BufferFormat(1, N0f8)] + outputs = [:color => BufferFormat(4, N0f8)] return Stage(:OIT; inputs, outputs) end function FXAAStage() - inputs = (:color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) - outputs = (:color_luma => BufferFormat(4, Float8),) + inputs = [:color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] + outputs = [:color_luma => BufferFormat(4, N0f8)] stage1 = Stage(:FXAA1; inputs, outputs) - inputs = (:color_luma => BufferFormat(4, Float8),) - outputs = (:color => BufferFormat(4, Float8),) + inputs = [:color_luma => BufferFormat(4, N0f8)] + outputs = [:color => BufferFormat(4, N0f8)] stage2 = Stage(:FXAA2; inputs, outputs) pipeline = Pipeline(stage1, stage2) @@ -506,7 +645,7 @@ function FXAAStage() end function DisplayStage() - inputs = (:color => BufferFormat(4, Float8), :objectid => BufferFormat(2, UInt32)) + inputs = [:color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] return Stage(:Display; inputs) end @@ -518,29 +657,25 @@ function default_SSAO_pipeline() # matching master with SSAO enabled pipeline = Pipeline() - push!(pipeline, SortStage()) # 1 - push!(pipeline, RenderStage()) # 2 - push!(pipeline, SSAOStage()) # 3, 4 - push!(pipeline, RenderStage()) # 5 - push!(pipeline, TransparentRenderStage()) # 6 - push!(pipeline, OITStage()) # 7 - push!(pipeline, FXAAStage()) # 8, 9 - push!(pipeline, DisplayStage()) # 10 - - connect!(pipeline, 2, :position, 3, :position) - connect!(pipeline, 2, :normal, 3, :normal) - connect!(pipeline, 2, :color, 4, :color) - connect!(pipeline, 2, :objectid, 4, :objectid) - connect!(pipeline, 2, :objectid, 8, :objectid) - connect!(pipeline, 2, :objectid, 10, :objectid) - connect!(pipeline, 4, :color, 8, :color) - connect!(pipeline, 5, :color, 8, :color) - connect!(pipeline, 5, :objectid, 10, :objectid) # will get bundled so we don't need to repeat - connect!(pipeline, 6, :weighted_color_sum, 7, :weighted_color_sum) - connect!(pipeline, 6, :objectid, 10, :objectid) - connect!(pipeline, 6, :alpha_product, 7, :alpha_product) - connect!(pipeline, 7, :color, 8, :color) - connect!(pipeline, 9, :color, 10, :color) + push!(pipeline, SortStage()) + render1 = push!(pipeline, RenderStage()) + ssao = push!(pipeline, SSAOStage()) + render2 = push!(pipeline, RenderStage()) + render3 = push!(pipeline, TransparentRenderStage()) + oit = push!(pipeline, OITStage()) + fxaa = push!(pipeline, FXAAStage()) + display = push!(pipeline, DisplayStage()) + + connect!(pipeline, render1, ssao) + connect!(pipeline, render1, fxaa, :objectid) + connect!(pipeline, render1, display, :objectid) + connect!(pipeline, ssao, fxaa, :color) + connect!(pipeline, render2, fxaa, :color) + connect!(pipeline, render2, display, :objectid) # will get bundled so we don't need to repeat + connect!(pipeline, render3, oit) + connect!(pipeline, render3, display, :objectid) + connect!(pipeline, oit, fxaa, :color) + connect!(pipeline, fxaa, display, :color) return pipeline end @@ -552,23 +687,21 @@ function default_pipeline() pipeline = Pipeline() push!(pipeline, SortStage()) - push!(pipeline, RenderStage()) - push!(pipeline, RenderStage()) - push!(pipeline, TransparentRenderStage()) - push!(pipeline, OITStage()) - push!(pipeline, FXAAStage()) - push!(pipeline, DisplayStage()) - - connect!(pipeline, 2, :color, 6, :color) - connect!(pipeline, 2, :objectid, 6, :objectid) - connect!(pipeline, 2, :objectid, 8, :objectid) - connect!(pipeline, 3, :color, 6, :color) - connect!(pipeline, 3, :objectid, 8, :objectid) - connect!(pipeline, 4, :weighted_color_sum, 5, :weighted_color_sum) - connect!(pipeline, 4, :objectid, 8, :objectid) - connect!(pipeline, 4, :alpha_product, 5, :alpha_product) - connect!(pipeline, 5, :color, 6, :color) - connect!(pipeline, 7, :color, 8, :color) + render1 = push!(pipeline, RenderStage()) + render2 = push!(pipeline, RenderStage()) + render3 = push!(pipeline, TransparentRenderStage()) + oit = push!(pipeline, OITStage()) + fxaa = push!(pipeline, FXAAStage()) + display = push!(pipeline, DisplayStage()) + + connect!(pipeline, render1, fxaa) + connect!(pipeline, render1, display, :objectid) + connect!(pipeline, render2, fxaa) + connect!(pipeline, render2, display, :objectid) + connect!(pipeline, render3, oit) + connect!(pipeline, render3, display, :objectid) + connect!(pipeline, oit, display, :color) + connect!(pipeline, fxaa, display, :color) return pipeline end From 25bff2fd0cf5faf3a2d2fb327a99b5122e8a23d9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 18:53:20 +0100 Subject: [PATCH 065/135] add stage attributes, format extras --- GLMakie/src/postprocessing.jl | 2 +- GLMakie/src/render_pipeline.jl | 32 ++++---- src/utilities/RenderPipeline.jl | 126 +++++++++++++++++--------------- 3 files changed, 81 insertions(+), 79 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index b8fd748480f..4cd27e02af2 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -395,7 +395,7 @@ struct BlitToScreen <: AbstractRenderStep screen_framebuffer_id::Int # Screen not available yet - function BlitToScreen(framebuffer, attachment, screen_framebuffer_id::Integer = 0) + function BlitToScreen(framebuffer::GLFramebuffer, attachment::GLuint, screen_framebuffer_id::Integer = 0) @debug "Creating to screen postprocessor" return new(framebuffer, attachment, screen_framebuffer_id) end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 9ad33e32dee..2c80d0a699f 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -29,8 +29,14 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF end if !found - isfloattype = eltype(T) == N0f8 || eltype(T) <: AbstractFloat - interp = isfloattype ? (:linear) : (:nearest) + if haskey(format.extras, :minfilter) + interp = format.extras[:minfilter] + if !(eltype(T) == N0f8 || eltype(T) <: AbstractFloat) && (interp == :linear) + error("Cannot use :linear interpolation with non float types.") + end + else + interp = :nearest + end tex = Texture(T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) push!(factory.buffers, tex) end @@ -85,25 +91,15 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) SortPlots() elseif stage.name == :Render # TODO: - if first_render - attach_colorbuffer(factory.fb, :objectid, get_buffer(framebuffer, :objectid)) - first_render = false - RenderPlots(screen, framebuffer, inputs, :SSAO) - else - RenderPlots(screen, framebuffer, inputs, :FXAA) - end + RenderPlots(screen, framebuffer, inputs, stage.attributes[:target]::Symbol) elseif stage.name == :TransparentRender RenderPlots(screen, framebuffer, inputs, :OIT) elseif stage.name == :Display - # TODO: hacky - prev = last(render_pipeline) - framebuffer = prev.framebuffer - # TODO: Technically need to find connection from prev to this stage - # that ends up in :color - # Assuming that connection attached to a :color output: - attachment = get_attachment(framebuffer, :color) - attach_colorbuffer(factory.fb, :color, get_buffer(framebuffer, :color)) - BlitToScreen(framebuffer, attachment) + # Need these for colorbuffer() and picking + attach_colorbuffer(factory.fb, :color, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :color)])) + attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :objectid)])) + + BlitToScreen(factory.fb, get_attachment(factory.fb, :color)) elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] RenderPass{stage.name}(screen, framebuffer, inputs) else diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index d7ad4dbaa40..767911163d8 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -103,6 +103,8 @@ struct Stage # v these are not input_connections::Vector{ConnectionT{Stage}} output_connections::Vector{ConnectionT{Stage}} + + attributes::Dict{Symbol, Any} end const Connection = ConnectionT{Stage} @@ -114,23 +116,31 @@ Creates a new `Stage` from the given `name`, `inputs` and `outputs`. A `Stage` represents an action taken during rendering, e.g. rendering (a subset of) render objects, running a post processor or sorting render objects. """ -function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbol, BufferFormat}[]) - stage = Stage(Symbol(name), - Dict{Symbol, Int}(), Dict{Symbol, Int}(), - BufferFormat[], BufferFormat[], - Connection[], Connection[] +function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbol, BufferFormat}[], kwargs...) + return Stage(Symbol(name), + Dict{Symbol, Int}([k => idx for (idx, (k, v)) in enumerate(inputs)]), + BufferFormat[v for (k, v) in inputs], + Dict{Symbol, Int}([k => idx for (idx, (k, v)) in enumerate(outputs)]), + BufferFormat[v for (k, v) in outputs]; + kwargs... + ) +end +function Stage(name, inputs, input_formats, outputs, output_formats; kwargs...) + stage = Stage( + Symbol(name), + inputs, outputs, + input_formats, output_formats, + Connection[], Connection[], + Dict{Symbol, Any}(kwargs) ) - foreach(enumerate(inputs)) do (i, (k, v)) - stage.inputs[k] = i - push!(stage.input_formats, v) - end - foreach(enumerate(outputs)) do (i, (k, v)) - stage.outputs[k] = i - push!(stage.output_formats, v) - end return stage end +get_input_connection(stage::Stage, key::Symbol) = stage.input_connections[stage.inputs[key]] +get_output_connection(stage::Stage, key::Symbol) = stage.output_connections[stage.outputs[key]] +get_input_format(stage::Stage, key::Symbol) = stage.input_formats[stage.inputs[key]] +get_output_format(stage::Stage, key::Symbol) = stage.output_formats[stage.outputs[key]] + """ Connection(source::Stage, output::Integer, target::Stage, input::Integer) @@ -471,7 +481,11 @@ end function Base.show(io::IO, format::BufferFormat) - print(io, "BufferFormat($(format.dims), $(format.type))") + print(io, "BufferFormat($(format.dims), $(format.type)") + for (k, v) in format.extras + print(io, ", :", k, " => ", v) + end + print(io, ")") end Base.show(io::IO, stage::Stage) = print(io, "Stage($(stage.name))") @@ -587,66 +601,61 @@ end SortStage() = Stage(:ZSort) -# I guess we should have multiple versions of this? Because SSAO render and OIT render don't really fit? -# SSAO color objectid position normal -# simple color objectid -# OIT HDR_color objectid weight -function RenderStage() - outputs = [ - :color => BufferFormat(4, N0f8), - :objectid => BufferFormat(2, UInt32), - :position => BufferFormat(3, Float16), - :normal => BufferFormat(3, Float16), - ] - Stage(:Render, outputs = outputs) +function RenderStage(; kwargs...) + outputs = Dict(:color => 1, :objectid => 2, :position => 3, :normal => 4) + output_formats = [BufferFormat(4, N0f8), BufferFormat(2, UInt32), BufferFormat(3, Float16), BufferFormat(3, Float16)] + return Stage(:Render, Dict{Symbol, Int}(), BufferFormat[], outputs, output_formats; kwargs...) end + function TransparentRenderStage() - outputs = [ - :weighted_color_sum => BufferFormat(4, Float16), - :objectid => BufferFormat(2, UInt32), - :alpha_product => BufferFormat(1, N0f8), - ] - Stage(:TransparentRender, outputs = outputs) + outputs = Dict(:weighted_color_sum => 1, :objectid => 2, :alpha_product => 3) + output_formats = [BufferFormat(4, Float16), BufferFormat(2, UInt32), BufferFormat(1, N0f8)] + return Stage(:TransparentRender, Dict{Symbol, Int}(), BufferFormat[], outputs, output_formats) end -# Want a MultiSzage kinda thing -function SSAOStage() - inputs = [:position => BufferFormat(3, Float32), :normal => BufferFormat(3, Float16)] - stage1 = Stage(:SSAO1; inputs, outputs = [:occlusion => BufferFormat(1, N0f8)]) +function SSAOStage(; kwargs...) + inputs = Dict(:position => 1, :normal => 2) + input_formats = [BufferFormat(3, Float32), BufferFormat(3, Float16)] + stage1 = Stage(:SSAO1, inputs, input_formats, Dict(:occlusion => 1), [BufferFormat(1, N0f8)]; kwargs...) - inputs = [:occlusion => BufferFormat(1, N0f8), :color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] - stage2 = Stage(:SSAO2, inputs = inputs, outputs = [:color => BufferFormat()]) + inputs = Dict(:occlusion => 1, :color => 2, :objectid => 3) + input_formats = [BufferFormat(1, N0f8), BufferFormat(4, N0f8), BufferFormat(2, UInt32)] + stage2 = Stage(:SSAO2, inputs, input_formats, Dict(:color => 1), [BufferFormat()]; kwargs...) pipeline = Pipeline(stage1, stage2) - connect!(pipeline, 1, :occlusion, 2, :occlusion) + connect!(pipeline, stage1, 1, stage2, 1) return pipeline end -function OITStage() - inputs = [:weighted_color_sum => BufferFormat(4, Float16), :alpha_product => BufferFormat(1, N0f8)] - outputs = [:color => BufferFormat(4, N0f8)] - return Stage(:OIT; inputs, outputs) +function OITStage(; kwargs...) + inputs = Dict(:weighted_color_sum => 1, :alpha_product => 2) + input_formats = [BufferFormat(4, Float16), BufferFormat(1, N0f8)] + outputs = Dict(:color => 1) + output_formats = [BufferFormat(4, N0f8)] + return Stage(:OIT, inputs, input_formats, outputs, output_formats; kwargs...) end function FXAAStage() - inputs = [:color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] - outputs = [:color_luma => BufferFormat(4, N0f8)] - stage1 = Stage(:FXAA1; inputs, outputs) + inputs = Dict(:color => 1, :objectid => 2) + input_formats = [BufferFormat(4, N0f8), BufferFormat(2, UInt32)] + stage1 = Stage(:FXAA1, inputs, input_formats, Dict(:color_luma => 1), [BufferFormat(4, N0f8)]) - inputs = [:color_luma => BufferFormat(4, N0f8)] - outputs = [:color => BufferFormat(4, N0f8)] - stage2 = Stage(:FXAA2; inputs, outputs) + stage2 = Stage(:FXAA2, + Dict(:color_luma => 1), [BufferFormat(4, N0f8, minfilter = :linear)], + Dict(:color => 1), [BufferFormat(4, N0f8)] + ) pipeline = Pipeline(stage1, stage2) - connect!(pipeline, 1, :color_luma, 2, :color_luma) + connect!(pipeline, stage1, 1, stage2, 1) return pipeline end function DisplayStage() - inputs = [:color => BufferFormat(4, N0f8), :objectid => BufferFormat(2, UInt32)] - return Stage(:Display; inputs) + inputs = Dict(:color => 1, :objectid => 2) + input_formats = [BufferFormat(4, N0f8), BufferFormat(2, UInt32)] + return Stage(:Display, inputs, input_formats, Dict{Symbol, Int}(), BufferFormat[]) end @@ -658,9 +667,9 @@ function default_SSAO_pipeline() pipeline = Pipeline() push!(pipeline, SortStage()) - render1 = push!(pipeline, RenderStage()) + render1 = push!(pipeline, RenderStage(target = :SSAO)) ssao = push!(pipeline, SSAOStage()) - render2 = push!(pipeline, RenderStage()) + render2 = push!(pipeline, RenderStage(target = :FXAA)) render3 = push!(pipeline, TransparentRenderStage()) oit = push!(pipeline, OITStage()) fxaa = push!(pipeline, FXAAStage()) @@ -687,19 +696,16 @@ function default_pipeline() pipeline = Pipeline() push!(pipeline, SortStage()) - render1 = push!(pipeline, RenderStage()) - render2 = push!(pipeline, RenderStage()) - render3 = push!(pipeline, TransparentRenderStage()) + render1 = push!(pipeline, RenderStage(target = :FXAA)) + render2 = push!(pipeline, TransparentRenderStage()) oit = push!(pipeline, OITStage()) fxaa = push!(pipeline, FXAAStage()) display = push!(pipeline, DisplayStage()) connect!(pipeline, render1, fxaa) connect!(pipeline, render1, display, :objectid) - connect!(pipeline, render2, fxaa) + connect!(pipeline, render2, oit) connect!(pipeline, render2, display, :objectid) - connect!(pipeline, render3, oit) - connect!(pipeline, render3, display, :objectid) connect!(pipeline, oit, display, :color) connect!(pipeline, fxaa, display, :color) From 57d3af065c5fd2b65bbd5a85c17e7b6e2a544a0d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 20:28:58 +0100 Subject: [PATCH 066/135] add pipeline show and fix OIT --- src/utilities/RenderPipeline.jl | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 767911163d8..0de8d237432 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -593,6 +593,46 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) return end +function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) + println(io, "Pipeline():") + print(io, "Stages:") + conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) + pad = 1 + floor(Int, log10(length(pipeline.connections))) + + for stage in pipeline.stages + print(io, "\n Stage($(stage.name))") + if !isempty(stage.input_connections) + print(io, "\n inputs: ") + # keep order + strs = map(eachindex(stage.input_connections)) do i + k = findfirst(==(i), stage.inputs) + c = stage.input_connections[i] + ci = string(conn2idx[c]) + return "[$ci] $k" + end + join(io, strs, ", ") + end + if !isempty(stage.output_connections) + print(io, "\n outputs: ") + strs = map(eachindex(stage.output_connections)) do i + k = findfirst(==(i), stage.outputs) + c = stage.output_connections[i] + ci = string(conn2idx[c]) + return "[$ci] $k" + end + join(io, strs, ", ") + end + end + + println(io, "\nConnections:") + for (i, c) in enumerate(pipeline.connections) + s = lpad("$i", pad) + println(io, " [$s] ", c) + end + + return +end + ################################################################################ ### Defaults @@ -706,7 +746,7 @@ function default_pipeline() connect!(pipeline, render1, display, :objectid) connect!(pipeline, render2, oit) connect!(pipeline, render2, display, :objectid) - connect!(pipeline, oit, display, :color) + connect!(pipeline, oit, fxaa, :color) connect!(pipeline, fxaa, display, :color) return pipeline From 9c3e27ffbd2cb64a2b20b976de95c3e90b425b14 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 21:08:41 +0100 Subject: [PATCH 067/135] add GLRenderPipeline --- GLMakie/src/glwindow.jl | 2 ++ GLMakie/src/postprocessing.jl | 10 ++++------ GLMakie/src/render_pipeline.jl | 10 ++++------ GLMakie/src/rendering.jl | 10 ++++------ GLMakie/src/screen.jl | 4 ++-- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 70402da069f..7994c943726 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -10,6 +10,8 @@ end Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T(s.id), T(s.index)) Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) + + mutable struct FramebufferFactory fb::GLFramebuffer # core framebuffer (more or less for #4150) # holding depth, stencil, objectid[, output_color] diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 4cd27e02af2..d3aa6356b74 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -34,7 +34,6 @@ Initialization is grouped together and runs before all run steps. If you need to initialize just before your run, bundle it with the run. """ abstract type AbstractRenderStep end -prepare_step(screen, glscene, ::AbstractRenderStep) = nothing run_step(screen, glscene, ::AbstractRenderStep) = nothing function destroy!(step::T) where {T <: AbstractRenderStep} @@ -49,14 +48,13 @@ function destroy!(step::T) where {T <: AbstractRenderStep} return end -struct RenderPipeline +struct GLRenderPipeline + parent::Makie.Pipeline steps::Vector{AbstractRenderStep} end +GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) -function render_frame(screen, glscene, pipeline::RenderPipeline) - for step in pipeline.steps - prepare_step(screen, glscene, step) - end +function render_frame(screen, glscene, pipeline::GLRenderPipeline) for step in pipeline.steps run_step(screen, glscene, step) end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 2c80d0a699f..2a16b7cd062 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -54,19 +54,15 @@ end function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # TODO: check if pipeline is different from the last one before replacing it - render_pipeline = screen.render_pipeline factory = screen.framebuffer_factory - empty!(render_pipeline) - # Resolve pipeline buffers, connection2idx = Makie.generate_buffers(pipeline) # Add required buffers reset!(factory, buffers) - first_render = true - + render_pipeline = AbstractRenderStep[] for stage in pipeline.stages inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] @@ -112,5 +108,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) push!(render_pipeline, pass) end - return render_pipeline + screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) + + return end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index b2f76f2d531..252993aa884 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -51,14 +51,12 @@ function setup!(screen::Screen, resize_buffers) end """ -Renders a single frame of a `window` + render_frame(screen[; resize_buffer = true]) + +Renders a single frame of a `screen` """ function render_frame(screen::Screen; resize_buffers=true) setup!(screen, resize_buffers) - - for step in screen.render_pipeline - run_step(screen, nothing, step) - end - + render_frame(screen, nothing, screen.render_pipeline) return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 37c1fae19be..b26808f0f44 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -172,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} - render_pipeline::Vector{AbstractRenderStep} + render_pipeline::GLRenderPipeline cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, Plot} framecache::Matrix{RGB{N0f8}} @@ -208,7 +208,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen, owns_glscreen, shader_cache, framebuffer_factory, config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, - screens, renderlist, AbstractRenderStep[], cache, cache2plot, + screens, renderlist, GLRenderPipeline(), cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) From 8a900d73412ce5e4cbe2e48c59388777408ac0da Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 22:13:28 +0100 Subject: [PATCH 068/135] add construct and reconstruct, unify input buffer names --- .../shader/postprocessing/OIT_blend.frag | 8 +- .../assets/shader/postprocessing/SSAO.frag | 4 +- .../shader/postprocessing/SSAO_blur.frag | 17 +-- .../assets/shader/postprocessing/copy.frag | 4 +- .../assets/shader/postprocessing/fxaa.frag | 28 ++-- .../shader/postprocessing/postprocess.frag | 8 +- GLMakie/src/postprocessing.jl | 130 +++++++++--------- GLMakie/src/render_pipeline.jl | 35 ++--- 8 files changed, 110 insertions(+), 124 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/OIT_blend.frag b/GLMakie/assets/shader/postprocessing/OIT_blend.frag index 63d4343fb0c..85aa152a249 100644 --- a/GLMakie/assets/shader/postprocessing/OIT_blend.frag +++ b/GLMakie/assets/shader/postprocessing/OIT_blend.frag @@ -6,16 +6,16 @@ in vec2 frag_uv; // contains sum_i C_i * weight(depth_i, alpha_i) -uniform sampler2D sum_color; +uniform sampler2D weighted_color_sum_buffer; // contains pod_i (1 - alpha_i) -uniform sampler2D prod_alpha; +uniform sampler2D alpha_product_buffer; out vec4 fragment_color; void main(void) { - vec4 summed_color_weight = texture(sum_color, frag_uv); - float transmittance = texture(prod_alpha, frag_uv).r; + vec4 summed_color_weight = texture(weighted_color_sum_buffer, frag_uv); + float transmittance = texture(alpha_product_buffer, frag_uv).r; vec3 weighted_transparent = summed_color_weight.rgb / max(summed_color_weight.a, 0.00001); vec3 full_weighted_transparent = weighted_transparent * (1 - transmittance); diff --git a/GLMakie/assets/shader/postprocessing/SSAO.frag b/GLMakie/assets/shader/postprocessing/SSAO.frag index 3d932689bf6..37e350907ac 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO.frag @@ -22,7 +22,7 @@ out float o_occlusion; void main(void) { vec3 view_pos = texture(position_buffer, frag_uv).xyz; - vec3 normal = texture(normal_buffer, frag_uv).xyz; + vec3 _normal = texture(normal_buffer, frag_uv).xyz; // The normal buffer gets cleared every frame. (also position, color etc) // If normal == vec3(1) then there is no geometry at this fragment. @@ -79,7 +79,7 @@ void main(void) sample_frag_pos.xy += (frag_uv - 0.5) * clip_pos_w / sample_clip_pos_w; - float sample_depth = texture(position_buffer, sample_frag_pos.xy).z; + float sample_depth = texture(position, sample_frag_pos.xy).z; float range_check = smoothstep(0.0, 1.0, radius / abs(view_pos.z - sample_depth)); occlusion += (sample_depth >= sample_view_offset.z + view_pos.z + bias ? 1.0 : 0.0) * range_check; } diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag index 4691763b4ad..4d88f8303a4 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -1,9 +1,9 @@ {{GLSL_VERSION}} // occlusion.w is the occlusion value -uniform sampler2D occlusion; -uniform sampler2D color_texture; -uniform usampler2D ids; +uniform sampler2D occlusion_buffer; +uniform sampler2D color_buffer; +uniform usampler2D objectid_buffer; uniform vec2 inv_texel_size; // Settings/Attributes uniform int blur_range; @@ -14,9 +14,9 @@ out vec4 fragment_color; void main(void) { // occlusion blur - uvec2 id0 = texture(ids, frag_uv).xy; + uvec2 id0 = texture(objectid, frag_uv).xy; if (id0.x == uint(0)){ - fragment_color = texture(color_texture, frag_uv); + fragment_color = texture(color_buffer, frag_uv); // fragment_color = vec4(1,0,1,1); // show discarded return; } @@ -24,6 +24,7 @@ void main(void) float blurred_occlusion = 0.0; float weight = 0; + // TODO: Could use :linear filtering and/ro mipmap to simplify this maybe? for (int x = -blur_range; x <= blur_range; ++x){ for (int y = -blur_range; y <= blur_range; ++y){ vec2 offset = vec2(float(x), float(y)) * inv_texel_size; @@ -31,14 +32,14 @@ void main(void) // Without this, a high (low) occlusion from one object can bleed // into the low (high) occlusion of another, giving an unwanted // shine effect. - uvec2 id = texture(ids, frag_uv + offset).xy; + uvec2 id = texture(objectid_buffer, frag_uv + offset).xy; float valid = float(id == id0); - blurred_occlusion += valid * texture(occlusion, frag_uv + offset).x; + blurred_occlusion += valid * texture(occlusion_buffer, frag_uv + offset).x; weight += valid; } } blurred_occlusion = 1.0 - blurred_occlusion / weight; - fragment_color = texture(color_texture, frag_uv) * blurred_occlusion; + fragment_color = texture(color_buffer, frag_uv) * blurred_occlusion; // Display occlusion instead: // fragment_color = vec4(vec3(blurred_occlusion), 1.0); } diff --git a/GLMakie/assets/shader/postprocessing/copy.frag b/GLMakie/assets/shader/postprocessing/copy.frag index 9ec63ec90af..e3af207bbe2 100644 --- a/GLMakie/assets/shader/postprocessing/copy.frag +++ b/GLMakie/assets/shader/postprocessing/copy.frag @@ -1,12 +1,12 @@ {{GLSL_VERSION}} -uniform sampler2D color_texture; +uniform sampler2D color_buffer; in vec2 frag_uv; out vec4 fragment_color; void main(void) { - vec4 color = texture(color_texture, frag_uv); + vec4 color = texture(color_buffer, frag_uv); fragment_color.rgb = color.rgb; fragment_color.a = 1.0; } diff --git a/GLMakie/assets/shader/postprocessing/fxaa.frag b/GLMakie/assets/shader/postprocessing/fxaa.frag index 56d4eb678b9..2e44bfa266c 100644 --- a/GLMakie/assets/shader/postprocessing/fxaa.frag +++ b/GLMakie/assets/shader/postprocessing/fxaa.frag @@ -1019,7 +1019,7 @@ FxaaFloat4 FxaaPixelShader( precision highp float; -uniform sampler2D color_texture; +uniform sampler2D color_luma_buffer; uniform vec2 RCPFrame; in vec2 frag_uv; @@ -1031,20 +1031,20 @@ void main(void) // fragment_color = texture(color_texture, frag_uv); fragment_color.rgb = FxaaPixelShader( frag_uv, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, - color_texture, // FxaaTex tex, - color_texture, // FxaaTex fxaaConsole360TexExpBiasNegOne, - color_texture, // FxaaTex fxaaConsole360TexExpBiasNegTwo, - RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, + color_luma_buffer, // FxaaTex tex, + color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegOne, + color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegTwo, + RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, 0.75f, // FxaaFloat fxaaQualitySubpix, - 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, - 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, - 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, - 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, - 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, + 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, + 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, + 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, + 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, + 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f) // FxaaFloat fxaaConsole360ConstDir, ).rgb; } diff --git a/GLMakie/assets/shader/postprocessing/postprocess.frag b/GLMakie/assets/shader/postprocessing/postprocess.frag index ff5e2bde304..8586f1348f5 100644 --- a/GLMakie/assets/shader/postprocessing/postprocess.frag +++ b/GLMakie/assets/shader/postprocessing/postprocess.frag @@ -2,8 +2,8 @@ in vec2 frag_uv; -uniform sampler2D color_texture; -uniform usampler2D object_ids; +uniform sampler2D color_buffer; +uniform usampler2D objectid_buffer; layout(location=0) out vec4 fragment_color; @@ -21,12 +21,12 @@ bool unpack_bool(uint id) { void main(void) { - vec4 color = texture(color_texture, frag_uv).rgba; + vec4 color = texture(color_buffer, frag_uv).rgba; if(color.a <= 0){ discard; } - uint id = texture(object_ids, frag_uv).x; + uint id = texture(objectid_buffer, frag_uv).x; // do tonemappings //opaque = linear_tone_mapping(color.rgb, 1.8); // linear color output fragment_color.rgb = color.rgb; diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index d3aa6356b74..9ecb573d9ff 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -38,16 +38,20 @@ run_step(screen, glscene, ::AbstractRenderStep) = nothing function destroy!(step::T) where {T <: AbstractRenderStep} @debug "Default destructor of $T" - if hasfield(T, :passes) - while !isempty(step.passes) - destroy!(pop!(step.passes)) - end - elseif hasfield(T, :pass) - destroy!(step.pass) - end + hasfield(T, :robj) && destroy!(step.robj) return end +# fore reference: +# construct(::Val{Name}, screen, framebuffer, inputs, parent::Makie.Stage) +function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) where {T <: AbstractRenderStep} + # @debug "reconstruct() not defined for $T, calling construct()" + destroy!(old) + return construct(Val(parent.name), screen, framebuffer, input, parent) +end + + + struct GLRenderPipeline parent::Makie.Pipeline steps::Vector{AbstractRenderStep} @@ -69,6 +73,8 @@ struct EmptyRenderStep <: AbstractRenderStep end struct SortPlots <: AbstractRenderStep end +construct(::Val{:ZSort}, screen, framebuffer, inputs, parent) = SortPlots() + function run_step(screen, glscene, ::SortPlots) function sortby(x) robj = x[3] @@ -102,21 +108,23 @@ struct RenderPlots <: AbstractRenderStep fxaa::FilterOptions end -function RenderPlots(screen, framebuffer, inputs, stage) - if stage === :SSAO +function construct(::Val{:Render}, screen, framebuffer, inputs, parent) + if parent.attributes[:target] === :SSAO return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) - elseif stage === :FXAA + elseif parent.attributes[:target] === :FXAA return RenderPlots(framebuffer, Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) - elseif stage === :OIT - # HDR_color containing sums clears to 0 - # OIT_weight containing products clears to 1 - clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) else - error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") + error("Incorrect target = $(parent.target) given. Should be :SSAOor :FXAA.") end end +function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) + # HDR_color containing sums clears to 0 + # OIT_weight containing products clears to 1 + clear = [1 => Vec4f(0), 3 => Vec4f(1)] + return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) +end + function id2scene(screen, id1) # TODO maybe we should use a different data structure for (id2, scene) in screen.screens @@ -172,9 +180,18 @@ struct RenderPass{Name} <: AbstractRenderStep robj::RenderObject end +function reconstruct(pass::RP, screen, framebuffer, inputs, ::Makie.Stage) where {RP <: RenderPass} + for (k, v) in inputs + if haskey(pass.uniforms, k) + pass.robj.uniforms[k] = v + else + @error("Input $k does not exist in recreated RenderPass.") + end + end + return RP(framebuffer, pass.robj) +end - -function RenderPass{:OIT}(screen, framebuffer, inputs) +function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) @debug "Creating OIT postprocessor" # Based on https://jcgt.org/published/0002/02/09/, see #1390 @@ -185,12 +202,8 @@ function RenderPass{:OIT}(screen, framebuffer, inputs) loadshader("postprocessing/OIT_blend.frag") ) # TODO: rename in shader - data = Dict{Symbol, Any}( - :sum_color => inputs[:weighted_color_sum], - :prod_alpha => inputs[:alpha_product], - ) robj = RenderObject( - data, shader, + inputs, shader, () -> begin glDepthMask(GL_TRUE) glDisable(GL_DEPTH_TEST) @@ -204,7 +217,7 @@ function RenderPass{:OIT}(screen, framebuffer, inputs) # opaque.a = 0 * src.a + 1 * opaque.a glBlendFuncSeparate(GL_ONE, GL_SRC_ALPHA, GL_ZERO, GL_ONE) end, - nothing, shader_cache.context + nothing, screen.glscreen ) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) @@ -220,7 +233,7 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) return end -function RenderPass{:SSAO1}(screen, framebuffer, inputs) +function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -228,8 +241,9 @@ function RenderPass{:SSAO1}(screen, framebuffer, inputs) kernel = map(1:N_samples) do i n = normalize([2.0rand() .- 1.0, 2.0rand() .- 1.0, rand()]) scale = lerp_min + (lerp_max - lerp_min) * (i / N_samples)^2 - v = Vec3f(scale * rand() * n) + return Vec3f(scale * rand() * n) end + noise = [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4] # compute occlusion shader = LazyShader( @@ -240,40 +254,28 @@ function RenderPass{:SSAO1}(screen, framebuffer, inputs) "N_samples" => "$N_samples" ) ) - data = Dict{Symbol, Any}( - :position_buffer => inputs[:position], - :normal_buffer => inputs[:normal], - :kernel => kernel, - :noise => Texture( - shader_cache.context, [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], - minfilter = :nearest, x_repeat = :repeat - ), - :noise_scale => Vec2f(0.25f0 .* size(screen)), - :projection => Observable(Mat4f(I)), - :bias => 0.025f0, - :radius => 0.5f0 - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + inputs[:kernel] = kernel + inputs[:noise] = Texture(screen.glscreen, noise, minfilter = :nearest, x_repeat = :repeat) + inputs[:noise_scale] = Vec2f(0.25f0 .* size(screen)) + inputs[:projection] = Mat4f(I) + inputs[:bias] = 0.025f0 + inputs[:radius] = 0.5f0 + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO1}(framebuffer, robj) end -function RenderPass{:SSAO2}(screen, framebuffer, inputs) +function construct(::Val{:SSAO2}, screen, framebuffer, inputs, parent) # blur occlusion and combine with color shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO_blur.frag") ) - data = Dict{Symbol, Any}( - :occlusion => inputs[:occlusion], - :color_texture => inputs[:color], - :ids => inputs[:objectid], - :inv_texel_size => rcpframe(size(screen)), - :blur_range => Int32(2) - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + inputs[:inv_texel_size] = rcpframe(size(screen)) + inputs[:blur_range] = Int32(2) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO2}(framebuffer, robj) @@ -328,35 +330,28 @@ function run_step(screen, glscene, step::RenderPass{:SSAO2}) end -function RenderPass{:FXAA1}(screen, framebuffer, inputs) +function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/postprocess.frag") ) - data = Dict{Symbol, Any}( - :color_texture => inputs[:color], - :object_ids => inputs[:objectid], - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA1}(framebuffer, robj) end -function RenderPass{:FXAA2}(screen, framebuffer, inputs) +function construct(::Val{:FXAA2}, screen, framebuffer, inputs, parent) # perform FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/fxaa.frag") ) - data = Dict{Symbol, Any}( - :color_texture => inputs[:color_luma], - :RCPFrame => rcpframe(size(framebuffer)), - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, shader_cache.context) + inputs[:RCPFrame] = rcpframe(size(framebuffer)) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA2}(framebuffer, robj) @@ -389,20 +384,19 @@ end # TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer - attachment::GLuint screen_framebuffer_id::Int +end - # Screen not available yet - function BlitToScreen(framebuffer::GLFramebuffer, attachment::GLuint, screen_framebuffer_id::Integer = 0) - @debug "Creating to screen postprocessor" - return new(framebuffer, attachment, screen_framebuffer_id) - end +function construct(::Val{:Display}, screen, ::Nothing, inputs, parent::Makie.Stage) + framebuffer = screen.framebuffer_factory.fb + id = get(parent.attributes, :screen_framebuffer_id, 0) + return BlitToScreen(framebuffer, id) end function run_step(screen, ::Nothing, step::BlitToScreen) # Set source glBindFramebuffer(GL_READ_FRAMEBUFFER, step.framebuffer.id) - glReadBuffer(step.attachment) # for safety + glReadBuffer(get_attachment(step.framebuffer, :color)) # for safety # TODO: Is this an observable? Can this be static? # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 2a16b7cd062..eab6b4e72f6 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -53,6 +53,8 @@ end function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) + pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") + # TODO: check if pipeline is different from the last one before replacing it factory = screen.framebuffer_factory @@ -62,11 +64,18 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Add required buffers reset!(factory, buffers) + # Add back output color and objectid attachments + buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :color)] + attach_colorbuffer(factory.fb, :color, get_buffer(factory, buffer_idx)) + buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :objectid)] + attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) + + render_pipeline = AbstractRenderStep[] for stage in pipeline.stages - inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key + inputs = Dict{Symbol, Any}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] - return key => get_buffer(factory, connection2idx[connection]) + return Symbol(key, :_buffer) => get_buffer(factory, connection2idx[connection]) end) N = length(stage.output_connections) @@ -82,25 +91,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end end - # TODO: hmm... - pass = if stage.name == :ZSort - SortPlots() - elseif stage.name == :Render - # TODO: - RenderPlots(screen, framebuffer, inputs, stage.attributes[:target]::Symbol) - elseif stage.name == :TransparentRender - RenderPlots(screen, framebuffer, inputs, :OIT) - elseif stage.name == :Display - # Need these for colorbuffer() and picking - attach_colorbuffer(factory.fb, :color, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :color)])) - attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :objectid)])) - - BlitToScreen(factory.fb, get_attachment(factory.fb, :color)) - elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] - RenderPass{stage.name}(screen, framebuffer, inputs) - else - error("Unknown stage $(stage.name)") - end + pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) # I guess stage should also have extra information for settings? Or should # that be in scene.theme? @@ -111,4 +102,4 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) return -end +end \ No newline at end of file From d84e986a63aed548994100d4d63f649ca51de313 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 22:27:13 +0100 Subject: [PATCH 069/135] use reconstruct to reuse postprocessor render objects --- GLMakie/assets/shader/postprocessing/SSAO.frag | 4 ++-- .../assets/shader/postprocessing/SSAO_blur.frag | 2 +- GLMakie/src/postprocessing.jl | 4 ++-- GLMakie/src/render_pipeline.jl | 16 +++++++++++++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/SSAO.frag b/GLMakie/assets/shader/postprocessing/SSAO.frag index 37e350907ac..3d932689bf6 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO.frag @@ -22,7 +22,7 @@ out float o_occlusion; void main(void) { vec3 view_pos = texture(position_buffer, frag_uv).xyz; - vec3 _normal = texture(normal_buffer, frag_uv).xyz; + vec3 normal = texture(normal_buffer, frag_uv).xyz; // The normal buffer gets cleared every frame. (also position, color etc) // If normal == vec3(1) then there is no geometry at this fragment. @@ -79,7 +79,7 @@ void main(void) sample_frag_pos.xy += (frag_uv - 0.5) * clip_pos_w / sample_clip_pos_w; - float sample_depth = texture(position, sample_frag_pos.xy).z; + float sample_depth = texture(position_buffer, sample_frag_pos.xy).z; float range_check = smoothstep(0.0, 1.0, radius / abs(view_pos.z - sample_depth)); occlusion += (sample_depth >= sample_view_offset.z + view_pos.z + bias ? 1.0 : 0.0) * range_check; } diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag index 4d88f8303a4..660270fc4c1 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -14,7 +14,7 @@ out vec4 fragment_color; void main(void) { // occlusion blur - uvec2 id0 = texture(objectid, frag_uv).xy; + uvec2 id0 = texture(objectid_buffer, frag_uv).xy; if (id0.x == uint(0)){ fragment_color = texture(color_buffer, frag_uv); // fragment_color = vec4(1,0,1,1); // show discarded diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 9ecb573d9ff..3cee1bdf6f8 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -47,7 +47,7 @@ end function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) where {T <: AbstractRenderStep} # @debug "reconstruct() not defined for $T, calling construct()" destroy!(old) - return construct(Val(parent.name), screen, framebuffer, input, parent) + return construct(Val(parent.name), screen, framebuffer, inputs, parent) end @@ -182,7 +182,7 @@ end function reconstruct(pass::RP, screen, framebuffer, inputs, ::Makie.Stage) where {RP <: RenderPass} for (k, v) in inputs - if haskey(pass.uniforms, k) + if haskey(pass.robj.uniforms, k) pass.robj.uniforms[k] = v else @error("Input $k does not exist in recreated RenderPass.") diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index eab6b4e72f6..abbbcdbb0c7 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -54,6 +54,7 @@ end function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") + previous_pipeline = screen.render_pipeline # TODO: check if pipeline is different from the last one before replacing it factory = screen.framebuffer_factory @@ -91,7 +92,20 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end end - pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) + # can we reconstruct? (reconstruct should update framebuffer, and replace + # inputs if necessary, i.e. handle differences in connections and attributes) + idx = findfirst(previous_pipeline.parent.stages) do old + (old.name == stage.name) && + (old.inputs == stage.inputs) && (old.outputs == stage.outputs) && + (old.input_formats == stage.input_formats) && + (old.output_formats == stage.output_formats) + end + + if idx === nothing + pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) + else + pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) + end # I guess stage should also have extra information for settings? Or should # that be in scene.theme? From cd7b8984ffd552cf2b53c129713240ae9695873d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Dec 2024 23:01:48 +0100 Subject: [PATCH 070/135] reuse GLRenderPipeline & be a bit more careful with caching --- GLMakie/src/render_pipeline.jl | 3 +++ src/utilities/RenderPipeline.jl | 13 +++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index abbbcdbb0c7..5b2e08c22d2 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -56,6 +56,9 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") previous_pipeline = screen.render_pipeline + # Exit early if the pipeline is already up to date + previous_pipeline.parent == pipeline && return + # TODO: check if pipeline is different from the last one before replacing it factory = screen.framebuffer_factory diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 0de8d237432..38d3aad72f5 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -94,17 +94,18 @@ struct Stage name::Symbol # order matters for outputs + # these are "const" after init inputs::Dict{Symbol, Int} outputs::Dict{Symbol, Int} input_formats::Vector{BufferFormat} output_formats::Vector{BufferFormat} - # ^ technically all of these are constants (no push, pop, delete, setindex allowed) - # v these are not + # "const" after setting up connections input_connections::Vector{ConnectionT{Stage}} output_connections::Vector{ConnectionT{Stage}} - attributes::Dict{Symbol, Any} + # const for caching (which does quite a lot actually) + attributes::NamedTuple end const Connection = ConnectionT{Stage} @@ -126,14 +127,13 @@ function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbo ) end function Stage(name, inputs, input_formats, outputs, output_formats; kwargs...) - stage = Stage( + return Stage( Symbol(name), inputs, outputs, input_formats, output_formats, Connection[], Connection[], - Dict{Symbol, Any}(kwargs) + NamedTuple{keys(kwargs)}(values(kwargs)) ) - return stage end get_input_connection(stage::Stage, key::Symbol) = stage.input_connections[stage.inputs[key]] @@ -699,6 +699,7 @@ function DisplayStage() end +# TODO: caching is dangerous with mutable attributes... const PIPELINE_CACHE = Dict{Symbol, Pipeline}() function default_SSAO_pipeline() From 02474d949c7d8047699c9af886609b159903eac9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 03:07:30 +0100 Subject: [PATCH 071/135] temp fix SSAO clearing bug --- .../assets/shader/postprocessing/fxaa.frag | 3 +- GLMakie/src/postprocessing.jl | 10 ++++ GLMakie/src/rendering.jl | 57 +++++++++++-------- GLMakie/src/screen.jl | 11 ++++ src/utilities/RenderPipeline.jl | 12 +++- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/fxaa.frag b/GLMakie/assets/shader/postprocessing/fxaa.frag index 2e44bfa266c..87e9e2b555c 100644 --- a/GLMakie/assets/shader/postprocessing/fxaa.frag +++ b/GLMakie/assets/shader/postprocessing/fxaa.frag @@ -1028,7 +1028,8 @@ out vec4 fragment_color; void main(void) { - // fragment_color = texture(color_texture, frag_uv); + // fragment_color = texture(color_luma_buffer, frag_uv); + // fragment_color.rgb = vec3(texture(color_luma_buffer, frag_uv).a); fragment_color.rgb = FxaaPixelShader( frag_uv, FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 3cee1bdf6f8..896fe68bd5f 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -306,12 +306,22 @@ function run_step(screen, glscene, step::RenderPass{:SSAO1}) GLAbstraction.render(step.robj) end + glDisable(GL_SCISSOR_TEST) + return end function run_step(screen, glscene, step::RenderPass{:SSAO2}) + # TODO: SSAO doesn't copy the full color buffer and writes to a buffer + # previously used for normals. Figure out a better solution than this: + setup!(screen, step.framebuffer) + # SSAO - blur occlusion and apply to color set_draw_buffers(step.framebuffer) # color buffer + wh = size(step.framebuffer) + glViewport(0, 0, wh[1], wh[2]) + + glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) data = step.robj.uniforms for (screenid, scene) in screen.screens diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index 252993aa884..e7c391c240a 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,28 +1,8 @@ -function setup!(screen::Screen, resize_buffers) - isempty(screen.framebuffer_factory.children) && return - - # Make sure this context is active (for multi-window rendering) - nw = to_native(screen) - ShaderAbstractions.switch_context!(nw) - GLAbstraction.require_context(nw) - - # Resize framebuffer to window size - # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and - # no earlier stage uses color or objectid - # Also assumes specific names - fb = screen.framebuffer_factory.children[1] - if resize_buffers && !isnothing(screen.scene) - ppu = screen.px_per_unit[] - resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) - end - +# TODO: needs to run before first draw to color buffers. +# With SSAO that ends up being at first render and in SSAO, as SSAO2 draws +# color to a buffer previously used for normals +function setup!(screen::Screen, fb) GLAbstraction.bind(fb) - glViewport(0, 0, size(fb)...) - - # clear objectid, depth and stencil - glDrawBuffer(get_attachment(fb, :objectid)) - glClearColor(0, 0, 0, 0) - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) # clear color buffer glDrawBuffer(get_attachment(fb, :color)) @@ -56,7 +36,34 @@ end Renders a single frame of a `screen` """ function render_frame(screen::Screen; resize_buffers=true) - setup!(screen, resize_buffers) + isempty(screen.framebuffer_factory.children) && return + + # Make sure this context is active (for multi-window rendering) + nw = to_native(screen) + ShaderAbstractions.switch_context!(nw) + GLAbstraction.require_context(nw) + + # Resize framebuffer to window size + # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and + # no earlier stage uses color or objectid + # Also assumes specific names + fb = screen.framebuffer_factory.children[1] + if resize_buffers && !isnothing(screen.scene) + ppu = screen.px_per_unit[] + resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) + end + + GLAbstraction.bind(fb) + glViewport(0, 0, size(fb)...) + + # clear objectid, depth and stencil + glDrawBuffer(get_attachment(fb, :objectid)) + glClearColor(0, 0, 0, 0) + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) + + # TODO: figure out something better for setup!() + setup!(screen, fb) + render_frame(screen, nothing, screen.render_pipeline) return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index b26808f0f44..b6e8e8fa1ff 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -758,6 +758,17 @@ function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) wher return end +function Makie.colorbuffer(screen::Screen, source::Texture{T, 2}) where T + ShaderAbstractions.switch_context!(screen.glscreen) + # render_frame(screen, resize_buffers=false) # let it render + glFinish() # block until opengl is done rendering + img = Matrix{eltype(source)}(undef, size(source)) + GLAbstraction.bind(source) + GLAbstraction.glGetTexImage(source.texturetype, 0, source.format, source.pixeltype, img) + GLAbstraction.bind(source, 0) + return img +end + """ depthbuffer(screen::Screen) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 38d3aad72f5..30405f562dd 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -594,10 +594,16 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) end function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) + conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) + return show_resolved(io, pipeline, pipeline.connections, conn2idx) +end +function show_resolved(pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) + return show_resolved(stdout, pipeline, buffers, conn2idx) +end +function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) println(io, "Pipeline():") print(io, "Stages:") - conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) - pad = 1 + floor(Int, log10(length(pipeline.connections))) + pad = 1 + floor(Int, log10(length(buffers))) for stage in pipeline.stages print(io, "\n Stage($(stage.name))") @@ -625,7 +631,7 @@ function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) end println(io, "\nConnections:") - for (i, c) in enumerate(pipeline.connections) + for (i, c) in enumerate(buffers) s = lpad("$i", pad) println(io, " [$s] ", c) end From 15bca85270dad8138f29cae1e8168a74bd2cecbd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 03:10:38 +0100 Subject: [PATCH 072/135] add more destroy!() to maybe fix CI --- GLMakie/src/glwindow.jl | 8 ++++++++ GLMakie/src/postprocessing.jl | 6 ++++++ GLMakie/src/render_pipeline.jl | 6 ++++++ GLMakie/src/screen.jl | 4 ++-- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 7994c943726..cad3e9c89d8 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -59,6 +59,14 @@ function unsafe_empty!(factory::FramebufferFactory) return factory end +function destroy!(factory::FramebufferFactory) + GLAbstraction.free.(factory.buffers) + GLAbstraction.free.(factory.children) + GLAbstraction.free(factory.fb) + empty!(factory.buffers) + empty!(factory.children) +end + function Base.push!(factory::FramebufferFactory, tex::Texture) push!(factory.buffers, tex) return factory diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 896fe68bd5f..2ed2a041e3d 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -65,6 +65,12 @@ function render_frame(screen, glscene, pipeline::GLRenderPipeline) return end +function destroy!(pipeline::GLRenderPipeline) + destroy!.(pipeline.steps) + empty!(pipeline.steps) + return +end + # TODO: temporary, we should get to the point where this is not needed struct EmptyRenderStep <: AbstractRenderStep end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 5b2e08c22d2..842c067d52b 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -62,6 +62,11 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # TODO: check if pipeline is different from the last one before replacing it factory = screen.framebuffer_factory + # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline + # with an empty one while we mess with it? + wait(screen) + screen.render_pipeline = GLRenderPipeline() + # Resolve pipeline buffers, connection2idx = Makie.generate_buffers(pipeline) @@ -117,6 +122,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) + screen.requires_update = true return end \ No newline at end of file diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index b6e8e8fa1ff..8a4e34b75d2 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -648,8 +648,8 @@ function destroy!(screen::Screen) empty!(screen) end @assert screen.rendertask === nothing - foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates - destroy!(screen.framebuffer) + destroy!(screen.framebuffer_factory) + destroy!(screen.render_pipeline) cleanup_texture_atlas!(window) GLAbstraction.free(screen.shader_cache) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) From fc84123d0a8888d6151360f0bd6c2913cb5e2d80 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 15:02:55 +0100 Subject: [PATCH 073/135] is it buffer reuse? --- GLMakie/src/render_pipeline.jl | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 842c067d52b..d45171132ea 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -10,7 +10,6 @@ function format_to_type(format::Makie.BufferFormat) end function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) - # empty!(factory.buffer_key2idx) empty!(factory.children) # reuse buffers that match formats (and make sure that factory.buffers @@ -20,13 +19,13 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF for format in formats T = format_to_type(format) found = false - for (i, buffer) in enumerate(buffers) - if T == eltype(buffer) # TODO: && extra parameters match... - found = true - push!(factory.buffers, popat!(buffers, i)) - break - end - end + # for (i, buffer) in enumerate(buffers) + # if T == eltype(buffer) && (get(format.extras, :minfilter, :nearest) == buffer.parameters.minfilter) + # found = true + # push!(factory.buffers, popat!(buffers, i)) + # break + # end + # end if !found if haskey(format.extras, :minfilter) @@ -45,7 +44,14 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF # Always rebuild this though, since we don't know which buffers are the # final output buffers fb = GLFramebuffer(size(factory)) - attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) + depth_buffer = Texture( + Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), size(factory), + minfilter = :nearest, x_repeat = :clamp_to_edge, + internalformat = GL_DEPTH24_STENCIL8, + format = GL_DEPTH_STENCIL + ) + attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) + # attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) factory.fb = fb return factory From 9dc0b27e96ceab26c79f5cdf6d78c53d8223eb97 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 15:37:57 +0100 Subject: [PATCH 074/135] is it reconstruct? --- GLMakie/src/render_pipeline.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index d45171132ea..01b8318cbb1 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -115,11 +115,11 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) (old.output_formats == stage.output_formats) end - if idx === nothing + # if idx === nothing pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) - else - pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) - end + # else + # pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) + # end # I guess stage should also have extra information for settings? Or should # that be in scene.theme? From 8411c13b6daeaf1af0839a04891982a9dedc8567 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 18:31:32 +0100 Subject: [PATCH 075/135] Revert to green CI --- .../shader/postprocessing/OIT_blend.frag | 8 +- .../shader/postprocessing/SSAO_blur.frag | 17 +- .../assets/shader/postprocessing/copy.frag | 4 +- .../assets/shader/postprocessing/fxaa.frag | 31 ++-- .../shader/postprocessing/postprocess.frag | 8 +- GLMakie/src/glwindow.jl | 10 -- GLMakie/src/postprocessing.jl | 154 +++++++++--------- GLMakie/src/render_pipeline.jl | 86 ++++------ GLMakie/src/rendering.jl | 61 ++++--- GLMakie/src/screen.jl | 15 +- src/utilities/RenderPipeline.jl | 25 +-- 11 files changed, 179 insertions(+), 240 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/OIT_blend.frag b/GLMakie/assets/shader/postprocessing/OIT_blend.frag index 85aa152a249..63d4343fb0c 100644 --- a/GLMakie/assets/shader/postprocessing/OIT_blend.frag +++ b/GLMakie/assets/shader/postprocessing/OIT_blend.frag @@ -6,16 +6,16 @@ in vec2 frag_uv; // contains sum_i C_i * weight(depth_i, alpha_i) -uniform sampler2D weighted_color_sum_buffer; +uniform sampler2D sum_color; // contains pod_i (1 - alpha_i) -uniform sampler2D alpha_product_buffer; +uniform sampler2D prod_alpha; out vec4 fragment_color; void main(void) { - vec4 summed_color_weight = texture(weighted_color_sum_buffer, frag_uv); - float transmittance = texture(alpha_product_buffer, frag_uv).r; + vec4 summed_color_weight = texture(sum_color, frag_uv); + float transmittance = texture(prod_alpha, frag_uv).r; vec3 weighted_transparent = summed_color_weight.rgb / max(summed_color_weight.a, 0.00001); vec3 full_weighted_transparent = weighted_transparent * (1 - transmittance); diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag index 660270fc4c1..4691763b4ad 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -1,9 +1,9 @@ {{GLSL_VERSION}} // occlusion.w is the occlusion value -uniform sampler2D occlusion_buffer; -uniform sampler2D color_buffer; -uniform usampler2D objectid_buffer; +uniform sampler2D occlusion; +uniform sampler2D color_texture; +uniform usampler2D ids; uniform vec2 inv_texel_size; // Settings/Attributes uniform int blur_range; @@ -14,9 +14,9 @@ out vec4 fragment_color; void main(void) { // occlusion blur - uvec2 id0 = texture(objectid_buffer, frag_uv).xy; + uvec2 id0 = texture(ids, frag_uv).xy; if (id0.x == uint(0)){ - fragment_color = texture(color_buffer, frag_uv); + fragment_color = texture(color_texture, frag_uv); // fragment_color = vec4(1,0,1,1); // show discarded return; } @@ -24,7 +24,6 @@ void main(void) float blurred_occlusion = 0.0; float weight = 0; - // TODO: Could use :linear filtering and/ro mipmap to simplify this maybe? for (int x = -blur_range; x <= blur_range; ++x){ for (int y = -blur_range; y <= blur_range; ++y){ vec2 offset = vec2(float(x), float(y)) * inv_texel_size; @@ -32,14 +31,14 @@ void main(void) // Without this, a high (low) occlusion from one object can bleed // into the low (high) occlusion of another, giving an unwanted // shine effect. - uvec2 id = texture(objectid_buffer, frag_uv + offset).xy; + uvec2 id = texture(ids, frag_uv + offset).xy; float valid = float(id == id0); - blurred_occlusion += valid * texture(occlusion_buffer, frag_uv + offset).x; + blurred_occlusion += valid * texture(occlusion, frag_uv + offset).x; weight += valid; } } blurred_occlusion = 1.0 - blurred_occlusion / weight; - fragment_color = texture(color_buffer, frag_uv) * blurred_occlusion; + fragment_color = texture(color_texture, frag_uv) * blurred_occlusion; // Display occlusion instead: // fragment_color = vec4(vec3(blurred_occlusion), 1.0); } diff --git a/GLMakie/assets/shader/postprocessing/copy.frag b/GLMakie/assets/shader/postprocessing/copy.frag index e3af207bbe2..9ec63ec90af 100644 --- a/GLMakie/assets/shader/postprocessing/copy.frag +++ b/GLMakie/assets/shader/postprocessing/copy.frag @@ -1,12 +1,12 @@ {{GLSL_VERSION}} -uniform sampler2D color_buffer; +uniform sampler2D color_texture; in vec2 frag_uv; out vec4 fragment_color; void main(void) { - vec4 color = texture(color_buffer, frag_uv); + vec4 color = texture(color_texture, frag_uv); fragment_color.rgb = color.rgb; fragment_color.a = 1.0; } diff --git a/GLMakie/assets/shader/postprocessing/fxaa.frag b/GLMakie/assets/shader/postprocessing/fxaa.frag index 87e9e2b555c..56d4eb678b9 100644 --- a/GLMakie/assets/shader/postprocessing/fxaa.frag +++ b/GLMakie/assets/shader/postprocessing/fxaa.frag @@ -1019,7 +1019,7 @@ FxaaFloat4 FxaaPixelShader( precision highp float; -uniform sampler2D color_luma_buffer; +uniform sampler2D color_texture; uniform vec2 RCPFrame; in vec2 frag_uv; @@ -1028,24 +1028,23 @@ out vec4 fragment_color; void main(void) { - // fragment_color = texture(color_luma_buffer, frag_uv); - // fragment_color.rgb = vec3(texture(color_luma_buffer, frag_uv).a); + // fragment_color = texture(color_texture, frag_uv); fragment_color.rgb = FxaaPixelShader( frag_uv, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, - color_luma_buffer, // FxaaTex tex, - color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegOne, - color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegTwo, - RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, + color_texture, // FxaaTex tex, + color_texture, // FxaaTex fxaaConsole360TexExpBiasNegOne, + color_texture, // FxaaTex fxaaConsole360TexExpBiasNegTwo, + RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, 0.75f, // FxaaFloat fxaaQualitySubpix, - 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, - 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, - 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, - 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, - 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, + 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, + 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, + 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, + 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, + 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f) // FxaaFloat fxaaConsole360ConstDir, ).rgb; } diff --git a/GLMakie/assets/shader/postprocessing/postprocess.frag b/GLMakie/assets/shader/postprocessing/postprocess.frag index 8586f1348f5..ff5e2bde304 100644 --- a/GLMakie/assets/shader/postprocessing/postprocess.frag +++ b/GLMakie/assets/shader/postprocessing/postprocess.frag @@ -2,8 +2,8 @@ in vec2 frag_uv; -uniform sampler2D color_buffer; -uniform usampler2D objectid_buffer; +uniform sampler2D color_texture; +uniform usampler2D object_ids; layout(location=0) out vec4 fragment_color; @@ -21,12 +21,12 @@ bool unpack_bool(uint id) { void main(void) { - vec4 color = texture(color_buffer, frag_uv).rgba; + vec4 color = texture(color_texture, frag_uv).rgba; if(color.a <= 0){ discard; } - uint id = texture(objectid_buffer, frag_uv).x; + uint id = texture(object_ids, frag_uv).x; // do tonemappings //opaque = linear_tone_mapping(color.rgb, 1.8); // linear color output fragment_color.rgb = color.rgb; diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index cad3e9c89d8..70402da069f 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -10,8 +10,6 @@ end Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T(s.id), T(s.index)) Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) - - mutable struct FramebufferFactory fb::GLFramebuffer # core framebuffer (more or less for #4150) # holding depth, stencil, objectid[, output_color] @@ -59,14 +57,6 @@ function unsafe_empty!(factory::FramebufferFactory) return factory end -function destroy!(factory::FramebufferFactory) - GLAbstraction.free.(factory.buffers) - GLAbstraction.free.(factory.children) - GLAbstraction.free(factory.fb) - empty!(factory.buffers) - empty!(factory.children) -end - function Base.push!(factory::FramebufferFactory, tex::Texture) push!(factory.buffers, tex) return factory diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 2ed2a041e3d..328715e71ac 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -34,43 +34,35 @@ Initialization is grouped together and runs before all run steps. If you need to initialize just before your run, bundle it with the run. """ abstract type AbstractRenderStep end +prepare_step(screen, glscene, ::AbstractRenderStep) = nothing run_step(screen, glscene, ::AbstractRenderStep) = nothing function destroy!(step::T) where {T <: AbstractRenderStep} @debug "Default destructor of $T" - hasfield(T, :robj) && destroy!(step.robj) + if hasfield(T, :passes) + while !isempty(step.passes) + destroy!(pop!(step.passes)) + end + elseif hasfield(T, :pass) + destroy!(step.pass) + end return end -# fore reference: -# construct(::Val{Name}, screen, framebuffer, inputs, parent::Makie.Stage) -function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) where {T <: AbstractRenderStep} - # @debug "reconstruct() not defined for $T, calling construct()" - destroy!(old) - return construct(Val(parent.name), screen, framebuffer, inputs, parent) -end - - - -struct GLRenderPipeline - parent::Makie.Pipeline +struct RenderPipeline steps::Vector{AbstractRenderStep} end -GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) -function render_frame(screen, glscene, pipeline::GLRenderPipeline) +function render_frame(screen, glscene, pipeline::RenderPipeline) + for step in pipeline.steps + prepare_step(screen, glscene, step) + end for step in pipeline.steps run_step(screen, glscene, step) end return end -function destroy!(pipeline::GLRenderPipeline) - destroy!.(pipeline.steps) - empty!(pipeline.steps) - return -end - # TODO: temporary, we should get to the point where this is not needed struct EmptyRenderStep <: AbstractRenderStep end @@ -79,8 +71,6 @@ struct EmptyRenderStep <: AbstractRenderStep end struct SortPlots <: AbstractRenderStep end -construct(::Val{:ZSort}, screen, framebuffer, inputs, parent) = SortPlots() - function run_step(screen, glscene, ::SortPlots) function sortby(x) robj = x[3] @@ -114,23 +104,21 @@ struct RenderPlots <: AbstractRenderStep fxaa::FilterOptions end -function construct(::Val{:Render}, screen, framebuffer, inputs, parent) - if parent.attributes[:target] === :SSAO +function RenderPlots(screen, framebuffer, inputs, stage) + if stage === :SSAO return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) - elseif parent.attributes[:target] === :FXAA + elseif stage === :FXAA return RenderPlots(framebuffer, Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) + elseif stage === :OIT + # HDR_color containing sums clears to 0 + # OIT_weight containing products clears to 1 + clear = [1 => Vec4f(0), 3 => Vec4f(1)] + return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) else - error("Incorrect target = $(parent.target) given. Should be :SSAOor :FXAA.") + error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") end end -function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) - # HDR_color containing sums clears to 0 - # OIT_weight containing products clears to 1 - clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) -end - function id2scene(screen, id1) # TODO maybe we should use a different data structure for (id2, scene) in screen.screens @@ -186,18 +174,9 @@ struct RenderPass{Name} <: AbstractRenderStep robj::RenderObject end -function reconstruct(pass::RP, screen, framebuffer, inputs, ::Makie.Stage) where {RP <: RenderPass} - for (k, v) in inputs - if haskey(pass.robj.uniforms, k) - pass.robj.uniforms[k] = v - else - @error("Input $k does not exist in recreated RenderPass.") - end - end - return RP(framebuffer, pass.robj) -end -function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) + +function RenderPass{:OIT}(screen, framebuffer, inputs) @debug "Creating OIT postprocessor" # Based on https://jcgt.org/published/0002/02/09/, see #1390 @@ -208,8 +187,12 @@ function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) loadshader("postprocessing/OIT_blend.frag") ) # TODO: rename in shader + data = Dict{Symbol, Any}( + :sum_color => inputs[:weighted_color_sum], + :prod_alpha => inputs[:alpha_product], + ) robj = RenderObject( - inputs, shader, + data, shader, () -> begin glDepthMask(GL_TRUE) glDisable(GL_DEPTH_TEST) @@ -239,7 +222,7 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) return end -function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) +function RenderPass{:SSAO1}(screen, framebuffer, inputs) # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -247,9 +230,8 @@ function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) kernel = map(1:N_samples) do i n = normalize([2.0rand() .- 1.0, 2.0rand() .- 1.0, rand()]) scale = lerp_min + (lerp_max - lerp_min) * (i / N_samples)^2 - return Vec3f(scale * rand() * n) + v = Vec3f(scale * rand() * n) end - noise = [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4] # compute occlusion shader = LazyShader( @@ -260,28 +242,40 @@ function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) "N_samples" => "$N_samples" ) ) - inputs[:kernel] = kernel - inputs[:noise] = Texture(screen.glscreen, noise, minfilter = :nearest, x_repeat = :repeat) - inputs[:noise_scale] = Vec2f(0.25f0 .* size(screen)) - inputs[:projection] = Mat4f(I) - inputs[:bias] = 0.025f0 - inputs[:radius] = 0.5f0 - robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) + data = Dict{Symbol, Any}( + :position_buffer => inputs[:position], + :normal_buffer => inputs[:normal], + :kernel => kernel, + :noise => Texture( + [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], + minfilter = :nearest, x_repeat = :repeat + ), + :noise_scale => Vec2f(0.25f0 .* size(screen)), + :projection => Observable(Mat4f(I)), + :bias => 0.025f0, + :radius => 0.5f0 + ) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO1}(framebuffer, robj) end -function construct(::Val{:SSAO2}, screen, framebuffer, inputs, parent) +function RenderPass{:SSAO2}(screen, framebuffer, inputs) # blur occlusion and combine with color shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO_blur.frag") ) - inputs[:inv_texel_size] = rcpframe(size(screen)) - inputs[:blur_range] = Int32(2) - robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) + data = Dict{Symbol, Any}( + :occlusion => inputs[:occlusion], + :color_texture => inputs[:color], + :ids => inputs[:objectid], + :inv_texel_size => rcpframe(size(screen)), + :blur_range => Int32(2) + ) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO2}(framebuffer, robj) @@ -312,22 +306,12 @@ function run_step(screen, glscene, step::RenderPass{:SSAO1}) GLAbstraction.render(step.robj) end - glDisable(GL_SCISSOR_TEST) - return end function run_step(screen, glscene, step::RenderPass{:SSAO2}) - # TODO: SSAO doesn't copy the full color buffer and writes to a buffer - # previously used for normals. Figure out a better solution than this: - setup!(screen, step.framebuffer) - # SSAO - blur occlusion and apply to color set_draw_buffers(step.framebuffer) # color buffer - wh = size(step.framebuffer) - glViewport(0, 0, wh[1], wh[2]) - - glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) data = step.robj.uniforms for (screenid, scene) in screen.screens @@ -346,28 +330,35 @@ function run_step(screen, glscene, step::RenderPass{:SSAO2}) end -function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) +function RenderPass{:FXAA1}(screen, framebuffer, inputs) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/postprocess.frag") ) - robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) + data = Dict{Symbol, Any}( + :color_texture => inputs[:color], + :object_ids => inputs[:objectid], + ) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA1}(framebuffer, robj) end -function construct(::Val{:FXAA2}, screen, framebuffer, inputs, parent) +function RenderPass{:FXAA2}(screen, framebuffer, inputs) # perform FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/fxaa.frag") ) - inputs[:RCPFrame] = rcpframe(size(framebuffer)) - robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) + data = Dict{Symbol, Any}( + :color_texture => inputs[:color_luma], + :RCPFrame => rcpframe(size(framebuffer)), + ) + robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA2}(framebuffer, robj) @@ -400,19 +391,20 @@ end # TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer + attachment::GLuint screen_framebuffer_id::Int -end -function construct(::Val{:Display}, screen, ::Nothing, inputs, parent::Makie.Stage) - framebuffer = screen.framebuffer_factory.fb - id = get(parent.attributes, :screen_framebuffer_id, 0) - return BlitToScreen(framebuffer, id) + # Screen not available yet + function BlitToScreen(framebuffer::GLFramebuffer, attachment::GLuint, screen_framebuffer_id::Integer = 0) + @debug "Creating to screen postprocessor" + return new(framebuffer, attachment, screen_framebuffer_id) + end end function run_step(screen, ::Nothing, step::BlitToScreen) # Set source glBindFramebuffer(GL_READ_FRAMEBUFFER, step.framebuffer.id) - glReadBuffer(get_attachment(step.framebuffer, :color)) # for safety + glReadBuffer(step.attachment) # for safety # TODO: Is this an observable? Can this be static? # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 01b8318cbb1..2c80d0a699f 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -10,6 +10,7 @@ function format_to_type(format::Makie.BufferFormat) end function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) + # empty!(factory.buffer_key2idx) empty!(factory.children) # reuse buffers that match formats (and make sure that factory.buffers @@ -19,13 +20,13 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF for format in formats T = format_to_type(format) found = false - # for (i, buffer) in enumerate(buffers) - # if T == eltype(buffer) && (get(format.extras, :minfilter, :nearest) == buffer.parameters.minfilter) - # found = true - # push!(factory.buffers, popat!(buffers, i)) - # break - # end - # end + for (i, buffer) in enumerate(buffers) + if T == eltype(buffer) # TODO: && extra parameters match... + found = true + push!(factory.buffers, popat!(buffers, i)) + break + end + end if !found if haskey(format.extras, :minfilter) @@ -44,14 +45,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF # Always rebuild this though, since we don't know which buffers are the # final output buffers fb = GLFramebuffer(size(factory)) - depth_buffer = Texture( - Ptr{GLAbstraction.DepthStencil_24_8}(C_NULL), size(factory), - minfilter = :nearest, x_repeat = :clamp_to_edge, - internalformat = GL_DEPTH24_STENCIL8, - format = GL_DEPTH_STENCIL - ) - attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) - # attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) + attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) factory.fb = fb return factory @@ -59,19 +53,11 @@ end function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) - pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") - previous_pipeline = screen.render_pipeline - - # Exit early if the pipeline is already up to date - previous_pipeline.parent == pipeline && return - # TODO: check if pipeline is different from the last one before replacing it + render_pipeline = screen.render_pipeline factory = screen.framebuffer_factory - # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline - # with an empty one while we mess with it? - wait(screen) - screen.render_pipeline = GLRenderPipeline() + empty!(render_pipeline) # Resolve pipeline buffers, connection2idx = Makie.generate_buffers(pipeline) @@ -79,18 +65,12 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Add required buffers reset!(factory, buffers) - # Add back output color and objectid attachments - buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :color)] - attach_colorbuffer(factory.fb, :color, get_buffer(factory, buffer_idx)) - buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :objectid)] - attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) - + first_render = true - render_pipeline = AbstractRenderStep[] for stage in pipeline.stages - inputs = Dict{Symbol, Any}(map(collect(keys(stage.inputs))) do key + inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] - return Symbol(key, :_buffer) => get_buffer(factory, connection2idx[connection]) + return key => get_buffer(factory, connection2idx[connection]) end) N = length(stage.output_connections) @@ -106,29 +86,31 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end end - # can we reconstruct? (reconstruct should update framebuffer, and replace - # inputs if necessary, i.e. handle differences in connections and attributes) - idx = findfirst(previous_pipeline.parent.stages) do old - (old.name == stage.name) && - (old.inputs == stage.inputs) && (old.outputs == stage.outputs) && - (old.input_formats == stage.input_formats) && - (old.output_formats == stage.output_formats) + # TODO: hmm... + pass = if stage.name == :ZSort + SortPlots() + elseif stage.name == :Render + # TODO: + RenderPlots(screen, framebuffer, inputs, stage.attributes[:target]::Symbol) + elseif stage.name == :TransparentRender + RenderPlots(screen, framebuffer, inputs, :OIT) + elseif stage.name == :Display + # Need these for colorbuffer() and picking + attach_colorbuffer(factory.fb, :color, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :color)])) + attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :objectid)])) + + BlitToScreen(factory.fb, get_attachment(factory.fb, :color)) + elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] + RenderPass{stage.name}(screen, framebuffer, inputs) + else + error("Unknown stage $(stage.name)") end - # if idx === nothing - pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) - # else - # pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) - # end - # I guess stage should also have extra information for settings? Or should # that be in scene.theme? # Maybe just leave it there for now push!(render_pipeline, pass) end - screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) - screen.requires_update = true - - return -end \ No newline at end of file + return render_pipeline +end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index e7c391c240a..b2f76f2d531 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,8 +1,28 @@ -# TODO: needs to run before first draw to color buffers. -# With SSAO that ends up being at first render and in SSAO, as SSAO2 draws -# color to a buffer previously used for normals -function setup!(screen::Screen, fb) +function setup!(screen::Screen, resize_buffers) + isempty(screen.framebuffer_factory.children) && return + + # Make sure this context is active (for multi-window rendering) + nw = to_native(screen) + ShaderAbstractions.switch_context!(nw) + GLAbstraction.require_context(nw) + + # Resize framebuffer to window size + # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and + # no earlier stage uses color or objectid + # Also assumes specific names + fb = screen.framebuffer_factory.children[1] + if resize_buffers && !isnothing(screen.scene) + ppu = screen.px_per_unit[] + resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) + end + GLAbstraction.bind(fb) + glViewport(0, 0, size(fb)...) + + # clear objectid, depth and stencil + glDrawBuffer(get_attachment(fb, :objectid)) + glClearColor(0, 0, 0, 0) + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) # clear color buffer glDrawBuffer(get_attachment(fb, :color)) @@ -31,39 +51,14 @@ function setup!(screen::Screen, fb) end """ - render_frame(screen[; resize_buffer = true]) - -Renders a single frame of a `screen` +Renders a single frame of a `window` """ function render_frame(screen::Screen; resize_buffers=true) - isempty(screen.framebuffer_factory.children) && return - - # Make sure this context is active (for multi-window rendering) - nw = to_native(screen) - ShaderAbstractions.switch_context!(nw) - GLAbstraction.require_context(nw) + setup!(screen, resize_buffers) - # Resize framebuffer to window size - # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and - # no earlier stage uses color or objectid - # Also assumes specific names - fb = screen.framebuffer_factory.children[1] - if resize_buffers && !isnothing(screen.scene) - ppu = screen.px_per_unit[] - resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) + for step in screen.render_pipeline + run_step(screen, nothing, step) end - GLAbstraction.bind(fb) - glViewport(0, 0, size(fb)...) - - # clear objectid, depth and stencil - glDrawBuffer(get_attachment(fb, :objectid)) - glClearColor(0, 0, 0, 0) - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) - - # TODO: figure out something better for setup!() - setup!(screen, fb) - - render_frame(screen, nothing, screen.render_pipeline) return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 8a4e34b75d2..35fce007e93 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -172,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} - render_pipeline::GLRenderPipeline + render_pipeline::Vector{AbstractRenderStep} cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, Plot} framecache::Matrix{RGB{N0f8}} @@ -208,7 +208,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen, owns_glscreen, shader_cache, framebuffer_factory, config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, - screens, renderlist, GLRenderPipeline(), cache, cache2plot, + screens, renderlist, AbstractRenderStep[], cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) @@ -758,17 +758,6 @@ function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) wher return end -function Makie.colorbuffer(screen::Screen, source::Texture{T, 2}) where T - ShaderAbstractions.switch_context!(screen.glscreen) - # render_frame(screen, resize_buffers=false) # let it render - glFinish() # block until opengl is done rendering - img = Matrix{eltype(source)}(undef, size(source)) - GLAbstraction.bind(source) - GLAbstraction.glGetTexImage(source.texturetype, 0, source.format, source.pixeltype, img) - GLAbstraction.bind(source, 0) - return img -end - """ depthbuffer(screen::Screen) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 30405f562dd..0de8d237432 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -94,18 +94,17 @@ struct Stage name::Symbol # order matters for outputs - # these are "const" after init inputs::Dict{Symbol, Int} outputs::Dict{Symbol, Int} input_formats::Vector{BufferFormat} output_formats::Vector{BufferFormat} + # ^ technically all of these are constants (no push, pop, delete, setindex allowed) - # "const" after setting up connections + # v these are not input_connections::Vector{ConnectionT{Stage}} output_connections::Vector{ConnectionT{Stage}} - # const for caching (which does quite a lot actually) - attributes::NamedTuple + attributes::Dict{Symbol, Any} end const Connection = ConnectionT{Stage} @@ -127,13 +126,14 @@ function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbo ) end function Stage(name, inputs, input_formats, outputs, output_formats; kwargs...) - return Stage( + stage = Stage( Symbol(name), inputs, outputs, input_formats, output_formats, Connection[], Connection[], - NamedTuple{keys(kwargs)}(values(kwargs)) + Dict{Symbol, Any}(kwargs) ) + return stage end get_input_connection(stage::Stage, key::Symbol) = stage.input_connections[stage.inputs[key]] @@ -594,16 +594,10 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) end function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) - conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) - return show_resolved(io, pipeline, pipeline.connections, conn2idx) -end -function show_resolved(pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) - return show_resolved(stdout, pipeline, buffers, conn2idx) -end -function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) println(io, "Pipeline():") print(io, "Stages:") - pad = 1 + floor(Int, log10(length(buffers))) + conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) + pad = 1 + floor(Int, log10(length(pipeline.connections))) for stage in pipeline.stages print(io, "\n Stage($(stage.name))") @@ -631,7 +625,7 @@ function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Conne end println(io, "\nConnections:") - for (i, c) in enumerate(buffers) + for (i, c) in enumerate(pipeline.connections) s = lpad("$i", pad) println(io, " [$s] ", c) end @@ -705,7 +699,6 @@ function DisplayStage() end -# TODO: caching is dangerous with mutable attributes... const PIPELINE_CACHE = Dict{Symbol, Pipeline}() function default_SSAO_pipeline() From 114d0c6a1d266adf3b422ea80d64342288133fd1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 19:24:29 +0100 Subject: [PATCH 076/135] Revert revert + undo debugging --- .../shader/postprocessing/OIT_blend.frag | 8 +- .../shader/postprocessing/SSAO_blur.frag | 17 +- .../assets/shader/postprocessing/copy.frag | 4 +- .../assets/shader/postprocessing/fxaa.frag | 31 ++-- .../shader/postprocessing/postprocess.frag | 8 +- GLMakie/src/glwindow.jl | 10 ++ GLMakie/src/postprocessing.jl | 154 +++++++++--------- GLMakie/src/render_pipeline.jl | 66 +++++--- GLMakie/src/rendering.jl | 61 +++---- GLMakie/src/screen.jl | 15 +- src/utilities/RenderPipeline.jl | 25 ++- 11 files changed, 228 insertions(+), 171 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/OIT_blend.frag b/GLMakie/assets/shader/postprocessing/OIT_blend.frag index 63d4343fb0c..85aa152a249 100644 --- a/GLMakie/assets/shader/postprocessing/OIT_blend.frag +++ b/GLMakie/assets/shader/postprocessing/OIT_blend.frag @@ -6,16 +6,16 @@ in vec2 frag_uv; // contains sum_i C_i * weight(depth_i, alpha_i) -uniform sampler2D sum_color; +uniform sampler2D weighted_color_sum_buffer; // contains pod_i (1 - alpha_i) -uniform sampler2D prod_alpha; +uniform sampler2D alpha_product_buffer; out vec4 fragment_color; void main(void) { - vec4 summed_color_weight = texture(sum_color, frag_uv); - float transmittance = texture(prod_alpha, frag_uv).r; + vec4 summed_color_weight = texture(weighted_color_sum_buffer, frag_uv); + float transmittance = texture(alpha_product_buffer, frag_uv).r; vec3 weighted_transparent = summed_color_weight.rgb / max(summed_color_weight.a, 0.00001); vec3 full_weighted_transparent = weighted_transparent * (1 - transmittance); diff --git a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag index 4691763b4ad..660270fc4c1 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO_blur.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO_blur.frag @@ -1,9 +1,9 @@ {{GLSL_VERSION}} // occlusion.w is the occlusion value -uniform sampler2D occlusion; -uniform sampler2D color_texture; -uniform usampler2D ids; +uniform sampler2D occlusion_buffer; +uniform sampler2D color_buffer; +uniform usampler2D objectid_buffer; uniform vec2 inv_texel_size; // Settings/Attributes uniform int blur_range; @@ -14,9 +14,9 @@ out vec4 fragment_color; void main(void) { // occlusion blur - uvec2 id0 = texture(ids, frag_uv).xy; + uvec2 id0 = texture(objectid_buffer, frag_uv).xy; if (id0.x == uint(0)){ - fragment_color = texture(color_texture, frag_uv); + fragment_color = texture(color_buffer, frag_uv); // fragment_color = vec4(1,0,1,1); // show discarded return; } @@ -24,6 +24,7 @@ void main(void) float blurred_occlusion = 0.0; float weight = 0; + // TODO: Could use :linear filtering and/ro mipmap to simplify this maybe? for (int x = -blur_range; x <= blur_range; ++x){ for (int y = -blur_range; y <= blur_range; ++y){ vec2 offset = vec2(float(x), float(y)) * inv_texel_size; @@ -31,14 +32,14 @@ void main(void) // Without this, a high (low) occlusion from one object can bleed // into the low (high) occlusion of another, giving an unwanted // shine effect. - uvec2 id = texture(ids, frag_uv + offset).xy; + uvec2 id = texture(objectid_buffer, frag_uv + offset).xy; float valid = float(id == id0); - blurred_occlusion += valid * texture(occlusion, frag_uv + offset).x; + blurred_occlusion += valid * texture(occlusion_buffer, frag_uv + offset).x; weight += valid; } } blurred_occlusion = 1.0 - blurred_occlusion / weight; - fragment_color = texture(color_texture, frag_uv) * blurred_occlusion; + fragment_color = texture(color_buffer, frag_uv) * blurred_occlusion; // Display occlusion instead: // fragment_color = vec4(vec3(blurred_occlusion), 1.0); } diff --git a/GLMakie/assets/shader/postprocessing/copy.frag b/GLMakie/assets/shader/postprocessing/copy.frag index 9ec63ec90af..e3af207bbe2 100644 --- a/GLMakie/assets/shader/postprocessing/copy.frag +++ b/GLMakie/assets/shader/postprocessing/copy.frag @@ -1,12 +1,12 @@ {{GLSL_VERSION}} -uniform sampler2D color_texture; +uniform sampler2D color_buffer; in vec2 frag_uv; out vec4 fragment_color; void main(void) { - vec4 color = texture(color_texture, frag_uv); + vec4 color = texture(color_buffer, frag_uv); fragment_color.rgb = color.rgb; fragment_color.a = 1.0; } diff --git a/GLMakie/assets/shader/postprocessing/fxaa.frag b/GLMakie/assets/shader/postprocessing/fxaa.frag index 56d4eb678b9..87e9e2b555c 100644 --- a/GLMakie/assets/shader/postprocessing/fxaa.frag +++ b/GLMakie/assets/shader/postprocessing/fxaa.frag @@ -1019,7 +1019,7 @@ FxaaFloat4 FxaaPixelShader( precision highp float; -uniform sampler2D color_texture; +uniform sampler2D color_luma_buffer; uniform vec2 RCPFrame; in vec2 frag_uv; @@ -1028,23 +1028,24 @@ out vec4 fragment_color; void main(void) { - // fragment_color = texture(color_texture, frag_uv); + // fragment_color = texture(color_luma_buffer, frag_uv); + // fragment_color.rgb = vec3(texture(color_luma_buffer, frag_uv).a); fragment_color.rgb = FxaaPixelShader( frag_uv, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, - color_texture, // FxaaTex tex, - color_texture, // FxaaTex fxaaConsole360TexExpBiasNegOne, - color_texture, // FxaaTex fxaaConsole360TexExpBiasNegTwo, - RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, - FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsolePosPos, + color_luma_buffer, // FxaaTex tex, + color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegOne, + color_luma_buffer, // FxaaTex fxaaConsole360TexExpBiasNegTwo, + RCPFrame, // FxaaFloat2 fxaaQualityRcpFrame, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsoleRcpFrameOpt2, + FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f), // FxaaFloat4 fxaaConsole360RcpFrameOpt2, 0.75f, // FxaaFloat fxaaQualitySubpix, - 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, - 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, - 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, - 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, - 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, + 0.166f, // FxaaFloat fxaaQualityEdgeThreshold, + 0.0833f, // FxaaFloat fxaaQualityEdgeThresholdMin, + 0.0f, // FxaaFloat fxaaConsoleEdgeSharpness, + 0.0f, // FxaaFloat fxaaConsoleEdgeThreshold, + 0.0f, // FxaaFloat fxaaConsoleEdgeThresholdMin, FxaaFloat4(0.0f, 0.0f, 0.0f, 0.0f) // FxaaFloat fxaaConsole360ConstDir, ).rgb; } diff --git a/GLMakie/assets/shader/postprocessing/postprocess.frag b/GLMakie/assets/shader/postprocessing/postprocess.frag index ff5e2bde304..8586f1348f5 100644 --- a/GLMakie/assets/shader/postprocessing/postprocess.frag +++ b/GLMakie/assets/shader/postprocessing/postprocess.frag @@ -2,8 +2,8 @@ in vec2 frag_uv; -uniform sampler2D color_texture; -uniform usampler2D object_ids; +uniform sampler2D color_buffer; +uniform usampler2D objectid_buffer; layout(location=0) out vec4 fragment_color; @@ -21,12 +21,12 @@ bool unpack_bool(uint id) { void main(void) { - vec4 color = texture(color_texture, frag_uv).rgba; + vec4 color = texture(color_buffer, frag_uv).rgba; if(color.a <= 0){ discard; } - uint id = texture(object_ids, frag_uv).x; + uint id = texture(objectid_buffer, frag_uv).x; // do tonemappings //opaque = linear_tone_mapping(color.rgb, 1.8); // linear color output fragment_color.rgb = color.rgb; diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 70402da069f..cad3e9c89d8 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -10,6 +10,8 @@ end Base.convert(::Type{SelectionID{T}}, s::SelectionID) where T = SelectionID{T}(T(s.id), T(s.index)) Base.zero(::Type{GLMakie.SelectionID{T}}) where T = SelectionID{T}(T(0), T(0)) + + mutable struct FramebufferFactory fb::GLFramebuffer # core framebuffer (more or less for #4150) # holding depth, stencil, objectid[, output_color] @@ -57,6 +59,14 @@ function unsafe_empty!(factory::FramebufferFactory) return factory end +function destroy!(factory::FramebufferFactory) + GLAbstraction.free.(factory.buffers) + GLAbstraction.free.(factory.children) + GLAbstraction.free(factory.fb) + empty!(factory.buffers) + empty!(factory.children) +end + function Base.push!(factory::FramebufferFactory, tex::Texture) push!(factory.buffers, tex) return factory diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 328715e71ac..2ed2a041e3d 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -34,35 +34,43 @@ Initialization is grouped together and runs before all run steps. If you need to initialize just before your run, bundle it with the run. """ abstract type AbstractRenderStep end -prepare_step(screen, glscene, ::AbstractRenderStep) = nothing run_step(screen, glscene, ::AbstractRenderStep) = nothing function destroy!(step::T) where {T <: AbstractRenderStep} @debug "Default destructor of $T" - if hasfield(T, :passes) - while !isempty(step.passes) - destroy!(pop!(step.passes)) - end - elseif hasfield(T, :pass) - destroy!(step.pass) - end + hasfield(T, :robj) && destroy!(step.robj) return end -struct RenderPipeline +# fore reference: +# construct(::Val{Name}, screen, framebuffer, inputs, parent::Makie.Stage) +function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) where {T <: AbstractRenderStep} + # @debug "reconstruct() not defined for $T, calling construct()" + destroy!(old) + return construct(Val(parent.name), screen, framebuffer, inputs, parent) +end + + + +struct GLRenderPipeline + parent::Makie.Pipeline steps::Vector{AbstractRenderStep} end +GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) -function render_frame(screen, glscene, pipeline::RenderPipeline) - for step in pipeline.steps - prepare_step(screen, glscene, step) - end +function render_frame(screen, glscene, pipeline::GLRenderPipeline) for step in pipeline.steps run_step(screen, glscene, step) end return end +function destroy!(pipeline::GLRenderPipeline) + destroy!.(pipeline.steps) + empty!(pipeline.steps) + return +end + # TODO: temporary, we should get to the point where this is not needed struct EmptyRenderStep <: AbstractRenderStep end @@ -71,6 +79,8 @@ struct EmptyRenderStep <: AbstractRenderStep end struct SortPlots <: AbstractRenderStep end +construct(::Val{:ZSort}, screen, framebuffer, inputs, parent) = SortPlots() + function run_step(screen, glscene, ::SortPlots) function sortby(x) robj = x[3] @@ -104,21 +114,23 @@ struct RenderPlots <: AbstractRenderStep fxaa::FilterOptions end -function RenderPlots(screen, framebuffer, inputs, stage) - if stage === :SSAO +function construct(::Val{:Render}, screen, framebuffer, inputs, parent) + if parent.attributes[:target] === :SSAO return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) - elseif stage === :FXAA + elseif parent.attributes[:target] === :FXAA return RenderPlots(framebuffer, Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) - elseif stage === :OIT - # HDR_color containing sums clears to 0 - # OIT_weight containing products clears to 1 - clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) else - error("Incorrect stage = $stage given. Should be :SSAO, :FXAA or :OIT.") + error("Incorrect target = $(parent.target) given. Should be :SSAOor :FXAA.") end end +function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) + # HDR_color containing sums clears to 0 + # OIT_weight containing products clears to 1 + clear = [1 => Vec4f(0), 3 => Vec4f(1)] + return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) +end + function id2scene(screen, id1) # TODO maybe we should use a different data structure for (id2, scene) in screen.screens @@ -174,9 +186,18 @@ struct RenderPass{Name} <: AbstractRenderStep robj::RenderObject end +function reconstruct(pass::RP, screen, framebuffer, inputs, ::Makie.Stage) where {RP <: RenderPass} + for (k, v) in inputs + if haskey(pass.robj.uniforms, k) + pass.robj.uniforms[k] = v + else + @error("Input $k does not exist in recreated RenderPass.") + end + end + return RP(framebuffer, pass.robj) +end - -function RenderPass{:OIT}(screen, framebuffer, inputs) +function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) @debug "Creating OIT postprocessor" # Based on https://jcgt.org/published/0002/02/09/, see #1390 @@ -187,12 +208,8 @@ function RenderPass{:OIT}(screen, framebuffer, inputs) loadshader("postprocessing/OIT_blend.frag") ) # TODO: rename in shader - data = Dict{Symbol, Any}( - :sum_color => inputs[:weighted_color_sum], - :prod_alpha => inputs[:alpha_product], - ) robj = RenderObject( - data, shader, + inputs, shader, () -> begin glDepthMask(GL_TRUE) glDisable(GL_DEPTH_TEST) @@ -222,7 +239,7 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) return end -function RenderPass{:SSAO1}(screen, framebuffer, inputs) +function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -230,8 +247,9 @@ function RenderPass{:SSAO1}(screen, framebuffer, inputs) kernel = map(1:N_samples) do i n = normalize([2.0rand() .- 1.0, 2.0rand() .- 1.0, rand()]) scale = lerp_min + (lerp_max - lerp_min) * (i / N_samples)^2 - v = Vec3f(scale * rand() * n) + return Vec3f(scale * rand() * n) end + noise = [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4] # compute occlusion shader = LazyShader( @@ -242,40 +260,28 @@ function RenderPass{:SSAO1}(screen, framebuffer, inputs) "N_samples" => "$N_samples" ) ) - data = Dict{Symbol, Any}( - :position_buffer => inputs[:position], - :normal_buffer => inputs[:normal], - :kernel => kernel, - :noise => Texture( - [normalize(Vec2f(2.0rand(2) .- 1.0)) for _ in 1:4, __ in 1:4], - minfilter = :nearest, x_repeat = :repeat - ), - :noise_scale => Vec2f(0.25f0 .* size(screen)), - :projection => Observable(Mat4f(I)), - :bias => 0.025f0, - :radius => 0.5f0 - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) + inputs[:kernel] = kernel + inputs[:noise] = Texture(screen.glscreen, noise, minfilter = :nearest, x_repeat = :repeat) + inputs[:noise_scale] = Vec2f(0.25f0 .* size(screen)) + inputs[:projection] = Mat4f(I) + inputs[:bias] = 0.025f0 + inputs[:radius] = 0.5f0 + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO1}(framebuffer, robj) end -function RenderPass{:SSAO2}(screen, framebuffer, inputs) +function construct(::Val{:SSAO2}, screen, framebuffer, inputs, parent) # blur occlusion and combine with color shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/SSAO_blur.frag") ) - data = Dict{Symbol, Any}( - :occlusion => inputs[:occlusion], - :color_texture => inputs[:color], - :ids => inputs[:objectid], - :inv_texel_size => rcpframe(size(screen)), - :blur_range => Int32(2) - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) + inputs[:inv_texel_size] = rcpframe(size(screen)) + inputs[:blur_range] = Int32(2) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:SSAO2}(framebuffer, robj) @@ -306,12 +312,22 @@ function run_step(screen, glscene, step::RenderPass{:SSAO1}) GLAbstraction.render(step.robj) end + glDisable(GL_SCISSOR_TEST) + return end function run_step(screen, glscene, step::RenderPass{:SSAO2}) + # TODO: SSAO doesn't copy the full color buffer and writes to a buffer + # previously used for normals. Figure out a better solution than this: + setup!(screen, step.framebuffer) + # SSAO - blur occlusion and apply to color set_draw_buffers(step.framebuffer) # color buffer + wh = size(step.framebuffer) + glViewport(0, 0, wh[1], wh[2]) + + glEnable(GL_SCISSOR_TEST) ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) data = step.robj.uniforms for (screenid, scene) in screen.screens @@ -330,35 +346,28 @@ function run_step(screen, glscene, step::RenderPass{:SSAO2}) end -function RenderPass{:FXAA1}(screen, framebuffer, inputs) +function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/postprocess.frag") ) - data = Dict{Symbol, Any}( - :color_texture => inputs[:color], - :object_ids => inputs[:objectid], - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA1}(framebuffer, robj) end -function RenderPass{:FXAA2}(screen, framebuffer, inputs) +function construct(::Val{:FXAA2}, screen, framebuffer, inputs, parent) # perform FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), loadshader("postprocessing/fxaa.frag") ) - data = Dict{Symbol, Any}( - :color_texture => inputs[:color_luma], - :RCPFrame => rcpframe(size(framebuffer)), - ) - robj = RenderObject(data, shader, PostprocessPrerender(), nothing, screen.glscreen) + inputs[:RCPFrame] = rcpframe(size(framebuffer)) + robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) return RenderPass{:FXAA2}(framebuffer, robj) @@ -391,20 +400,19 @@ end # TODO: Could also handle integration with Gtk, CImGui, etc with a dedicated struct struct BlitToScreen <: AbstractRenderStep framebuffer::GLFramebuffer - attachment::GLuint screen_framebuffer_id::Int +end - # Screen not available yet - function BlitToScreen(framebuffer::GLFramebuffer, attachment::GLuint, screen_framebuffer_id::Integer = 0) - @debug "Creating to screen postprocessor" - return new(framebuffer, attachment, screen_framebuffer_id) - end +function construct(::Val{:Display}, screen, ::Nothing, inputs, parent::Makie.Stage) + framebuffer = screen.framebuffer_factory.fb + id = get(parent.attributes, :screen_framebuffer_id, 0) + return BlitToScreen(framebuffer, id) end function run_step(screen, ::Nothing, step::BlitToScreen) # Set source glBindFramebuffer(GL_READ_FRAMEBUFFER, step.framebuffer.id) - glReadBuffer(step.attachment) # for safety + glReadBuffer(get_attachment(step.framebuffer, :color)) # for safety # TODO: Is this an observable? Can this be static? # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 2c80d0a699f..79e57ed903c 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -10,7 +10,7 @@ function format_to_type(format::Makie.BufferFormat) end function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) - # empty!(factory.buffer_key2idx) + @assert factory.fb.id != 0 "Cannot reset a destroyed FramebufferFactory" empty!(factory.children) # reuse buffers that match formats (and make sure that factory.buffers @@ -21,7 +21,9 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF T = format_to_type(format) found = false for (i, buffer) in enumerate(buffers) - if T == eltype(buffer) # TODO: && extra parameters match... + if (buffer.id != 0) && (T == eltype(buffer)) && + (get(format.extras, :minfilter, :nearest) == buffer.parameters.minfilter) + found = true push!(factory.buffers, popat!(buffers, i)) break @@ -53,11 +55,19 @@ end function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) + pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") + previous_pipeline = screen.render_pipeline + + # Exit early if the pipeline is already up to date + previous_pipeline.parent == pipeline && return + # TODO: check if pipeline is different from the last one before replacing it - render_pipeline = screen.render_pipeline factory = screen.framebuffer_factory - empty!(render_pipeline) + # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline + # with an empty one while we mess with it? + wait(screen) + screen.render_pipeline = GLRenderPipeline() # Resolve pipeline buffers, connection2idx = Makie.generate_buffers(pipeline) @@ -65,12 +75,18 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Add required buffers reset!(factory, buffers) - first_render = true + # Add back output color and objectid attachments + buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :color)] + attach_colorbuffer(factory.fb, :color, get_buffer(factory, buffer_idx)) + buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :objectid)] + attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) + + render_pipeline = AbstractRenderStep[] for stage in pipeline.stages - inputs = Dict{Symbol, Texture}(map(collect(keys(stage.inputs))) do key + inputs = Dict{Symbol, Any}(map(collect(keys(stage.inputs))) do key connection = stage.input_connections[stage.inputs[key]] - return key => get_buffer(factory, connection2idx[connection]) + return Symbol(key, :_buffer) => get_buffer(factory, connection2idx[connection]) end) N = length(stage.output_connections) @@ -86,24 +102,19 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end end - # TODO: hmm... - pass = if stage.name == :ZSort - SortPlots() - elseif stage.name == :Render - # TODO: - RenderPlots(screen, framebuffer, inputs, stage.attributes[:target]::Symbol) - elseif stage.name == :TransparentRender - RenderPlots(screen, framebuffer, inputs, :OIT) - elseif stage.name == :Display - # Need these for colorbuffer() and picking - attach_colorbuffer(factory.fb, :color, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :color)])) - attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, connection2idx[Makie.get_input_connection(stage, :objectid)])) - - BlitToScreen(factory.fb, get_attachment(factory.fb, :color)) - elseif stage.name in [:SSAO1, :SSAO2, :FXAA1, :FXAA2, :OIT] - RenderPass{stage.name}(screen, framebuffer, inputs) + # can we reconstruct? (reconstruct should update framebuffer, and replace + # inputs if necessary, i.e. handle differences in connections and attributes) + idx = findfirst(previous_pipeline.parent.stages) do old + (old.name == stage.name) && + (old.inputs == stage.inputs) && (old.outputs == stage.outputs) && + (old.input_formats == stage.input_formats) && + (old.output_formats == stage.output_formats) + end + + if idx === nothing + pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) else - error("Unknown stage $(stage.name)") + pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) end # I guess stage should also have extra information for settings? Or should @@ -112,5 +123,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) push!(render_pipeline, pass) end - return render_pipeline -end + screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) + screen.requires_update = true + + return +end \ No newline at end of file diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index b2f76f2d531..e7c391c240a 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,28 +1,8 @@ -function setup!(screen::Screen, resize_buffers) - isempty(screen.framebuffer_factory.children) && return - - # Make sure this context is active (for multi-window rendering) - nw = to_native(screen) - ShaderAbstractions.switch_context!(nw) - GLAbstraction.require_context(nw) - - # Resize framebuffer to window size - # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and - # no earlier stage uses color or objectid - # Also assumes specific names - fb = screen.framebuffer_factory.children[1] - if resize_buffers && !isnothing(screen.scene) - ppu = screen.px_per_unit[] - resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) - end - +# TODO: needs to run before first draw to color buffers. +# With SSAO that ends up being at first render and in SSAO, as SSAO2 draws +# color to a buffer previously used for normals +function setup!(screen::Screen, fb) GLAbstraction.bind(fb) - glViewport(0, 0, size(fb)...) - - # clear objectid, depth and stencil - glDrawBuffer(get_attachment(fb, :objectid)) - glClearColor(0, 0, 0, 0) - glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) # clear color buffer glDrawBuffer(get_attachment(fb, :color)) @@ -51,14 +31,39 @@ function setup!(screen::Screen, resize_buffers) end """ -Renders a single frame of a `window` + render_frame(screen[; resize_buffer = true]) + +Renders a single frame of a `screen` """ function render_frame(screen::Screen; resize_buffers=true) - setup!(screen, resize_buffers) + isempty(screen.framebuffer_factory.children) && return + + # Make sure this context is active (for multi-window rendering) + nw = to_native(screen) + ShaderAbstractions.switch_context!(nw) + GLAbstraction.require_context(nw) - for step in screen.render_pipeline - run_step(screen, nothing, step) + # Resize framebuffer to window size + # TODO: Hacky, assumes our first draw is a render (ZSort doesn't draw) and + # no earlier stage uses color or objectid + # Also assumes specific names + fb = screen.framebuffer_factory.children[1] + if resize_buffers && !isnothing(screen.scene) + ppu = screen.px_per_unit[] + resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) end + GLAbstraction.bind(fb) + glViewport(0, 0, size(fb)...) + + # clear objectid, depth and stencil + glDrawBuffer(get_attachment(fb, :objectid)) + glClearColor(0, 0, 0, 0) + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT) + + # TODO: figure out something better for setup!() + setup!(screen, fb) + + render_frame(screen, nothing, screen.render_pipeline) return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 35fce007e93..8a4e34b75d2 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -172,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} renderlist::Vector{Tuple{ZIndex, ScreenID, RenderObject}} - render_pipeline::Vector{AbstractRenderStep} + render_pipeline::GLRenderPipeline cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, Plot} framecache::Matrix{RGB{N0f8}} @@ -208,7 +208,7 @@ mutable struct Screen{GLWindow} <: MakieScreen glscreen, owns_glscreen, shader_cache, framebuffer_factory, config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, - screens, renderlist, AbstractRenderStep[], cache, cache2plot, + screens, renderlist, GLRenderPipeline(), cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) @@ -758,6 +758,17 @@ function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) wher return end +function Makie.colorbuffer(screen::Screen, source::Texture{T, 2}) where T + ShaderAbstractions.switch_context!(screen.glscreen) + # render_frame(screen, resize_buffers=false) # let it render + glFinish() # block until opengl is done rendering + img = Matrix{eltype(source)}(undef, size(source)) + GLAbstraction.bind(source) + GLAbstraction.glGetTexImage(source.texturetype, 0, source.format, source.pixeltype, img) + GLAbstraction.bind(source, 0) + return img +end + """ depthbuffer(screen::Screen) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 0de8d237432..30405f562dd 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -94,17 +94,18 @@ struct Stage name::Symbol # order matters for outputs + # these are "const" after init inputs::Dict{Symbol, Int} outputs::Dict{Symbol, Int} input_formats::Vector{BufferFormat} output_formats::Vector{BufferFormat} - # ^ technically all of these are constants (no push, pop, delete, setindex allowed) - # v these are not + # "const" after setting up connections input_connections::Vector{ConnectionT{Stage}} output_connections::Vector{ConnectionT{Stage}} - attributes::Dict{Symbol, Any} + # const for caching (which does quite a lot actually) + attributes::NamedTuple end const Connection = ConnectionT{Stage} @@ -126,14 +127,13 @@ function Stage(name; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbo ) end function Stage(name, inputs, input_formats, outputs, output_formats; kwargs...) - stage = Stage( + return Stage( Symbol(name), inputs, outputs, input_formats, output_formats, Connection[], Connection[], - Dict{Symbol, Any}(kwargs) + NamedTuple{keys(kwargs)}(values(kwargs)) ) - return stage end get_input_connection(stage::Stage, key::Symbol) = stage.input_connections[stage.inputs[key]] @@ -594,10 +594,16 @@ function Base.show(io::IO, ::MIME"text/plain", connection::Connection) end function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) + conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) + return show_resolved(io, pipeline, pipeline.connections, conn2idx) +end +function show_resolved(pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) + return show_resolved(stdout, pipeline, buffers, conn2idx) +end +function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) println(io, "Pipeline():") print(io, "Stages:") - conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) - pad = 1 + floor(Int, log10(length(pipeline.connections))) + pad = 1 + floor(Int, log10(length(buffers))) for stage in pipeline.stages print(io, "\n Stage($(stage.name))") @@ -625,7 +631,7 @@ function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) end println(io, "\nConnections:") - for (i, c) in enumerate(pipeline.connections) + for (i, c) in enumerate(buffers) s = lpad("$i", pad) println(io, " [$s] ", c) end @@ -699,6 +705,7 @@ function DisplayStage() end +# TODO: caching is dangerous with mutable attributes... const PIPELINE_CACHE = Dict{Symbol, Pipeline}() function default_SSAO_pipeline() From 326713cc20c8b1eae425039e42bc7b7ac2e2b295 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Dec 2024 23:30:07 +0100 Subject: [PATCH 077/135] add some more context switches, idk --- GLMakie/src/postprocessing.jl | 1 + GLMakie/src/render_pipeline.jl | 1 + 2 files changed, 2 insertions(+) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 2ed2a041e3d..7a4e8b6696c 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -60,6 +60,7 @@ GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) function render_frame(screen, glscene, pipeline::GLRenderPipeline) for step in pipeline.steps + ShaderAbstractions.switch_context!(screen.glscreen) run_step(screen, glscene, step) end return diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 79e57ed903c..89d1ed66cc0 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -67,6 +67,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline # with an empty one while we mess with it? wait(screen) + ShaderAbstractions.switch_context!(screen.glscreen) screen.render_pipeline = GLRenderPipeline() # Resolve pipeline From ffb63219d011e9a6dc9cbd261d20342547c83536 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 15:10:56 +0100 Subject: [PATCH 078/135] add more context switches doesn't fix the issue but maybe helps? --- GLMakie/src/postprocessing.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 7a4e8b6696c..b06c72ec295 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -143,6 +143,7 @@ end function run_step(screen, glscene, step::RenderPlots) # Somehow errors in here get ignored silently!? try + ShaderAbstractions.switch_context!(screen.glscreen) GLAbstraction.bind(step.framebuffer) for (idx, color) in step.clear @@ -165,6 +166,7 @@ function run_step(screen, glscene, step::RenderPlots) found, scene = id2scene(screen, screenid) (found && scene.visible[]) || continue + ShaderAbstractions.switch_context!(screen.glscreen) ppu = screen.px_per_unit[] a = viewport(scene)[] glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) @@ -200,6 +202,7 @@ end function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) @debug "Creating OIT postprocessor" + ShaderAbstractions.switch_context!(screen.glscreen) # Based on https://jcgt.org/published/0002/02/09/, see #1390 # OIT setup @@ -241,6 +244,8 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) end function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) + ShaderAbstractions.switch_context!(screen.glscreen) + # SSAO setup N_samples = 64 lerp_min = 0.1f0 @@ -274,6 +279,8 @@ function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) end function construct(::Val{:SSAO2}, screen, framebuffer, inputs, parent) + ShaderAbstractions.switch_context!(screen.glscreen) + # blur occlusion and combine with color shader = LazyShader( screen.shader_cache, @@ -348,6 +355,8 @@ end function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) + ShaderAbstractions.switch_context!(screen.glscreen) + # calculate luma for FXAA shader = LazyShader( screen.shader_cache, @@ -361,6 +370,8 @@ function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) end function construct(::Val{:FXAA2}, screen, framebuffer, inputs, parent) + ShaderAbstractions.switch_context!(screen.glscreen) + # perform FXAA shader = LazyShader( screen.shader_cache, @@ -405,6 +416,7 @@ struct BlitToScreen <: AbstractRenderStep end function construct(::Val{:Display}, screen, ::Nothing, inputs, parent::Makie.Stage) + ShaderAbstractions.switch_context!(screen.glscreen) framebuffer = screen.framebuffer_factory.fb id = get(parent.attributes, :screen_framebuffer_id, 0) return BlitToScreen(framebuffer, id) From 94d0ead6c682c7d15ac0327d1ee9db602bde9adb Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 15:16:23 +0100 Subject: [PATCH 079/135] cleanup enum -> name --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 30 +++++----------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 4f7ea0c2fb9..5d2652d9e08 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -104,29 +104,6 @@ function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) return attach(fb, key, buffer, length(fb.attachments) + 1, GL_DEPTH_STENCIL_ATTACHMENT) end -# for error messages -const ATTACHMENT_LOOKUP = Dict{Int, String}( - GL_DEPTH_ATTACHMENT => "GL_DEPTH_ATTACHMENT", - GL_STENCIL_ATTACHMENT => "GL_STENCIL_ATTACHMENT", - GL_DEPTH_STENCIL_ATTACHMENT => "GL_DEPTH_STENCIL_ATTACHMENT", - GL_COLOR_ATTACHMENT0 => "GL_COLOR_ATTACHMENT0", - GL_COLOR_ATTACHMENT1 => "GL_COLOR_ATTACHMENT1", - GL_COLOR_ATTACHMENT2 => "GL_COLOR_ATTACHMENT2", - GL_COLOR_ATTACHMENT3 => "GL_COLOR_ATTACHMENT3", - GL_COLOR_ATTACHMENT4 => "GL_COLOR_ATTACHMENT4", - GL_COLOR_ATTACHMENT5 => "GL_COLOR_ATTACHMENT5", - GL_COLOR_ATTACHMENT6 => "GL_COLOR_ATTACHMENT6", - GL_COLOR_ATTACHMENT7 => "GL_COLOR_ATTACHMENT7", - GL_COLOR_ATTACHMENT8 => "GL_COLOR_ATTACHMENT8", - GL_COLOR_ATTACHMENT9 => "GL_COLOR_ATTACHMENT9", - GL_COLOR_ATTACHMENT10 => "GL_COLOR_ATTACHMENT10", - GL_COLOR_ATTACHMENT11 => "GL_COLOR_ATTACHMENT11", - GL_COLOR_ATTACHMENT12 => "GL_COLOR_ATTACHMENT12", - GL_COLOR_ATTACHMENT13 => "GL_COLOR_ATTACHMENT13", - GL_COLOR_ATTACHMENT14 => "GL_COLOR_ATTACHMENT14", - GL_COLOR_ATTACHMENT15 => "GL_COLOR_ATTACHMENT15" -) - function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment::GLenum) haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") if attachment in fb.attachments @@ -147,7 +124,12 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment gl_attach(buffer, attachment) check_framebuffer() catch e - @info "$key -> $attachment = $(get(ATTACHMENT_LOOKUP, attachment, "UNKNOWN")) failed, with framebuffer id = $(fb.id)" + if GL_COLOR_ATTACHMENT0 <= attachment <= GL_COLOR_ATTACHMENT15 + # If we failed to attach correctly we should probably overwrite + # the attachment next time we try? + fb.counter -= 1 + end + @info "$key -> $(GLENUM(attachment).name) failed with framebuffer id = $(fb.id)" rethrow(e) end # (1) requires us to keep depth/stenctil/depth_stencil at end so that the first From a26dbe847adca224be4807971c0678c79f74369f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 17:29:11 +0100 Subject: [PATCH 080/135] fix plots not displaying with ssao = true in non SSAO pipeline --- GLMakie/src/postprocessing.jl | 11 ++++------- src/utilities/RenderPipeline.jl | 8 ++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index b06c72ec295..4580b5d0764 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -116,13 +116,10 @@ struct RenderPlots <: AbstractRenderStep end function construct(::Val{:Render}, screen, framebuffer, inputs, parent) - if parent.attributes[:target] === :SSAO - return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], FilterTrue, FilterFalse, FilterAny) - elseif parent.attributes[:target] === :FXAA - return RenderPlots(framebuffer, Pair{Int, Vec4f}[], FilterFalse, FilterFalse, FilterAny) - else - error("Incorrect target = $(parent.target) given. Should be :SSAOor :FXAA.") - end + ssao = FilterOptions(get(parent.attributes, :ssao, 2)) # can't do FilterOptions(::FilterOptions) ??? + fxaa = FilterOptions(get(parent.attributes, :fxaa, 2)) + transparency = FilterOptions(get(parent.attributes, :transparency, 2)) + return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], ssao, transparency, fxaa) end function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 30405f562dd..3f16f47fbd3 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -714,10 +714,10 @@ function default_SSAO_pipeline() pipeline = Pipeline() push!(pipeline, SortStage()) - render1 = push!(pipeline, RenderStage(target = :SSAO)) + render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) ssao = push!(pipeline, SSAOStage()) - render2 = push!(pipeline, RenderStage(target = :FXAA)) - render3 = push!(pipeline, TransparentRenderStage()) + render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) + render3 = push!(pipeline, TransparentRenderStage()) # implied transparency = true oit = push!(pipeline, OITStage()) fxaa = push!(pipeline, FXAAStage()) display = push!(pipeline, DisplayStage()) @@ -743,7 +743,7 @@ function default_pipeline() pipeline = Pipeline() push!(pipeline, SortStage()) - render1 = push!(pipeline, RenderStage(target = :FXAA)) + render1 = push!(pipeline, RenderStage(transparency = false)) render2 = push!(pipeline, TransparentRenderStage()) oit = push!(pipeline, OITStage()) fxaa = push!(pipeline, FXAAStage()) From 807d2ffdc1e953353768dafaac2e478081f32cce Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 18:45:01 +0100 Subject: [PATCH 081/135] add test to verify all render stage parameters render --- GLMakie/test/glmakie_refimages.jl | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index afc81dd8c17..269d21cfc58 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -167,4 +167,39 @@ end mesh!(scene, Rect2f(0, 0, 300, 200), color = :red) scene +end + +@reference_test "render stage parameters with SSAO postprocessor" begin + GLMakie.closeall() + GLMakie.activate!(ssao = true) + f = Figure() + ps = [Point3f(x, y, sin(x * y + y-x)) for x in range(-2, 2, length=21) for y in range(-2, 2, length=21)] + for (i, ssao) in zip(1:2, (false, true)) + for (j, fxaa) in zip(1:2, (false, true)) + for (k, transparency) in zip(1:2, (false, true)) + Label(f[2i-1, k + 2(j-1)], "$ssao, $fxaa, $transparency", tellwidth = false) + a, p = meshscatter( + f[2i, k + 2(j-1)], ps, marker = Rect3f(Point3f(-0.5), Vec3f(1)), + markersize = 1, color = :white; ssao, fxaa, transparency) + end + end + end + f +end + +@reference_test "render stage parameters - without SSAO postprocessor" begin + GLMakie.closeall() + GLMakie.activate!(ssao = false) + f = Figure() + for (i, ssao) in zip(1:2, (false, true)) + for (j, fxaa) in zip(1:2, (false, true)) + for (k, transparency) in zip(1:2, (false, true)) + Label(f[2i-1, k + 2(j-1)], "$ssao, $fxaa, $transparency", tellwidth = false) + a, p = meshscatter( + f[2i, k + 2(j-1)], ps, marker = Rect3f(Point3f(-0.5), Vec3f(1)), + markersize = 1, color = :white; ssao, fxaa, transparency) + end + end + end + f end \ No newline at end of file From 44a7c2f9a70d01fb8595cbd80beda72aaf7b108e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 18:59:40 +0100 Subject: [PATCH 082/135] fix tick test --- GLMakie/src/render_pipeline.jl | 1 - GLMakie/src/screen.jl | 4 ++++ GLMakie/test/runtests.jl | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 89d1ed66cc0..a7714e547a6 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -125,7 +125,6 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) - screen.requires_update = true return end \ No newline at end of file diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 8a4e34b75d2..672de97e7c9 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -365,6 +365,7 @@ end function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::Bool=true) @debug("Applying screen config! to existing screen") + glw = screen.glscreen if screen.owns_glscreen @@ -403,6 +404,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B resize!(screen, size(screen.scene)...) end set_screen_visibility!(screen, config.visible) + + screen.requires_update = true + return screen end diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index bdf540cbbe9..fb20a4d0a47 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -103,15 +103,23 @@ end sleep(0.1) GLMakie.closeall() - # Why does it start with a skipped tick? + # Screen initalizes with `requires_update = false`, so it may skip renders i = 1 while tick_record[i].state == Makie.SkippedRenderTick check_tick(tick_record[1], Makie.SkippedRenderTick, i) i += 1 end + # apply_config!() and scene insertion cause renders + # (number of ticks is probably performance sensitive here so require 1, allow 1..3) check_tick(tick_record[i], Makie.RegularRenderTick, i) i += 1 + for j in 1:2 + if tick_record[i].state == Makie.RegularRenderTick + check_tick(tick_record[i], Makie.RegularRenderTick, i) + i += 1 + end + end while tick_record[i].state == Makie.SkippedRenderTick check_tick(tick_record[i], Makie.SkippedRenderTick, i) From 82325b9d02204e21a677310f2e7d83d8c26304c5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 30 Dec 2024 21:03:46 +0100 Subject: [PATCH 083/135] fix test --- GLMakie/test/glmakie_refimages.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 269d21cfc58..2a5f35550d9 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -187,10 +187,11 @@ end f end -@reference_test "render stage parameters - without SSAO postprocessor" begin +@reference_test "render stage parameters without SSAO postprocessor" begin GLMakie.closeall() GLMakie.activate!(ssao = false) f = Figure() + ps = [Point3f(x, y, sin(x * y + y-x)) for x in range(-2, 2, length=21) for y in range(-2, 2, length=21)] for (i, ssao) in zip(1:2, (false, true)) for (j, fxaa) in zip(1:2, (false, true)) for (k, transparency) in zip(1:2, (false, true)) From 28fb7e52a1b0a352f8c2a474ca0153abd75ba191 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 31 Dec 2024 00:04:39 +0100 Subject: [PATCH 084/135] switch config to include (Makie) render pipeline & cover all settings --- GLMakie/src/GLAbstraction/GLRender.jl | 2 +- GLMakie/src/GLAbstraction/GLRenderObject.jl | 24 ----- GLMakie/src/glshaders/visualize_interface.jl | 10 +- GLMakie/src/postprocessing.jl | 51 ++++++++-- GLMakie/src/screen.jl | 21 ++--- src/display.jl | 9 ++ src/utilities/RenderPipeline.jl | 97 +++++++++++--------- 7 files changed, 120 insertions(+), 94 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index 9131df2053d..a7c99b2be7e 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -175,8 +175,8 @@ end # Generic render functions ##### function enabletransparency() + glDisable(GL_BLEND) glEnablei(GL_BLEND, 0) - glDisablei(GL_BLEND, 1) # This does: # target.rgb = source.a * source.rgb + (1 - source.a) * target.rgb # target.a = 0 * source.a + 1 * target.a diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl index b4c8142dbe3..01245a8838c 100644 --- a/GLMakie/src/GLAbstraction/GLRenderObject.jl +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -33,30 +33,6 @@ function (sp::StandardPrerender)() # Disable cullface for now, until all rendering code is corrected! glDisable(GL_CULL_FACE) # glCullFace(GL_BACK) - - if sp.transparency[] - # disable depth buffer writing - glDepthMask(GL_FALSE) - - # Blending - glEnable(GL_BLEND) - glBlendEquation(GL_FUNC_ADD) - - # buffer 0 contains weight * color.rgba, should do sum - # destination <- 1 * source + 1 * destination - glBlendFunci(0, GL_ONE, GL_ONE) - - # buffer 1 is objectid, do nothing - glDisablei(GL_BLEND, 1) - - # buffer 2 is color.a, should do product - # destination <- 0 * source + (source) * destination - glBlendFunci(2, GL_ZERO, GL_SRC_COLOR) - - else - glDepthMask(GL_TRUE) - enabletransparency() - end end struct StandardPostrender diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 134151db34e..c5f9f58c7e1 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -151,11 +151,12 @@ to_index_buffer(ctx, x) = error( ) function output_buffers(screen::Screen, transparency = false) - if transparency + pipeline = screen.config.render_pipeline + if transparency && any(stage -> stage.name == :OIT, pipeline.stages) """ layout(location=2) out float coverage; """ - elseif screen.config.ssao + elseif any(stage -> stage.name == :SSAO1, pipeline.stages) """ layout(location=2) out vec3 fragment_position; layout(location=3) out vec3 fragment_normal; @@ -166,7 +167,8 @@ function output_buffers(screen::Screen, transparency = false) end function output_buffer_writes(screen::Screen, transparency = false) - if transparency + pipeline = screen.config.render_pipeline + if transparency && any(stage -> stage.name == :OIT, pipeline.stages) scale = screen.config.transparency_weight_scale """ float weight = color.a * max(0.01, $scale * pow((1 - gl_FragCoord.z), 3)); @@ -174,7 +176,7 @@ function output_buffer_writes(screen::Screen, transparency = false) fragment_color.rgb = weight * color.rgb; fragment_color.a = weight; """ - elseif screen.config.ssao + elseif any(stage -> stage.name == :SSAO1, pipeline.stages) """ fragment_color = color; fragment_position = o_view_pos; diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 4580b5d0764..64dd1d1489f 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -50,6 +50,8 @@ function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) w return construct(Val(parent.name), screen, framebuffer, inputs, parent) end +# convenience +Broadcast.broadcastable(x::AbstractRenderStep) = Ref(x) struct GLRenderPipeline @@ -113,20 +115,22 @@ struct RenderPlots <: AbstractRenderStep ssao::FilterOptions transparency::FilterOptions fxaa::FilterOptions + + for_oit::Bool end function construct(::Val{:Render}, screen, framebuffer, inputs, parent) ssao = FilterOptions(get(parent.attributes, :ssao, 2)) # can't do FilterOptions(::FilterOptions) ??? fxaa = FilterOptions(get(parent.attributes, :fxaa, 2)) transparency = FilterOptions(get(parent.attributes, :transparency, 2)) - return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], ssao, transparency, fxaa) + return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], ssao, transparency, fxaa, false) end function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) # HDR_color containing sums clears to 0 # OIT_weight containing products clears to 1 clear = [1 => Vec4f(0), 3 => Vec4f(1)] - return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny) + return RenderPlots(framebuffer, clear, FilterAny, FilterTrue, FilterAny, true) end function id2scene(screen, id1) @@ -137,6 +141,13 @@ function id2scene(screen, id1) return false, nothing end +function should_render(robj::RenderObject, step::RenderPlots) + return robj.visible && + compare(robj[:ssao][], step.ssao) && + compare(robj[:transparency][], step.transparency) && + compare(robj[:fxaa][], step.fxaa) +end + function run_step(screen, glscene, step::RenderPlots) # Somehow errors in here get ignored silently!? try @@ -144,7 +155,6 @@ function run_step(screen, glscene, step::RenderPlots) GLAbstraction.bind(step.framebuffer) for (idx, color) in step.clear - # TODO: Hacky idx <= step.framebuffer.counter || continue glDrawBuffer(step.framebuffer.attachments[idx]) glClearColor(color...) @@ -154,19 +164,42 @@ function run_step(screen, glscene, step::RenderPlots) set_draw_buffers(step.framebuffer) for (zindex, screenid, elem) in screen.renderlist - should_render = elem.visible && - compare(elem[:ssao][], step.ssao) && - compare(elem[:transparency][], step.transparency) && - compare(elem[:fxaa][], step.fxaa) - should_render || continue + should_render(elem, step) || continue found, scene = id2scene(screen, screenid) (found && scene.visible[]) || continue - ShaderAbstractions.switch_context!(screen.glscreen) ppu = screen.px_per_unit[] a = viewport(scene)[] + + ShaderAbstractions.switch_context!(screen.glscreen) glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) + + # TODO: Can we move this outside the loop? + if step.for_oit + # disable depth buffer writing + glDepthMask(GL_FALSE) + + # Blending + glEnable(GL_BLEND) + glBlendEquation(GL_FUNC_ADD) + + # buffer 0 contains weight * color.rgba, should do sum + # destination <- 1 * source + 1 * destination + glBlendFunci(0, GL_ONE, GL_ONE) + + # buffer 1 is objectid, do nothing + glDisablei(GL_BLEND, 1) + + # buffer 2 is color.a, should do product + # destination <- 0 * source + (source) * destination + glBlendFunci(2, GL_ZERO, GL_SRC_COLOR) + + else + glDepthMask(GL_TRUE) + GLAbstraction.enabletransparency() + end + render(elem) end catch e diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 672de97e7c9..0bdb4ccb4f3 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -60,9 +60,7 @@ mutable struct ScreenConfig scalefactor::Union{Nothing, Float32} # Render Constants & Postprocessor - oit::Bool - fxaa::Bool - ssao::Bool + render_pipeline::Makie.Pipeline transparency_weight_scale::Float32 max_lights::Int max_light_parameters::Int @@ -87,9 +85,7 @@ mutable struct ScreenConfig scalefactor::Union{Makie.Automatic, Number}, # Preprocessor - oit::Bool, - fxaa::Bool, - ssao::Bool, + render_pipeline::Makie.Pipeline, transparency_weight_scale::Number, max_lights::Int, max_light_parameters::Int) @@ -113,9 +109,10 @@ mutable struct ScreenConfig scalefactor isa Makie.Automatic ? nothing : Float32(scalefactor), # Preproccessor # Preprocessor - oit, - fxaa, - ssao, + # oit, + # fxaa, + # ssao, + render_pipeline, transparency_weight_scale, max_lights, max_light_parameters) @@ -385,11 +382,7 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] # TODO: FXAA, OIT on-off - if config.ssao - gl_render_pipeline!(screen, Makie.default_SSAO_pipeline()) - else - gl_render_pipeline!(screen, Makie.default_pipeline()) - end + gl_render_pipeline!(screen, config.render_pipeline) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters diff --git a/src/display.jl b/src/display.jl index 336c6028922..9fa389d4ffb 100644 --- a/src/display.jl +++ b/src/display.jl @@ -70,6 +70,15 @@ function merge_screen_config(::Type{Config}, config::Dict) where Config backend = parentmodule(Config) key = nameof(backend) backend_defaults = CURRENT_DEFAULT_THEME[key] + # To not deprecate ssao, fxaa, oit: + if key == :GLMakie + get!(config, :render_pipeline) do + ssao = to_value(get(config, :ssao, backend_defaults[:ssao])) + fxaa = to_value(get(config, :fxaa, backend_defaults[:fxaa])) + oit = to_value(get(config, :oit, backend_defaults[:oit])) + return default_pipeline(; ssao, fxaa, oit) + end + end arguments = map(fieldnames(Config)) do name if haskey(config, name) return config[name] diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 3f16f47fbd3..e6938a95361 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -514,6 +514,14 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) end end + if !isempty(stage.attributes) + print(io, "\nattributes:") + pad = mapreduce(k -> length(":$k"), max, keys(stage.attributes)) + for (k, v) in pairs(stage.attributes) + print(io, "\n ", lpad(":$k", pad), " = ", v) + end + end + return end @@ -682,14 +690,15 @@ function OITStage(; kwargs...) return Stage(:OIT, inputs, input_formats, outputs, output_formats; kwargs...) end -function FXAAStage() - inputs = Dict(:color => 1, :objectid => 2) - input_formats = [BufferFormat(4, N0f8), BufferFormat(2, UInt32)] - stage1 = Stage(:FXAA1, inputs, input_formats, Dict(:color_luma => 1), [BufferFormat(4, N0f8)]) +function FXAAStage(; kwargs...) + stage1 = Stage(:FXAA1, + Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], + Dict(:color_luma => 1), [BufferFormat(4, N0f8)]; kwargs... + ) stage2 = Stage(:FXAA2, Dict(:color_luma => 1), [BufferFormat(4, N0f8, minfilter = :linear)], - Dict(:color => 1), [BufferFormat(4, N0f8)] + Dict(:color => 1), [BufferFormat(4, N0f8)]; kwargs... ) pipeline = Pipeline(stage1, stage2) @@ -708,53 +717,57 @@ end # TODO: caching is dangerous with mutable attributes... const PIPELINE_CACHE = Dict{Symbol, Pipeline}() -function default_SSAO_pipeline() - return get!(PIPELINE_CACHE, :default_SSAO_pipeline) do - # matching master with SSAO enabled - pipeline = Pipeline() +function default_pipeline(; ssao = false, fxaa = true, oit = true) + name = Symbol(:default_pipeline, Int(ssao), Int(fxaa), Int(oit)) + # Mimic GLMakie's old hard coded render pipeline + get!(PIPELINE_CACHE, name) do + + pipeline = Pipeline() push!(pipeline, SortStage()) + + # Note - order imporant! + # TODO: maybe add insert!()? render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) - ssao = push!(pipeline, SSAOStage()) + if ssao + _ssao = push!(pipeline, SSAOStage()) + end render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) - render3 = push!(pipeline, TransparentRenderStage()) # implied transparency = true - oit = push!(pipeline, OITStage()) - fxaa = push!(pipeline, FXAAStage()) + if oit + render3 = push!(pipeline, TransparentRenderStage()) + _oit = push!(pipeline, OITStage()) + else + render3 = push!(pipeline, RenderStage(transparency = true)) + end + if fxaa + _fxaa = push!(pipeline, FXAAStage(filter_in_shader = true)) + end display = push!(pipeline, DisplayStage()) - connect!(pipeline, render1, ssao) - connect!(pipeline, render1, fxaa, :objectid) - connect!(pipeline, render1, display, :objectid) - connect!(pipeline, ssao, fxaa, :color) - connect!(pipeline, render2, fxaa, :color) - connect!(pipeline, render2, display, :objectid) # will get bundled so we don't need to repeat - connect!(pipeline, render3, oit) - connect!(pipeline, render3, display, :objectid) - connect!(pipeline, oit, fxaa, :color) - connect!(pipeline, fxaa, display, :color) - - return pipeline - end -end - -function default_pipeline() - return get!(PIPELINE_CACHE, :default_pipeline) do - # matching master - pipeline = Pipeline() - push!(pipeline, SortStage()) - render1 = push!(pipeline, RenderStage(transparency = false)) - render2 = push!(pipeline, TransparentRenderStage()) - oit = push!(pipeline, OITStage()) - fxaa = push!(pipeline, FXAAStage()) - display = push!(pipeline, DisplayStage()) + if ssao + connect!(pipeline, render1, _ssao) + connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) + else + connect!(pipeline, render1, fxaa ? _fxaa : display, :color) + end + connect!(pipeline, render2, fxaa ? _fxaa : display, :color) + if oit + connect!(pipeline, render3, _oit) + connect!(pipeline, _oit, fxaa ? _fxaa : display, :color) + else + connect!(pipeline, render3, fxaa ? _fxaa : display, :color) + end + if fxaa + # will also connect render2, 3 since connections bundle + connect!(pipeline, render1, _fxaa, :objectid) + connect!(pipeline, _fxaa, display, :color) + end - connect!(pipeline, render1, fxaa) + # generic connect!(pipeline, render1, display, :objectid) - connect!(pipeline, render2, oit) connect!(pipeline, render2, display, :objectid) - connect!(pipeline, oit, fxaa, :color) - connect!(pipeline, fxaa, display, :color) + connect!(pipeline, render3, display, :objectid) return pipeline end From 0f7f9f5e45f60a7f74e1db163aecd0978e0aab6a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 31 Dec 2024 01:12:39 +0100 Subject: [PATCH 085/135] fix docs --- src/display.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/display.jl b/src/display.jl index 9fa389d4ffb..1bdb9b36dcd 100644 --- a/src/display.jl +++ b/src/display.jl @@ -66,18 +66,21 @@ function set_screen_config!(backend::Module, new_values) return backend_defaults end -function merge_screen_config(::Type{Config}, config::Dict) where Config +function merge_screen_config(::Type{Config}, _config::Dict) where Config backend = parentmodule(Config) key = nameof(backend) backend_defaults = CURRENT_DEFAULT_THEME[key] # To not deprecate ssao, fxaa, oit: if key == :GLMakie + config = Dict{Symbol, Any}(_config) get!(config, :render_pipeline) do ssao = to_value(get(config, :ssao, backend_defaults[:ssao])) fxaa = to_value(get(config, :fxaa, backend_defaults[:fxaa])) oit = to_value(get(config, :oit, backend_defaults[:oit])) return default_pipeline(; ssao, fxaa, oit) end + else + config = _config end arguments = map(fieldnames(Config)) do name if haskey(config, name) From 620e3a1a8e5b9b83a2d62007f72ac8f8dfa39e04 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 31 Dec 2024 16:00:15 +0100 Subject: [PATCH 086/135] test a few other pipelines --- .../shader/postprocessing/postprocess.frag | 7 ++ GLMakie/src/postprocessing.jl | 7 +- src/utilities/RenderPipeline.jl | 70 ++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/postprocess.frag b/GLMakie/assets/shader/postprocessing/postprocess.frag index 8586f1348f5..3dc9b0c9ca0 100644 --- a/GLMakie/assets/shader/postprocessing/postprocess.frag +++ b/GLMakie/assets/shader/postprocessing/postprocess.frag @@ -1,5 +1,7 @@ {{GLSL_VERSION}} +{{FILTER_IN_SHADER}} + in vec2 frag_uv; uniform sampler2D color_buffer; @@ -30,6 +32,8 @@ void main(void) // do tonemappings //opaque = linear_tone_mapping(color.rgb, 1.8); // linear color output fragment_color.rgb = color.rgb; + +#ifdef FILTER_IN_SHADER // we store fxaa = true/false in highbit of the object id if (unpack_bool(id)) { fragment_color.a = dot(color.rgb, vec3(0.299, 0.587, 0.114)); // compute luma @@ -37,4 +41,7 @@ void main(void) // we disable fxaa by setting luma to 1 fragment_color.a = 1.0; } +#else + fragment_color.a = dot(color.rgb, vec3(0.299, 0.587, 0.114)); // compute luma +#endif } diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 64dd1d1489f..b78798eada0 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -385,13 +385,16 @@ end function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) - ShaderAbstractions.switch_context!(screen.glscreen) + filter_fxaa_in_shader = get(parent.attributes, :filter_in_shader, true) + + ShaderAbstractions.switch_context!(screen.glscreen) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, loadshader("postprocessing/fullscreen.vert"), - loadshader("postprocessing/postprocess.frag") + loadshader("postprocessing/postprocess.frag"), + view = Dict("FILTER_IN_SHADER" => filter_fxaa_in_shader ? "#define FILTER_IN_SHADER" : "") ) robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index e6938a95361..937c2b10884 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -726,7 +726,7 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) pipeline = Pipeline() push!(pipeline, SortStage()) - # Note - order imporant! + # Note - order important! # TODO: maybe add insert!()? render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) if ssao @@ -772,3 +772,71 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) return pipeline end end + +function test_pipeline_3D() + pipeline = Pipeline() + + render1 = push!(pipeline, RenderStage(ssao = true, transparency = false, fxaa = true)) + ssao = push!(pipeline, SSAOStage()) + render2 = push!(pipeline, RenderStage(ssao = false, transparency = false, fxaa = true)) + render3 = push!(pipeline, TransparentRenderStage()) + oit = push!(pipeline, OITStage()) + fxaa = push!(pipeline, FXAAStage(filter_in_shader = false)) + # dedicated fxaa = false render improves sdf (lines, scatter, text) intersections with FXAA + render4 = push!(pipeline, RenderStage(transparency = false, fxaa = false)) + display = push!(pipeline, DisplayStage()) + + connect!(pipeline, render1, ssao) + connect!(pipeline, render1, fxaa, :objectid) + connect!(pipeline, render1, display, :objectid) + connect!(pipeline, ssao, fxaa, :color) + connect!(pipeline, render2, fxaa, :color) + connect!(pipeline, render2, display, :objectid) + connect!(pipeline, render3, oit) + connect!(pipeline, render3, display, :objectid) + connect!(pipeline, oit, fxaa, :color) + connect!(pipeline, fxaa, display, :color) + connect!(pipeline, render4, display) + + return pipeline +end + +function test_pipeline_2D() + pipeline = Pipeline() + + # dedicated fxaa = false pass + no SSAO + render1 = push!(pipeline, RenderStage(transparency = false, fxaa = true)) + render2 = push!(pipeline, TransparentRenderStage()) + oit = push!(pipeline, OITStage()) + fxaa = push!(pipeline, FXAAStage(filter_in_shader = false)) + render3 = push!(pipeline, RenderStage(transparency = false, fxaa = false)) + display = push!(pipeline, DisplayStage()) + + connect!(pipeline, render1, fxaa) + connect!(pipeline, render1, display, :objectid) + connect!(pipeline, render2, oit) + connect!(pipeline, render2, display, :objectid) + connect!(pipeline, oit, fxaa, :color) + connect!(pipeline, fxaa, display, :color) + connect!(pipeline, render3, display) + + return pipeline +end + +function test_pipeline_GUI() + pipeline = Pipeline() + + # GUI elements don't need OIT because they are (usually?) layered plot by + # plot, rather than element by element. Do need FXAA occasionally, e.g. Toggle + render1 = push!(pipeline, RenderStage(fxaa = true)) + fxaa = push!(pipeline, FXAAStage(filter_in_shader = false)) + render2 = push!(pipeline, RenderStage(fxaa = false)) + display = push!(pipeline, DisplayStage()) + + connect!(pipeline, render1, display) + connect!(pipeline, render1, fxaa) + connect!(pipeline, fxaa, display) + connect!(pipeline, render2, display) + + return pipeline +end From 658d4db9af9436052acd6673600a78d43017ac8b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 31 Dec 2024 19:41:51 +0100 Subject: [PATCH 087/135] free more things --- GLMakie/src/GLAbstraction/GLRender.jl | 1 + GLMakie/src/gl_backend.jl | 9 +++++++++ GLMakie/src/glwindow.jl | 2 ++ GLMakie/src/render_pipeline.jl | 4 ++++ src/utilities/RenderPipeline.jl | 12 ++++++++++++ 5 files changed, 28 insertions(+) diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index a7c99b2be7e..c04ed4874e9 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -74,6 +74,7 @@ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray renderobject.prerenderfunction() setup_clip_planes(to_value(get(renderobject.uniforms, :num_clip_planes, 0))) program = vertexarray.program + program.id == 0 && error("Program unloaded") glUseProgram(program.id) for (key, value) in program.uniformloc if haskey(renderobject.uniforms, key) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 1217350ebb1..ed25c64b9c1 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -45,6 +45,15 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer return false end end +end + +function get_texture!(atlas::Makie.TextureAtlas) + current_ctx = GLAbstraction.current_context() + if !GLAbstraction.context_alive(current_ctx) + return nothing + end + + cleanup_texture_atlas(current_ctx, atlas) if haskey(atlas_texture_cache, (atlas, context)) return atlas_texture_cache[(atlas, context)][1] diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index cad3e9c89d8..7af6ce40f7f 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -62,6 +62,8 @@ end function destroy!(factory::FramebufferFactory) GLAbstraction.free.(factory.buffers) GLAbstraction.free.(factory.children) + # make sure depth, stencil get cleared too (and maybe core color buffers in the future) + GLAbstraction.free.(factory.fb.buffers) GLAbstraction.free(factory.fb) empty!(factory.buffers) empty!(factory.children) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index a7714e547a6..5a103450d6b 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -11,6 +11,7 @@ end function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) @assert factory.fb.id != 0 "Cannot reset a destroyed FramebufferFactory" + GLAbstraction.free.(factory.children) empty!(factory.children) # reuse buffers that match formats (and make sure that factory.buffers @@ -44,6 +45,9 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF end end + # clean up leftovers? + GLAbstraction.free.(buffers) + # Always rebuild this though, since we don't know which buffers are the # final output buffers fb = GLFramebuffer(size(factory)) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 937c2b10884..b8ab8597f41 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -840,3 +840,15 @@ function test_pipeline_GUI() return pipeline end + +function test_pipeline_minimal() + pipeline = Pipeline() + + # GUI elements don't need OIT because they are (usually?) layered plot by + # plot, rather than element by element. Do need FXAA occasionally, e.g. Toggle + render = push!(pipeline, RenderStage()) + display = push!(pipeline, DisplayStage()) + connect!(pipeline, render, display) + + return pipeline +end From 8bd6f67fd17336a0462d1ebdfd2457be18a2ece3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 1 Jan 2025 14:25:53 +0100 Subject: [PATCH 088/135] some performance tweaks --- GLMakie/src/render_pipeline.jl | 64 ++++++++++++++++--------------- src/precompiles.jl | 1 + src/utilities/RenderPipeline.jl | 67 ++++++++++----------------------- 3 files changed, 55 insertions(+), 77 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 5a103450d6b..59c88f3d3de 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -8,45 +8,46 @@ end function format_to_type(format::Makie.BufferFormat) return format.dims == 1 ? format.type : Vec{format.dims, format.type} end +function format_to_gl_format(format::Makie.BufferFormat) + return format.dims == 1 ? format.type : Vec{format.dims, format.type} +end function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) @assert factory.fb.id != 0 "Cannot reset a destroyed FramebufferFactory" GLAbstraction.free.(factory.children) empty!(factory.children) + # function barrier for types? + function get_buffer!(buffers, T, extras) + # reuse + internalformat = GLAbstraction.default_internalcolorformat(T) + for (i, buffer) in enumerate(buffers) + if (buffer.id != 0) && (internalformat == buffer.internalformat) && + (get(extras, :minfilter, :nearest) == buffer.parameters.minfilter) + return popat!(buffers, i) + end + end + + # create new + interp = get(extras, :minfilter, :nearest) + if !(eltype(T) == N0f8 || eltype(T) <: AbstractFloat) && (interp == :linear) + error("Cannot use :linear interpolation with non float types.") + end + return Texture(T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) + end + # reuse buffers that match formats (and make sure that factory.buffers # follows the order of formats) buffers = copy(factory.buffers) empty!(factory.buffers) for format in formats T = format_to_type(format) - found = false - for (i, buffer) in enumerate(buffers) - if (buffer.id != 0) && (T == eltype(buffer)) && - (get(format.extras, :minfilter, :nearest) == buffer.parameters.minfilter) - - found = true - push!(factory.buffers, popat!(buffers, i)) - break - end - end - - if !found - if haskey(format.extras, :minfilter) - interp = format.extras[:minfilter] - if !(eltype(T) == N0f8 || eltype(T) <: AbstractFloat) && (interp == :linear) - error("Cannot use :linear interpolation with non float types.") - end - else - interp = :nearest - end - tex = Texture(T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) - push!(factory.buffers, tex) - end + tex = get_buffer!(buffers, T, format.extras) + push!(factory.buffers, tex) end - # clean up leftovers? - GLAbstraction.free.(buffers) + # clean up leftovers + foreach(GLAbstraction.free, buffers) # Always rebuild this though, since we don't know which buffers are the # final output buffers @@ -70,7 +71,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline # with an empty one while we mess with it? - wait(screen) + was_running = renderloop_running(screen) + was_running && stop_renderloop!(screen, true) ShaderAbstractions.switch_context!(screen.glscreen) screen.render_pipeline = GLRenderPipeline() @@ -89,10 +91,11 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) render_pipeline = AbstractRenderStep[] for stage in pipeline.stages - inputs = Dict{Symbol, Any}(map(collect(keys(stage.inputs))) do key + inputs = Dict{Symbol, Any}() + for key in keys(stage.inputs) connection = stage.input_connections[stage.inputs[key]] - return Symbol(key, :_buffer) => get_buffer(factory, connection2idx[connection]) - end) + inputs[Symbol(key, :_buffer)] = get_buffer(factory, connection2idx[connection]) + end N = length(stage.output_connections) if N == 0 @@ -101,7 +104,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) idx2name = Dict([idx => k for (k, idx) in stage.outputs]) outputs = [connection2idx[stage.output_connections[i]] => idx2name[i] for i in 1:N] try - framebuffer = generate_framebuffer(factory,outputs...) + framebuffer = generate_framebuffer(factory, outputs...) catch e rethrow(e) end @@ -129,6 +132,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) + was_running && start_renderloop!(screen) return end \ No newline at end of file diff --git a/src/precompiles.jl b/src/precompiles.jl index c76599dafe5..b716e0f2859 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -37,6 +37,7 @@ let empty!(DEFAULT_FONT) empty!(ALTERNATIVE_FONTS) Makie.CURRENT_FIGURE[] = nothing + generate_buffers(default_pipeline()) end nothing end diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index b8ab8597f41..c88c9d57799 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -238,19 +238,19 @@ function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage}, output::Symbol, trg::Union{Pipeline, Stage}, input::Symbol ) - function sources(pipeline::Pipeline, output) - return [(stage, stage.outputs[output]) for stage in pipeline.stages if haskey(stage.outputs, output)] - end - sources(stage::Stage, output) = [(stage, stage.outputs[output])] - function targets(pipeline::Pipeline, input) - return [(stage, stage.inputs[input]) for stage in pipeline.stages if haskey(stage.inputs, input)] - end - targets(stage::Stage, input) = [(stage, stage.inputs[input])] + iterable(pipeline::Pipeline) = pipeline.stages + iterable(stage::Stage) = Ref(stage) - for (source, output_idx) in sources(src, output) - for (target, input_idx) in targets(trg, input) - @inbounds connect!(pipeline, source, output_idx, target, input_idx) + for source in iterable(src) + if haskey(source.outputs, output) + output_idx = source.outputs[output] + for target in iterable(trg) + if haskey(target.inputs, input) + input_idx = target.inputs[input] + connect!(pipeline, source, output_idx, target, input_idx) + end + end end end @@ -341,33 +341,10 @@ function Observables.connect!(pipeline::Pipeline, return connection end -# This checks that no connection goes backwards, i.e. every connection is written -# to before being read. -function verify(pipeline::Pipeline) - for connection in pipeline.connections - earliest_input = length(pipeline.stages) - for (stage, _) in connection.inputs - idx = findfirst(==(stage), pipeline.stages) - isnothing(idx) && error("Could not find $(stage.name) in pipeline.") - earliest_input = min(earliest_input, idx) - end - - earliest_output = length(pipeline.stages) - for (stage, _) in connection.outputs - idx = findfirst(==(stage), pipeline.stages) - isnothing(idx) && error("Could not find $(stage.name) in pipeline.") - earliest_output = min(earliest_output, idx) - end - - if earliest_input >= earliest_output - inputs = join(("$(stage.name).$idx" for (stage, idx) in connection.inputs), ", ") - outputs = join(("$(stage.name).$idx" for (stage, idx) in connection.outputs), ", ") - error("Connection ($inputs) -> ($outputs) is read before being written to. Not allowed. ($earliest_input ≥ $earliest_output)") - end - end - - return true -end +format_complexity(c::Connection) = format_complexity(c.format) +format_complexity(f::BufferFormat) = format_complexity(f.dims, sizeof(f.type)) +format_complexity(f1::BufferFormat, f2::BufferFormat) = max(f1.dims, f2.dims) * max(sizeof(f1.type), sizeof(f2.type)) +format_complexity(dims, type) = dims * sizeof(type) """ generate_buffers(pipeline) @@ -378,19 +355,15 @@ optimize buffers for the lowest memory overhead. I.e. it will reuse buffers for multiple connections and upgrade them if it is cheaper than creating a new one. """ function generate_buffers(pipeline::Pipeline) - format_complexity(c::Connection) = format_complexity(c.format) - format_complexity(f::BufferFormat) = f.dims * sizeof(f.type) - - verify(pipeline) - # We can make this more efficient later... - stage2idx = Dict([stage => i for (i, stage) in enumerate(pipeline.stages)]) + stage2idx = Dict([objectid(stage) => i for (i, stage) in enumerate(pipeline.stages)]) # Group connections that exist between stages usage_per_transfer = [Connection[] for _ in 1:length(pipeline.stages)-1] for connection in pipeline.connections - first = mapreduce(kv -> stage2idx[kv[1]], min, connection.inputs) - last = mapreduce(kv -> stage2idx[kv[1]]-1, max, connection.outputs) + first = mapreduce(kv -> stage2idx[objectid(kv[1])], min, connection.inputs) + last = mapreduce(kv -> stage2idx[objectid(kv[1])]-1, max, connection.outputs) + first <= last || error("Connection $connection is read before it is written to. $first $last") for i in first:last push!(usage_per_transfer[i], connection) end @@ -440,7 +413,7 @@ function generate_buffers(pipeline::Pipeline) # - using it is cheaper than creating a new buffer # - it is more compatible than the last when both are 0 cost # (i.e prefer 3, Float16 over 3 Float8 for 3 Float16 target) - updated_comp = format_complexity(BufferFormat(buffers[i], connection.format)) + updated_comp = format_complexity(buffers[i], connection.format) buffer_comp = format_complexity(buffers[i]) delta = updated_comp - buffer_comp is_cheaper = (delta < prev_delta) && (delta <= conn_comp) From 593057981be1681e18fbe9ea4c4ef41ff87338fc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 1 Jan 2025 16:21:21 +0100 Subject: [PATCH 089/135] some more safety tweaks --- GLMakie/src/render_pipeline.jl | 3 ++- GLMakie/src/screen.jl | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 59c88f3d3de..7289cb17995 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -72,7 +72,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline # with an empty one while we mess with it? was_running = renderloop_running(screen) - was_running && stop_renderloop!(screen, true) + was_running && stop_renderloop!(screen, false) + yield() ShaderAbstractions.switch_context!(screen.glscreen) screen.render_pipeline = GLRenderPipeline() diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 0bdb4ccb4f3..61bc8c62ace 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -605,10 +605,11 @@ function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot, called_f end function Base.empty!(screen::Screen) - @debug("empty screen!") # we should never just "empty" an already destroyed screen @assert !was_destroyed(screen.glscreen) + foreach(t -> t[3].visible = false, screen.renderlist) + for plot in collect(values(screen.cache2plot)) delete!(screen, Makie.rootparent(plot), plot, false) end @@ -682,7 +683,6 @@ function Base.close(screen::Screen; reuse=true) empty!(screen) if reuse && screen.reuse - @debug("reusing screen!") push!(SCREEN_REUSE_POOL, screen) end From 188a2940c18fe84336828d351f2cd40538afbeb7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 1 Jan 2025 17:53:02 +0100 Subject: [PATCH 090/135] add tests for Makie render Pipeline --- src/utilities/RenderPipeline.jl | 96 +++++++++--- test/render_pipeline.jl | 259 ++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 331 insertions(+), 25 deletions(-) create mode 100644 test/render_pipeline.jl diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index c88c9d57799..f6949ffb4b5 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -25,6 +25,10 @@ function BufferFormat(dims::Integer = 4, type::DataType = N0f8; extras...) return BufferFormat(dims, type, Dict{Symbol, Any}(extras)) end +function Base.:(==)(f1::BufferFormat, f2::BufferFormat) + return (f1.dims == f2.dims) && (f1.type == f2.type) && (f1.extras == f2.extras) +end + """ BufferFormat(f1::BufferFormat, f2::BufferFormat) @@ -70,7 +74,7 @@ function is_compatible(f1::BufferFormat, f2::BufferFormat) # is_compatible(f1.type, f2.type), but still does runtime dispatch on == T1 = f1.type T2 = f2.type - return ( + type_compat = ( ((T1 == N0f8) || (T1 == Float16) || (T1 == Float32)) && ((T2 == N0f8) || (T2 == Float16) || (T2 == Float32)) ) || ( @@ -80,6 +84,12 @@ function is_compatible(f1::BufferFormat, f2::BufferFormat) ((T1 == UInt8) || (T1 == UInt16) || (T1 == UInt32)) && ((T2 == UInt8) || (T2 == UInt16) || (T2 == UInt32)) ) + type_compat || return false + extras_compat = true + for (k, v) in f1.extras + extras_compat = extras_compat && (get(f2.extras, k, v) == v) + end + return extras_compat end # Connections can have multiple inputs and outputs @@ -141,6 +151,26 @@ get_output_connection(stage::Stage, key::Symbol) = stage.output_connections[stag get_input_format(stage::Stage, key::Symbol) = stage.input_formats[stage.inputs[key]] get_output_format(stage::Stage, key::Symbol) = stage.output_formats[stage.outputs[key]] +function Base.:(==)(s1::Stage, s2::Stage) + # It's not clear what this actually means for stages... + + # Must match: + return (s1.name == s2.name) && (s1.input_formats == s2.input_formats) && + # input names are currently used to generate buffer names in GLMakie but + # they could be considered syntax sugar + (s1.inputs == s2.inputs) && + # outputs are actually just syntax sugar... + (s1.outputs == s2.outputs) + # And connections should probably be excluded as they are relevant to the + # Pipeline/Graph, not the Stage/Node +end + + +function Base.:(==)(f1::Connection, f2::Connection) + return f1.format == f2.format && all(f1.inputs .=== f2.inputs) && all(f1.outputs .=== f2.outputs) +end + + """ Connection(source::Stage, output::Integer, target::Stage, input::Integer) @@ -355,8 +385,20 @@ optimize buffers for the lowest memory overhead. I.e. it will reuse buffers for multiple connections and upgrade them if it is cheaper than creating a new one. """ function generate_buffers(pipeline::Pipeline) - # We can make this more efficient later... - stage2idx = Dict([objectid(stage) => i for (i, stage) in enumerate(pipeline.stages)]) + stage2idx = Dict{UInt64, Int64}() + for (i, stage) in enumerate(pipeline.stages) + # TODO: treat this? + # undef inputs should probably not be allowed at all because the shader + # need to get something and we don't know a reasonable default + # undef (intermediate) outputs could be allowed. They just need to exist + # for 1+ transfer, i.e. could be rewritten immediately after + valid_inputs = all(i -> isassigned(stage.input_connections, i), eachindex(stage.input_connections)) + valid_outputs = all(i -> isassigned(stage.output_connections, i), eachindex(stage.output_connections)) + if !(valid_inputs && valid_outputs) + error("$stage has an incomplete set of $(valid_inputs ? "output" : "input") connections") + end + stage2idx[objectid(stage)] = i + end # Group connections that exist between stages usage_per_transfer = [Connection[] for _ in 1:length(pipeline.stages)-1] @@ -593,9 +635,13 @@ function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Conne # keep order strs = map(eachindex(stage.input_connections)) do i k = findfirst(==(i), stage.inputs) - c = stage.input_connections[i] - ci = string(conn2idx[c]) - return "[$ci] $k" + if isassigned(stage.input_connections, i) + c = stage.input_connections[i] + ci = string(conn2idx[c]) + return "[$ci] $k" + else + return "[ ] $k" + end end join(io, strs, ", ") end @@ -603,9 +649,13 @@ function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Conne print(io, "\n outputs: ") strs = map(eachindex(stage.output_connections)) do i k = findfirst(==(i), stage.outputs) - c = stage.output_connections[i] - ci = string(conn2idx[c]) - return "[$ci] $k" + if isassigned(stage.output_connections, i) + c = stage.output_connections[i] + ci = string(conn2idx[c]) + return "[$ci] $k" + else + return "[#undef] $k" + end end join(io, strs, ", ") end @@ -681,9 +731,9 @@ function FXAAStage(; kwargs...) end function DisplayStage() - inputs = Dict(:color => 1, :objectid => 2) - input_formats = [BufferFormat(4, N0f8), BufferFormat(2, UInt32)] - return Stage(:Display, inputs, input_formats, Dict{Symbol, Int}(), BufferFormat[]) + return Stage(:Display, + Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], + Dict{Symbol, Int}(), BufferFormat[]) end @@ -701,11 +751,13 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) # Note - order important! # TODO: maybe add insert!()? - render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) if ssao + render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) _ssao = push!(pipeline, SSAOStage()) + render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) + else + render2 = push!(pipeline, RenderStage(transparency = false)) end - render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) if oit render3 = push!(pipeline, TransparentRenderStage()) _oit = push!(pipeline, OITStage()) @@ -720,28 +772,22 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) if ssao connect!(pipeline, render1, _ssao) - connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) - else - connect!(pipeline, render1, fxaa ? _fxaa : display, :color) + connect!(pipeline, render1, display, :objectid) + connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) end - connect!(pipeline, render2, fxaa ? _fxaa : display, :color) + connect!(pipeline, render2, fxaa ? _fxaa : display) + connect!(pipeline, render2, display, :objectid) # make sure this merges with other objectids if oit connect!(pipeline, render3, _oit) connect!(pipeline, _oit, fxaa ? _fxaa : display, :color) else connect!(pipeline, render3, fxaa ? _fxaa : display, :color) end + connect!(pipeline, render3, display, :objectid) if fxaa - # will also connect render2, 3 since connections bundle - connect!(pipeline, render1, _fxaa, :objectid) connect!(pipeline, _fxaa, display, :color) end - # generic - connect!(pipeline, render1, display, :objectid) - connect!(pipeline, render2, display, :objectid) - connect!(pipeline, render3, display, :objectid) - return pipeline end end diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl new file mode 100644 index 00000000000..2b88bea8c04 --- /dev/null +++ b/test/render_pipeline.jl @@ -0,0 +1,259 @@ +using Makie +using Makie: BufferFormat, N0f8, is_compatible +using Makie: Stage, get_input_connection, get_output_connection, get_input_format, get_output_format +using Makie: Connection, Pipeline, connect! +using Makie: generate_buffers, default_pipeline + +@testset "Render Pipeline" begin + + @testset "BufferFormat" begin + @testset "Constructors" begin + f = BufferFormat() + @test f.dims == 4 + @test f.type == N0f8 + @test isempty(f.extras) + + f = BufferFormat(1, Float16, a = 1, b = 2) + @test f.dims == 1 + @test f.type == Float16 + @test haskey(f.extras, :a) && (f.extras[:a] == 1) + @test haskey(f.extras, :b) && (f.extras[:b] == 2) + @test length(keys(f.extras)) == 2 + end + + types = [(N0f8, Float16, Float32), (Int8, Int16, Int32), (UInt8, UInt16, UInt32)] + groups = [[BufferFormat(rand(1:4), T) for T in types[i]] for i in 1:3] + + @testset "is_compatible" begin + # All types that should or should not be compatible + for i in 1:3, j in 1:3 + for a in groups[i], b in groups[j] + @test (i == j) == is_compatible(a, b) + @test (i == j) == is_compatible(b, a) + end + end + + # extras + @test is_compatible(BufferFormat(a = 1), BufferFormat()) + @test is_compatible(BufferFormat(a = 1), BufferFormat(a = 1)) + @test !is_compatible(BufferFormat(a = 1), BufferFormat(a = 2)) + @test is_compatible(BufferFormat(a = 1), BufferFormat(b = 2)) + @test !is_compatible(BufferFormat(2, Int8, a = 1), BufferFormat(b = 2)) + @test is_compatible(BufferFormat(2, Int8, a = 1), BufferFormat(1, Int32, b = 2, c = 3)) + end + + @testset "BufferFormat merging" begin + for i in 1:3, j in 1:3 + for m in 1:3, n in 1:3 + a = groups[i][m]; b = groups[j][n] + if i == j + expected = BufferFormat(max(a.dims, b.dims), types[i][max(m, n)]) + @test BufferFormat(a, b) == expected + @test BufferFormat(b, a) == expected + else + @test_throws ErrorException BufferFormat(a, b) + @test_throws ErrorException BufferFormat(b, a) + end + end + end + + @test begin + B = BufferFormat(BufferFormat(a = :a), BufferFormat()) + haskey(B.extras, :a) && (B.extras[:a] == :a) + end + @test begin + B = BufferFormat(BufferFormat(2, N0f8, a = "s"), BufferFormat(3, Float16, a = "s")) + haskey(B.extras, :a) && (B.extras[:a] == "s") + end + @test_throws ErrorException BufferFormat(BufferFormat(a = :a), BufferFormat(a = :b)) + @test_throws ErrorException BufferFormat(BufferFormat(a = :a), BufferFormat(a = 1)) + end + end + + + @testset "Stage" begin + @test_throws MethodError Stage() + @test Stage(:name) == Stage("name") + stage = Stage(:test) + @test stage.name == :test + @test stage.inputs == Dict{Symbol, Int}() + @test stage.outputs == Dict{Symbol, Int}() + @test stage.input_formats == BufferFormat[] + @test stage.output_formats == BufferFormat[] + @test stage.input_connections == Connection[] + @test stage.output_connections == Connection[] + @test stage.attributes == NamedTuple() + + stage = Stage(:test, + inputs = [:a => BufferFormat(), :b => BufferFormat(2)], + outputs = [:c => BufferFormat(1, Int8)], + attr = 17f0) + @test stage.name == :test + @test stage.inputs == Dict(:a => 1, :b => 2) + @test stage.outputs == Dict(:c => 1) + @test stage.input_formats == [BufferFormat(), BufferFormat(2)] + @test stage.output_formats == [BufferFormat(1, Int8)] + @test stage.input_connections == Connection[] + @test stage.output_connections == Connection[] + @test stage.attributes == (attr = 17f0,) + + @test get_input_format(stage, :a) == stage.input_formats[stage.inputs[:a]] + @test get_output_format(stage, :c) == stage.output_formats[stage.outputs[:c]] + end + + @testset "Pipeline & Connections" begin + pipeline = Pipeline() + + @test isempty(pipeline.stages) + @test isempty(pipeline.connections) + + stage1 = Stage(:stage1, outputs = [:a => BufferFormat(), :b => BufferFormat(2)]) + stage2 = Stage(:stage2, + inputs = [:b => BufferFormat(2)], + outputs = [:c => BufferFormat(1, Int16)], + attr = 17f0) + stage3 = Stage(:stage3, inputs = [:b => BufferFormat(4, Float16), :c => BufferFormat(2, Int8)]) + stage4 = Stage(:stage4, inputs = [:x => BufferFormat()], outputs = [:y => BufferFormat()]) + stage5 = Stage(:stage5, inputs = [:z => BufferFormat()]) + + push!(pipeline, stage1) + push!(pipeline, stage2) + push!(pipeline, stage3) + push!(pipeline, stage4) + push!(pipeline, stage5) + + @test all(pipeline.stages .== [stage1, stage2, stage3, stage4, stage5]) + @test isempty(pipeline.connections) + + connect!(pipeline, stage1, stage2) + + @test length(pipeline.connections) == 1 + c1 = pipeline.connections[end] + @test c1.format == BufferFormat(2) + @test c1.inputs == [stage1 => 2] + @test c1.outputs == [stage2 => 1] + @test stage1.output_connections[2] == c1 + @test stage2.input_connections[1] == c1 + + connect!(pipeline, stage2, stage3, :c) + + @test length(pipeline.connections) == 2 + c2 = pipeline.connections[end] + @test c2.format == BufferFormat(2, Int16) + @test c2.inputs == [stage2 => 1] + @test c2.outputs == [stage3 => 2] + @test stage2.output_connections[1] == c2 + @test stage3.input_connections[2] == c2 + + connect!(pipeline, stage1, stage3, :b) + + @test length(pipeline.connections) == 2 + c3 = pipeline.connections[end] + @test c1 !== c3 + @test c2 !== c3 + @test c3.format == BufferFormat(4, Float16) + @test c3.inputs == [stage1 => 2] + @test c3.outputs == [stage3 => 1, stage2 => 1] # technically order irrelevant + @test stage1.output_connections[2] == c3 + @test stage2.input_connections[1] == c3 + @test stage3.input_connections[1] == c3 + + # Stage 1 incomplete - if output 2 is connected all previous outputs must be connected too + @test_throws Exception generate_buffers(pipeline) + + connect!(pipeline, stage1, :a, stage4, :x) + + @test length(pipeline.connections) == 3 + c4 = pipeline.connections[end] + @test c4.format == BufferFormat() + @test c4.inputs == [stage1 => 1] + @test c4.outputs == [stage4 => 1] + @test stage1.output_connections[1] == c4 + @test stage4.input_connections[1] == c4 + + connect!(pipeline, stage4, :y, stage5, :z) + + @test length(pipeline.connections) == 4 + c5 = pipeline.connections[end] + @test c5.format == BufferFormat() + @test c5.inputs == [stage4 => 1] + @test c5.outputs == [stage5 => 1] + @test stage4.output_connections[1] == c5 + @test stage5.input_connections[1] == c5 + + #= + 1 2 3 4 5 (stages) + a -----------------> x ---> z (inputs outputs) + b -+----------> b + '-> b c ---> c + =# + buffers, conn2idx = generate_buffers(pipeline) + @test length(buffers) == 3 # 3 buffer textures are needed for transfers + @test length(conn2idx) == 4 # 4 connections map to them + @test buffers[conn2idx[c2]] == c2.format + @test buffers[conn2idx[c3]] == c3.format + @test buffers[conn2idx[c4]] == c4.format + # 1 a --> 4 x not yet available for reuse + # 1 b --> 2 b, 3b available, upgrades + # 2 c --> 3 c not allowed, incompatible types + @test buffers[conn2idx[c5]] == c3.format + end + + @testset "default pipeline" begin + pipeline = default_pipeline() + + # These directly check what the call should construct. (This indirectly + # also checks that connect!() works for pipelines) + @test length(pipeline.stages) == 7 + @test pipeline.stages[1] == Stage(:ZSort) + @test pipeline.stages[2] == Stage(:Render, Dict{Symbol, Int}(), BufferFormat[], + Dict(:color => 1, :objectid => 2, :position => 3, :normal => 4), + [BufferFormat(4, N0f8), BufferFormat(2, UInt32), BufferFormat(3, Float16), BufferFormat(3, Float16)], + transparency = false) + @test pipeline.stages[3] == Stage(:TransparentRender, Dict{Symbol, Int}(), BufferFormat[], + Dict(:weighted_color_sum => 1, :objectid => 2, :alpha_product => 3), + [BufferFormat(4, Float16), BufferFormat(2, UInt32), BufferFormat(1, N0f8)]) + @test pipeline.stages[4] == Stage(:OIT, + Dict(:weighted_color_sum => 1, :alpha_product => 2), [BufferFormat(4, Float16), BufferFormat(1, N0f8)], + Dict(:color => 1), [BufferFormat(4, N0f8)]) + @test pipeline.stages[5] == Stage(:FXAA1, + Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], + Dict(:color_luma => 1), [BufferFormat(4, N0f8)]) + @test pipeline.stages[6] == Stage(:FXAA2, + Dict(:color_luma => 1), [BufferFormat(4, N0f8, minfilter = :linear)], + Dict(:color => 1), [BufferFormat(4, N0f8)]) + @test pipeline.stages[7] == Stage(:Display, + Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], + Dict{Symbol, Int}(), BufferFormat[]) + + # Note: Order technically irrelevant but it's easier to test with order + # Same for inputs and outputs here + @test length(pipeline.connections) == 6 + stage1, stage2, stage3, stage4, stage5, stage6, stage7 = pipeline.stages + @test pipeline.connections[1] == Connection([stage5 => 1], [stage6 => 1], BufferFormat(4, N0f8, minfilter = :linear)) + @test pipeline.connections[2] == Connection([stage3 => 1], [stage4 => 1], BufferFormat(4, Float16)) + @test pipeline.connections[3] == Connection([stage3 => 3], [stage4 => 2], BufferFormat(1, N0f8)) + @test pipeline.connections[4] == Connection([stage4 => 1, stage2 => 1], [stage5 => 1], BufferFormat(4, N0f8)) + @test pipeline.connections[5] == Connection([stage3 => 2, stage2 => 2], [stage7 => 2, stage5 => 2], BufferFormat(2, UInt32)) + @test pipeline.connections[6] == Connection([stage6 => 1], [stage7 => 1], BufferFormat(4, N0f8)) + + # Verify buffer generation with this more complex example + buffers, conn2idx = generate_buffers(pipeline) + + # Order irrelevant but the mapping between connections and buffers needs + # to be correct. Easier to test with order as well + @test length(buffers) == 4 + @test buffers[1] == BufferFormat() + @test buffers[2] == BufferFormat(2, UInt32) + @test buffers[3] == BufferFormat(4, Float16, minfilter = :linear) + @test buffers[4] == BufferFormat(1, N0f8) + + @test length(conn2idx) == 6 + @test conn2idx[pipeline.connections[1]] == 3 + @test conn2idx[pipeline.connections[2]] == 3 # compatible and no overlap with connection (1) + @test conn2idx[pipeline.connections[3]] == 4 + @test conn2idx[pipeline.connections[4]] == 1 + @test conn2idx[pipeline.connections[5]] == 2 + @test conn2idx[pipeline.connections[6]] == 1 # compatible and no overlap with (4) + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index d378d05f175..f1b94dc0ec2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -51,4 +51,5 @@ using Makie: volume include("float32convert.jl") include("dim-converts.jl") include("Plane.jl") + include("render_pipeline.jl") end From fde46f839fcb807b0b7deeb0906cf10ca1ad0d01 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 2 Jan 2025 12:53:42 +0100 Subject: [PATCH 091/135] revert fix attempts & assert correct context instead --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 19 ++++++++++++++++--- GLMakie/src/GLAbstraction/GLShader.jl | 4 +++- GLMakie/src/GLAbstraction/GLTypes.jl | 3 +++ GLMakie/src/postprocessing.jl | 18 +++++++++--------- GLMakie/src/render_pipeline.jl | 3 ++- GLMakie/src/screen.jl | 4 ++-- 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index 5d2652d9e08..c379149a19c 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -44,23 +44,33 @@ Activates the first N color buffers attached to the given GLFramebuffer. If N is not given all color attachments are activated. """ function set_draw_buffers(fb::GLFramebuffer, N::Integer = fb.counter) + require_context(fb.context) bind(fb) glDrawBuffers(N, fb.attachments) + return end function set_draw_buffers(fb::GLFramebuffer, key::Symbol) + require_context(fb.context) bind(fb) glDrawBuffer(get_attachment(fb, key)) + return end function set_draw_buffers(fb::GLFramebuffer, keys::Symbol...) + require_context(fb.context) bind(fb) glDrawBuffer(get_attachment.(Ref(fb), keys)) + return end function unsafe_free(x::GLFramebuffer) # don't free if already freed x.id == 0 && return # don't free from other context - GLAbstraction.context_alive(x.context) || return + if require_cleanup_before_context_deletion() + GLAbstraction.context_alive(x.context) || error("Can't delete Program - context not alive.") + else + GLAbstraction.context_alive(x.context) || return + end GLAbstraction.switch_context!(x.context) id = Ref(x.id) glDeleteFramebuffers(1, id) @@ -105,7 +115,8 @@ function attach_depthstencilbuffer(fb::GLFramebuffer, key::Symbol, buffer) end function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment::GLenum) - haskey(fb, key) && error("Cannot attach $key to Framebuffer because it is already set.") + require_context(fb.context) + haskey(fb, key) && error("Cannot attach " * string(key) * " to Framebuffer because it is already set.") if attachment in fb.attachments if attachment == GL_DEPTH_ATTACHMENT type = "depth" @@ -116,13 +127,15 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment else type = "color" end - error("Cannot attach $key as a $type attachment as it is already attached.") + error("Cannot attach " * string(key) * " as a " * type * " attachment as it is already attached.") end try + require_context(fb.context) bind(fb) gl_attach(buffer, attachment) check_framebuffer() + require_context(fb.context) catch e if GL_COLOR_ATTACHMENT0 <= attachment <= GL_COLOR_ATTACHMENT15 # If we failed to attach correctly we should probably overwrite diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 13da407dc31..829ea8507d4 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -233,7 +233,9 @@ function gl_convert(ctx::GLContext, cache::ShaderCache, lazyshader::AbstractLazy shaders[i] = get_shader!(cache, shader_source, tr) end ShaderAbstractions.switch_context!(cache.context) - return compile_program(shaders, fragdatalocation) + x = compile_program(shaders, fragdatalocation) + require_context(cache.context) + return x end end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 8ecaa9e2c98..09e37cf367a 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -225,6 +225,7 @@ function GLVertexArray(bufferdict::Dict, program::GLProgram) if indexes == -1 indexes = len end + require_context(program.context) obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indexes) finalizer(verify_free, obj) return obj @@ -298,6 +299,7 @@ mutable struct RenderObject{Pre} prerenderfunctions, postrenderfunctions, visible ) where Pre + require_context(context, vertexarray.context) fxaa = Bool(to_value(get!(uniforms, :fxaa, true))) RENDER_OBJECT_ID_COUNTER[] += one(UInt32) # Store fxaa in ID, so we can access it in the shader to create a mask @@ -394,6 +396,7 @@ function RenderObject( delete!(data, k) end end + require_context(context) robj = RenderObject{Pre}( context, diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index b78798eada0..4b3814713b3 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -62,7 +62,7 @@ GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) function render_frame(screen, glscene, pipeline::GLRenderPipeline) for step in pipeline.steps - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) run_step(screen, glscene, step) end return @@ -151,7 +151,7 @@ end function run_step(screen, glscene, step::RenderPlots) # Somehow errors in here get ignored silently!? try - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) GLAbstraction.bind(step.framebuffer) for (idx, color) in step.clear @@ -172,7 +172,7 @@ function run_step(screen, glscene, step::RenderPlots) ppu = screen.px_per_unit[] a = viewport(scene)[] - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) # TODO: Can we move this outside the loop? @@ -232,7 +232,7 @@ end function construct(::Val{:OIT}, screen, framebuffer, inputs, parent) @debug "Creating OIT postprocessor" - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) # Based on https://jcgt.org/published/0002/02/09/, see #1390 # OIT setup @@ -274,7 +274,7 @@ function run_step(screen, glscene, step::RenderPass{:OIT}) end function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) # SSAO setup N_samples = 64 @@ -309,7 +309,7 @@ function construct(::Val{:SSAO1}, screen, framebuffer, inputs, parent) end function construct(::Val{:SSAO2}, screen, framebuffer, inputs, parent) - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) # blur occlusion and combine with color shader = LazyShader( @@ -388,7 +388,7 @@ function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) filter_fxaa_in_shader = get(parent.attributes, :filter_in_shader, true) - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) # calculate luma for FXAA shader = LazyShader( screen.shader_cache, @@ -403,7 +403,7 @@ function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) end function construct(::Val{:FXAA2}, screen, framebuffer, inputs, parent) - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) # perform FXAA shader = LazyShader( @@ -449,7 +449,7 @@ struct BlitToScreen <: AbstractRenderStep end function construct(::Val{:Display}, screen, ::Nothing, inputs, parent::Makie.Stage) - ShaderAbstractions.switch_context!(screen.glscreen) + require_context(screen.glscreen) framebuffer = screen.framebuffer_factory.fb id = get(parent.attributes, :screen_framebuffer_id, 0) return BlitToScreen(framebuffer, id) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 7289cb17995..af6ccb611fc 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -69,12 +69,13 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # TODO: check if pipeline is different from the last one before replacing it factory = screen.framebuffer_factory + # TODO: OpengGL ERRORS # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline # with an empty one while we mess with it? was_running = renderloop_running(screen) was_running && stop_renderloop!(screen, false) - yield() ShaderAbstractions.switch_context!(screen.glscreen) + screen.render_pipeline = GLRenderPipeline() # Resolve pipeline diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 61bc8c62ace..0bdb4ccb4f3 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -605,11 +605,10 @@ function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot, called_f end function Base.empty!(screen::Screen) + @debug("empty screen!") # we should never just "empty" an already destroyed screen @assert !was_destroyed(screen.glscreen) - foreach(t -> t[3].visible = false, screen.renderlist) - for plot in collect(values(screen.cache2plot)) delete!(screen, Makie.rootparent(plot), plot, false) end @@ -683,6 +682,7 @@ function Base.close(screen::Screen; reuse=true) empty!(screen) if reuse && screen.reuse + @debug("reusing screen!") push!(SCREEN_REUSE_POOL, screen) end From 0bc8751a6f8930aeeac71ef43d795d3ae594192d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 17:41:03 +0100 Subject: [PATCH 092/135] use enum for type handling to avoid runtime dispatch --- GLMakie/src/render_pipeline.jl | 22 ++------- src/utilities/RenderPipeline.jl | 80 ++++++++++++++++++--------------- test/render_pipeline.jl | 37 ++++++++------- 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index af6ccb611fc..b7bc4c11ddd 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -1,17 +1,3 @@ -function create_buffer!(factory::FramebufferFactory, format::Makie.BufferFormat) - T = format_to_type(format) - tex = Texture(T, size(factory), minfilter = :linear, x_repeat = :clamp_to_edge) - # tex = Texture(T, size(factory), minfilter = :nearest, x_repeat = :clamp_to_edge) - push!(factory, tex) -end - -function format_to_type(format::Makie.BufferFormat) - return format.dims == 1 ? format.type : Vec{format.dims, format.type} -end -function format_to_gl_format(format::Makie.BufferFormat) - return format.dims == 1 ? format.type : Vec{format.dims, format.type} -end - function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferFormat}) @assert factory.fb.id != 0 "Cannot reset a destroyed FramebufferFactory" GLAbstraction.free.(factory.children) @@ -41,7 +27,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF buffers = copy(factory.buffers) empty!(factory.buffers) for format in formats - T = format_to_type(format) + T = Makie.format_to_type(format) tex = get_buffer!(buffers, T, format.extras) push!(factory.buffers, tex) end @@ -72,8 +58,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # TODO: OpengGL ERRORS # Maybe safer to wait on rendertask to finish and replace the GLRenderPipeline # with an empty one while we mess with it? - was_running = renderloop_running(screen) - was_running && stop_renderloop!(screen, false) + # was_running = renderloop_running(screen) + # was_running && stop_renderloop!(screen, close_after_renderloop = false) ShaderAbstractions.switch_context!(screen.glscreen) screen.render_pipeline = GLRenderPipeline() @@ -134,7 +120,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) end screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) - was_running && start_renderloop!(screen) + # was_running && start_renderloop!(screen) return end \ No newline at end of file diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index f6949ffb4b5..256d5e04a79 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -2,12 +2,35 @@ import FixedPointNumbers: N0f8 +module BFT + import FixedPointNumbers: N0f8 + + @enum BufferFormatType::UInt8 begin + float8 = 0; float16 = 1; float32 = 3; + int8 = 4; int16 = 5; int32 = 7; + uint8 = 8; uint16 = 9; uint32 = 11; + end + + # lowest 2 bits do variation between 8, 16 and 32 bit types, others do variation of base type + is_compatible(a::BufferFormatType, b::BufferFormatType) = (UInt8(a) & 0b11111100) == (UInt8(b) & 0b11111100) + + # assuming compatible types (max is a bitwise thing here btw) + _promote(a::BufferFormatType, b::BufferFormatType) = BufferFormatType(max(UInt8(a), UInt8(b))) + + # we matched the lowest 2 bits to bytesize + bytesize(x::BufferFormatType) = Int((UInt8(x) & 0b11) + 1) + + const type_lookup = (N0f8, Float16, Nothing, Float32, Int8, Int16, Nothing, Int32, UInt8, UInt16, Nothing, UInt32) + to_type(t::BufferFormatType) = type_lookup[Int(t) + 1] +end + + # TODO: try `BufferFormat{T} ... type::Type{T}` for better performance? # TODO: consider adding a "immediately reusable" flag here or to Stage so that # Stage can communicate that it allows output = input struct BufferFormat dims::Int - type::DataType + type::BFT.BufferFormatType extras::Dict{Symbol, Any} end @@ -24,6 +47,10 @@ The `BufferFormat` may also include extra requirements such as function BufferFormat(dims::Integer = 4, type::DataType = N0f8; extras...) return BufferFormat(dims, type, Dict{Symbol, Any}(extras)) end +@generated function BufferFormat(dims::Integer, ::Type{T}, extras::Dict{Symbol, Any}) where {T} + type = BFT.BufferFormatType(UInt8(findfirst(x -> x === T, BFT.type_lookup)) - 0x01) + return :(BufferFormat(dims, $type, extras)) +end function Base.:(==)(f1::BufferFormat, f2::BufferFormat) return (f1.dims == f2.dims) && (f1.type == f2.type) && (f1.extras == f2.extras) @@ -43,22 +70,10 @@ Rules: - If only one format contains a specific extra requirement it is accepted and copied to the output. """ function BufferFormat(f1::BufferFormat, f2::BufferFormat) - if is_compatible(f1, f2) + if BFT.is_compatible(f1.type, f2.type) dims = max(f1.dims, f2.dims) - T1 = f1.type; T2 = f2.type - type = if T1 == T2; T1 - elseif (T1 == Float32) || (T2 == Float32); Float32 - elseif (T1 == Float16) || (T2 == Float16); Float16 - elseif (T1 == N0f8) || (T2 == N0f8); N0f8 - elseif (T1 == Int32) || (T2 == Int32); Int32 - elseif (T1 == Int16) || (T2 == Int16); Int16 - elseif (T1 == Int8) || (T2 == Int8); Int8 - elseif (T1 == UInt32) || (T2 == UInt32); UInt32 - elseif (T1 == UInt16) || (T2 == UInt16); UInt16 - elseif (T1 == UInt8) || (T2 == UInt8); UInt8 - else error("Failed to merge BufferFormat: Types $T1 and $T2 are not allowed.") - end - extras = copy(f1.extras) + type = BFT._promote(f1.type, f2.type) + extras = deepcopy(f1.extras) for (k, v) in f2.extras get!(extras, k, v) == v || error("Failed to merge BufferFormat: Extra requirement $(extras[k]) != $v.") end @@ -68,23 +83,8 @@ function BufferFormat(f1::BufferFormat, f2::BufferFormat) end end - function is_compatible(f1::BufferFormat, f2::BufferFormat) - # About 10% faster for default_SSAO_pipeline() (with no caching) than - # is_compatible(f1.type, f2.type), but still does runtime dispatch on == - T1 = f1.type - T2 = f2.type - type_compat = ( - ((T1 == N0f8) || (T1 == Float16) || (T1 == Float32)) && - ((T2 == N0f8) || (T2 == Float16) || (T2 == Float32)) - ) || ( - ((T1 == Int8) || (T1 == Int16) || (T1 == Int32)) && - ((T2 == Int8) || (T2 == Int16) || (T2 == Int32)) - ) || ( - ((T1 == UInt8) || (T1 == UInt16) || (T1 == UInt32)) && - ((T2 == UInt8) || (T2 == UInt16) || (T2 == UInt32)) - ) - type_compat || return false + BFT.is_compatible(f1.type, f2.type) || return false extras_compat = true for (k, v) in f1.extras extras_compat = extras_compat && (get(f2.extras, k, v) == v) @@ -92,6 +92,11 @@ function is_compatible(f1::BufferFormat, f2::BufferFormat) return extras_compat end +function format_to_type(format::BufferFormat) + eltype = BFT.to_type(format.type) + return format.dims == 1 ? eltype : Vec{format.dims, eltype} +end + # Connections can have multiple inputs and outputs # e.g. multiple Renders write to objectid and FXAA, SSAO, Display/pick read it struct ConnectionT{T} @@ -167,7 +172,9 @@ end function Base.:(==)(f1::Connection, f2::Connection) - return f1.format == f2.format && all(f1.inputs .=== f2.inputs) && all(f1.outputs .=== f2.outputs) + return (f1.format == f2.format) && + (length(f1.inputs) == length(f2.inputs)) && all(f1.inputs .=== f2.inputs) && + (length(f1.outputs) == length(f2.outputs)) && all(f1.outputs .=== f2.outputs) end @@ -372,9 +379,10 @@ function Observables.connect!(pipeline::Pipeline, end format_complexity(c::Connection) = format_complexity(c.format) -format_complexity(f::BufferFormat) = format_complexity(f.dims, sizeof(f.type)) -format_complexity(f1::BufferFormat, f2::BufferFormat) = max(f1.dims, f2.dims) * max(sizeof(f1.type), sizeof(f2.type)) -format_complexity(dims, type) = dims * sizeof(type) +format_complexity(f::BufferFormat) = format_complexity(f.dims, f.type) +format_complexity(dims, type) = dims * BFT.bytesize(type) +# complexity of merged, not max of either +format_complexity(f1::BufferFormat, f2::BufferFormat) = max(f1.dims, f2.dims) * max(BFT.bytesize(f1.type), BFT.bytesize(f2.type)) """ generate_buffers(pipeline) diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index 2b88bea8c04..1883e94ae10 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -1,5 +1,5 @@ using Makie -using Makie: BufferFormat, N0f8, is_compatible +using Makie: BufferFormat, N0f8, is_compatible, BFT using Makie: Stage, get_input_connection, get_output_connection, get_input_format, get_output_format using Makie: Connection, Pipeline, connect! using Makie: generate_buffers, default_pipeline @@ -10,12 +10,12 @@ using Makie: generate_buffers, default_pipeline @testset "Constructors" begin f = BufferFormat() @test f.dims == 4 - @test f.type == N0f8 + @test f.type == BFT.float8 @test isempty(f.extras) f = BufferFormat(1, Float16, a = 1, b = 2) @test f.dims == 1 - @test f.type == Float16 + @test f.type == BFT.float16 @test haskey(f.extras, :a) && (f.extras[:a] == 1) @test haskey(f.extras, :b) && (f.extras[:b] == 2) @test length(keys(f.extras)) == 2 @@ -240,20 +240,27 @@ using Makie: generate_buffers, default_pipeline # Verify buffer generation with this more complex example buffers, conn2idx = generate_buffers(pipeline) - # Order irrelevant but the mapping between connections and buffers needs - # to be correct. Easier to test with order as well + # Order irrelevant @test length(buffers) == 4 - @test buffers[1] == BufferFormat() - @test buffers[2] == BufferFormat(2, UInt32) - @test buffers[3] == BufferFormat(4, Float16, minfilter = :linear) - @test buffers[4] == BufferFormat(1, N0f8) + formats = [ + :color => BufferFormat(), :objectid => BufferFormat(2, UInt32), :weight => BufferFormat(1, N0f8), + :HDR => BufferFormat(4, Float16, minfilter = :linear) + ] + lookup = Dict{Symbol, Int}() + for (name, format) in formats + idx = findfirst(==(format), buffers) + @test idx !== nothing + lookup[name] = idx::Int + end + # Sanity check for assumption that none of the formats are equal + @test sum(values(lookup)) == 1 + 2 + 3 + 4 @test length(conn2idx) == 6 - @test conn2idx[pipeline.connections[1]] == 3 - @test conn2idx[pipeline.connections[2]] == 3 # compatible and no overlap with connection (1) - @test conn2idx[pipeline.connections[3]] == 4 - @test conn2idx[pipeline.connections[4]] == 1 - @test conn2idx[pipeline.connections[5]] == 2 - @test conn2idx[pipeline.connections[6]] == 1 # compatible and no overlap with (4) + @test conn2idx[pipeline.connections[1]] == lookup[:HDR] + @test conn2idx[pipeline.connections[2]] == lookup[:HDR] # compatible and no overlap with connection (1) + @test conn2idx[pipeline.connections[3]] == lookup[:weight] + @test conn2idx[pipeline.connections[4]] == lookup[:color] + @test conn2idx[pipeline.connections[5]] == lookup[:objectid] + @test conn2idx[pipeline.connections[6]] == lookup[:color] # compatible and no overlap with (4) end end \ No newline at end of file From d187a65c89dfc218681dce5d93ff56f60c3a2170 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 3 Jan 2025 22:50:09 +0100 Subject: [PATCH 093/135] remove Connection type for better performance --- GLMakie/src/render_pipeline.jl | 40 ++-- src/utilities/RenderPipeline.jl | 404 ++++++++++++-------------------- test/render_pipeline.jl | 177 +++++++------- 3 files changed, 264 insertions(+), 357 deletions(-) diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index b7bc4c11ddd..75b44cff429 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -65,33 +65,44 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) screen.render_pipeline = GLRenderPipeline() # Resolve pipeline - buffers, connection2idx = Makie.generate_buffers(pipeline) + buffers, remap = Makie.generate_buffers(pipeline) # Add required buffers reset!(factory, buffers) # Add back output color and objectid attachments - buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :color)] + N = length(pipeline.stages) + buffer_idx = remap[pipeline.stageio2idx[(N, -1)]] attach_colorbuffer(factory.fb, :color, get_buffer(factory, buffer_idx)) - buffer_idx = connection2idx[Makie.get_input_connection(pipeline.stages[end], :objectid)] + buffer_idx = remap[pipeline.stageio2idx[(N, -2)]] attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) render_pipeline = AbstractRenderStep[] - for stage in pipeline.stages + for (stage_idx, stage) in enumerate(pipeline.stages) inputs = Dict{Symbol, Any}() - for key in keys(stage.inputs) - connection = stage.input_connections[stage.inputs[key]] - inputs[Symbol(key, :_buffer)] = get_buffer(factory, connection2idx[connection]) + for (key, input_idx) in stage.inputs + idx = remap[pipeline.stageio2idx[(stage_idx, -input_idx)]] + inputs[Symbol(key, :_buffer)] = get_buffer(factory, idx) end - N = length(stage.output_connections) - if N == 0 + connection_indices = map(eachindex(stage.output_formats)) do output_idx + return get(pipeline.stageio2idx, (stage_idx, output_idx), -1) + end + + N = length(connection_indices) + 1 + while (N > 1) && (connection_indices[N-1] == -1) + N = N-1 + end + + if isempty(connection_indices) framebuffer = nothing else - idx2name = Dict([idx => k for (k, idx) in stage.outputs]) - outputs = [connection2idx[stage.output_connections[i]] => idx2name[i] for i in 1:N] try + idx2name = Dict([idx => k for (k, idx) in stage.outputs]) + outputs = ntuple(N-1) do n + remap[connection_indices[n]] => idx2name[n] + end framebuffer = generate_framebuffer(factory, outputs...) catch e rethrow(e) @@ -100,12 +111,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) # can we reconstruct? (reconstruct should update framebuffer, and replace # inputs if necessary, i.e. handle differences in connections and attributes) - idx = findfirst(previous_pipeline.parent.stages) do old - (old.name == stage.name) && - (old.inputs == stage.inputs) && (old.outputs == stage.outputs) && - (old.input_formats == stage.input_formats) && - (old.output_formats == stage.output_formats) - end + idx = findfirst(==(stage), previous_pipeline.parent.stages) if idx === nothing pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 256d5e04a79..86f9502aadd 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -97,13 +97,6 @@ function format_to_type(format::BufferFormat) return format.dims == 1 ? eltype : Vec{format.dims, eltype} end -# Connections can have multiple inputs and outputs -# e.g. multiple Renders write to objectid and FXAA, SSAO, Display/pick read it -struct ConnectionT{T} - inputs::Vector{Pair{T, Int}} - outputs::Vector{Pair{T, Int}} - format::BufferFormat # derived from inputs & outputs formats -end struct Stage name::Symbol @@ -115,16 +108,10 @@ struct Stage input_formats::Vector{BufferFormat} output_formats::Vector{BufferFormat} - # "const" after setting up connections - input_connections::Vector{ConnectionT{Stage}} - output_connections::Vector{ConnectionT{Stage}} - # const for caching (which does quite a lot actually) - attributes::NamedTuple + attributes::Dict{Symbol, Any} # TODO: rename -> settings? end -const Connection = ConnectionT{Stage} - """ Stage(name::Symbol[; inputs = Pair{Symbol, BufferFormat}[], outputs = Pair{Symbol, BufferFormat}[]) @@ -146,56 +133,34 @@ function Stage(name, inputs, input_formats, outputs, output_formats; kwargs...) Symbol(name), inputs, outputs, input_formats, output_formats, - Connection[], Connection[], - NamedTuple{keys(kwargs)}(values(kwargs)) + Dict{Symbol, Any}(pairs(kwargs)) ) end -get_input_connection(stage::Stage, key::Symbol) = stage.input_connections[stage.inputs[key]] -get_output_connection(stage::Stage, key::Symbol) = stage.output_connections[stage.outputs[key]] get_input_format(stage::Stage, key::Symbol) = stage.input_formats[stage.inputs[key]] get_output_format(stage::Stage, key::Symbol) = stage.output_formats[stage.outputs[key]] function Base.:(==)(s1::Stage, s2::Stage) - # It's not clear what this actually means for stages... - - # Must match: + # For two stages to be equal: + # name & format must match as they define the backend implementation and required buffers + # outputs don't strictly need to match as they are just names. + # inputs do as they are used as uniform names (could change) + # attributes probably should match because they change uniforms and compile time constants + # (this may allow us to merge pipelines later) return (s1.name == s2.name) && (s1.input_formats == s2.input_formats) && - # input names are currently used to generate buffer names in GLMakie but - # they could be considered syntax sugar - (s1.inputs == s2.inputs) && - # outputs are actually just syntax sugar... - (s1.outputs == s2.outputs) - # And connections should probably be excluded as they are relevant to the - # Pipeline/Graph, not the Stage/Node -end - - -function Base.:(==)(f1::Connection, f2::Connection) - return (f1.format == f2.format) && - (length(f1.inputs) == length(f2.inputs)) && all(f1.inputs .=== f2.inputs) && - (length(f1.outputs) == length(f2.outputs)) && all(f1.outputs .=== f2.outputs) -end - - -""" - Connection(source::Stage, output::Integer, target::Stage, input::Integer) - -Creates a `Connection` between the `output` of a `source` stage and the `input` -of a `target` Stage. The `output` and `input` can be either the name of that -output/input or the index. - -Note: This constructor does not update the given stages or make any checks. It -should be considered internal. Use `connect!()` instead. -""" -function Connection(source::Stage, input::Integer, target::Stage, output::Integer) - format = BufferFormat(source.output_formats[input], target.input_formats[output]) - return Connection([source => input], [target => output], format) + (s1.output_formats == s2.output_formats) && + (s1.inputs == s2.inputs) && (s1.outputs == s2.outputs) && + (s1.attributes == s2.attributes) end struct Pipeline stages::Vector{Stage} - connections::Vector{Connection} + + # (stage_idx, negative input index or positive output index) -> connection format index + stageio2idx::Dict{Tuple{Int, Int}, Int} + formats::Vector{BufferFormat} # of connections + # TODO: consider adding: + # endpoints::Vector{Tuple{Int, Int}} end """ @@ -206,7 +171,7 @@ given. The pipeline represents a series of actions (stages) executed during rendering. """ function Pipeline() - return Pipeline(Stage[], Connection[]) + return Pipeline(Stage[], Dict{Tuple{Int, Int}, Int}(), BufferFormat[]) end function Pipeline(stages::Stage...) pipeline = Pipeline() @@ -221,14 +186,16 @@ end # render -> effect 2 -' # where render is the same (name/task, inputs, outputs) function Base.push!(pipeline::Pipeline, stage::Stage) - isempty(stage.input_connections) || error("Pushed stage $(stage.name) must not have input connections.") - isempty(stage.output_connections) || error("Pushed stage $(stage.name) must not have output connections.") push!(pipeline.stages, stage) return stage # for convenience end function Base.push!(pipeline::Pipeline, other::Pipeline) + N = length(pipeline.stages); M = length(pipeline.formats) append!(pipeline.stages, other.stages) - append!(pipeline.connections, other.connections) + for ((stage_idx, io_idx), format_idx) in other.stageio2idx + pipeline.stageio2idx[(stage_idx + N, io_idx)] = M + format_idx + end + append!(pipeline.formats, other.formats) return other # for convenience end @@ -295,10 +262,12 @@ function Observables.connect!(pipeline::Pipeline, end function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) - return connect!(pipeline, pipeline.stages[src], output, pipeline.stages[trg], input) + return connect!(pipeline, src, output, trg, input) end -function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, trg::Integer, input::Integer) - return connect!(pipeline, pipeline.stages[src], output, pipeline.stages[trg], input) +function Observables.connect!(pipeline::Pipeline, source::Stage, output::Integer, target::Stage, input::Integer) + src = findfirst(x -> x === source, pipeline.stages) + trg = findfirst(x -> x === target, pipeline.stages) + return connect!(pipeline, src, output, trg, input) end function Observables.connect!(pipeline::Pipeline, source::Stage, output::Symbol, target::Stage, input::Symbol) @@ -306,79 +275,75 @@ function Observables.connect!(pipeline::Pipeline, source::Stage, output::Symbol, haskey(target.inputs, input) || error("input $input does not exist in target stage") output_idx = source.outputs[output] input_idx = target.inputs[input] - return @inbounds connect!(pipeline, source, output_idx, target, input_idx) + return connect!(pipeline, source, output_idx, target, input_idx) end -function Observables.connect!(pipeline::Pipeline, - source::Stage, output_idx::Integer, target::Stage, input_idx::Integer) - +function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, trg::Integer, input::Integer) @boundscheck begin - checkbounds(source.output_formats, output_idx) - checkbounds(target.input_formats, input_idx) + checkbounds(pipeline.stages, src) + checkbounds(pipeline.stages, trg) end - # resize if too small (this allows later connections to be skipped) - if length(source.output_connections) < output_idx - resize!(source.output_connections, output_idx) - end - if length(target.input_connections) < input_idx - resize!(target.input_connections, input_idx) + @boundscheck begin + checkbounds(pipeline.stages[src].output_formats, output) + checkbounds(pipeline.stages[trg].input_formats, input) end # Don't make a new connection if the connection already exists # (the format must be correct if it exists) - if isassigned(source.output_connections, output_idx) && - isassigned(target.input_connections, input_idx) && - (source.output_connections[output_idx] === target.input_connections[input_idx]) - return source.output_connections[output_idx] + if get(pipeline.stageio2idx, (src, output), 0) === + get(pipeline.stageio2idx, (trg, -input), -1) + return end - function unsafe_merge(c1::Connection, c2::Connection) - for (stage, idx) in c2.inputs - any(kv -> kv[1] === stage, c1.inputs) || push!(c1.inputs, stage => idx) - end - for (stage, idx) in c2.outputs - any(kv -> kv[1] === stage, c1.outputs) || push!(c1.outputs, stage => idx) + # format for the requested connection + format = BufferFormat(pipeline.stages[src].output_formats[output], pipeline.stages[trg].input_formats[input]) + + # Resolve format and update existing connection of src & trg + if haskey(pipeline.stageio2idx, (src, output)) + # at least src exists, update format + format_idx = pipeline.stageio2idx[(src, output)] + format = BufferFormat(pipeline.formats[format_idx], format) + pipeline.formats[format_idx] = format + + if haskey(pipeline.stageio2idx, (trg, -input)) + # both exist - update + other_idx = pipeline.stageio2idx[(src, output)] + format = BufferFormat(pipeline.formats[other_idx], format) + # replace format of lower index + format_idx, other_idx = minmax(format_idx, other_idx) + pipeline.formats[format_idx] = format + # connect higher index to format and adjust later indices + for (k, v) in pipeline.stageio2idx + pipeline.stage2idx[k] = ifelse(v == other_idx, format_idx, ifelse(v > other_idx, v - 1, v)) + end + # remove orphaned format + deleteat!(pipeline.formats, other_idx) + else + # src exists, trg doesn't -> connect trg + pipeline.stageio2idx[(trg, -input)] = format_idx end - return Connection(c1.inputs, c1.outputs, BufferFormat(c1.format, c2.format)) - end + elseif haskey(pipeline.stageio2idx, (trg, -input)) - # create requested connection - connection = Connection(source, output_idx, target, input_idx) - - # if the input or output already has an edge, merge it with the create edge - # e.g. the color output of source is used for a second stage - # or the color input of target is written to by second stage - if isassigned(source.output_connections, output_idx) - old = source.output_connections[output_idx] - # There should be exactly one matching connection, and it's probably - # near the end? - idx = findlast(c -> c === old, pipeline.connections)::Int - deleteat!(pipeline.connections, idx) - connection = unsafe_merge(connection, old) - end - if isassigned(target.input_connections, input_idx) - old = target.input_connections[input_idx] - idx = findlast(c -> c === old, pipeline.connections)::Int - deleteat!(pipeline.connections, idx) - connection = unsafe_merge(connection, old) - end + # src doesn't exist, trg does, modify target and connect src + format_idx = pipeline.stageio2idx[(trg, -input)] + format = BufferFormat(pipeline.formats[format_idx], format) + pipeline.formats[format_idx] = format + pipeline.stageio2idx[(src, output)] = format_idx - # attach connection to every input and output - for (stage, idx) in connection.inputs - stage.output_connections[idx] = connection - end - for (stage, idx) in connection.outputs - stage.input_connections[idx] = connection - end + else - push!(pipeline.connections, connection) + # neither exists, add new and connect both + push!(pipeline.formats, format) + pipeline.stageio2idx[(src, output)] = length(pipeline.formats) + pipeline.stageio2idx[(trg, -input)] = length(pipeline.formats) + + end - return connection + return end -format_complexity(c::Connection) = format_complexity(c.format) format_complexity(f::BufferFormat) = format_complexity(f.dims, f.type) format_complexity(dims, type) = dims * BFT.bytesize(type) # complexity of merged, not max of either @@ -393,35 +358,43 @@ optimize buffers for the lowest memory overhead. I.e. it will reuse buffers for multiple connections and upgrade them if it is cheaper than creating a new one. """ function generate_buffers(pipeline::Pipeline) - stage2idx = Dict{UInt64, Int64}() - for (i, stage) in enumerate(pipeline.stages) - # TODO: treat this? - # undef inputs should probably not be allowed at all because the shader - # need to get something and we don't know a reasonable default - # undef (intermediate) outputs could be allowed. They just need to exist - # for 1+ transfer, i.e. could be rewritten immediately after - valid_inputs = all(i -> isassigned(stage.input_connections, i), eachindex(stage.input_connections)) - valid_outputs = all(i -> isassigned(stage.output_connections, i), eachindex(stage.output_connections)) - if !(valid_inputs && valid_outputs) - error("$stage has an incomplete set of $(valid_inputs ? "output" : "input") connections") + # Verify that outputs are continuously connected (i.e. if N then 1..N-1 as well) + output_max = zeros(Int, length(pipeline.stages)) + output_sum = zeros(Int, length(pipeline.stages)) + for (stage_idx, io_idx) in keys(pipeline.stageio2idx) + io_idx > 0 || continue # inputs irrelevant + output_max[stage_idx] = max(output_max[stage_idx], io_idx) + output_sum[stage_idx] = output_sum[stage_idx] + io_idx # or use max(0, io_idx) and skip continue? + end + # sum must be 1 + 2 + ... + n = n(n+1)/2 + for i in eachindex(output_sum) + s = output_sum[i]; m = output_max[i] + if s != div(m*(m+1), 2) + error("Stage $i has an incomplete set of output connections.") end - stage2idx[objectid(stage)] = i end # Group connections that exist between stages - usage_per_transfer = [Connection[] for _ in 1:length(pipeline.stages)-1] - for connection in pipeline.connections - first = mapreduce(kv -> stage2idx[objectid(kv[1])], min, connection.inputs) - last = mapreduce(kv -> stage2idx[objectid(kv[1])]-1, max, connection.outputs) - first <= last || error("Connection $connection is read before it is written to. $first $last") - for i in first:last - push!(usage_per_transfer[i], connection) + endpoints = [(999_999, 0) for _ in pipeline.formats] + for ((stage_idx, io_idx), format_idx) in pipeline.stageio2idx + start, stop = endpoints[format_idx] + start = min(start, stage_idx + (io_idx < 0) * 999_999) # always pick start if stage_idx is input + stop = max(stop, (io_idx < 0) * stage_idx - 1) # pick stop if io_idx is output + endpoints[format_idx] = (start, stop) + end + filter!(x -> x != (999_999, 0), endpoints) + + usage_per_transfer = [Int[] for _ in 1:length(pipeline.stages)-1] + for (conn_idx, (start, stop)) in enumerate(endpoints) + start <= stop || error("Connection $conn_idx is read before it is written to. $start $stop") + for i in start:stop + push!(usage_per_transfer[i], conn_idx) end end buffers = BufferFormat[] - connection2idx = Dict{Connection, Int}() # into buffer - needs_buffer = Connection[] + conn2merged = fill(-1, length(pipeline.formats)) + needs_buffer = Int[] available = Int[] # Let's simplify to get correct behavior first... @@ -434,36 +407,37 @@ function generate_buffers(pipeline::Pipeline) empty!(needs_buffer) for j in max(1, i-1):i # for j in i:min(length(usage_per_transfer), i+1) # reverse - for connection in usage_per_transfer[j] - if haskey(connection2idx, connection) - idx = connection2idx[connection] + for conn_idx in usage_per_transfer[j] + if conn2merged[conn_idx] != -1 + idx = conn2merged[conn_idx] filter!(!=(idx), available) elseif j == i - push!(needs_buffer, connection) + push!(needs_buffer, conn_idx) end end end # Handle most expensive connections first - sort!(needs_buffer, by = format_complexity, rev = true) + sort!(needs_buffer, by = i -> format_complexity(pipeline.formats[i]), rev = true) - for connection in needs_buffer + for conn_idx in needs_buffer # search for most compatible buffer best_match = 0 prev_comp = 999999 prev_delta = 999999 - conn_comp = format_complexity(connection.format) + conn_format = pipeline.formats[conn_idx] + conn_comp = format_complexity(conn_format) for i in available - if buffers[i] == connection.format # exact match + if buffers[i] == conn_format # exact match best_match = i break - elseif is_compatible(buffers[i], connection.format) + elseif is_compatible(buffers[i], conn_format) # found compatible buffer, but we only use it if # - using it is cheaper than using the last # - using it is cheaper than creating a new buffer # - it is more compatible than the last when both are 0 cost # (i.e prefer 3, Float16 over 3 Float8 for 3 Float16 target) - updated_comp = format_complexity(buffers[i], connection.format) + updated_comp = format_complexity(buffers[i], conn_format) buffer_comp = format_complexity(buffers[i]) delta = updated_comp - buffer_comp is_cheaper = (delta < prev_delta) && (delta <= conn_comp) @@ -478,23 +452,23 @@ function generate_buffers(pipeline::Pipeline) if best_match == 0 # nothing compatible found/available, add format - push!(buffers, connection.format) + push!(buffers, conn_format) best_match = length(buffers) - elseif buffers[best_match] != connection.format + elseif buffers[best_match] != conn_format # found upgradeable format, upgrade it (or use it) - new_format = BufferFormat(buffers[best_match], connection.format) + new_format = BufferFormat(buffers[best_match], conn_format) buffers[best_match] = new_format end # Link to new found or upgraded format - connection2idx[connection] = best_match + conn2merged[conn_idx] = best_match # Can't use a buffer twice in one transfer filter!(!=(best_match), available) end end - return buffers, connection2idx + return buffers, conn2merged end @@ -521,8 +495,7 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) sort!(ks, by = k -> stage.inputs[k]) pad = mapreduce(k -> length(string(k)), max, ks) for (i, k) in enumerate(ks) - mark = isassigned(stage.input_connections, i) ? 'x' : ' ' - print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.input_formats[i]) + print(io, "\n [?] ", lpad(string(k), pad), "::", stage.input_formats[i]) end end @@ -532,8 +505,7 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) sort!(ks, by = k -> stage.outputs[k]) pad = mapreduce(k -> length(string(k)), max, ks) for (i, k) in enumerate(ks) - mark = isassigned(stage.output_connections, i) ? 'x' : ' ' - print(io, "\n [$mark] ", lpad(string(k), pad), "::", stage.output_formats[i]) + print(io, "\n [?] ", lpad(string(k), pad), "::", stage.output_formats[i]) end end @@ -548,128 +520,58 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) return end -function _names(connection::Connection) - names = Set{Symbol}() - for (stage, idx) in connection.inputs - for (k, i) in stage.outputs - if idx == i - push!(names, k) - break - end - end - end - for (stage, idx) in connection.outputs - for (k, i) in stage.inputs - if idx == i - push!(names, k) - break - end - end - end - return collect(names) -end - -function Base.show(io::IO, connection::Connection) - names = _names(connection) - print(io, "Connection(") - if length(names) == 1 - print(io, names[1]) - else - print(io, names) - end - print(io, " -> $(connection.format))") +function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) + return show_resolved(io, pipeline, pipeline.formats, collect(eachindex(pipeline.formats))) end -function Base.show(io::IO, ::MIME"text/plain", connection::Connection) - print(io, "Connection($(connection.format))") - - if !isempty(connection.inputs) - print(io, "\ninputs:") - elements = map(connection.inputs) do (stage, idx) - key = :temp - for (k, i) in stage.outputs - if idx == i - key = k - break - end - end - return (string(stage.name), string(key), stage.output_formats[idx]) - end - pad1 = mapreduce(x -> length(x[1]), max, elements) - pad2 = mapreduce(x -> length(x[2]), max, elements) - for (name, key, format) in elements - print(io, "\n ", rpad(name, pad1), " -> ", lpad(key, pad2), "::", format) - end - end - - if !isempty(connection.outputs) - print(io, "\noutputs:") - elements = map(connection.outputs) do (stage, idx) - key = :temp - for (k, i) in stage.inputs - if idx == i - key = k - break - end - end - return (string(stage.name), string(key), stage.input_formats[idx]) - end - pad1 = mapreduce(x -> length(x[1]), max, elements) - pad2 = mapreduce(x -> length(x[2]), max, elements) - for (name, key, format) in elements - print(io, "\n ", rpad(name, pad1), " -> ", lpad(key, pad2), "::", format) - end - end - - return +function show_resolved(pipeline::Pipeline, buffers, remap) + return show_resolved(stdout, pipeline, buffers, remap) end -function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) - conn2idx = Dict{Connection, Int}([c => i for (i, c) in enumerate(pipeline.connections)]) - return show_resolved(io, pipeline, pipeline.connections, conn2idx) -end -function show_resolved(pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) - return show_resolved(stdout, pipeline, buffers, conn2idx) -end -function show_resolved(io::IO, pipeline::Pipeline, buffers, conn2idx::Dict{Connection, Int}) +function show_resolved(io::IO, pipeline::Pipeline, buffers, remap) println(io, "Pipeline():") print(io, "Stages:") - pad = 1 + floor(Int, log10(length(buffers))) + pad = isempty(buffers) ? 0 : 1 + floor(Int, log10(length(buffers))) - for stage in pipeline.stages + for (stage_idx, stage) in enumerate(pipeline.stages) print(io, "\n Stage($(stage.name))") - if !isempty(stage.input_connections) + + if !isempty(stage.input_formats) print(io, "\n inputs: ") - # keep order - strs = map(eachindex(stage.input_connections)) do i + strs = map(eachindex(stage.input_formats)) do i k = findfirst(==(i), stage.inputs) - if isassigned(stage.input_connections, i) - c = stage.input_connections[i] - ci = string(conn2idx[c]) - return "[$ci] $k" + if haskey(pipeline.stageio2idx, (stage_idx, -i)) + conn = string(remap[pipeline.stageio2idx[(stage_idx, -i)]]) else - return "[ ] $k" + conn = " " end + return "[$conn] $k" + end + while length(strs) > 0 && startswith(last(strs), "[ ]") + pop!(strs) end join(io, strs, ", ") end - if !isempty(stage.output_connections) + + if !isempty(stage.output_formats) print(io, "\n outputs: ") - strs = map(eachindex(stage.output_connections)) do i + strs = map(eachindex(stage.output_formats)) do i k = findfirst(==(i), stage.outputs) - if isassigned(stage.output_connections, i) - c = stage.output_connections[i] - ci = string(conn2idx[c]) - return "[$ci] $k" + if haskey(pipeline.stageio2idx, (stage_idx, i)) + conn = string(remap[pipeline.stageio2idx[(stage_idx, i)]]) else - return "[#undef] $k" + conn = "#undef" end + return "[$conn] $k" + end + while length(strs) > 0 && startswith(last(strs), "[#undef]") + pop!(strs) end join(io, strs, ", ") end end - println(io, "\nConnections:") + println(io, "\nConnection Formats:") for (i, c) in enumerate(buffers) s = lpad("$i", pad) println(io, " [$s] ", c) diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index 1883e94ae10..a74c90f92ce 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -1,7 +1,7 @@ using Makie using Makie: BufferFormat, N0f8, is_compatible, BFT using Makie: Stage, get_input_connection, get_output_connection, get_input_format, get_output_format -using Makie: Connection, Pipeline, connect! +using Makie: Pipeline, connect! using Makie: generate_buffers, default_pipeline @testset "Render Pipeline" begin @@ -80,9 +80,7 @@ using Makie: generate_buffers, default_pipeline @test stage.outputs == Dict{Symbol, Int}() @test stage.input_formats == BufferFormat[] @test stage.output_formats == BufferFormat[] - @test stage.input_connections == Connection[] - @test stage.output_connections == Connection[] - @test stage.attributes == NamedTuple() + @test stage.attributes == Dict{Symbol, Any}() stage = Stage(:test, inputs = [:a => BufferFormat(), :b => BufferFormat(2)], @@ -93,9 +91,7 @@ using Makie: generate_buffers, default_pipeline @test stage.outputs == Dict(:c => 1) @test stage.input_formats == [BufferFormat(), BufferFormat(2)] @test stage.output_formats == [BufferFormat(1, Int8)] - @test stage.input_connections == Connection[] - @test stage.output_connections == Connection[] - @test stage.attributes == (attr = 17f0,) + @test stage.attributes == Dict{Symbol, Any}(:attr => 17f0) @test get_input_format(stage, :a) == stage.input_formats[stage.inputs[:a]] @test get_output_format(stage, :c) == stage.output_formats[stage.outputs[:c]] @@ -105,7 +101,8 @@ using Makie: generate_buffers, default_pipeline pipeline = Pipeline() @test isempty(pipeline.stages) - @test isempty(pipeline.connections) + @test isempty(pipeline.stageio2idx) + @test isempty(pipeline.formats) stage1 = Stage(:stage1, outputs = [:a => BufferFormat(), :b => BufferFormat(2)]) stage2 = Stage(:stage2, @@ -123,63 +120,54 @@ using Makie: generate_buffers, default_pipeline push!(pipeline, stage5) @test all(pipeline.stages .== [stage1, stage2, stage3, stage4, stage5]) - @test isempty(pipeline.connections) + @test isempty(pipeline.stageio2idx) + @test isempty(pipeline.formats) - connect!(pipeline, stage1, stage2) + for _ in 1:2 # also verify that double-connect doesn't ruin things + connect!(pipeline, stage1, stage2) - @test length(pipeline.connections) == 1 - c1 = pipeline.connections[end] - @test c1.format == BufferFormat(2) - @test c1.inputs == [stage1 => 2] - @test c1.outputs == [stage2 => 1] - @test stage1.output_connections[2] == c1 - @test stage2.input_connections[1] == c1 + @test length(pipeline.formats) == 1 + @test length(pipeline.stageio2idx) == 2 + @test pipeline.formats[end] == BufferFormat(2) + @test pipeline.stageio2idx[(1, 2)] == 1 + @test pipeline.stageio2idx[(2, -1)] == 1 + end connect!(pipeline, stage2, stage3, :c) - @test length(pipeline.connections) == 2 - c2 = pipeline.connections[end] - @test c2.format == BufferFormat(2, Int16) - @test c2.inputs == [stage2 => 1] - @test c2.outputs == [stage3 => 2] - @test stage2.output_connections[1] == c2 - @test stage3.input_connections[2] == c2 + @test length(pipeline.formats) == 2 + @test length(pipeline.stageio2idx) == 4 + @test pipeline.formats[end] == BufferFormat(2, Int16) + @test pipeline.stageio2idx[(2, 1)] == 2 + @test pipeline.stageio2idx[(3, -2)] == 2 connect!(pipeline, stage1, stage3, :b) - @test length(pipeline.connections) == 2 - c3 = pipeline.connections[end] - @test c1 !== c3 - @test c2 !== c3 - @test c3.format == BufferFormat(4, Float16) - @test c3.inputs == [stage1 => 2] - @test c3.outputs == [stage3 => 1, stage2 => 1] # technically order irrelevant - @test stage1.output_connections[2] == c3 - @test stage2.input_connections[1] == c3 - @test stage3.input_connections[1] == c3 + @test length(pipeline.formats) == 2 + @test length(pipeline.stageio2idx) == 5 + @test pipeline.formats[pipeline.stageio2idx[(1, 2)]] == BufferFormat(4, Float16) + @test pipeline.stageio2idx[(1, 2)] == 1 + @test pipeline.stageio2idx[(2, -1)] == 1 + @test pipeline.stageio2idx[(3, -1)] == 1 # Stage 1 incomplete - if output 2 is connected all previous outputs must be connected too @test_throws Exception generate_buffers(pipeline) connect!(pipeline, stage1, :a, stage4, :x) - @test length(pipeline.connections) == 3 - c4 = pipeline.connections[end] - @test c4.format == BufferFormat() - @test c4.inputs == [stage1 => 1] - @test c4.outputs == [stage4 => 1] - @test stage1.output_connections[1] == c4 - @test stage4.input_connections[1] == c4 + @test length(pipeline.formats) == 3 + @test length(pipeline.stageio2idx) == 7 + @test pipeline.formats[end] == BufferFormat() + @test pipeline.stageio2idx[(1, 1)] == 3 + @test pipeline.stageio2idx[(4, -1)] == 3 connect!(pipeline, stage4, :y, stage5, :z) - @test length(pipeline.connections) == 4 - c5 = pipeline.connections[end] - @test c5.format == BufferFormat() - @test c5.inputs == [stage4 => 1] - @test c5.outputs == [stage5 => 1] - @test stage4.output_connections[1] == c5 - @test stage5.input_connections[1] == c5 + @test length(pipeline.formats) == 4 + @test length(pipeline.stageio2idx) == 9 + @test pipeline.formats[end] == BufferFormat() + @test pipeline.stageio2idx[(4, 1)] == 4 + @test pipeline.stageio2idx[(5, -1)] == 4 #= 1 2 3 4 5 (stages) @@ -187,16 +175,17 @@ using Makie: generate_buffers, default_pipeline b -+----------> b '-> b c ---> c =# - buffers, conn2idx = generate_buffers(pipeline) + buffers, remap = generate_buffers(pipeline) @test length(buffers) == 3 # 3 buffer textures are needed for transfers - @test length(conn2idx) == 4 # 4 connections map to them - @test buffers[conn2idx[c2]] == c2.format - @test buffers[conn2idx[c3]] == c3.format - @test buffers[conn2idx[c4]] == c4.format - # 1 a --> 4 x not yet available for reuse - # 1 b --> 2 b, 3b available, upgrades - # 2 c --> 3 c not allowed, incompatible types - @test buffers[conn2idx[c5]] == c3.format + @test length(remap) == 4 # 4 connections map to them + @test buffers[remap[1]] == pipeline.formats[1] + @test buffers[remap[2]] == pipeline.formats[2] + @test buffers[remap[3]] == pipeline.formats[3] + # reuse from (4 x --> 5 z): + # (1 a --> 4 x) not yet available for reuse + # (1 b --> 2 b, 3b) available, upgrades + # (2 c --> 3 c) not allowed, incompatible types (int16) + @test buffers[remap[4]] == pipeline.formats[1] end @testset "default pipeline" begin @@ -218,49 +207,59 @@ using Makie: generate_buffers, default_pipeline Dict(:color => 1), [BufferFormat(4, N0f8)]) @test pipeline.stages[5] == Stage(:FXAA1, Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], - Dict(:color_luma => 1), [BufferFormat(4, N0f8)]) + Dict(:color_luma => 1), [BufferFormat(4, N0f8)], filter_in_shader = true) @test pipeline.stages[6] == Stage(:FXAA2, Dict(:color_luma => 1), [BufferFormat(4, N0f8, minfilter = :linear)], - Dict(:color => 1), [BufferFormat(4, N0f8)]) + Dict(:color => 1), [BufferFormat(4, N0f8)], filter_in_shader = true) @test pipeline.stages[7] == Stage(:Display, Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], Dict{Symbol, Int}(), BufferFormat[]) # Note: Order technically irrelevant but it's easier to test with order # Same for inputs and outputs here - @test length(pipeline.connections) == 6 - stage1, stage2, stage3, stage4, stage5, stage6, stage7 = pipeline.stages - @test pipeline.connections[1] == Connection([stage5 => 1], [stage6 => 1], BufferFormat(4, N0f8, minfilter = :linear)) - @test pipeline.connections[2] == Connection([stage3 => 1], [stage4 => 1], BufferFormat(4, Float16)) - @test pipeline.connections[3] == Connection([stage3 => 3], [stage4 => 2], BufferFormat(1, N0f8)) - @test pipeline.connections[4] == Connection([stage4 => 1, stage2 => 1], [stage5 => 1], BufferFormat(4, N0f8)) - @test pipeline.connections[5] == Connection([stage3 => 2, stage2 => 2], [stage7 => 2, stage5 => 2], BufferFormat(2, UInt32)) - @test pipeline.connections[6] == Connection([stage6 => 1], [stage7 => 1], BufferFormat(4, N0f8)) + @test length(pipeline.formats) == 6 + @test length(pipeline.stageio2idx) == 15 + @test pipeline.formats[pipeline.stageio2idx[(5, 1)]] == BufferFormat(4, N0f8, minfilter = :linear) + @test pipeline.formats[pipeline.stageio2idx[(6, -1)]] == BufferFormat(4, N0f8, minfilter = :linear) + @test pipeline.formats[pipeline.stageio2idx[(3, 1)]] == BufferFormat(4, Float16) + @test pipeline.formats[pipeline.stageio2idx[(4, -1)]] == BufferFormat(4, Float16) + @test pipeline.formats[pipeline.stageio2idx[(3, 3)]] == BufferFormat(1, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(4, -2)]] == BufferFormat(1, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(4, 1)]] == BufferFormat(4, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(2, 1)]] == BufferFormat(4, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(5, -1)]] == BufferFormat(4, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(3, 2)]] == BufferFormat(2, UInt32) + @test pipeline.formats[pipeline.stageio2idx[(2, 2)]] == BufferFormat(2, UInt32) + @test pipeline.formats[pipeline.stageio2idx[(7, -2)]] == BufferFormat(2, UInt32) + @test pipeline.formats[pipeline.stageio2idx[(5, -2)]] == BufferFormat(2, UInt32) + @test pipeline.formats[pipeline.stageio2idx[(6, 1)]] == BufferFormat(4, N0f8) + @test pipeline.formats[pipeline.stageio2idx[(7, -1)]] == BufferFormat(4, N0f8) # Verify buffer generation with this more complex example - buffers, conn2idx = generate_buffers(pipeline) + buffers, remap = generate_buffers(pipeline) - # Order irrelevant + # Order in buffers irrelevant as long as the mapping works + # Note: all outputs are unique so we don't have to explicitly test + # which one we hit + # Note: Changes to generate_buffers could change how formats get merged + # and cause different correct results @test length(buffers) == 4 - formats = [ - :color => BufferFormat(), :objectid => BufferFormat(2, UInt32), :weight => BufferFormat(1, N0f8), - :HDR => BufferFormat(4, Float16, minfilter = :linear) - ] - lookup = Dict{Symbol, Int}() - for (name, format) in formats - idx = findfirst(==(format), buffers) - @test idx !== nothing - lookup[name] = idx::Int - end - # Sanity check for assumption that none of the formats are equal - @test sum(values(lookup)) == 1 + 2 + 3 + 4 - - @test length(conn2idx) == 6 - @test conn2idx[pipeline.connections[1]] == lookup[:HDR] - @test conn2idx[pipeline.connections[2]] == lookup[:HDR] # compatible and no overlap with connection (1) - @test conn2idx[pipeline.connections[3]] == lookup[:weight] - @test conn2idx[pipeline.connections[4]] == lookup[:color] - @test conn2idx[pipeline.connections[5]] == lookup[:objectid] - @test conn2idx[pipeline.connections[6]] == lookup[:color] # compatible and no overlap with (4) + @test length(remap) == 6 + + @test buffers[remap[pipeline.stageio2idx[(5, 1)]]] == BufferFormat(4, Float16, minfilter = :linear) + @test buffers[remap[pipeline.stageio2idx[(6, -1)]]] == BufferFormat(4, Float16, minfilter = :linear) + @test buffers[remap[pipeline.stageio2idx[(3, 1)]]] == BufferFormat(4, Float16, minfilter = :linear) + @test buffers[remap[pipeline.stageio2idx[(4, -1)]]] == BufferFormat(4, Float16, minfilter = :linear) + @test buffers[remap[pipeline.stageio2idx[(3, 3)]]] == BufferFormat(1, N0f8) + @test buffers[remap[pipeline.stageio2idx[(4, -2)]]] == BufferFormat(1, N0f8) + @test buffers[remap[pipeline.stageio2idx[(4, 1)]]] == BufferFormat(4, N0f8) + @test buffers[remap[pipeline.stageio2idx[(2, 1)]]] == BufferFormat(4, N0f8) + @test buffers[remap[pipeline.stageio2idx[(5, -1)]]] == BufferFormat(4, N0f8) + @test buffers[remap[pipeline.stageio2idx[(3, 2)]]] == BufferFormat(2, UInt32) + @test buffers[remap[pipeline.stageio2idx[(2, 2)]]] == BufferFormat(2, UInt32) + @test buffers[remap[pipeline.stageio2idx[(7, -2)]]] == BufferFormat(2, UInt32) + @test buffers[remap[pipeline.stageio2idx[(5, -2)]]] == BufferFormat(2, UInt32) + @test buffers[remap[pipeline.stageio2idx[(6, 1)]]] == BufferFormat(4, N0f8) + @test buffers[remap[pipeline.stageio2idx[(7, -1)]]] == BufferFormat(4, N0f8) end end \ No newline at end of file From 6992b741251396036128246cf11dd6f5ce07271e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 02:14:20 +0100 Subject: [PATCH 094/135] cleanup rebase --- GLMakie/src/GLAbstraction/GLFrameBuffer.jl | 19 +++++-------------- GLMakie/src/GLAbstraction/GLInfo.jl | 11 +++++++++-- GLMakie/src/GLAbstraction/GLTypes.jl | 3 ++- GLMakie/src/gl_backend.jl | 9 --------- GLMakie/src/glwindow.jl | 4 ++-- GLMakie/src/render_pipeline.jl | 15 ++++++++++----- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl index c379149a19c..57e04bd25a1 100644 --- a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLFrameBuffer.jl @@ -10,16 +10,18 @@ mutable struct GLFramebuffer buffers::Vector{Texture} counter::UInt32 # for color attachments - function GLFramebuffer(size::NTuple{2, Int}) + function GLFramebuffer(context, size::NTuple{2, Int}) + require_context(context) + # Create framebuffer id = glGenFramebuffers() glBindFramebuffer(GL_FRAMEBUFFER, id) obj = new( - id, size, current_context(), + id, size, context, Dict{Symbol, Int}(), GLenum[], Texture[], UInt32(0) ) - finalizer(free, obj) + finalizer(verify_free, obj) return obj end @@ -63,15 +65,6 @@ function set_draw_buffers(fb::GLFramebuffer, keys::Symbol...) end function unsafe_free(x::GLFramebuffer) - # don't free if already freed - x.id == 0 && return - # don't free from other context - if require_cleanup_before_context_deletion() - GLAbstraction.context_alive(x.context) || error("Can't delete Program - context not alive.") - else - GLAbstraction.context_alive(x.context) || return - end - GLAbstraction.switch_context!(x.context) id = Ref(x.id) glDeleteFramebuffers(1, id) x.id = 0 @@ -131,11 +124,9 @@ function attach(fb::GLFramebuffer, key::Symbol, buffer, idx::Integer, attachment end try - require_context(fb.context) bind(fb) gl_attach(buffer, attachment) check_framebuffer() - require_context(fb.context) catch e if GL_COLOR_ATTACHMENT0 <= attachment <= GL_COLOR_ATTACHMENT15 # If we failed to attach correctly we should probably overwrite diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl index 7b86a0d026c..ad60e731d9c 100644 --- a/GLMakie/src/GLAbstraction/GLInfo.jl +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -100,9 +100,16 @@ end const FAILED_FREE_COUNTER = Ref(0) -function verify_free(obj::T, name = string(T)) where T +function verify_free(obj::T, name = T) where T if obj.id != 0 FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 Threads.@spawn println(stderr, "Error: $name has not been freed.") end -end \ No newline at end of file +end + +# function verify_free(obj::T, name = string(T)) where T +# if obj.id != 0 +# FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 +# Threads.@spawn println(stderr, "Error: $name has not been freed.") +# end +# end \ No newline at end of file diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 09e37cf367a..411b5d1ac9e 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -436,7 +436,8 @@ function free(x::T, called_from_finalizer = false) where {T} end else if !context_alive(x.context) - @warn "free(::$T) called with dead context." + # @warn "free(::$T) called with dead context." + error("free(::$T) called with dead context.") x.id = 0 return end diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index ed25c64b9c1..1217350ebb1 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -45,15 +45,6 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer return false end end -end - -function get_texture!(atlas::Makie.TextureAtlas) - current_ctx = GLAbstraction.current_context() - if !GLAbstraction.context_alive(current_ctx) - return nothing - end - - cleanup_texture_atlas(current_ctx, atlas) if haskey(atlas_texture_cache, (atlas, context)) return atlas_texture_cache[(atlas, context)][1] diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 7af6ce40f7f..b15706c6c48 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -36,7 +36,7 @@ Makie.@noconstprop function FramebufferFactory(context, fb_size::NTuple{2, Int}) format = GL_DEPTH_STENCIL ) - fb = GLFramebuffer(fb_size) + fb = GLFramebuffer(context, fb_size) attach_depthstencilbuffer(fb, :depth_stencil, depth_buffer) return FramebufferFactory(fb, Texture[], GLFramebuffer[]) @@ -85,7 +85,7 @@ end Makie.@noconstprop function generate_framebuffer(factory::FramebufferFactory, idx2name::Pair{Int, Symbol}...) filter!(fb -> fb.id != 0, factory.children) # cleanup? - fb = GLFramebuffer(size(factory)) + fb = GLFramebuffer(factory.fb.context, size(factory)) for (idx, name) in idx2name haskey(fb, name) && error("Can't add duplicate buffer $lookup => $name") diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 75b44cff429..e300bfc6027 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -4,7 +4,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF empty!(factory.children) # function barrier for types? - function get_buffer!(buffers, T, extras) + function get_buffer!(context, buffers, T, extras) # reuse internalformat = GLAbstraction.default_internalcolorformat(T) for (i, buffer) in enumerate(buffers) @@ -19,7 +19,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF if !(eltype(T) == N0f8 || eltype(T) <: AbstractFloat) && (interp == :linear) error("Cannot use :linear interpolation with non float types.") end - return Texture(T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) + return Texture(context, T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) end # reuse buffers that match formats (and make sure that factory.buffers @@ -28,7 +28,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF empty!(factory.buffers) for format in formats T = Makie.format_to_type(format) - tex = get_buffer!(buffers, T, format.extras) + tex = get_buffer!(factory.fb.context, buffers, T, format.extras) push!(factory.buffers, tex) end @@ -37,8 +37,9 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF # Always rebuild this though, since we don't know which buffers are the # final output buffers - fb = GLFramebuffer(size(factory)) + fb = GLFramebuffer(factory.fb.context, size(factory)) attach_depthstencilbuffer(fb, :depth_stencil, get_buffer(factory.fb, :depth_stencil)) + GLAbstraction.free(factory.fb) factory.fb = fb return factory @@ -77,8 +78,9 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) buffer_idx = remap[pipeline.stageio2idx[(N, -2)]] attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) - + needs_cleanup = collect(eachindex(previous_pipeline.steps)) render_pipeline = AbstractRenderStep[] + for (stage_idx, stage) in enumerate(pipeline.stages) inputs = Dict{Symbol, Any}() for (key, input_idx) in stage.inputs @@ -117,6 +119,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) else pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) + filter!(!=(idx), needs_cleanup) end # I guess stage should also have extra information for settings? Or should @@ -125,6 +128,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) push!(render_pipeline, pass) end + foreach(i -> destroy!(previous_pipeline.steps[i]), needs_cleanup) + screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) # was_running && start_renderloop!(screen) From e327caa976a2e47f9f4682f89e1e2ee56d7d04cb Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 02:30:08 +0100 Subject: [PATCH 095/135] minor cleanup --- GLMakie/src/GLAbstraction/GLInfo.jl | 9 +-------- GLMakie/src/GLAbstraction/GLRender.jl | 1 - GLMakie/src/GLAbstraction/GLShader.jl | 4 +--- GLMakie/src/GLAbstraction/GLTypes.jl | 6 +----- GLMakie/src/screen.jl | 10 +++------- 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl index ad60e731d9c..8534676f38c 100644 --- a/GLMakie/src/GLAbstraction/GLInfo.jl +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -105,11 +105,4 @@ function verify_free(obj::T, name = T) where T FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 Threads.@spawn println(stderr, "Error: $name has not been freed.") end -end - -# function verify_free(obj::T, name = string(T)) where T -# if obj.id != 0 -# FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 -# Threads.@spawn println(stderr, "Error: $name has not been freed.") -# end -# end \ No newline at end of file +end \ No newline at end of file diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index c04ed4874e9..a7c99b2be7e 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -74,7 +74,6 @@ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray renderobject.prerenderfunction() setup_clip_planes(to_value(get(renderobject.uniforms, :num_clip_planes, 0))) program = vertexarray.program - program.id == 0 && error("Program unloaded") glUseProgram(program.id) for (key, value) in program.uniformloc if haskey(renderobject.uniforms, key) diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 829ea8507d4..13da407dc31 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -233,9 +233,7 @@ function gl_convert(ctx::GLContext, cache::ShaderCache, lazyshader::AbstractLazy shaders[i] = get_shader!(cache, shader_source, tr) end ShaderAbstractions.switch_context!(cache.context) - x = compile_program(shaders, fragdatalocation) - require_context(cache.context) - return x + return compile_program(shaders, fragdatalocation) end end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 411b5d1ac9e..8ecaa9e2c98 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -225,7 +225,6 @@ function GLVertexArray(bufferdict::Dict, program::GLProgram) if indexes == -1 indexes = len end - require_context(program.context) obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indexes) finalizer(verify_free, obj) return obj @@ -299,7 +298,6 @@ mutable struct RenderObject{Pre} prerenderfunctions, postrenderfunctions, visible ) where Pre - require_context(context, vertexarray.context) fxaa = Bool(to_value(get!(uniforms, :fxaa, true))) RENDER_OBJECT_ID_COUNTER[] += one(UInt32) # Store fxaa in ID, so we can access it in the shader to create a mask @@ -396,7 +394,6 @@ function RenderObject( delete!(data, k) end end - require_context(context) robj = RenderObject{Pre}( context, @@ -436,8 +433,7 @@ function free(x::T, called_from_finalizer = false) where {T} end else if !context_alive(x.context) - # @warn "free(::$T) called with dead context." - error("free(::$T) called with dead context.") + @warn "free(::$T) called with dead context." x.id = 0 return end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 0bdb4ccb4f3..25ab4fc452d 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -107,11 +107,6 @@ mutable struct ScreenConfig monitor, visible, scalefactor isa Makie.Automatic ? nothing : Float32(scalefactor), - # Preproccessor - # Preprocessor - # oit, - # fxaa, - # ssao, render_pipeline, transparency_weight_scale, max_lights, @@ -381,7 +376,6 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] - # TODO: FXAA, OIT on-off gl_render_pipeline!(screen, config.render_pipeline) # TODO: replace shader programs with lighting to update N_lights & N_light_parameters @@ -645,11 +639,13 @@ function destroy!(screen::Screen) empty!(screen) end @assert screen.rendertask === nothing + # before texture atlas, otherwise it regenerates destroy!(screen.framebuffer_factory) destroy!(screen.render_pipeline) cleanup_texture_atlas!(window) GLAbstraction.free(screen.shader_cache) - # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) + # Since those are sets, we can just delete them from there, even if they + # weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) if screen in SINGLETON_SCREEN From abe3d91568895c457b4c53a6b864d734fa9b0638 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 14:01:52 +0100 Subject: [PATCH 096/135] put finalizers behind debug constant --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 ++ GLMakie/src/GLAbstraction/GLBuffer.jl | 2 +- GLMakie/src/GLAbstraction/GLInfo.jl | 3 +-- GLMakie/src/GLAbstraction/GLTexture.jl | 2 +- GLMakie/src/GLAbstraction/GLTypes.jl | 8 ++++---- GLMakie/test/runtests.jl | 3 +++ 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 4fd40232bee..005f81c4f1b 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -19,6 +19,8 @@ import FixedPointNumbers: N0f8, N0f16, N0f8, Normed import Base: merge, resize!, similar, length, getindex, setindex! # Debug tools +const GLMAKIE_DEBUG = Ref(false) + require_context() = nothing # implemented in GLMakie/glwindow export require_context diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index 8c67fbcc5db..18994a590ff 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -18,7 +18,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} obj = new( id, (buff_length,), buffertype, usage, context, Observables.ObserverFunction[]) - finalizer(verify_free, obj) + GLMAKIE_DEBUG[] && finalizer(verify_free, obj) obj end end diff --git a/GLMakie/src/GLAbstraction/GLInfo.jl b/GLMakie/src/GLAbstraction/GLInfo.jl index 7b86a0d026c..b11cd2d6953 100644 --- a/GLMakie/src/GLAbstraction/GLInfo.jl +++ b/GLMakie/src/GLAbstraction/GLInfo.jl @@ -99,8 +99,7 @@ function getProgramInfo(p::GLProgram) end const FAILED_FREE_COUNTER = Ref(0) - -function verify_free(obj::T, name = string(T)) where T +function verify_free(obj::T, name = T) where T if obj.id != 0 FAILED_FREE_COUNTER[] = FAILED_FREE_COUNTER[] + 1 Threads.@spawn println(stderr, "Error: $name has not been freed.") diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 3f232696f24..5b4f30ebb88 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -40,7 +40,7 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} Observables.ObserverFunction[] ) GLAbstraction.require_context(context) - finalizer(verify_free, tex) + GLMAKIE_DEBUG[] && finalizer(verify_free, tex) tex end end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index e60c2b57a54..fd2bf50f60e 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -30,7 +30,7 @@ mutable struct Shader function Shader(context, name, source, typ, id) obj = new(Symbol(name), source, typ, id, context) - finalizer(verify_free, obj) + GLMAKIE_DEBUG[] && finalizer(verify_free, obj) return obj end end @@ -66,7 +66,7 @@ mutable struct GLProgram context::GLContext function GLProgram(id::GLuint, shader::Vector{Shader}, nametype::Dict{Symbol,GLenum}, uniformloc::Dict{Symbol,Tuple}, context = first(shader).context) obj = new(id, shader, nametype, uniformloc, context) - finalizer(verify_free, obj) + GLMAKIE_DEBUG[] && finalizer(verify_free, obj) obj end end @@ -246,7 +246,7 @@ function GLVertexArray(bufferdict::Dict, program::GLProgram) indexes = len end obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indexes) - finalizer(verify_free, obj) + GLMAKIE_DEBUG[] && finalizer(verify_free, obj) return obj end using ShaderAbstractions: Buffer @@ -272,7 +272,7 @@ function GLVertexArray(program::GLProgram, buffers::Buffer, triangles::AbstractV glBindVertexArray(0) indices = indexbuffer(triangles) obj = GLVertexArray{typeof(indexes)}(program, id, len, buffers, indices) - finalizer(verify_free, obj) + GLMAKIE_DEBUG[] && finalizer(verify_free, obj) return obj end diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index bdf540cbbe9..a0cc65517e7 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -6,6 +6,9 @@ using GeometryBasics: origin using Random using ReferenceTests +# verify OpenGL object cleanup +GLMakie.GLAbstraction.GLMAKIE_DEBUG[] = true + if !GLMakie.ModernGL.enable_opengl_debugging # can't error, since we can't enable debugging for users @warn("TESTING WITHOUT OPENGL DEBUGGING") From bab465ea8d747af4e9e5eb706c856ae621458a5c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 14:30:46 +0100 Subject: [PATCH 097/135] don't skip observable cleanup in free --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 11 ++++++-- GLMakie/src/GLAbstraction/GLTypes.jl | 29 +++++++++++++--------- GLMakie/src/glwindow.jl | 2 +- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 005f81c4f1b..2dafd756762 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -21,8 +21,15 @@ import Base: merge, resize!, similar, length, getindex, setindex! # Debug tools const GLMAKIE_DEBUG = Ref(false) -require_context() = nothing # implemented in GLMakie/glwindow -export require_context +require_context() = nothing # implemented in GLMakie/glwindow.jl +function require_context_no_error(args...) + try + require_context(args...) + catch e + @error exception = (e, Base.catch_backtrace()) + end +end +export require_context, check_context include("AbstractGPUArray.jl") diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index fd2bf50f60e..2c76b5b2923 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -436,15 +436,14 @@ include("GLRenderObject.jl") #################################################################################### # freeing -# This may be called from the scene finalizer in which case no errors, no printing allowed function free(x::T, called_from_finalizer = false) where {T} - # don't free if already freed + # don't free if already freed (this should only be set by unsafe_free) x.id == 0 && return + + # This may be called from the scene finalizer in which case no errors, no printing allowed if called_from_finalizer - if !context_alive(x.context) + if GLMAKIE_DEBUG[] && !context_alive(x.context) Threads.@spawn println(stderr, "Warning: free(::$T) called with dead context from scene finalizer.") - x.id = 0 - return end try unsafe_free(x) @@ -452,13 +451,9 @@ function free(x::T, called_from_finalizer = false) where {T} Threads.@spawn Base.showerror(stderr, e) end else - if !context_alive(x.context) - @warn "free(::$T) called with dead context." - x.id = 0 - return - end - # context must be valid - require_context(x.context) + # This should be called with a valid, active context, but we shouldn't error + # here because unsafe_free() also sometimes cleans up observables + require_context_no_error(x.context) unsafe_free(x) end return @@ -474,12 +469,16 @@ end # OpenGL has the annoying habit of reusing id's when creating a new context # We need to make sure to only free the current one function unsafe_free(x::GLProgram) + x.id = ifelse(context_alive(x.context), x.id, 0) + is_context_active(x.context) || return glDeleteProgram(x.id) x.id = 0 return end function unsafe_free(x::Shader) + x.id = ifelse(context_alive(x.context), x.id, 0) + is_context_active(x.context) || return glDeleteShader(x.id) x.id = 0 return @@ -487,6 +486,8 @@ end function unsafe_free(x::GLBuffer) clean_up_observables(x) + x.id = ifelse(context_alive(x.context), x.id, 0) + is_context_active(x.context) || return id = Ref(x.id) glDeleteBuffers(1, id) x.id = 0 @@ -495,6 +496,8 @@ end function unsafe_free(x::Texture) clean_up_observables(x) + x.id = ifelse(context_alive(x.context), x.id, 0) + is_context_active(x.context) || return id = Ref(x.id) glDeleteTextures(x.id) x.id = 0 @@ -508,6 +511,8 @@ function unsafe_free(x::GLVertexArray) if x.indices isa GPUArray unsafe_free(x.indices) end + x.id = ifelse(context_alive(x.context), x.id, 0) + is_context_active(x.context) || return id = Ref(x.id) glDeleteVertexArrays(1, id) x.id = 0 diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 1c006e8aa5d..fa74b9c605e 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -220,7 +220,7 @@ function ShaderAbstractions.native_context_alive(x::GLFW.Window) end # require_context(ctx, current = nothing) = nothing -function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context(); warn = false) +function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context()) @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." @assert !was_destroyed(ctx) "Context $ctx must not be destroyed." @assert ctx.handle == current.handle "Context $ctx must be current, but $current is." From fa61c24fefad02cc084b4308fea729a999f5cd2c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 16:07:21 +0100 Subject: [PATCH 098/135] cleanup require_context --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 14 +++++---- GLMakie/src/GLAbstraction/GLBuffer.jl | 2 +- GLMakie/src/GLAbstraction/GLRender.jl | 5 +--- GLMakie/src/GLAbstraction/GLShader.jl | 8 ++++-- GLMakie/src/GLAbstraction/GLTexture.jl | 7 +---- GLMakie/src/GLAbstraction/GLTypes.jl | 26 +++++++++++------ GLMakie/src/gl_backend.jl | 25 +++++++++------- GLMakie/src/glwindow.jl | 33 +++++++++++++++++----- GLMakie/src/postprocessing.jl | 5 ++-- GLMakie/src/rendering.jl | 2 -- GLMakie/src/screen.jl | 23 +++++++++------ 11 files changed, 94 insertions(+), 56 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 2dafd756762..7440da82ac6 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -22,11 +22,15 @@ import Base: merge, resize!, similar, length, getindex, setindex! const GLMAKIE_DEBUG = Ref(false) require_context() = nothing # implemented in GLMakie/glwindow.jl -function require_context_no_error(args...) - try - require_context(args...) - catch e - @error exception = (e, Base.catch_backtrace()) +function require_context_no_error(args...; async = false) + if async + require_context(args..., true) + else + try + require_context(args...) + catch e + @error exception = (e, Base.catch_backtrace()) + end end end export require_context, check_context diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index 18994a590ff..3b902c0cedd 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -8,6 +8,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} observers::Vector{Observables.ObserverFunction} function GLBuffer{T}(context, ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T + require_context(context) id = glGenBuffers() glBindBuffer(buffertype, id) # size of 0 can segfault it seems @@ -34,7 +35,6 @@ end bind(buffer::GLBuffer, other_target) = glBindBuffer(buffer.buffertype, other_target) function similar(x::GLBuffer{T}, buff_length::Int) where T - require_context(x.context) return GLBuffer{T}(x.context, Ptr{T}(C_NULL), buff_length, x.buffertype, x.usage) end diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index 9131df2053d..9a6fdad6f07 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -14,6 +14,7 @@ function setup_clip_planes(N::Integer) end end +# Note: context required in renderloop, not per renderobject here """ When rendering a specialised list of Renderables, we can do some optimizations @@ -21,7 +22,6 @@ When rendering a specialised list of Renderables, we can do some optimizations function render(list::Vector{RenderObject{Pre}}) where Pre isempty(list) && return nothing first(list).prerenderfunction() - require_context(first(list).context) vertexarray = first(list).vertexarray program = vertexarray.program glUseProgram(program.id) @@ -51,7 +51,6 @@ function render(list::Vector{RenderObject{Pre}}) where Pre end end renderobject.postrenderfunction() - require_context(renderobject.context) end # we need to assume, that we're done here, which is why # we need to bind VertexArray to 0. @@ -70,7 +69,6 @@ a lot of objects. """ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray) if renderobject.visible - require_context(renderobject.context) renderobject.prerenderfunction() setup_clip_planes(to_value(get(renderobject.uniforms, :num_clip_planes, 0))) program = vertexarray.program @@ -94,7 +92,6 @@ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray bind(vertexarray) renderobject.postrenderfunction() glBindVertexArray(0) - require_context(renderobject.context) end return end diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index 13da407dc31..3a5f3f9eba9 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -103,15 +103,17 @@ function ShaderCache(context) ) end -function free(cache::ShaderCache, called_from_finalizer = false) +function free(cache::ShaderCache) + ShaderAbstractions.switch_context!(cache.context) for (k, v) in cache.shader_cache for (k2, shader) in v - free(shader, called_from_finalizer) + free(shader) end end for program in values(cache.program_cache) - free(program, called_from_finalizer) + free(program) end + require_context_no_error(cache.context) # just so we don't need try .. catch at call site return end diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 5b4f30ebb88..251be7506a7 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -92,7 +92,6 @@ Makie.@noconstprop function Texture( mipmap = false, parameters... # rest should be texture parameters ) where {T, NDim} - require_context(context) texparams = TextureParameters(T, NDim; parameters...) id = glGenTextures() glBindTexture(texturetype, id) @@ -160,7 +159,6 @@ function Texture( format::GLenum = default_colorformat(T), parameters... ) where T <: GLArrayEltypes - require_context(context) texparams = TextureParameters(T, 2; parameters...) id = glGenTextures() @@ -188,13 +186,12 @@ function Texture( tuple(maxdims...) ) set_parameters(texture) - texture + return texture end function TextureBuffer(buffer::GLBuffer{T}) where T <: GLArrayEltypes - require_context(buffer.context) texture_type = GL_TEXTURE_BUFFER id = glGenTextures() glBindTexture(texture_type, id) @@ -359,13 +356,11 @@ gpu_getindex(t::TextureBuffer{T}, i::UnitRange{Int64}) where {T} = t.buffer[i] similar(t::Texture{T, NDim}, newdims::Int...) where {T, NDim} = similar(t, newdims) function similar(t::TextureBuffer{T}, newdims::NTuple{1, Int}) where T - require_context(t.texture.context) buff = similar(t.buffer, newdims...) return TextureBuffer(buff) end function similar(t::Texture{T, NDim}, newdims::NTuple{NDim, Int}) where {T, NDim} - require_context(t.context) id = glGenTextures() glBindTexture(t.texturetype, id) glTexImage(t.texturetype, 0, t.internalformat, newdims..., 0, t.format, t.pixeltype, C_NULL) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 2c76b5b2923..f55f2e53051 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -404,6 +404,18 @@ function RenderObject( buffers = filter(((key, value),) -> isa(value, GLBuffer) || key === :indices, data) program = gl_convert(context, to_value(program), data) # "compile" lazyshader vertexarray = GLVertexArray(Dict(buffers), program) + require_context(context) + + # Validate context of things in RenderObject + if GLMAKIE_DEBUG[] + require_context(program.context, context) + require_context(vertexarray.context, context) + for v in values(data) + if v isa GPUArray + require_context(v.context, context) + end + end + end # remove all uniforms not occurring in shader # ssao, instances transparency are special for rendering passes. TODO do this more cleanly @@ -440,20 +452,19 @@ function free(x::T, called_from_finalizer = false) where {T} # don't free if already freed (this should only be set by unsafe_free) x.id == 0 && return - # This may be called from the scene finalizer in which case no errors, no printing allowed + # Note: context is checked higher up in the call stack but isn't guaranteed + # to be active or alive here, because unsafe_free() may also do + # cleanup that doesn't depend on context + + # This may be called from the scene finalizer in which case no errors and + # no printing allowed from the current task if called_from_finalizer - if GLMAKIE_DEBUG[] && !context_alive(x.context) - Threads.@spawn println(stderr, "Warning: free(::$T) called with dead context from scene finalizer.") - end try unsafe_free(x) catch e Threads.@spawn Base.showerror(stderr, e) end else - # This should be called with a valid, active context, but we shouldn't error - # here because unsafe_free() also sometimes cleans up observables - require_context_no_error(x.context) unsafe_free(x) end return @@ -498,7 +509,6 @@ function unsafe_free(x::Texture) clean_up_observables(x) x.id = ifelse(context_alive(x.context), x.id, 0) is_context_active(x.context) || return - id = Ref(x.id) glDeleteTextures(x.id) x.id = 0 return diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index d2a674043f2..a51ef3576ce 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -16,14 +16,14 @@ using .GLAbstraction const atlas_texture_cache = Dict{Any, Tuple{Texture{Float16, 2}, Function}}() -function cleanup_texture_atlas!(context, called_from_finalizer = false) +function cleanup_texture_atlas!(context) to_delete = filter(atlas_ctx -> atlas_ctx[2] == context, keys(atlas_texture_cache)) - called_from_finalizer || require_context(context) for (atlas, ctx) in to_delete tex, func = pop!(atlas_texture_cache, (atlas, ctx)) Makie.remove_font_render_callback!(atlas, func) - GLAbstraction.free(tex, called_from_finalizer) + GLAbstraction.free(tex) end + GLAbstraction.require_context_no_error(context) # avoid try .. catch at call site return end @@ -33,12 +33,17 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer if GLAbstraction.context_alive(ctx) return true else - if !called_from_finalizer - @error("Cached atlas textures should be removed explicitly! $ctx") - println("Reason:", GLFW.is_initialized() ? "" : " not initialized", was_destroyed(ctx) ? " destroyed" : "") - Base.show_backtrace(stderr, Base.catch_backtrace()) - else - Threads.@spawn println(stderr, "Cached atlas textures did not get cleaned up for context ", ctx) + # Adding extra context, so no require_context_no_error(ctx, async = called_from_finalizer) + try + require_context(ctx) + catch e + if called_from_finalizer + Threads.@spawn begin + @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) + end + else + @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) + end end tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed Makie.remove_font_render_callback!(atlas, tex_func[2]) @@ -51,7 +56,7 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer elseif called_from_finalizer return nothing else - require_context(context) + require_context(context, async = called_from_finalizer) tex = Texture( context, atlas.data, minfilter = :linear, diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index fa74b9c605e..7287c551b17 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -157,11 +157,19 @@ Makie.@noconstprop function GLFramebuffer(context, fb_size::NTuple{2, Int}) end function destroy!(fb::GLFramebuffer) - # context required via free(tex) - @assert !isempty(fb.buffers) + fb.id == 0 && return + @assert !isempty(fb.buffers) "GLFramebuffer was cleared incorrectly (i.e. not by destroy!())" + ctx = first(values(fb.buffers)).context + ShaderAbstractions.switch_context!(ctx) for tex in values(fb.buffers) GLAbstraction.free(tex) end + # avoid try .. catch at call site + GLAbstraction.require_context_no_error(ctx) + if !ShaderAbstractions.is_context_active(ctx) + fb.id = 0 + return + end id = [fb.id] glDeleteFramebuffers(1, id) fb.id = 0 @@ -219,11 +227,22 @@ function ShaderAbstractions.native_context_alive(x::GLFW.Window) GLFW.is_initialized() && !was_destroyed(x) end -# require_context(ctx, current = nothing) = nothing -function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context()) - @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." - @assert !was_destroyed(ctx) "Context $ctx must not be destroyed." - @assert ctx.handle == current.handle "Context $ctx must be current, but $current is." +function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context(); async = false) + if async + if !GLFW.is_initialized() + Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must be initialized, but is not."), backtrace()) + end + if GLAbstraction.GLMAKIE_DEBUG[] && was_destroyed(ctx) + Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must not be destroyed."), backtrace()) + end + if ctx != current + Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must be current, but $current is."), backtrace()) + end + else + @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." + @assert !GLAbstraction.GLMAKIE_DEBUG[] || !was_destroyed(ctx) "Context $ctx must not be destroyed." + @assert ctx == current "Context $ctx must be current, but $current is." + end end function destroy!(nw::GLFW.Window) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index fded72f1cf5..89458f86051 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -80,7 +80,8 @@ end function ssao_postprocessor(framebuffer, shader_cache) ShaderAbstractions.switch_context!(shader_cache.context) - require_context(shader_cache.context) + require_context(shader_cache.context) # for framebuffer, uniform textures + # Add missing buffers if !haskey(framebuffer, :position) glBindFramebuffer(GL_FRAMEBUFFER, framebuffer.id[1]) @@ -208,7 +209,7 @@ Returns a PostProcessor that handles fxaa. """ function fxaa_postprocessor(framebuffer, shader_cache) ShaderAbstractions.switch_context!(shader_cache.context) - require_context(shader_cache.context) + require_context(shader_cache.context) # for framebuffer, uniform textures # Add missing buffers if !haskey(framebuffer, :color_luma) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index ee717ea950e..205f39e48a1 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -118,7 +118,6 @@ end function GLAbstraction.render(filter_elem_func, screen::Screen) # Somehow errors in here get ignored silently!? try - GLAbstraction.require_context(screen.glscreen) for (zindex, screenid, elem) in screen.renderlist filter_elem_func(elem)::Bool || continue @@ -130,7 +129,6 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) render(elem) end - GLAbstraction.require_context(screen.glscreen) catch e @error "Error while rendering!" exception = e rethrow(e) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index f1ed3eef24a..d4bf25e7537 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -579,7 +579,7 @@ end function destroy!(rob::RenderObject, called_from_finalizer = false) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. - GLAbstraction.switch_context!(rob.context) + ShaderAbstractions.switch_context!(rob.context) tex = get_texture!(rob.context, gl_texture_atlas(), called_from_finalizer) for (k, v) in rob.uniforms if v isa Observable @@ -598,6 +598,8 @@ function destroy!(rob::RenderObject, called_from_finalizer = false) Observables.clear(obs) end GLAbstraction.free(rob.vertexarray, called_from_finalizer) + # avoid try .. catch at call site (call site usually destroys multipel renderobjects) + GLAbstraction.require_context_no_error(rob.context; async = called_from_finalizer) end # Note: called from scene finalizer, must not error @@ -663,16 +665,20 @@ function destroy!(screen::Screen) empty!(screen) end @assert screen.rendertask === nothing - foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates - destroy!(screen.framebuffer) - cleanup_texture_atlas!(window) - GLAbstraction.free(screen.shader_cache) - # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) + + # Since those are sets, we can just delete them from there, even if they + # weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) if screen in SINGLETON_SCREEN empty!(SINGLETON_SCREEN) end + + foreach(destroy!, screen.postprocessors) # before texture atlas, otherwise it regenerates + destroy!(screen.framebuffer) + cleanup_texture_atlas!(window) + GLAbstraction.free(screen.shader_cache) + destroy!(window) return end @@ -685,12 +691,13 @@ Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=t """ function Base.close(screen::Screen; reuse=true) @debug("Close screen!") + + # If the context is dead we should completely destroy the screen if !GLAbstraction.context_alive(screen.glscreen) destroy!(screen) + return end - # TODO: CI sometimes fails to adjust visibility with a GLFW init error - # This should not stop us from cleaning up OpenGL objects! set_screen_visibility!(screen, false) if screen.window_open[] # otherwise we trigger an infinite loop of closing screen.window_open[] = false From 8c58f93fc2bc926786ba1713690cf40f13d6ef48 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 16:18:26 +0100 Subject: [PATCH 099/135] fix typo --- GLMakie/src/glwindow.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 7287c551b17..97cd04f6762 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -230,13 +230,19 @@ end function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context(); async = false) if async if !GLFW.is_initialized() - Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must be initialized, but is not."), backtrace()) + Threads.@spawn begin + @error "Failed to require context:" exception = (ErrorException("Context $ctx must be initialized, but is not."), backtrace()) + end end if GLAbstraction.GLMAKIE_DEBUG[] && was_destroyed(ctx) - Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must not be destroyed."), backtrace()) + Threads.@spawn begin + @error "Failed to require context:" exception = (ErrorException("Context $ctx must not be destroyed."), backtrace()) + end end if ctx != current - Threads.@spawn "Failed to require context:" exception = (ErrorException("Context $ctx must be current, but $current is."), backtrace()) + Threads.@spawn begin + @error "Failed to require context:" exception = (ErrorException("Context $ctx must be current, but $current is."), backtrace()) + end end else @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." From 667094d1cce35b3e850af13c5c78264005a20878 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 16:40:57 +0100 Subject: [PATCH 100/135] fix test errors --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 +- GLMakie/src/GLAbstraction/GLTypes.jl | 5 ++++- GLMakie/src/glwindow.jl | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 7440da82ac6..cd41a680886 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -24,7 +24,7 @@ const GLMAKIE_DEBUG = Ref(false) require_context() = nothing # implemented in GLMakie/glwindow.jl function require_context_no_error(args...; async = false) if async - require_context(args..., true) + require_context(args..., async = true) else try require_context(args...) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index f55f2e53051..4c3f62042f9 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -411,7 +411,10 @@ function RenderObject( require_context(program.context, context) require_context(vertexarray.context, context) for v in values(data) - if v isa GPUArray + if v isa TextureBuffer + require_context(v.buffer.context, context) + require_context(v.texture.context, context) + elseif v isa GPUArray require_context(v.context, context) end end diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 97cd04f6762..2e2f1338f26 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -178,6 +178,8 @@ end function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) (w > 0 && h > 0 && (w, h) != size(fb)) || return + isempty(fb.buffers) && return # or error? + ShaderAbstractions.switch_context!(first(values(fb.buffers)).context) for (name, buffer) in fb.buffers resize_nocopy!(buffer, (w, h)) end From d3ff367d7a1822b0c61e4b34165336ee56d432d6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 4 Jan 2025 18:04:33 +0100 Subject: [PATCH 101/135] fix typo --- src/utilities/RenderPipeline.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 86f9502aadd..3f497cbb63c 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -315,7 +315,7 @@ function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, pipeline.formats[format_idx] = format # connect higher index to format and adjust later indices for (k, v) in pipeline.stageio2idx - pipeline.stage2idx[k] = ifelse(v == other_idx, format_idx, ifelse(v > other_idx, v - 1, v)) + pipeline.stageio2idx[k] = ifelse(v == other_idx, format_idx, ifelse(v > other_idx, v - 1, v)) end # remove orphaned format deleteat!(pipeline.formats, other_idx) From 33aff9ca18ca546f7a439012f9b44be3eec011e7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 16:47:24 +0100 Subject: [PATCH 102/135] rename OIT buffers --- GLMakie/assets/shader/postprocessing/OIT_blend.frag | 8 ++++---- GLMakie/src/postprocessing.jl | 2 +- src/utilities/RenderPipeline.jl | 6 +++--- test/render_pipeline.jl | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/GLMakie/assets/shader/postprocessing/OIT_blend.frag b/GLMakie/assets/shader/postprocessing/OIT_blend.frag index 85aa152a249..a68693e301e 100644 --- a/GLMakie/assets/shader/postprocessing/OIT_blend.frag +++ b/GLMakie/assets/shader/postprocessing/OIT_blend.frag @@ -6,16 +6,16 @@ in vec2 frag_uv; // contains sum_i C_i * weight(depth_i, alpha_i) -uniform sampler2D weighted_color_sum_buffer; +uniform sampler2D color_sum_buffer; // contains pod_i (1 - alpha_i) -uniform sampler2D alpha_product_buffer; +uniform sampler2D transmittance_buffer; out vec4 fragment_color; void main(void) { - vec4 summed_color_weight = texture(weighted_color_sum_buffer, frag_uv); - float transmittance = texture(alpha_product_buffer, frag_uv).r; + vec4 summed_color_weight = texture(color_sum_buffer, frag_uv); + float transmittance = texture(transmittance_buffer, frag_uv).r; vec3 weighted_transparent = summed_color_weight.rgb / max(summed_color_weight.a, 0.00001); vec3 full_weighted_transparent = weighted_transparent * (1 - transmittance); diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 4b3814713b3..bf1df6c2d6a 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -126,7 +126,7 @@ function construct(::Val{:Render}, screen, framebuffer, inputs, parent) return RenderPlots(framebuffer, [3 => Vec4f(0), 4 => Vec4f(0)], ssao, transparency, fxaa, false) end -function construct(::Val{:TransparentRender}, screen, framebuffer, inputs, parent) +function construct(::Val{Symbol("OIT Render")}, screen, framebuffer, inputs, parent) # HDR_color containing sums clears to 0 # OIT_weight containing products clears to 1 clear = [1 => Vec4f(0), 3 => Vec4f(1)] diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 3f497cbb63c..12e07359715 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -595,9 +595,9 @@ function RenderStage(; kwargs...) end function TransparentRenderStage() - outputs = Dict(:weighted_color_sum => 1, :objectid => 2, :alpha_product => 3) + outputs = Dict(:color_sum => 1, :objectid => 2, :transmittance => 3) output_formats = [BufferFormat(4, Float16), BufferFormat(2, UInt32), BufferFormat(1, N0f8)] - return Stage(:TransparentRender, Dict{Symbol, Int}(), BufferFormat[], outputs, output_formats) + return Stage(Symbol("OIT Render"), Dict{Symbol, Int}(), BufferFormat[], outputs, output_formats) end function SSAOStage(; kwargs...) @@ -616,7 +616,7 @@ function SSAOStage(; kwargs...) end function OITStage(; kwargs...) - inputs = Dict(:weighted_color_sum => 1, :alpha_product => 2) + inputs = Dict(:color_sum => 1, :transmittance => 2) input_formats = [BufferFormat(4, Float16), BufferFormat(1, N0f8)] outputs = Dict(:color => 1) output_formats = [BufferFormat(4, N0f8)] diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index a74c90f92ce..aecccd91c42 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -199,11 +199,11 @@ using Makie: generate_buffers, default_pipeline Dict(:color => 1, :objectid => 2, :position => 3, :normal => 4), [BufferFormat(4, N0f8), BufferFormat(2, UInt32), BufferFormat(3, Float16), BufferFormat(3, Float16)], transparency = false) - @test pipeline.stages[3] == Stage(:TransparentRender, Dict{Symbol, Int}(), BufferFormat[], - Dict(:weighted_color_sum => 1, :objectid => 2, :alpha_product => 3), + @test pipeline.stages[3] == Stage(Symbol("OIT Render"), Dict{Symbol, Int}(), BufferFormat[], + Dict(:color_sum => 1, :objectid => 2, :transmittance => 3), [BufferFormat(4, Float16), BufferFormat(2, UInt32), BufferFormat(1, N0f8)]) @test pipeline.stages[4] == Stage(:OIT, - Dict(:weighted_color_sum => 1, :alpha_product => 2), [BufferFormat(4, Float16), BufferFormat(1, N0f8)], + Dict(:color_sum => 1, :transmittance => 2), [BufferFormat(4, Float16), BufferFormat(1, N0f8)], Dict(:color => 1), [BufferFormat(4, N0f8)]) @test pipeline.stages[5] == Stage(:FXAA1, Dict(:color => 1, :objectid => 2), [BufferFormat(4, N0f8), BufferFormat(2, UInt32)], From d31feed094d83cac0237c1e89be330f6d3566df1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 16:48:07 +0100 Subject: [PATCH 103/135] fix & test connecting two already connected nodes --- src/utilities/RenderPipeline.jl | 2 +- test/render_pipeline.jl | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 12e07359715..f1230b429d6 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -308,7 +308,7 @@ function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, if haskey(pipeline.stageio2idx, (trg, -input)) # both exist - update - other_idx = pipeline.stageio2idx[(src, output)] + other_idx = pipeline.stageio2idx[(trg, -input)] format = BufferFormat(pipeline.formats[other_idx], format) # replace format of lower index format_idx, other_idx = minmax(format_idx, other_idx) diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index aecccd91c42..cbfc465c8b3 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -186,6 +186,23 @@ using Makie: generate_buffers, default_pipeline # (1 b --> 2 b, 3b) available, upgrades # (2 c --> 3 c) not allowed, incompatible types (int16) @test buffers[remap[4]] == pipeline.formats[1] + + # text connecting two nodes that each have connections + connect!(pipeline, stage1, :b, stage5, :z) + + @test length(pipeline.formats) == 3 + @test length(pipeline.stageio2idx) == 9 + @test pipeline.formats == [BufferFormat(4, Float16), BufferFormat(2, Int16), BufferFormat(4, N0f8)] + for (k, v) in [(3, -1) => 1, (1, 2) => 1, (1, 1) => 3, (4, -1) => 3, (5, -1) => 1, (4, 1) => 1, (2, -1) => 1, (2, 1) => 2, (3, -2) => 2] + @test pipeline.stageio2idx[k] == v + end + + buffers, remap = generate_buffers(pipeline) + @test length(buffers) == 3 + @test length(remap) == 3 + @test buffers[remap[1]] == pipeline.formats[1] + @test buffers[remap[2]] == pipeline.formats[2] + @test buffers[remap[3]] == pipeline.formats[3] end @testset "default pipeline" begin From 836421dc02b92e798838fddc847d4fe1da92fd45 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 16:48:18 +0100 Subject: [PATCH 104/135] add experimental GUI --- src/utilities/RenderPipeline.jl | 217 ++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index f1230b429d6..9b0f6d014dc 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -781,3 +781,220 @@ function test_pipeline_minimal() return pipeline end + +################################################################################ +## Experimental GUI for Pipeline +################################################################################ + +function pipeline_gui!(ax, pipeline) + width = 5 + + rects = Rect2f[] + header_line = Point2f[] + header = Tuple{String, Point2f}[] + marker_pos = Vector{Point2f}[] + input = Vector{Tuple{String, Point2f}}[] + output = Vector{Tuple{String, Point2f}}[] + stageio_lookup = Tuple{Int, Int}[] + + for (idx, stage) in enumerate(pipeline.stages) + output_height = length(stage.outputs) + height = length(stage.inputs) + output_height + + r = Rect2f(-width/2, -height, width, height+2) + push!(rects, r) + push!(header_line, Point2f(-0.5width, 0), Point2f(0.5width, 0)) + push!(header, (string(stage.name), Point2f(0, 1))) + + ops = sort!([(string(s), Point2f(0.5width, 0.5-y)) for (s, y) in stage.outputs], by = x -> -x[2][2]) + ips = sort!( + [(string(s), Point2f(-0.5width, 0.5 - y - output_height)) for (s, y) in stage.inputs], + by = x -> -x[2][2]) + + push!(input, ips) + push!(output, ops) + push!(marker_pos, vcat(last.(ips), last.(ops))) + append!(stageio_lookup, [(idx, -i) for i in 1:length(stage.inputs)]) + append!(stageio_lookup, [(idx, i) for i in 1:length(stage.outputs)]) + end + + origins = Observable([Point2f(8x, 0) for x in eachindex(pipeline.stages)]) + + rects_obs = Observable(Rect2f[]) + header_line_obs = Observable(Point2f[]) + header_obs = Observable(Tuple{String, Point2f}[]) + marker_pos_obs = Observable(Point2f[]) + input_obs = Observable(Tuple{String, Point2f}[]) + output_obs = Observable(Tuple{String, Point2f}[]) + path_ps_obs = Observable(Point2f[]) + path_cs_obs = Observable(RGBf[]) + + on(origins) do origins + rects_obs[] = rects .+ origins + header_line_obs[] = [origins[i] .+ header_line[2(i-1) + j] for i in eachindex(origins) for j in 1:2] + header_obs[] = map((x, pos) -> (x[1], pos + x[2]), header, origins) + + marker_pos_obs[] = mapreduce(vcat, marker_pos, origins) do ps, pos + return [p + pos for p in ps] + end + input_obs[] = mapreduce(vcat, input, origins) do input, pos + return [(x[1], x[2] + pos) for x in input] + end + output_obs[] = mapreduce(vcat, output, origins) do input, pos + return [(x[1], x[2] + pos) for x in input] + end + end + + function bezier_connect(p0, p1) + x0, y0 = ifelse(p0[1] < p1[1], p0, p1) + x1, y1 = ifelse(p0[1] < p1[1], p1, p0) + mid = 0.5 * (x0+x1) + path = Any[Makie.MoveTo(x0, y0)] + if (x1 - x0) > 10 + push!(path, + Makie.LineTo(Point2f(mid - 5, y0)), + Makie.CurveTo(Point2f(mid + 1, y0), Point2f(mid - 1, y1), Point2f(mid + 5, y1)), + Makie.LineTo(Point2f(x1, y1)) + ) + else + push!(path, Makie.CurveTo(Point2f(mid + 1, y0), Point2f(mid - 1, y1), Point2f(x1, y1))) + end + path = Makie.BezierPath(path) + return Makie.convert_arguments(PointBased(), path)[1] + end + + on(origins) do origins + # vector[connection idx] = [(stage idx, input/output index)] (- input, + output) + conn2stageio = [Tuple{Int, Int}[] for _ in eachindex(pipeline.formats)] + for (stageio, conn) in pipeline.stageio2idx + push!(conn2stageio[conn], stageio) + end + + paths = Point2f[] + color_pool = Makie.to_colormap(:seaborn_bright) + cs = RGBf[] + for (idx, stageio) in enumerate(conn2stageio) + sort!(stageio, by = first) + N = length(stageio) + for i in 1:N-1 + start_stage, start_idx = stageio[i] + start_idx < 0 && continue # is a stage input + for j in i+1:N + stop_stage, stop_idx = stageio[j] + stop_idx > 0 && continue # is a stage output + p0 = output[start_stage][start_idx][2] + origins[start_stage] + p1 = input[stop_stage][-stop_idx][2] + origins[stop_stage] + ps = bezier_connect(p0, p1) + append!(paths, ps) + push!(paths, Point2f(NaN)) + append!(cs, [color_pool[mod1(idx, end)] for _ in ps]) + push!(cs, RGBf(0,0,0)) + end + end + end + path_ps_obs.val = paths + path_cs_obs[] = cs + notify(path_ps_obs) + return + end + + notify(origins) + + scale = map(pv -> max(0.5, 10*min(pv[1,1], pv[2,2])), ax.scene.camera.projectionview) + + poly!(ax, rects_obs, strokewidth = scale, strokecolor = :black, fxaa = false, + shading = NoShading, color = :lightgray) + linesegments!(ax, header_line_obs, linewidth = scale, color = :black) + text!(ax, header_obs, markerspace = :data, fontsize = 0.8, color = :black, + align = (:center, :center)) + + text!(ax, output_obs, markerspace = :data, fontsize = 0.75, color = :black, + align = (:right, :center), offset = (-0.25, 0)) + text!(ax, input_obs, markerspace = :data, fontsize = 0.75, color = :black, + align = (:left, :center), offset = (0.25, 0)) + + p = scatter!(ax, marker_pos_obs, color = :black, markerspace = :data, marker = Circle, markersize = 0.3) + translate!(p, 0, 0, 1) + + p = lines!(ax, path_ps_obs, color = path_cs_obs) + translate!(p, 0, 0, -1) + + new_conn_plot = lines!(ax, Point2f[], color = :black, visible = false) + + # Drag Stages around & connect inputs/outputs + selected_idx = Ref(-1) + connecting = Ref(false) # true = drawing line - false = moving stage + drag_offset = Ref(Point2f(0)) + start_pos = Ref(Point2f(0)) + io_range = 0.4 + on(events(ax).mousebutton, priority = 100) do event + if event.button == Mouse.left + if event.action == Mouse.press + pos = mouseposition(ax) + for (i, p) in enumerate(marker_pos_obs[]) + if norm(pos - p) < io_range + selected_idx[] = i + connecting[] = true + start_pos[] = p + return Consume(true) + end + end + for (i, rect) in enumerate(rects_obs[]) + if pos in rect + selected_idx[] = i + connecting[] = false + drag_offset[] = origins[][i] - pos + return Consume(true) + end + end + elseif (event.action == Mouse.release) && (selected_idx[] != -1) + if connecting[] + pos = mouseposition(ax) + for (i, p) in enumerate(marker_pos_obs[]) + if norm(pos - p) < io_range + start_stage, start_io = stageio_lookup[selected_idx[]] + stop_stage, stop_io = stageio_lookup[i] + @info start_stage, start_io, stop_stage, stop_io + if (start_io > 0) && (stop_io < 0) && (start_stage < stop_stage) # output to input + Makie.connect!(pipeline, start_stage, start_io, stop_stage, -stop_io) + notify(origins) # trigger redraw of connections + elseif (start_io < 0) && (stop_io > 0) && (stop_stage < start_stage) # input to output + Makie.connect!(pipeline, stop_stage, stop_io, start_stage, -start_io) + notify(origins) # trigger redraw of connections + end + selected_idx[] = -1 + new_conn_plot.visible[] = false + return Consume(true) + end + end + end + new_conn_plot.visible[] = false + selected_idx[] = -1 + return Consume(true) + end + end + return Consume(false) + end + + on(events(ax).mouseposition, priority = 100) do event + if selected_idx[] != -1 + curr = mouseposition(ax) + if connecting[] + new_conn_plot[1][] = bezier_connect(start_pos[], curr) + new_conn_plot.visible[] = true + else + origins[][selected_idx[]] = curr + drag_offset[] + notify(origins) + end + return Consume(true) + end + return Consume(false) + end + + on(events(ax).keyboardbutton, priority = 100) do event + if (event.key == Keyboard.o) && (event.action == Keyboard.press) + @info origins[] + end + end + +end \ No newline at end of file From f07d3a4eec1b2fb23da45f8c0c7c60f9ff7ce0b3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 17:11:27 +0100 Subject: [PATCH 105/135] remove pipeline caching --- src/utilities/RenderPipeline.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 9b0f6d014dc..b5d5d214d4e 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -648,13 +648,13 @@ end # TODO: caching is dangerous with mutable attributes... -const PIPELINE_CACHE = Dict{Symbol, Pipeline}() +# const PIPELINE_CACHE = Dict{Symbol, Pipeline}() function default_pipeline(; ssao = false, fxaa = true, oit = true) - name = Symbol(:default_pipeline, Int(ssao), Int(fxaa), Int(oit)) + # name = Symbol(:default_pipeline, Int(ssao), Int(fxaa), Int(oit)) # Mimic GLMakie's old hard coded render pipeline - get!(PIPELINE_CACHE, name) do + # get!(PIPELINE_CACHE, name) do pipeline = Pipeline() push!(pipeline, SortStage()) @@ -699,7 +699,7 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) end return pipeline - end + # end end function test_pipeline_3D() From 60201e7216146dd19475cbc838c640e50d6fd2c5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 17:13:24 +0100 Subject: [PATCH 106/135] turn on ci against rebase branch --- .github/workflows/ci.yml | 1 + .github/workflows/compilation-benchmark.yaml | 1 + .github/workflows/reference_tests.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd5c16ed7d9..97278016965 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - '*.md' branches: - master + - ff/safe-context push: tags: - '*' diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index 7e6c6a62a8e..26a3c17594b 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -6,6 +6,7 @@ on: - '*.md' branches: - master + - ff/safe-context concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/reference_tests.yml b/.github/workflows/reference_tests.yml index 3903929641f..ad8ef46b39d 100644 --- a/.github/workflows/reference_tests.yml +++ b/.github/workflows/reference_tests.yml @@ -6,6 +6,7 @@ on: - '*.md' branches: - master + - ff/safe-context push: tags: - '*' From 1b61a16dc18019df29d6d665c0aa4abce9842ffa Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 18:46:58 +0100 Subject: [PATCH 107/135] fix freeing of reused framebuffer attachments, fix tests --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 +- GLMakie/src/GLAbstraction/GLTexture.jl | 2 +- GLMakie/src/postprocessing.jl | 10 ++--- GLMakie/src/render_pipeline.jl | 11 +++++- GLMakie/src/screen.jl | 4 +- GLMakie/test/runtests.jl | 44 +++++++++++++++++----- test/render_pipeline.jl | 2 +- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index 9d926fbdae2..fe34b33c512 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -116,7 +116,7 @@ export getUniformsInfo export getProgramInfo export getAttributesInfo -include("GLFrameBuffer.jl") +include("GLFramebuffer.jl") export GLRenderbuffer export GLFramebuffer export attach_colorbuffer, attach_depthbuffer, attach_stencilbuffer, attach_depthstencilbuffer diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 251be7506a7..4c0667bb189 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -231,7 +231,7 @@ Creates a texture from an Image # AbstractArrays default show assumes `getindex`. Try to catch all calls # https://discourse.julialang.org/t/overload-show-for-array-of-custom-types/9589 -Base.show(io::IO, t::Texture) = show(IOContext(io), MIME"text/plain"(), t) +Base.show(io::IO, t::Texture{T, D}) where {T, D} = print(io, "Texture{$T, $D}(ID: $(t.id), Size: $(size(t)))") function Base.show(io::IOContext, mime::MIME"text/plain", t::Texture{T,D}) where {T,D} if get(io, :compact, false) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index bf1df6c2d6a..86512a591cf 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -36,17 +36,17 @@ to initialize just before your run, bundle it with the run. abstract type AbstractRenderStep end run_step(screen, glscene, ::AbstractRenderStep) = nothing -function destroy!(step::T) where {T <: AbstractRenderStep} +function destroy!(step::T, keep_alive) where {T <: AbstractRenderStep} @debug "Default destructor of $T" - hasfield(T, :robj) && destroy!(step.robj) + hasfield(T, :robj) && destroy!(step.robj, false, keep_alive) return end # fore reference: # construct(::Val{Name}, screen, framebuffer, inputs, parent::Makie.Stage) -function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage) where {T <: AbstractRenderStep} +function reconstruct(old::T, screen, framebuffer, inputs, parent::Makie.Stage, keep_alive) where {T <: AbstractRenderStep} # @debug "reconstruct() not defined for $T, calling construct()" - destroy!(old) + destroy!(old, keep_alive) return construct(Val(parent.name), screen, framebuffer, inputs, parent) end @@ -69,7 +69,7 @@ function render_frame(screen, glscene, pipeline::GLRenderPipeline) end function destroy!(pipeline::GLRenderPipeline) - destroy!.(pipeline.steps) + destroy!.(pipeline.steps, Ref(UInt32[])) empty!(pipeline.steps) return end diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index e300bfc6027..8613ec1e871 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -78,6 +78,12 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) buffer_idx = remap[pipeline.stageio2idx[(N, -2)]] attach_colorbuffer(factory.fb, :objectid, get_buffer(factory, buffer_idx)) + # Careful - framebuffer attachments are used as inputs so they need to be + # filtered when destroying the stage! (Which may include other + # other textures that do need cleanup) + keep_alive = [tex.id for tex in screen.framebuffer_factory.buffers] + push!(keep_alive, get_buffer(factory.fb, :depth_stencil).id) + needs_cleanup = collect(eachindex(previous_pipeline.steps)) render_pipeline = AbstractRenderStep[] @@ -118,7 +124,7 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) if idx === nothing pass = construct(Val(stage.name), screen, framebuffer, inputs, stage) else - pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage) + pass = reconstruct(previous_pipeline.steps[idx], screen, framebuffer, inputs, stage, keep_alive) filter!(!=(idx), needs_cleanup) end @@ -128,7 +134,8 @@ function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) push!(render_pipeline, pass) end - foreach(i -> destroy!(previous_pipeline.steps[i]), needs_cleanup) + # Cleanup orphaned stages + foreach(i -> destroy!(previous_pipeline.steps[i], keep_alive), needs_cleanup) screen.render_pipeline = GLRenderPipeline(pipeline, render_pipeline) # was_running && start_renderloop!(screen) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 2da79869cf2..c75dbdf4d17 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -552,7 +552,7 @@ function Base.delete!(screen::Screen, scene::Scene, called_from_finalizer::Bool return end -function destroy!(rob::RenderObject, called_from_finalizer = false) +function destroy!(rob::RenderObject, called_from_finalizer = false, keep_alive = UInt32[]) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. ShaderAbstractions.switch_context!(rob.context) @@ -560,7 +560,7 @@ function destroy!(rob::RenderObject, called_from_finalizer = false) for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) - elseif v isa GPUArray && v !== tex + elseif v isa GPUArray && (v !== tex) && !in(v.id, keep_alive) # We usually don't share gpu data and it should be hard for users to share buffers.. # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 51acf6c9c81..6b2c4be11bb 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -145,18 +145,26 @@ end # texture atlas is triggered by text # include SSAO to make sure its cleanup works too f,a,p = image(rand(4,4)) + + # make sure switching the render pipeline doesn't cause errors + screen = display(f, ssao = true, visible = false) + colorbuffer(screen) + screen = display(f, ssao = false, visible = false) + colorbuffer(screen) screen = display(f, ssao = true, visible = false) colorbuffer(screen) # verify that SSAO is active - @test screen.postprocessors[1] != GLMakie.empty_postprocessor() + @test :SSAO1 in map(x -> x.name, screen.render_pipeline.parent.stages) - framebuffer = screen.framebuffer - framebuffer_textures = copy(screen.framebuffer.buffers) + framebuffer = screen.framebuffer_factory + framebuffer_depth = GLMakie.get_buffer(screen.framebuffer_factory.fb, :depth_stencil) + framebuffer_textures = copy(screen.framebuffer_factory.buffers) + framebuffer_children = copy(screen.framebuffer_factory.children) atlas_textures = first.(values(GLMakie.atlas_texture_cache)) shaders = vcat([[shader for shader in values(shaders)] for shaders in values(screen.shader_cache.shader_cache)]...) programs = [program for program in values(screen.shader_cache.program_cache)] - postprocessors = copy(screen.postprocessors) + pipeline = copy(screen.render_pipeline.steps) robjs = last.(screen.renderlist) GLMakie.destroy!(screen) @@ -169,11 +177,26 @@ end end @testset "Framebuffer" begin - @test framebuffer.id == 0 + # GLFramebuffer object + @test framebuffer.fb.id == 0 + @test all(x -> x.id == 0, framebuffer.fb.buffers) + + # FramebufferFactory object + @test isempty(framebuffer.children) + @test isempty(framebuffer.buffers) + @test !isempty(framebuffer_textures) - for (k, tex) in framebuffer_textures + for tex in framebuffer_textures @test tex.id == 0 end + + @test !isempty(framebuffer_children) + for fb in framebuffer_children + @test fb.id == 0 + @test framebuffer.fb.id == 0 + # should automatically be true if all framebuffer_textures == 0 + @test all(x -> x.id == 0, framebuffer.fb.buffers) + end end @testset "ShaderCache" begin @@ -207,10 +230,10 @@ end end @testset "PostProcessors" begin - @test map(pp -> length(pp.robjs), postprocessors) == [2,1,2,1] - for pp in postprocessors - for robj in pp.robjs - validate_robj(robj) + @test length(pipeline) == 10 + for step in pipeline + if hasfield(typeof(step), :robj) + validate_robj(getfield(step, :robj)) end end end @@ -223,5 +246,6 @@ end end # Check that no finalizers triggered on un-freed objects throughout all tests + GC.gc(true) @test GLMakie.GLAbstraction.FAILED_FREE_COUNTER[] == 0 end diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index cbfc465c8b3..0187fa8020b 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -1,6 +1,6 @@ using Makie using Makie: BufferFormat, N0f8, is_compatible, BFT -using Makie: Stage, get_input_connection, get_output_connection, get_input_format, get_output_format +using Makie: Stage, get_input_format, get_output_format using Makie: Pipeline, connect! using Makie: generate_buffers, default_pipeline From d9fa43df3d57e07f7c81e914c3fbaeb8addc5ca9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 19:56:09 +0100 Subject: [PATCH 108/135] git pls --- GLMakie/src/GLAbstraction/{GLFrameBuffer.jl => GLFramebuffer.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename GLMakie/src/GLAbstraction/{GLFrameBuffer.jl => GLFramebuffer.jl} (100%) diff --git a/GLMakie/src/GLAbstraction/GLFrameBuffer.jl b/GLMakie/src/GLAbstraction/GLFramebuffer.jl similarity index 100% rename from GLMakie/src/GLAbstraction/GLFrameBuffer.jl rename to GLMakie/src/GLAbstraction/GLFramebuffer.jl From f3980abb7c72e144fdfd047df2c661dea60a8770 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 5 Jan 2025 21:01:47 +0100 Subject: [PATCH 109/135] fix tests --- GLMakie/src/screen.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index c75dbdf4d17..73ea691d160 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -560,7 +560,7 @@ function destroy!(rob::RenderObject, called_from_finalizer = false, keep_alive = for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) - elseif v isa GPUArray && (v !== tex) && !in(v.id, keep_alive) + elseif v isa GPUArray && (v !== tex) && (!(v isa Texture) || !in(v.id, keep_alive)) # We usually don't share gpu data and it should be hard for users to share buffers.. # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources From 0577f78ad7577b6ae5d83013e622fc4488ed5266 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 6 Jan 2025 17:12:32 +0100 Subject: [PATCH 110/135] improve default placement of stages in GUI --- src/utilities/RenderPipeline.jl | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index b5d5d214d4e..d794d7549ab 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -797,6 +797,9 @@ function pipeline_gui!(ax, pipeline) output = Vector{Tuple{String, Point2f}}[] stageio_lookup = Tuple{Int, Int}[] + max_size = 0 + max_size2 = 0 + for (idx, stage) in enumerate(pipeline.stages) output_height = length(stage.outputs) height = length(stage.inputs) + output_height @@ -816,10 +819,47 @@ function pipeline_gui!(ax, pipeline) push!(marker_pos, vcat(last.(ips), last.(ops))) append!(stageio_lookup, [(idx, -i) for i in 1:length(stage.inputs)]) append!(stageio_lookup, [(idx, i) for i in 1:length(stage.outputs)]) + + if max_size < length(stage.inputs) + length(stage.outputs) + max_size2 = max_size + max_size = length(stage.inputs) + length(stage.outputs) + end end + origins = Observable([Point2f(8x, 0) for x in eachindex(pipeline.stages)]) + # Do something to get better starting layout... + begin + shift = 0.5 * (4 + max_size + max_size2 + 1) + # vector[connection idx] = [(stage idx, input/output index)] (- input, + output) + conn2stageio = [Tuple{Int, Int}[] for _ in eachindex(pipeline.formats)] + for (stageio, conn) in pipeline.stageio2idx + push!(conn2stageio[conn], stageio) + end + + for i in length(pipeline.stages):-1:1 + targets = Int[] + for j in eachindex(pipeline.stages[i].input_formats) + if haskey(pipeline.stageio2idx, (i, -j)) + conn_idx = pipeline.stageio2idx[(i, -j)] + for (stage_idx, io) in conn2stageio[conn_idx] + if io > 0 # is output + push!(targets, stage_idx) + end + end + end + end + + if !isempty(targets) + y0 = 0.5 * (length(targets)+1) + for (j, stage_idx) in enumerate(targets) + origins[][stage_idx] = Point2f(origins[][stage_idx][1], origins[][i][2]) + Point2f(0, shift * (y0 - j)) + end + end + end + end + rects_obs = Observable(Rect2f[]) header_line_obs = Observable(Point2f[]) header_obs = Observable(Tuple{String, Point2f}[]) From fa94cc30886f17e4e2fc95d6a7ee52f26054f41d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 6 Jan 2025 19:17:38 +0100 Subject: [PATCH 111/135] fix SSAO --- GLMakie/assets/shader/fragment_output.frag | 2 +- GLMakie/assets/shader/postprocessing/SSAO.frag | 3 ++- GLMakie/src/glshaders/visualize_interface.jl | 2 +- GLMakie/src/postprocessing.jl | 5 ++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index 837d5ccc3ec..6dc06fce29e 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -33,7 +33,7 @@ void write2framebuffer(vec4 color, uvec2 id){ // // if transparency == false && ssao = true // fragment_color = color; - // fragment_position = o_view_pos; + // fragment_position.xyz = o_view_pos; // fragment_normal_occlusion.xyz = o_view_normal; // // else diff --git a/GLMakie/assets/shader/postprocessing/SSAO.frag b/GLMakie/assets/shader/postprocessing/SSAO.frag index 3d932689bf6..9b3064c38ab 100644 --- a/GLMakie/assets/shader/postprocessing/SSAO.frag +++ b/GLMakie/assets/shader/postprocessing/SSAO.frag @@ -27,7 +27,7 @@ void main(void) // The normal buffer gets cleared every frame. (also position, color etc) // If normal == vec3(1) then there is no geometry at this fragment. // Therefore skip SSAO calculation - if (normal != vec3(1)) { + if (normal != vec3(0)) { vec3 rand_vec = vec3(texture(noise, frag_uv * noise_scale).xy, 0.0); vec3 tangent = normalize(rand_vec - normal * dot(rand_vec, normal)); vec3 bitangent = cross(normal, tangent); @@ -78,6 +78,7 @@ void main(void) sample_frag_pos.xyz = sample_frag_pos.xyz * 0.5 + 0.5; sample_frag_pos.xy += (frag_uv - 0.5) * clip_pos_w / sample_clip_pos_w; + if (texture(normal_buffer, sample_frag_pos.xy).xyz == vec3(0)) continue; float sample_depth = texture(position_buffer, sample_frag_pos.xy).z; float range_check = smoothstep(0.0, 1.0, radius / abs(view_pos.z - sample_depth)); diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index c5f9f58c7e1..d17bc0a5441 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -179,7 +179,7 @@ function output_buffer_writes(screen::Screen, transparency = false) elseif any(stage -> stage.name == :SSAO1, pipeline.stages) """ fragment_color = color; - fragment_position = o_view_pos; + fragment_position.xyz = o_view_pos; fragment_normal.xyz = o_view_normal; """ else diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 86512a591cf..a32276b8fb6 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -339,7 +339,10 @@ function run_step(screen, glscene, step::RenderPass{:SSAO1}) # This should be per scene because projection may vary between # scenes. It should be a leaf scene to avoid repeatedly shading # the same region (though this is not guaranteed...) - isempty(scene.children) || continue + if !isempty(scene.children) || isempty(scene.plots) || + !any(p -> to_value(get(p.attributes, :ssao, false)), scene.plots) + continue + end a = viewport(scene)[] glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms From 5a3abcd81931927a8893e4140f96ac257d1f654f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 6 Jan 2025 22:54:58 +0100 Subject: [PATCH 112/135] add test for custom render pass --- GLMakie/src/GLMakie.jl | 27 +++++++++- GLMakie/test/glmakie_refimages.jl | 84 +++++++++++++++++++++++++++++++ src/theming.jl | 1 + 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index 40642e1c077..286619b5ea8 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -48,13 +48,38 @@ struct ShaderSource name::String end -function ShaderSource(path) +""" + ShaderSource(filepath::String) + +Loads the source code from the given filepath. The shader type is derived from +the file extension (.vert, .frag, .geom or .comp). +""" +function ShaderSource(path::String) typ = GLAbstraction.shadertype(splitext(path)[2]) source = read(path, String) name = String(path) return ShaderSource(typ, source, name) end +const shader_counter = Ref(0) +function next_shader_num() + shader_counter[] = shader_counter[] + 1 + return shader_counter[] +end + +""" + ShaderSource(code::String, type::Symbol[, name]) + +Bundles the given source code with a shader type for later shader compilation. +`type` can be :vert, :frag, :geom or :comp. + +Use `loadshader(filename)` to load a cached shader from GLMakie's assets folder. +""" +function ShaderSource(source::String, type::Symbol, name = "inline_shader$(next_shader_num())") + type2gltype = (comp = GL_COMPUTE_SHADER, vert = GL_VERTEX_SHADER, frag = GL_FRAGMENT_SHADER, geom = GL_GEOMETRY_SHADER) + return ShaderSource(type2gltype[type], source, name) +end + const SHADER_DIR = normpath(joinpath(@__DIR__, "..", "assets", "shader")) const LOADED_SHADERS = Dict{String, ShaderSource}() const WARN_ON_LOAD = Ref(false) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 2a5f35550d9..0971ef44d36 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -203,4 +203,88 @@ end end end f +end + +@reference_test "Custom stage in render pipeline" begin + GLMakie.closeall() + + function GLMakie.construct(::Val{:Tint}, screen, framebuffer, inputs, parent) + frag_shader = """ + {{GLSL_VERSION}} + + in vec2 frag_uv; + out vec4 fragment_color; + + uniform sampler2D color_buffer; // \$(name of input)_buffer + uniform mat3 color_transform; // from Stage attributes + + void main(void) { + vec4 c = texture(color_buffer, frag_uv).rgba; + fragment_color = vec4(color_transform * c.rgb, c.a); + // fragment_color = vec4(1,0,0, c.a); + } + """ + + shader = GLMakie.LazyShader( + screen.shader_cache, + GLMakie.loadshader("postprocessing/fullscreen.vert"), + GLMakie.ShaderSource(frag_shader, :frag) + ) + + inputs[:color_transform] = parent.attributes[:color_transform] + + robj = GLMakie.RenderObject(inputs, shader, + GLMakie.PostprocessPrerender(), nothing, screen.glscreen + ) + robj.postrenderfunction = () -> GLMakie.draw_fullscreen(robj.vertexarray.id) + + return GLMakie.RenderPass{:Tint}(framebuffer, robj) + end + + function GLMakie.run_step(screen, glscene, step::GLMakie.RenderPass{:Tint}) + # Blend transparent onto opaque + wh = size(step.framebuffer) + GLMakie.set_draw_buffers(step.framebuffer) + GLMakie.glViewport(0, 0, wh[1], wh[2]) + GLMakie.GLAbstraction.render(step.robj) + return + end + + + begin + # Pipeline matches test_pipeline_2D up to color_tint + pipeline = Makie.Pipeline() + + render1 = push!(pipeline, Makie.RenderStage(transparency = false, fxaa = true)) + render2 = push!(pipeline, Makie.TransparentRenderStage()) + oit = push!(pipeline, Makie.OITStage()) + fxaa = push!(pipeline, Makie.FXAAStage(filter_in_shader = false)) + render3 = push!(pipeline, Makie.RenderStage(transparency = false, fxaa = false)) + color_tint = push!(pipeline, Makie.Stage(:Tint, + inputs = [:color => Makie.BufferFormat()], # defaults to 4x N0f8, i.e. 32Bit color + outputs = [:color => Makie.BufferFormat()], + color_transform = Observable(Makie.Mat3f( + # sepia filter + 0.393, 0.349, 0.272, + 0.769, 0.686, 0.534, + 0.189, 0.168, 0.131 + )) + )) + display_stage = push!(pipeline, Makie.DisplayStage()) + + connect!(pipeline, render1, fxaa) + connect!(pipeline, render1, display_stage, :objectid) + connect!(pipeline, render2, oit) + connect!(pipeline, render2, display_stage, :objectid) + connect!(pipeline, oit, fxaa, :color) + connect!(pipeline, fxaa, color_tint, :color) + connect!(pipeline, render3, color_tint, :color) + connect!(pipeline, render3, display_stage, :objectid) + connect!(pipeline, color_tint, display_stage, :color) + end + + + GLMakie.activate!(render_pipeline = pipeline) + cow = load(Makie.assetpath("cow.png")) + f,a,p = image(rotr90(cow)) end \ No newline at end of file diff --git a/src/theming.jl b/src/theming.jl index a4aebd95c33..464b79f912f 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -118,6 +118,7 @@ const MAKIE_DEFAULT_THEME = Attributes( oit = true, fxaa = true, ssao = false, + render_pipeline = default_pipeline(), # This adjusts a factor in the rendering shaders for order independent # transparency. This should be the same for all of them (within one rendering # pipeline) otherwise depth "order" will be broken. From 7d356cd446d3c3fab45aa583cc602164c755b210 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 6 Jan 2025 23:09:41 +0100 Subject: [PATCH 113/135] get renderpipeline to persist from activate!() --- src/display.jl | 12 ++++++++---- src/theming.jl | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/display.jl b/src/display.jl index 1bdb9b36dcd..523d0cbdb0f 100644 --- a/src/display.jl +++ b/src/display.jl @@ -74,10 +74,14 @@ function merge_screen_config(::Type{Config}, _config::Dict) where Config if key == :GLMakie config = Dict{Symbol, Any}(_config) get!(config, :render_pipeline) do - ssao = to_value(get(config, :ssao, backend_defaults[:ssao])) - fxaa = to_value(get(config, :fxaa, backend_defaults[:fxaa])) - oit = to_value(get(config, :oit, backend_defaults[:oit])) - return default_pipeline(; ssao, fxaa, oit) + if any(in(keys(config)), [:ssao, :fxaa, :oit]) || (backend_defaults[:render_pipeline] == automatic) + ssao = to_value(get(config, :ssao, backend_defaults[:ssao])) + fxaa = to_value(get(config, :fxaa, backend_defaults[:fxaa])) + oit = to_value(get(config, :oit, backend_defaults[:oit])) + return default_pipeline(; ssao, fxaa, oit) + else + return to_value(backend_defaults[:render_pipeline]) + end end else config = _config diff --git a/src/theming.jl b/src/theming.jl index 464b79f912f..bfefa5fbc68 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -118,7 +118,7 @@ const MAKIE_DEFAULT_THEME = Attributes( oit = true, fxaa = true, ssao = false, - render_pipeline = default_pipeline(), + render_pipeline = automatic, # This adjusts a factor in the rendering shaders for order independent # transparency. This should be the same for all of them (within one rendering # pipeline) otherwise depth "order" will be broken. From c07e4b980552d71d41da69a5be8c6dfdc8c5ba11 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 6 Jan 2025 23:19:38 +0100 Subject: [PATCH 114/135] fix missing to_value --- src/display.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/display.jl b/src/display.jl index 523d0cbdb0f..c2d05f26085 100644 --- a/src/display.jl +++ b/src/display.jl @@ -74,7 +74,8 @@ function merge_screen_config(::Type{Config}, _config::Dict) where Config if key == :GLMakie config = Dict{Symbol, Any}(_config) get!(config, :render_pipeline) do - if any(in(keys(config)), [:ssao, :fxaa, :oit]) || (backend_defaults[:render_pipeline] == automatic) + if any(in(keys(config)), [:ssao, :fxaa, :oit]) || + (to_value(backend_defaults[:render_pipeline]) == automatic) ssao = to_value(get(config, :ssao, backend_defaults[:ssao])) fxaa = to_value(get(config, :fxaa, backend_defaults[:fxaa])) oit = to_value(get(config, :oit, backend_defaults[:oit])) From 4d106cf455ed68d26e6e989a7fc54e1660cdfbbd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 7 Jan 2025 03:39:34 +0100 Subject: [PATCH 115/135] fix another incorrect free --- GLMakie/src/GLAbstraction/GLTypes.jl | 13 +++++++++++++ GLMakie/src/gl_backend.jl | 4 ++-- GLMakie/src/postprocessing.jl | 1 + GLMakie/test/glmakie_refimages.jl | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index a4d3f5e88ce..5936ca9ba61 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -431,6 +431,8 @@ include("GLRenderObject.jl") #################################################################################### # freeing +# const TRACE_FREE = Ref(false) + function free(x::T, called_from_finalizer = false) where {T} # don't free if already freed (this should only be set by unsafe_free) x.id == 0 && return @@ -439,6 +441,17 @@ function free(x::T, called_from_finalizer = false) where {T} # to be active or alive here, because unsafe_free() may also do # cleanup that doesn't depend on context + # if TRACE_FREE[] + # try + # error("tracing...") + # catch e + # bt = catch_backtrace() + # Threads.@spawn begin + # @warn "free(::$T)" exception = (e, bt) + # end + # end + # end + # This may be called from the scene finalizer in which case no errors and # no printing allowed from the current task if called_from_finalizer diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 13b9d0b9634..ec0237394f4 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -39,10 +39,10 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer catch e if called_from_finalizer Threads.@spawn begin - @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) + @error "Cached atlas textures should be removed explicitly! Dropping $(tex_func[1].id)" exception = (e, catch_backtrace()) end else - @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) + @error "Cached atlas textures should be removed explicitly! Dropping $(tex_func[1].id)" exception = (e, catch_backtrace()) end end tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index a32276b8fb6..467bd6df07e 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -399,6 +399,7 @@ function construct(::Val{:FXAA1}, screen, framebuffer, inputs, parent) loadshader("postprocessing/postprocess.frag"), view = Dict("FILTER_IN_SHADER" => filter_fxaa_in_shader ? "#define FILTER_IN_SHADER" : "") ) + filter_fxaa_in_shader || pop!(inputs, :objectid_buffer) robj = RenderObject(inputs, shader, PostprocessPrerender(), nothing, screen.glscreen) robj.postrenderfunction = () -> draw_fullscreen(robj.vertexarray.id) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 0971ef44d36..30dcfd0cef2 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -170,7 +170,7 @@ end end @reference_test "render stage parameters with SSAO postprocessor" begin - GLMakie.closeall() + GLMakie.closeall() # (mostly?) for recompilation of plot shaders GLMakie.activate!(ssao = true) f = Figure() ps = [Point3f(x, y, sin(x * y + y-x)) for x in range(-2, 2, length=21) for y in range(-2, 2, length=21)] From e02696926e95705ba48e017cd34882c9a263a87a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 7 Jan 2025 15:50:12 +0100 Subject: [PATCH 116/135] bit of convenience --- src/utilities/RenderPipeline.jl | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index d794d7549ab..69231625220 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -203,7 +203,7 @@ end """ connect!(pipeline::Pipeline, source::Union{Pipeline, Stage}, target::Union{Pipeline, Stage}) -Connects every output in `source` to every `input` in `target` that shares the +Connects every output in `source` to every input in `target` that shares the same name. For example, if `:a, :b, :c, :d` exist in source and `:b, :d, :e` exist in target, `:b, :d` will get connected. """ @@ -218,9 +218,19 @@ function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage}, t end return end + +""" + connect!(pipeline::Pipeline, [source = pipeline, target = pipeline], name::Symbol) + +Connects every output in `source` that uses the given `name` to every input in +`target` with the same `name`. `source` and `target` can be a pipeline, stage +or integer referring to stage in `pipeline`. If both are omitted inputs and +outputs from `pipeline` get connected. +""" function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage, Integer}, trg::Union{Pipeline, Stage, Integer}, key::Symbol) return connect!(pipeline, src, key, trg, key) end +Observables.connect!(pipeline::Pipeline, key::Symbol) = connect!(pipeline, pipeline, key, pipeline, key) # TODO: Not sure about this... Maybe it should be first/last instead? But what # then it wouldn't really work with e.g. SSAO, which needs color as an @@ -682,21 +692,19 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) if ssao connect!(pipeline, render1, _ssao) - connect!(pipeline, render1, display, :objectid) connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) end connect!(pipeline, render2, fxaa ? _fxaa : display) - connect!(pipeline, render2, display, :objectid) # make sure this merges with other objectids if oit connect!(pipeline, render3, _oit) connect!(pipeline, _oit, fxaa ? _fxaa : display, :color) else connect!(pipeline, render3, fxaa ? _fxaa : display, :color) end - connect!(pipeline, render3, display, :objectid) if fxaa connect!(pipeline, _fxaa, display, :color) end + connect!(pipeline, :objectid) return pipeline # end @@ -716,16 +724,13 @@ function test_pipeline_3D() display = push!(pipeline, DisplayStage()) connect!(pipeline, render1, ssao) - connect!(pipeline, render1, fxaa, :objectid) - connect!(pipeline, render1, display, :objectid) connect!(pipeline, ssao, fxaa, :color) connect!(pipeline, render2, fxaa, :color) - connect!(pipeline, render2, display, :objectid) connect!(pipeline, render3, oit) - connect!(pipeline, render3, display, :objectid) connect!(pipeline, oit, fxaa, :color) connect!(pipeline, fxaa, display, :color) connect!(pipeline, render4, display) + connect!(pipeline, :objectid) return pipeline end @@ -742,12 +747,11 @@ function test_pipeline_2D() display = push!(pipeline, DisplayStage()) connect!(pipeline, render1, fxaa) - connect!(pipeline, render1, display, :objectid) connect!(pipeline, render2, oit) - connect!(pipeline, render2, display, :objectid) connect!(pipeline, oit, fxaa, :color) connect!(pipeline, fxaa, display, :color) connect!(pipeline, render3, display) + connect!(pipeline, :objectid) return pipeline end From 41351c783e3e9811dde3c4304ffaea9c66eedb27 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 7 Jan 2025 18:32:10 +0100 Subject: [PATCH 117/135] clean up --- GLMakie/src/GLAbstraction/GLAbstraction.jl | 21 +++---- GLMakie/src/GLAbstraction/GLTypes.jl | 33 ++-------- GLMakie/src/gl_backend.jl | 18 +----- GLMakie/src/glwindow.jl | 33 ++++------ GLMakie/src/screen.jl | 71 ++++++++++++++-------- 5 files changed, 72 insertions(+), 104 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index cd41a680886..e368949e524 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -21,19 +21,16 @@ import Base: merge, resize!, similar, length, getindex, setindex! # Debug tools const GLMAKIE_DEBUG = Ref(false) -require_context() = nothing # implemented in GLMakie/glwindow.jl -function require_context_no_error(args...; async = false) - if async - require_context(args..., async = true) - else - try - require_context(args...) - catch e - @error exception = (e, Base.catch_backtrace()) - end - end +# implemented in GLMakie/glwindow.jl +function require_context_no_error(args...) end + +function require_context(ctx, current = ShaderAbstractions.current_context()) + msg = require_context_no_error(ctx, current) + isnothing(msg) && return nothing + error(msg) end -export require_context, check_context + +export require_context include("AbstractGPUArray.jl") diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 4c3f62042f9..7d377fbe984 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -451,25 +451,14 @@ include("GLRenderObject.jl") #################################################################################### # freeing -function free(x::T, called_from_finalizer = false) where {T} +function free(x::T) where {T} # don't free if already freed (this should only be set by unsafe_free) x.id == 0 && return - - # Note: context is checked higher up in the call stack but isn't guaranteed - # to be active or alive here, because unsafe_free() may also do - # cleanup that doesn't depend on context - - # This may be called from the scene finalizer in which case no errors and - # no printing allowed from the current task - if called_from_finalizer - try - unsafe_free(x) - catch e - Threads.@spawn Base.showerror(stderr, e) - end - else + clean_up_observables(x) + if context_alive(x.context) && is_context_active(x.context) unsafe_free(x) end + x.id = 0 return end @@ -483,25 +472,18 @@ end # OpenGL has the annoying habit of reusing id's when creating a new context # We need to make sure to only free the current one function unsafe_free(x::GLProgram) - x.id = ifelse(context_alive(x.context), x.id, 0) - is_context_active(x.context) || return glDeleteProgram(x.id) x.id = 0 return end function unsafe_free(x::Shader) - x.id = ifelse(context_alive(x.context), x.id, 0) - is_context_active(x.context) || return glDeleteShader(x.id) x.id = 0 return end function unsafe_free(x::GLBuffer) - clean_up_observables(x) - x.id = ifelse(context_alive(x.context), x.id, 0) - is_context_active(x.context) || return id = Ref(x.id) glDeleteBuffers(1, id) x.id = 0 @@ -509,9 +491,6 @@ function unsafe_free(x::GLBuffer) end function unsafe_free(x::Texture) - clean_up_observables(x) - x.id = ifelse(context_alive(x.context), x.id, 0) - is_context_active(x.context) || return glDeleteTextures(x.id) x.id = 0 return @@ -524,10 +503,8 @@ function unsafe_free(x::GLVertexArray) if x.indices isa GPUArray unsafe_free(x.indices) end - x.id = ifelse(context_alive(x.context), x.id, 0) - is_context_active(x.context) || return id = Ref(x.id) glDeleteVertexArrays(1, id) x.id = 0 return -end \ No newline at end of file +end diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index a51ef3576ce..cfb260552d9 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -27,24 +27,12 @@ function cleanup_texture_atlas!(context) return end -function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer = false) +function get_texture!(context, atlas::Makie.TextureAtlas) # clean up dead context! filter!(atlas_texture_cache) do ((ptr, ctx), tex_func) if GLAbstraction.context_alive(ctx) return true else - # Adding extra context, so no require_context_no_error(ctx, async = called_from_finalizer) - try - require_context(ctx) - catch e - if called_from_finalizer - Threads.@spawn begin - @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) - end - else - @error "Cached atlas textures should be removed explicitly!" exception = (e, catch_backtrace()) - end - end tex_func[1].id = 0 # Should get cleaned up when OpenGL context gets destroyed Makie.remove_font_render_callback!(atlas, tex_func[2]) return false @@ -53,10 +41,8 @@ function get_texture!(context, atlas::Makie.TextureAtlas, called_from_finalizer if haskey(atlas_texture_cache, (atlas, context)) return atlas_texture_cache[(atlas, context)][1] - elseif called_from_finalizer - return nothing else - require_context(context, async = called_from_finalizer) + require_context(context) tex = Texture( context, atlas.data, minfilter = :linear, diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 2e2f1338f26..170828ca4f5 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -229,28 +229,17 @@ function ShaderAbstractions.native_context_alive(x::GLFW.Window) GLFW.is_initialized() && !was_destroyed(x) end -function GLAbstraction.require_context(ctx, current = ShaderAbstractions.current_context(); async = false) - if async - if !GLFW.is_initialized() - Threads.@spawn begin - @error "Failed to require context:" exception = (ErrorException("Context $ctx must be initialized, but is not."), backtrace()) - end - end - if GLAbstraction.GLMAKIE_DEBUG[] && was_destroyed(ctx) - Threads.@spawn begin - @error "Failed to require context:" exception = (ErrorException("Context $ctx must not be destroyed."), backtrace()) - end - end - if ctx != current - Threads.@spawn begin - @error "Failed to require context:" exception = (ErrorException("Context $ctx must be current, but $current is."), backtrace()) - end - end - else - @assert GLFW.is_initialized() "Context $ctx must be initialized, but is not." - @assert !GLAbstraction.GLMAKIE_DEBUG[] || !was_destroyed(ctx) "Context $ctx must not be destroyed." - @assert ctx == current "Context $ctx must be current, but $current is." - end +function check_context(ctx) + !GLFW.is_initialized() && return "GLFW is not initialized, and therefore $ctx is invalid." + was_destroyed(ctx) && return "Context $ctx has been destroyed." + return nothing +end + +function GLAbstraction.require_context_no_error(ctx, current = ShaderAbstractions.current_context()) + msg = check_context(ctx) + msg !== nothing && return msg + ctx !== current && return "Context $ctx must be current, but $current is." + return nothing end function destroy!(nw::GLFW.Window) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index d4bf25e7537..2f54af8d331 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -537,12 +537,12 @@ function Makie.insertplots!(screen::Screen, scene::Scene) end # Note: called from scene finalizer, must not error -function Base.delete!(screen::Screen, scene::Scene, called_from_finalizer::Bool = true) +function Base.delete!(screen::Screen, scene::Scene) for child in scene.children - delete!(screen, child, called_from_finalizer) + delete!(screen, child) end for plot in scene.plots - delete!(screen, scene, plot, called_from_finalizer) + delete!(screen, scene, plot) end filter!(x -> x !== screen, scene.current_screens) if haskey(screen.screen2scene, WeakRef(scene)) @@ -576,34 +576,50 @@ function Base.delete!(screen::Screen, scene::Scene, called_from_finalizer::Bool return end -function destroy!(rob::RenderObject, called_from_finalizer = false) +function destroy!(rob::RenderObject) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. - ShaderAbstractions.switch_context!(rob.context) - tex = get_texture!(rob.context, gl_texture_atlas(), called_from_finalizer) - for (k, v) in rob.uniforms - if v isa Observable - Observables.clear(v) - elseif v isa GPUArray && v !== tex - # We usually don't share gpu data and it should be hard for users to share buffers.. - # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources - - # TODO, refcounting, or leaving freeing to GC... - # GC can cause random context switches, so immediate free is necessary. - # I guess as long as we make it hard for users to share buffers directly, this should be fine! - GLAbstraction.free(v, called_from_finalizer) + with_context(rob.context) do + # Get texture for texture atlas directly, to not trigger a new texture creation + tex = get(atlas_texture_cache, (gl_texture_atlas(), rob.context), nothing)[1] + for (k, v) in rob.uniforms + if v isa Observable + Observables.clear(v) + elseif v isa GPUArray && v !== tex + # We usually don't share gpu data and it should be hard for users to share buffers.. + # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources + + # TODO, refcounting, or leaving freeing to GC... + # GC can cause random context switches, so immediate free is necessary. + # I guess as long as we make it hard for users to share buffers directly, this should be fine! + GLAbstraction.free(v) + end + end + for obs in rob.observables + Observables.clear(obs) end + GLAbstraction.free(rob.vertexarray) end - for obs in rob.observables - Observables.clear(obs) + return +end + +function with_context(f, context) + old_ctx = ShaderAbstractions.ACTIVE_OPENGL_CONTEXT[] + GLAbstraction.switch_context!(context) + try + f() + finally + if isnothing(old_ctx) + GLAbstraction.switch_context!() + else + GLAbstraction.switch_context!(old_ctx) + end end - GLAbstraction.free(rob.vertexarray, called_from_finalizer) - # avoid try .. catch at call site (call site usually destroys multipel renderobjects) - GLAbstraction.require_context_no_error(rob.context; async = called_from_finalizer) end # Note: called from scene finalizer, must not error -function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot, called_from_finalizer::Bool = true) +function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) + if !isempty(plot.plots) # this plot consists of children, so we flatten it and delete the children instead for cplot in Makie.collect_atomic_plots(plot) @@ -614,9 +630,12 @@ function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot, called_f # TODO, is it? renderobject = get(screen.cache, objectid(plot), nothing) if !isnothing(renderobject) - destroy!(renderobject, called_from_finalizer) - filter!(x-> x[3] !== renderobject, screen.renderlist) - delete!(screen.cache2plot, renderobject.id) + # Switch to context, so we can delete the renderobjects + with_context(screen.glscreen) do + destroy!(renderobject) + filter!(x-> x[3] !== renderobject, screen.renderlist) + delete!(screen.cache2plot, renderobject.id) + end end delete!(screen.cache, objectid(plot)) end From 8529efb7063d0a8faacacb89a60ff05235ab817a Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 8 Jan 2025 13:06:46 +0100 Subject: [PATCH 118/135] fix errors --- GLMakie/src/GLAbstraction/GLTexture.jl | 6 +++--- GLMakie/src/screen.jl | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 251be7506a7..d919b8e6ade 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -66,9 +66,9 @@ end bind(t::Texture, id) = glBindTexture(t.texturetype, id) ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture.context) -function free(tb::TextureBuffer, called_from_finalizer = false) - free(tb.texture, called_from_finalizer) - free(tb.buffer, called_from_finalizer) +function free(tb::TextureBuffer) + free(tb.texture) + free(tb.buffer) end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 2f54af8d331..c97407c3ebf 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -581,7 +581,7 @@ function destroy!(rob::RenderObject) # remain when the plot is deleted. with_context(rob.context) do # Get texture for texture atlas directly, to not trigger a new texture creation - tex = get(atlas_texture_cache, (gl_texture_atlas(), rob.context), nothing)[1] + tex = get(atlas_texture_cache, (gl_texture_atlas(), rob.context), (nothing,))[1] for (k, v) in rob.uniforms if v isa Observable Observables.clear(v) @@ -604,7 +604,8 @@ function destroy!(rob::RenderObject) end function with_context(f, context) - old_ctx = ShaderAbstractions.ACTIVE_OPENGL_CONTEXT[] + CTX = ShaderAbstractions.ACTIVE_OPENGL_CONTEXT + old_ctx = isassigned(CTX) ? CTX[] : nothing GLAbstraction.switch_context!(context) try f() From a9526453675b6987bcea8a95bb4eff69fc56de04 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 8 Jan 2025 13:43:32 +0100 Subject: [PATCH 119/135] fix freeing of GLBuffer Observables --- GLMakie/src/GLAbstraction/GLTypes.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 7d377fbe984..087a0387679 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -453,8 +453,8 @@ include("GLRenderObject.jl") function free(x::T) where {T} # don't free if already freed (this should only be set by unsafe_free) - x.id == 0 && return clean_up_observables(x) + x.id == 0 && return if context_alive(x.context) && is_context_active(x.context) unsafe_free(x) end @@ -462,13 +462,21 @@ function free(x::T) where {T} return end -function clean_up_observables(x::T) where T +function clean_up_observables(x::GLVertexArray) where {T} + for (_, buffer) in x.buffers + clean_up_observables(buffer) + end + if x.indices isa GPUArray + clean_up_observables(x.indices) + end +end + +function clean_up_observables(x::T) where {T} if hasfield(T, :observers) foreach(off, x.observers) empty!(x.observers) end end - # OpenGL has the annoying habit of reusing id's when creating a new context # We need to make sure to only free the current one function unsafe_free(x::GLProgram) From 27e15c38a305b9828aed12ee20e0c1e899911956 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 16:52:07 +0100 Subject: [PATCH 120/135] rename Pipeline -> RenderPipeline --- GLMakie/src/postprocessing.jl | 4 +- GLMakie/src/render_pipeline.jl | 4 +- GLMakie/src/screen.jl | 4 +- src/utilities/RenderPipeline.jl | 78 ++++++++++++++++----------------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 467bd6df07e..cad8762ab5c 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -55,10 +55,10 @@ Broadcast.broadcastable(x::AbstractRenderStep) = Ref(x) struct GLRenderPipeline - parent::Makie.Pipeline + parent::Makie.RenderPipeline steps::Vector{AbstractRenderStep} end -GLRenderPipeline() = GLRenderPipeline(Makie.Pipeline(), AbstractRenderStep[]) +GLRenderPipeline() = GLRenderPipeline(Makie.RenderPipeline(), AbstractRenderStep[]) function render_frame(screen, glscene, pipeline::GLRenderPipeline) for step in pipeline.steps diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 8613ec1e871..71eacb555ad 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -46,8 +46,8 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF end -function gl_render_pipeline!(screen::Screen, pipeline::Makie.Pipeline) - pipeline.stages[end].name === :Display || error("Pipeline must end with a Display stage") +function gl_render_pipeline!(screen::Screen, pipeline::Makie.RenderPipeline) + pipeline.stages[end].name === :Display || error("RenderPipeline must end with a Display stage") previous_pipeline = screen.render_pipeline # Exit early if the pipeline is already up to date diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 73ea691d160..59c417008bd 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -60,7 +60,7 @@ mutable struct ScreenConfig scalefactor::Union{Nothing, Float32} # Render Constants & Postprocessor - render_pipeline::Makie.Pipeline + render_pipeline::Makie.RenderPipeline transparency_weight_scale::Float32 max_lights::Int max_light_parameters::Int @@ -85,7 +85,7 @@ mutable struct ScreenConfig scalefactor::Union{Makie.Automatic, Number}, # Preprocessor - render_pipeline::Makie.Pipeline, + render_pipeline::Makie.RenderPipeline, transparency_weight_scale::Number, max_lights::Int, max_light_parameters::Int) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 69231625220..50e52ad2831 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -153,7 +153,7 @@ function Base.:(==)(s1::Stage, s2::Stage) (s1.attributes == s2.attributes) end -struct Pipeline +struct RenderPipeline stages::Vector{Stage} # (stage_idx, negative input index or positive output index) -> connection format index @@ -164,17 +164,17 @@ struct Pipeline end """ - Pipeline([stages::Stage...]) + RenderPipeline([stages::Stage...]) -Creates a `Pipeline` from the given `stages` or an empty pipeline if none are +Creates a `RenderPipeline` from the given `stages` or an empty pipeline if none are given. The pipeline represents a series of actions (stages) executed during rendering. """ -function Pipeline() - return Pipeline(Stage[], Dict{Tuple{Int, Int}, Int}(), BufferFormat[]) +function RenderPipeline() + return RenderPipeline(Stage[], Dict{Tuple{Int, Int}, Int}(), BufferFormat[]) end -function Pipeline(stages::Stage...) - pipeline = Pipeline() +function RenderPipeline(stages::Stage...) + pipeline = RenderPipeline() foreach(stage -> push!(pipeline, stage), stages) return pipeline end @@ -185,11 +185,11 @@ end # |-> combine # render -> effect 2 -' # where render is the same (name/task, inputs, outputs) -function Base.push!(pipeline::Pipeline, stage::Stage) +function Base.push!(pipeline::RenderPipeline, stage::Stage) push!(pipeline.stages, stage) return stage # for convenience end -function Base.push!(pipeline::Pipeline, other::Pipeline) +function Base.push!(pipeline::RenderPipeline, other::RenderPipeline) N = length(pipeline.stages); M = length(pipeline.formats) append!(pipeline.stages, other.stages) for ((stage_idx, io_idx), format_idx) in other.stageio2idx @@ -201,14 +201,14 @@ end """ - connect!(pipeline::Pipeline, source::Union{Pipeline, Stage}, target::Union{Pipeline, Stage}) + connect!(pipeline::RenderPipeline, source::Union{RenderPipeline, Stage}, target::Union{RenderPipeline, Stage}) Connects every output in `source` to every input in `target` that shares the same name. For example, if `:a, :b, :c, :d` exist in source and `:b, :d, :e` exist in target, `:b, :d` will get connected. """ -function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage}, trg::Union{Pipeline, Stage}) - stages(pipeline::Pipeline) = pipeline.stages +function Observables.connect!(pipeline::RenderPipeline, src::Union{RenderPipeline, Stage}, trg::Union{RenderPipeline, Stage}) + stages(pipeline::RenderPipeline) = pipeline.stages stages(stage::Stage) = [stage] outputs = Set(mapreduce(stage -> keys(stage.outputs), union, stages(src))) @@ -220,17 +220,17 @@ function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage}, t end """ - connect!(pipeline::Pipeline, [source = pipeline, target = pipeline], name::Symbol) + connect!(pipeline::RenderPipeline, [source = pipeline, target = pipeline], name::Symbol) Connects every output in `source` that uses the given `name` to every input in `target` with the same `name`. `source` and `target` can be a pipeline, stage or integer referring to stage in `pipeline`. If both are omitted inputs and outputs from `pipeline` get connected. """ -function Observables.connect!(pipeline::Pipeline, src::Union{Pipeline, Stage, Integer}, trg::Union{Pipeline, Stage, Integer}, key::Symbol) +function Observables.connect!(pipeline::RenderPipeline, src::Union{RenderPipeline, Stage, Integer}, trg::Union{RenderPipeline, Stage, Integer}, key::Symbol) return connect!(pipeline, src, key, trg, key) end -Observables.connect!(pipeline::Pipeline, key::Symbol) = connect!(pipeline, pipeline, key, pipeline, key) +Observables.connect!(pipeline::RenderPipeline, key::Symbol) = connect!(pipeline, pipeline, key, pipeline, key) # TODO: Not sure about this... Maybe it should be first/last instead? But what # then it wouldn't really work with e.g. SSAO, which needs color as an @@ -243,17 +243,17 @@ to an `input` of `target`. If either already has a connection the new connection will be merged with the old. The source and target stage as well as the pipeline will be updated appropriately. -`source` and `target` can also be `Pipeline`s if both output and input are +`source` and `target` can also be `RenderPipeline`s if both output and input are `Symbol`s. In this case every stage in source with an appropriately named output is connected to every stage in target with an appropriately named input. Use with caution. """ -function Observables.connect!(pipeline::Pipeline, - src::Union{Pipeline, Stage}, output::Symbol, - trg::Union{Pipeline, Stage}, input::Symbol +function Observables.connect!(pipeline::RenderPipeline, + src::Union{RenderPipeline, Stage}, output::Symbol, + trg::Union{RenderPipeline, Stage}, input::Symbol ) - iterable(pipeline::Pipeline) = pipeline.stages + iterable(pipeline::RenderPipeline) = pipeline.stages iterable(stage::Stage) = Ref(stage) for source in iterable(src) @@ -271,16 +271,16 @@ function Observables.connect!(pipeline::Pipeline, return end -function Observables.connect!(pipeline::Pipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) +function Observables.connect!(pipeline::RenderPipeline, src::Integer, output::Symbol, trg::Integer, input::Symbol) return connect!(pipeline, src, output, trg, input) end -function Observables.connect!(pipeline::Pipeline, source::Stage, output::Integer, target::Stage, input::Integer) +function Observables.connect!(pipeline::RenderPipeline, source::Stage, output::Integer, target::Stage, input::Integer) src = findfirst(x -> x === source, pipeline.stages) trg = findfirst(x -> x === target, pipeline.stages) return connect!(pipeline, src, output, trg, input) end -function Observables.connect!(pipeline::Pipeline, source::Stage, output::Symbol, target::Stage, input::Symbol) +function Observables.connect!(pipeline::RenderPipeline, source::Stage, output::Symbol, target::Stage, input::Symbol) haskey(source.outputs, output) || error("output $output does not exist in source stage") haskey(target.inputs, input) || error("input $input does not exist in target stage") output_idx = source.outputs[output] @@ -288,7 +288,7 @@ function Observables.connect!(pipeline::Pipeline, source::Stage, output::Symbol, return connect!(pipeline, source, output_idx, target, input_idx) end -function Observables.connect!(pipeline::Pipeline, src::Integer, output::Integer, trg::Integer, input::Integer) +function Observables.connect!(pipeline::RenderPipeline, src::Integer, output::Integer, trg::Integer, input::Integer) @boundscheck begin checkbounds(pipeline.stages, src) checkbounds(pipeline.stages, trg) @@ -367,7 +367,7 @@ returns them together with a connection-to-index map. This will attempt to optimize buffers for the lowest memory overhead. I.e. it will reuse buffers for multiple connections and upgrade them if it is cheaper than creating a new one. """ -function generate_buffers(pipeline::Pipeline) +function generate_buffers(pipeline::RenderPipeline) # Verify that outputs are continuously connected (i.e. if N then 1..N-1 as well) output_max = zeros(Int, length(pipeline.stages)) output_sum = zeros(Int, length(pipeline.stages)) @@ -530,16 +530,16 @@ function Base.show(io::IO, ::MIME"text/plain", stage::Stage) return end -function Base.show(io::IO, ::MIME"text/plain", pipeline::Pipeline) +function Base.show(io::IO, ::MIME"text/plain", pipeline::RenderPipeline) return show_resolved(io, pipeline, pipeline.formats, collect(eachindex(pipeline.formats))) end -function show_resolved(pipeline::Pipeline, buffers, remap) +function show_resolved(pipeline::RenderPipeline, buffers, remap) return show_resolved(stdout, pipeline, buffers, remap) end -function show_resolved(io::IO, pipeline::Pipeline, buffers, remap) - println(io, "Pipeline():") +function show_resolved(io::IO, pipeline::RenderPipeline, buffers, remap) + println(io, "RenderPipeline():") print(io, "Stages:") pad = isempty(buffers) ? 0 : 1 + floor(Int, log10(length(buffers))) @@ -619,7 +619,7 @@ function SSAOStage(; kwargs...) input_formats = [BufferFormat(1, N0f8), BufferFormat(4, N0f8), BufferFormat(2, UInt32)] stage2 = Stage(:SSAO2, inputs, input_formats, Dict(:color => 1), [BufferFormat()]; kwargs...) - pipeline = Pipeline(stage1, stage2) + pipeline = RenderPipeline(stage1, stage2) connect!(pipeline, stage1, 1, stage2, 1) return pipeline @@ -644,7 +644,7 @@ function FXAAStage(; kwargs...) Dict(:color => 1), [BufferFormat(4, N0f8)]; kwargs... ) - pipeline = Pipeline(stage1, stage2) + pipeline = RenderPipeline(stage1, stage2) connect!(pipeline, stage1, 1, stage2, 1) return pipeline @@ -658,7 +658,7 @@ end # TODO: caching is dangerous with mutable attributes... -# const PIPELINE_CACHE = Dict{Symbol, Pipeline}() +# const PIPELINE_CACHE = Dict{Symbol, RenderPipeline}() function default_pipeline(; ssao = false, fxaa = true, oit = true) # name = Symbol(:default_pipeline, Int(ssao), Int(fxaa), Int(oit)) @@ -666,7 +666,7 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) # Mimic GLMakie's old hard coded render pipeline # get!(PIPELINE_CACHE, name) do - pipeline = Pipeline() + pipeline = RenderPipeline() push!(pipeline, SortStage()) # Note - order important! @@ -711,7 +711,7 @@ function default_pipeline(; ssao = false, fxaa = true, oit = true) end function test_pipeline_3D() - pipeline = Pipeline() + pipeline = RenderPipeline() render1 = push!(pipeline, RenderStage(ssao = true, transparency = false, fxaa = true)) ssao = push!(pipeline, SSAOStage()) @@ -736,7 +736,7 @@ function test_pipeline_3D() end function test_pipeline_2D() - pipeline = Pipeline() + pipeline = RenderPipeline() # dedicated fxaa = false pass + no SSAO render1 = push!(pipeline, RenderStage(transparency = false, fxaa = true)) @@ -757,7 +757,7 @@ function test_pipeline_2D() end function test_pipeline_GUI() - pipeline = Pipeline() + pipeline = RenderPipeline() # GUI elements don't need OIT because they are (usually?) layered plot by # plot, rather than element by element. Do need FXAA occasionally, e.g. Toggle @@ -775,7 +775,7 @@ function test_pipeline_GUI() end function test_pipeline_minimal() - pipeline = Pipeline() + pipeline = RenderPipeline() # GUI elements don't need OIT because they are (usually?) layered plot by # plot, rather than element by element. Do need FXAA occasionally, e.g. Toggle @@ -787,7 +787,7 @@ function test_pipeline_minimal() end ################################################################################ -## Experimental GUI for Pipeline +## Experimental GUI for RenderPipeline ################################################################################ function pipeline_gui!(ax, pipeline) @@ -944,7 +944,7 @@ function pipeline_gui!(ax, pipeline) notify(origins) - scale = map(pv -> max(0.5, 10*min(pv[1,1], pv[2,2])), ax.scene.camera.projectionview) + scale = map(pv -> max(0.5, 10*min(pv[1,1], pv[2,2])), get_scene(ax).camera.projectionview) poly!(ax, rects_obs, strokewidth = scale, strokecolor = :black, fxaa = false, shading = NoShading, color = :lightgray) From 1afd177a92203be606cff9c07b919781784485e5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 16:52:34 +0100 Subject: [PATCH 121/135] document rendering in Cairo, GL and WGLMakie --- docs/makedocs.jl | 1 + docs/src/explanations/render_pipeline.md | 285 +++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 docs/src/explanations/render_pipeline.md diff --git a/docs/makedocs.jl b/docs/makedocs.jl index d7f0b9e3cd7..263a7d10aaa 100644 --- a/docs/makedocs.jl +++ b/docs/makedocs.jl @@ -188,6 +188,7 @@ pages = [ "explanations/theming/predefined_themes.md", ], "explanations/transparency.md", + "explanations/render_pipeline.md" ], "How-Tos" => [ "how-to/match-figure-size-font-sizes-and-dpi.md", diff --git a/docs/src/explanations/render_pipeline.md b/docs/src/explanations/render_pipeline.md new file mode 100644 index 00000000000..8ed9bed4d53 --- /dev/null +++ b/docs/src/explanations/render_pipeline.md @@ -0,0 +1,285 @@ +# Render Pipeline + +The render pipeline determines in what order plots are rendered and how they get composed into the final image you see. +Currently each backend has its own pipeline which can cause them to behave differently in some cases. +This section aims to summarize how they work and what you can influence to potentially deal with those issues. +Note that this is a fairly in-depth topic that you should usually not need to deal with. +Note as well that we may change pipelines in the future to improve the quality of a backend or the consistency between backends. + +## CairoMakie + +CairoMakie's pipeline is the simplest out of the backends. +It first draws the backgrounds of each scene with `scene.clear[] == true` recursively, starting with the root scene of the scene tree. +Then it gathers all plots in the scene tree for rendering. +They get sorted based on `Makie.zvalue2d()` which queries `plot.transformation.model` and then rendered if they are visible. +In pseudocode it boils down to: + +```julia +function cairo_draw(root_scene) + draw_background_rec(root_scene) + + all_plots = collect_all_plots_recursively(root_scene) + for plot in sort!(all_plots, by = Makie.zvalue2d) + if plot.visible[] && parent_scene(plot).visible[] + clip_to_parent_scene(plot) + draw_plot(plot) + end + end +end + +function draw_background_rec(scene) + if scene.clear[] + draw_background(scene) + end + foreach(draw_background_rec, scene.children) +end +``` + +Note that Cairo or more specifically svg and pdf do not support per-pixel z order. +Therefore z order is generally limited to be per-plot. +Some mesh plots additionally sort triangles based on their average depth to improve on this. +This however is limited to each plot, i.e. another plot will either be completely below or above. + +## GLMakie + +In GLMakie each frame begins by clearing a color buffer, depth buffer and objectid buffer. +Next we go through all scenes in the scene tree recursively and draw their backgrounds to the initial color buffer if `scene.clear[] == true`. +After that rendering is determined by the `Makie.Renderpipeline` that is used by the screen. + +### Default RenderPipeline + +The default pipeline looks like this: + +```@figure +scene = Scene(size = (800, 300), camera = cam2d!) +Makie.pipeline_gui!(scene, Makie.default_pipeline()) +center!(scene) +scene +``` + +It begins with `ZSort` which sorts all plots based on `Makie.zvalue2d`. +Next is the `Render` stage which renders all plots with `plot.transparency[] = false` to the initial color buffer. +Then we get to two Order Independent Transparency Stages. +We render every plot with `plot.transparency[] = true` in `OIT Render`, weighing their colors based on depth and alpha values and then blend them with the opaque color buffer from the first `Render` stage. +The result continues through two anti-aliasing stages `FXAA1` and `FXAA2`. +The first calculates luma (brightness) values and the second uses them to find aliased edges and blur them. +The result moves on to the `Display` stage which copies the color buffer to the screen. + +### Plot and Scene Insertion + +There are two ways plots and scenes can be added to a GLMakie screen. +The first is by displaying a root scene, i.e. (implicitly) calling `display(fig)` or `display(scene)`. +In this case plots and scenes are added to the screen like in CairoMakie. +In pseudocode we effectively do: + +```julia +function add_scene!(screen, scene) + add_scene!(screen, scene) + foreach(plot -> add_plot!(screen, plot, scene), scene.plots) + foreach(scene -> add_scene!(screen, scene), scene.children) +end +``` + +The second way is to add them interactively, i.e. while the root figure or scene is already being displayed. +Adding a plot to a scene will effectively call the `add_plot!(screen, plot, scene)` function above. +Adding a scene however does not call `add_scene!(screen, scene)`. +It is only added once a plot is added to the new scene. +This is usually not a problem, but can cause differences in clearing order. +Deletion works the same for the plots and scenes - both immediately get removed from the screen. +For scenes all child scenes and child plots get deleted as well. + +Note that plot (insertion) order also plays a role in rendering. +When two plots are not separated due to render passes or z order, they will get rendered in the order they were inserted. + +### Overdraw Attribute + +The `overdraw` attribute does not affect when a plot gets rendered in terms of render order or render passes. +Instead it affects depth testing. +A plot with `overdraw = true` ignores depth values when drawing and does not write out its own depth values. +This means that such a plot will draw over everything that has been render, but not stop later plots from drawing over it. + +### SSAO RenderPipeline + +Screen Space Ambient Occlusion is an optional post-processing effect you can turn on in GLMakie. +It modifies the render loop to include 3 more stages - one Render stage and two SSAO stages: + +```@figure +scene = Scene(size = (1000, 300), camera = cam2d!) +Makie.pipeline_gui!(scene, Makie.default_pipeline(ssao = true)) +center!(scene) +scene +``` + +The `Render` stage includes options for filtering plots based on their values for `ssao`, `fxaa` and `transparency`. +The single Render in the default pipeline only filtered based on `transparency == false`. +Now we also filter based on `ssao`. +The first Render stage requires `ssao == true && transparency == false`, the second `ssao = false && transparency = false`. +OIT Render requires `transparency == true`, so that all combinations are covered. +`fxaa` is handled per-pixel in the FXAA stages in this pipeline, but can also be handled per plot. + +After the first Render Stage there are two SSAO stages. +The first calculates an occlusion factor based on the surrounding geometry using position and normal data recorded in the render stage. +The second smooths the occlusion factor and applies it to the recorded colors. +The next Render stage then continues to add to those colors before merging with OIT and applying FXAA. + +### Custom RenderPipeline + +It is possible to define your own pipeline and render stages though the latter requires knowledge of some GLMakie internals and OpenGL. +Explaining those is outside the scope of this section. +Instead we provide a small example you can use as a starting point. +You may also want to look at "GLMakie/src/postprocessing.jl" and "GLMakie/assets/postprocessors" to reference the existing implementations of pipeline stages. + +As an example we will create a stage that applies a Sepia effect to our figure. +Lets begin by setting up a pipeline that includes the stage. + +```@figure RenderPipeline backend=GLMakie +# Create an empty Pipeline +pipeline = Makie.RenderPipeline() + +# Add stages in order +render1 = push!(pipeline, Makie.RenderStage(transparency = false)) +render2 = push!(pipeline, Makie.TransparentRenderStage()) +oit = push!(pipeline, Makie.OITStage()) +fxaa = push!(pipeline, Makie.FXAAStage()) # includes FXAA1 & FXAA2 with color_luma connection + +# Our new stage takes a 32bit color and produces a new 32 bit color +color_tint = Makie.Stage(:Tint, + # BufferFormat defaults to 4x N0f8, i.e. 32Bit color + inputs = [:color => Makie.BufferFormat()], + outputs = [:color => Makie.BufferFormat()], + color_transform = Observable(Makie.Mat3f( + # sepia filter + 0.393, 0.349, 0.272, + 0.769, 0.686, 0.534, + 0.189, 0.168, 0.131 + )) +) +push!(pipeline, color_tint) +display_stage = push!(pipeline, Makie.DisplayStage()) + +# Connect stages +connect!(pipeline, render1, fxaa) # connect everything from render1 to FXAA1, FXAA2 +connect!(pipeline, render2, oit) # everything from OIT Render -> OIT +connect!(pipeline, :objectid) # connect all :objectid inputs and outputs +connect!(pipeline, oit, fxaa, :color) # OIT color output -> fxaa color input +connect!(pipeline, fxaa, color_tint, :color) # fxaa color output -> color tint input +connect!(pipeline, color_tint, display_stage, :color) # color tint -> display +``` + +Note that you can also create connections with `Makie.pipeline_gui!(ax, pipeline)`. + +When passing this pipeline to GLMakie, it will determine all the necessary buffers it needs to allocate. +Each stage will get a framebuffer based on the connected outputs of the pipeline and an `inputs` Dict based on the connected inputs of the pipeline. +The names match the names of the pipeline stage with a `_buffer` postfix. +What we now need to do is define a constructor for a `<: GLMakie.AbstractRenderStep` object which represents the stage in the `GLRenderPipeline`, and a `run_step()` method that executes the stage. + +```@figure RenderPipeline backend=GLMakie +using GLMakie + +function GLMakie.construct(::Val{:Tint}, screen, framebuffer, inputs, parent) + # Create the shader that applies the Sepia effect. + # For the vertex shader we can reuse "fullscreen.vert", which sets up frag_uv. + # For the fragment shader we add a new shader here, which reads a color + # from an input texture, applies a transformation and writes it to the + # output color buffer. + frag_shader = """ + {{GLSL_VERSION}} + + in vec2 frag_uv; + out vec4 fragment_color; + + uniform sampler2D color_buffer; // \$(name of input)_buffer + uniform mat3 color_transform; // from Stage attributes + + void main(void) { + vec4 c = texture(color_buffer, frag_uv).rgba; + fragment_color = vec4(color_transform * c.rgb, c.a); + } + """ + + # Create a GLMakie Shader that will get compiled later + shader = GLMakie.LazyShader( + screen.shader_cache, + GLMakie.loadshader("postprocessing/fullscreen.vert"), + GLMakie.ShaderSource(frag_shader, :frag) + ) + + # `inputs` are unique to each stage, so we can directly pass them as inputs + # of the RenderObject. On top of the `color_buffer` generated from the + # stage input we add a `color_transform` which connects to the color_transform + # in our stage: + inputs[:color_transform] = parent.attributes[:color_transform] + + # We then create a RenderObject: + robj = GLMakie.RenderObject( + inputs, # will lower to uniforms (and vertex attributes/vertex shader inputs) + shader, # will be compiled to a shader program + GLMakie.PostprocessPrerender(), # turn of depth testing, blending and culling before rendering + nothing, # postrender function - set later + screen.glscreen # OpenGL context of the RenderObject + ) + # postrenderfunction is the "nothing" from before + # this produces a draw call with two triangles representing th full framebuffer + robj.postrenderfunction = () -> GLMakie.draw_fullscreen(robj.vertexarray.id) + + # Finally we create a "RenderPass" which simply contains the render object and framebuffer + return GLMakie.RenderPass{:Tint}(framebuffer, robj) +end + +function GLMakie.run_step(screen, glscene, step::GLMakie.RenderPass{:Tint}) + wh = size(step.framebuffer) + # This binds all color buffers in the framebuffer (i.e. our one color output) + GLMakie.set_draw_buffers(step.framebuffer) + GLMakie.glViewport(0, 0, wh[1], wh[2]) + GLMakie.GLAbstraction.render(step.robj) + return +end +``` + +With that done we can now use our new pipeline for rendering: + +```@figure RenderPipeline backend=GLMakie +using FileIO + +GLMakie.activate!(render_pipeline = pipeline) +cow = load(Makie.assetpath("cow.png")) +f,a,p = image(rotr90(cow)) +``` + +## WGLMakie + +WGLMakie does not (yet) support the RenderPipeline. +Instead it relies on Three.js for rendering and adjusts a few settings. +It sets `antialias = true` for all plots, ignoring `fxaa` attributes. +It keeps `sortObjects = true` which enables sorting of plots with transparency. +The `transparency` attribute only affects depth writes. +The renderloop boils down to: + +```julia +function render_scene(scene) + if !scene.visible; return end + set_region(scene.viewport) + if scene.clear; clear(color, depth) end + foreach(render, scene.plots) + foreach(render_scene, scene.children) +end +``` + +Note that unlike CairoMakie and GLMakie, WGLMakie renders scene by scene. +I.e. if a child scene clears, nothing from the parent scene will be visible in that area. + +### Plot & Scene Insertion + +WGLMakie follows the same plot and scene insertion logic as GLMakie. +This includes a new scene only getting added when a plot gets added to it. +The main difference to GLMakie is that plots and scenes get serialized so they can be transferred to JavaScript instead of getting added to a screen directly. + +```julia +function serialize_scene(scene) + return Dict( + :plots => map(plot -> serialize_plot(scene, plot), scene.plots) + :children => map(serialize_scene, scene.children), + # scene data... + ) +end +``` \ No newline at end of file From 1ee95f732494acc38bf33825b1b4590e5d6dd877 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 21:11:55 +0100 Subject: [PATCH 122/135] cleanup unsafe_free --- .../src/GLAbstraction/GLExtendedFunctions.jl | 9 ++--- GLMakie/src/GLAbstraction/GLTypes.jl | 38 +++++-------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl b/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl index 15b7fa6d0d0..e1fb97bcce2 100644 --- a/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl +++ b/GLMakie/src/GLAbstraction/GLExtendedFunctions.jl @@ -145,18 +145,15 @@ function glGenFramebuffers() end function glDeleteTextures(id::GLuint) - arr = [id] - glDeleteTextures(1, arr) + glDeleteTextures(1, Ref(id)) end function glDeleteVertexArrays(id::GLuint) - arr = [id] - glDeleteVertexArrays(1, arr) + glDeleteVertexArrays(1, Ref(id)) end function glDeleteBuffers(id::GLuint) - arr = [id] - glDeleteBuffers(1, arr) + glDeleteBuffers(1, Ref(id)) end function glGetTexLevelParameteriv(target::GLenum, level, name::GLenum) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 087a0387679..87a3f6c496e 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -455,6 +455,9 @@ function free(x::T) where {T} # don't free if already freed (this should only be set by unsafe_free) clean_up_observables(x) x.id == 0 && return + + # OpenGL has the annoying habit of reusing id's when creating a new context + # We need to make sure to only free the current one if context_alive(x.context) && is_context_active(x.context) unsafe_free(x) end @@ -477,42 +480,21 @@ function clean_up_observables(x::T) where {T} empty!(x.observers) end end -# OpenGL has the annoying habit of reusing id's when creating a new context -# We need to make sure to only free the current one -function unsafe_free(x::GLProgram) - glDeleteProgram(x.id) - x.id = 0 - return -end - -function unsafe_free(x::Shader) - glDeleteShader(x.id) - x.id = 0 - return -end -function unsafe_free(x::GLBuffer) - id = Ref(x.id) - glDeleteBuffers(1, id) - x.id = 0 - return -end - -function unsafe_free(x::Texture) - glDeleteTextures(x.id) - x.id = 0 - return -end +unsafe_free(x::GLProgram) = glDeleteProgram(x.id) +unsafe_free(x::Shader) = glDeleteShader(x.id) +unsafe_free(x::GLBuffer) = glDeleteBuffers(x.id) +unsafe_free(x::Texture) = glDeleteTextures(x.id) function unsafe_free(x::GLVertexArray) for (key, buffer) in x.buffers unsafe_free(buffer) + buffer.id = 0 end if x.indices isa GPUArray unsafe_free(x.indices) + x.indices.id = 0 end - id = Ref(x.id) - glDeleteVertexArrays(1, id) - x.id = 0 + glDeleteVertexArrays(x.id) return end From d20a281140ca503ce35ea2f2d28c5e59595b33b2 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 21:13:42 +0100 Subject: [PATCH 123/135] cleanup called_from_finalizer & add comments to implied functions --- GLMakie/src/GLAbstraction/GLTypes.jl | 1 + GLMakie/src/screen.jl | 10 +++--- src/scenes.jl | 47 ++++++++++++++++++---------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 87a3f6c496e..2cac0f9a026 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -451,6 +451,7 @@ include("GLRenderObject.jl") #################################################################################### # freeing +# Note: can be called from scene finalizer, must not error or print unless to Core.stdout function free(x::T) where {T} # don't free if already freed (this should only be set by unsafe_free) clean_up_observables(x) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index c97407c3ebf..1fded79703a 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -536,7 +536,7 @@ function Makie.insertplots!(screen::Screen, scene::Scene) end end -# Note: called from scene finalizer, must not error +# Note: can be called from scene finalizer, must not error or print unless to Core.stdout function Base.delete!(screen::Screen, scene::Scene) for child in scene.children delete!(screen, child) @@ -576,6 +576,7 @@ function Base.delete!(screen::Screen, scene::Scene) return end +# Note: can be called from scene finalizer, must not error or print unless to Core.stdout function destroy!(rob::RenderObject) # These need explicit clean up because (some of) the source observables # remain when the plot is deleted. @@ -603,6 +604,7 @@ function destroy!(rob::RenderObject) return end +# Note: can be called from scene finalizer, must not error or print unless to Core.stdout function with_context(f, context) CTX = ShaderAbstractions.ACTIVE_OPENGL_CONTEXT old_ctx = isassigned(CTX) ? CTX[] : nothing @@ -618,7 +620,7 @@ function with_context(f, context) end end -# Note: called from scene finalizer, must not error +# Note: can be called from scene finalizer, must not error or print unless to Core.stdout function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) if !isempty(plot.plots) @@ -650,12 +652,12 @@ function Base.empty!(screen::Screen) @assert !was_destroyed(screen.glscreen) for plot in collect(values(screen.cache2plot)) - delete!(screen, Makie.rootparent(plot), plot, false) + delete!(screen, Makie.rootparent(plot), plot) end if !isnothing(screen.scene) Makie.disconnect_screen(screen.scene, screen) - delete!(screen, screen.scene, false) + delete!(screen, screen.scene) screen.scene = nothing end diff --git a/src/scenes.jl b/src/scenes.jl index 9c02361c7dd..b2b94bf74a9 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -140,7 +140,7 @@ mutable struct Scene <: AbstractScene scene.isclosed = true end end - finalizer(x -> free(x, true), scene) + finalizer(free, scene) return scene end end @@ -435,24 +435,37 @@ function delete_scene!(scene::Scene) return nothing end -function free(scene::Scene, called_from_finalizer = false) - empty!(scene, called_from_finalizer; free=true) - for field in [:backgroundcolor, :viewport, :visible] - Observables.clear(getfield(scene, field)) - end - for screen in copy(scene.current_screens) - delete!(screen, scene, called_from_finalizer) +function free(scene::Scene) + try + empty!(scene; free=true) + for field in [:backgroundcolor, :viewport, :visible] + Observables.clear(getfield(scene, field)) + end + for screen in copy(scene.current_screens) + delete!(screen, scene) + end + empty!(scene.current_screens) + scene.parent = nothing + catch e + # Since this is called from a finalizer task switches are not allowed. + # This means no errors, no @error, no printing except to Core.stdout, + # which is different from stdout. + # Note that any error here means that scene cleanup is incomplete. + # Therefore there should be not errors thrown in this call stack. + println(Core.stdout, "Failed to finalize Scene:") + Base.showerror(Core.stdout, e) + println(Core.stdout) + Base.show_backtrace(Core.stdout, Base.backtrace()) end - empty!(scene.current_screens) - scene.parent = nothing return end -function Base.empty!(scene::Scene, called_from_finalizer = false; free=false) +# Note: called from scene finalizer if free = true +function Base.empty!(scene::Scene; free=false) foreach(empty!, copy(scene.children)) # clear plots of this scene for plot in copy(scene.plots) - delete!(scene, plot, called_from_finalizer) + delete!(scene, plot) end # clear all child scenes @@ -493,17 +506,18 @@ function Base.push!(scene::Scene, @nospecialize(plot::Plot)) end end -Base.delete!(screen::MakieScreen, scene::Scene, p::AbstractPlot, ::Bool) = delete!(screen, scene, p) +# Note: can be called from scene finalizer - @debug may cause segfaults when active function Base.delete!(screen::MakieScreen, ::Scene, ::AbstractPlot) @debug "Deleting plots not implemented for backend: $(typeof(screen))" end -Base.delete!(screen::MakieScreen, scene::Scene, ::Bool) = delete!(screen, scene) +# Note: can be called from scene finalizer - @debug may cause segfaults when active function Base.delete!(screen::MakieScreen, ::Scene) # This may not be necessary for every backed @debug "Deleting scenes not implemented for backend: $(typeof(screen))" end +# Note: can be called from scene finalizer function free(plot::AbstractPlot) for f in plot.deregister_callbacks Observables.off(f) @@ -515,7 +529,8 @@ function free(plot::AbstractPlot) return end -function Base.delete!(scene::Scene, plot::AbstractPlot, called_from_finalizer = false) +# Note: can be called from scene finalizer +function Base.delete!(scene::Scene, plot::AbstractPlot) filter!(x -> x !== plot, scene.plots) # TODO, if we want to delete a subplot of a plot, # It won't be in scene.plots directly, but will still be deleted @@ -527,7 +542,7 @@ function Base.delete!(scene::Scene, plot::AbstractPlot, called_from_finalizer = # error("$(typeof(plot)) not in scene!") # end for screen in scene.current_screens - delete!(screen, scene, plot, called_from_finalizer) + delete!(screen, scene, plot) end free(plot) end From f86d313b0e41ec5cd701a8dff4cccec856bf2a5c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 22:25:14 +0100 Subject: [PATCH 124/135] some cleanup --- GLMakie/test/glmakie_refimages.jl | 2 +- src/utilities/RenderPipeline.jl | 89 ++++++++++++++----------------- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 30dcfd0cef2..665c183cae1 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -253,7 +253,7 @@ end begin # Pipeline matches test_pipeline_2D up to color_tint - pipeline = Makie.Pipeline() + pipeline = Makie.RenderPipeline() render1 = push!(pipeline, Makie.RenderStage(transparency = false, fxaa = true)) render2 = push!(pipeline, Makie.TransparentRenderStage()) diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index 50e52ad2831..b9869a59103 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -657,57 +657,48 @@ function DisplayStage() end -# TODO: caching is dangerous with mutable attributes... -# const PIPELINE_CACHE = Dict{Symbol, RenderPipeline}() - function default_pipeline(; ssao = false, fxaa = true, oit = true) - # name = Symbol(:default_pipeline, Int(ssao), Int(fxaa), Int(oit)) - - # Mimic GLMakie's old hard coded render pipeline - # get!(PIPELINE_CACHE, name) do - - pipeline = RenderPipeline() - push!(pipeline, SortStage()) - - # Note - order important! - # TODO: maybe add insert!()? - if ssao - render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) - _ssao = push!(pipeline, SSAOStage()) - render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) - else - render2 = push!(pipeline, RenderStage(transparency = false)) - end - if oit - render3 = push!(pipeline, TransparentRenderStage()) - _oit = push!(pipeline, OITStage()) - else - render3 = push!(pipeline, RenderStage(transparency = true)) - end - if fxaa - _fxaa = push!(pipeline, FXAAStage(filter_in_shader = true)) - end - display = push!(pipeline, DisplayStage()) + pipeline = RenderPipeline() + push!(pipeline, SortStage()) + + # Note - order important! + # TODO: maybe add insert!()? + if ssao + render1 = push!(pipeline, RenderStage(ssao = true, transparency = false)) + _ssao = push!(pipeline, SSAOStage()) + render2 = push!(pipeline, RenderStage(ssao = false, transparency = false)) + else + render2 = push!(pipeline, RenderStage(transparency = false)) + end + if oit + render3 = push!(pipeline, TransparentRenderStage()) + _oit = push!(pipeline, OITStage()) + else + render3 = push!(pipeline, RenderStage(transparency = true)) + end + if fxaa + _fxaa = push!(pipeline, FXAAStage(filter_in_shader = true)) + end + display = push!(pipeline, DisplayStage()) - if ssao - connect!(pipeline, render1, _ssao) - connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) - end - connect!(pipeline, render2, fxaa ? _fxaa : display) - if oit - connect!(pipeline, render3, _oit) - connect!(pipeline, _oit, fxaa ? _fxaa : display, :color) - else - connect!(pipeline, render3, fxaa ? _fxaa : display, :color) - end - if fxaa - connect!(pipeline, _fxaa, display, :color) - end - connect!(pipeline, :objectid) + if ssao + connect!(pipeline, render1, _ssao) + connect!(pipeline, _ssao, fxaa ? _fxaa : display, :color) + end + connect!(pipeline, render2, fxaa ? _fxaa : display) + if oit + connect!(pipeline, render3, _oit) + connect!(pipeline, _oit, fxaa ? _fxaa : display, :color) + else + connect!(pipeline, render3, fxaa ? _fxaa : display, :color) + end + if fxaa + connect!(pipeline, _fxaa, display, :color) + end + connect!(pipeline, :objectid) - return pipeline - # end + return pipeline end function test_pipeline_3D() @@ -777,8 +768,6 @@ end function test_pipeline_minimal() pipeline = RenderPipeline() - # GUI elements don't need OIT because they are (usually?) layered plot by - # plot, rather than element by element. Do need FXAA occasionally, e.g. Toggle render = push!(pipeline, RenderStage()) display = push!(pipeline, DisplayStage()) connect!(pipeline, render, display) @@ -947,7 +936,7 @@ function pipeline_gui!(ax, pipeline) scale = map(pv -> max(0.5, 10*min(pv[1,1], pv[2,2])), get_scene(ax).camera.projectionview) poly!(ax, rects_obs, strokewidth = scale, strokecolor = :black, fxaa = false, - shading = NoShading, color = :lightgray) + shading = NoShading, color = :lightgray, transparency = false) linesegments!(ax, header_line_obs, linewidth = scale, color = :black) text!(ax, header_obs, markerspace = :data, fontsize = 0.8, color = :black, align = (:center, :center)) From 44cef5fc77c2f0b5f1549b9df4a00de46381d74f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 8 Jan 2025 22:50:52 +0100 Subject: [PATCH 125/135] fix more tests --- test/render_pipeline.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index 0187fa8020b..20af2b7d1cf 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -1,10 +1,10 @@ using Makie using Makie: BufferFormat, N0f8, is_compatible, BFT using Makie: Stage, get_input_format, get_output_format -using Makie: Pipeline, connect! +using Makie: RenderPipeline, connect! using Makie: generate_buffers, default_pipeline -@testset "Render Pipeline" begin +@testset "Render RenderPipeline" begin @testset "BufferFormat" begin @testset "Constructors" begin @@ -97,8 +97,8 @@ using Makie: generate_buffers, default_pipeline @test get_output_format(stage, :c) == stage.output_formats[stage.outputs[:c]] end - @testset "Pipeline & Connections" begin - pipeline = Pipeline() + @testset "RenderPipeline & Connections" begin + pipeline = RenderPipeline() @test isempty(pipeline.stages) @test isempty(pipeline.stageio2idx) From df82be664a5d66dc5a9733c8620fac825bf84155 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 14:25:59 +0100 Subject: [PATCH 126/135] merge fixes --- GLMakie/src/rendering.jl | 6 ++++-- GLMakie/src/screen.jl | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index eb61c745578..512302eed80 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -36,7 +36,9 @@ end Renders a single frame of a `screen` """ function render_frame(screen::Screen; resize_buffers=true) - isempty(screen.framebuffer_factory.children) && return + if isempty(screen.framebuffer_factory.children) || isnothing(screen.scene) + return + end # Make sure this context is active (for multi-window rendering) nw = to_native(screen) @@ -48,7 +50,7 @@ function render_frame(screen::Screen; resize_buffers=true) # no earlier stage uses color or objectid # Also assumes specific names fb = screen.framebuffer_factory.children[1] - if resize_buffers && !isnothing(screen.scene) + if resize_buffers ppu = screen.px_per_unit[] resize!(screen.framebuffer_factory, round.(Int, ppu .* size(screen.scene))...) end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index be2952e59fc..c110cf444fd 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -218,7 +218,7 @@ framebuffer_size(screen::Screen) = size(screen.framebuffer_factory) # The size of the window in Makie's own units makie_window_size(screen::Screen) = round.(Int, scene_size(screen) .* screen.scalefactor[]) -# The size of the window in Makie, device indepentent units +# The size of the window in Makie, device independent units scene_size(screen::Screen) = size(screen.scene) Makie.isvisible(screen::Screen) = screen.config.visible @@ -794,7 +794,7 @@ function Base.resize!(screen::Screen, w::Int, h::Int) # w/h are in device independent Makie units (scene size) ppu = screen.px_per_unit[] fbw, fbh = round.(Int, ppu .* (w, h)) - resize!(screen.framebuffer, fbw, fbh) + resize!(screen.framebuffer_factory, fbw, fbh) if screen.owns_glscreen # Resize the window which appears on the user desktop (if necessary). From 85600f9e05bcc483f2f44e677e293d95f24f3d74 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 14:29:07 +0100 Subject: [PATCH 127/135] revert ci changes --- .github/workflows/ci.yml | 1 - .github/workflows/compilation-benchmark.yaml | 1 - .github/workflows/reference_tests.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97278016965..cd5c16ed7d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: - '*.md' branches: - master - - ff/safe-context push: tags: - '*' diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index 26a3c17594b..7e6c6a62a8e 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -6,7 +6,6 @@ on: - '*.md' branches: - master - - ff/safe-context concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/reference_tests.yml b/.github/workflows/reference_tests.yml index ad8ef46b39d..3903929641f 100644 --- a/.github/workflows/reference_tests.yml +++ b/.github/workflows/reference_tests.yml @@ -6,7 +6,6 @@ on: - '*.md' branches: - master - - ff/safe-context push: tags: - '*' From 41881400e9e2dd1654f6e5821e9237ebffe21ac3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 14:34:07 +0100 Subject: [PATCH 128/135] try fix docs --- docs/src/explanations/render_pipeline.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/explanations/render_pipeline.md b/docs/src/explanations/render_pipeline.md index 8ed9bed4d53..c6b9b8d2b96 100644 --- a/docs/src/explanations/render_pipeline.md +++ b/docs/src/explanations/render_pipeline.md @@ -132,7 +132,7 @@ You may also want to look at "GLMakie/src/postprocessing.jl" and "GLMakie/assets As an example we will create a stage that applies a Sepia effect to our figure. Lets begin by setting up a pipeline that includes the stage. -```@figure RenderPipeline backend=GLMakie +```@example RenderPipeline backend=GLMakie # Create an empty Pipeline pipeline = Makie.RenderPipeline() @@ -173,7 +173,7 @@ Each stage will get a framebuffer based on the connected outputs of the pipeline The names match the names of the pipeline stage with a `_buffer` postfix. What we now need to do is define a constructor for a `<: GLMakie.AbstractRenderStep` object which represents the stage in the `GLRenderPipeline`, and a `run_step()` method that executes the stage. -```@figure RenderPipeline backend=GLMakie +```@example RenderPipeline backend=GLMakie using GLMakie function GLMakie.construct(::Val{:Tint}, screen, framebuffer, inputs, parent) From bebbee7928ba860a6c231061789c047e268e3bd2 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 15:51:27 +0100 Subject: [PATCH 129/135] inline texture parameters in BufferFormat --- GLMakie/src/GLAbstraction/GLTexture.jl | 8 +-- GLMakie/src/render_pipeline.jl | 28 ++++++++--- src/utilities/RenderPipeline.jl | 68 ++++++++++++++++---------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 03b24dd2e34..d30500030d0 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -4,6 +4,7 @@ struct TextureParameters{NDim} repeat ::NTuple{NDim, Symbol} anisotropic::Float32 swizzle_mask::Vector{GLenum} + mipmap::Bool end abstract type OpenglTexture{T, NDIM} <: GPUArray{T, NDIM} end @@ -92,7 +93,7 @@ Makie.@noconstprop function Texture( mipmap = false, parameters... # rest should be texture parameters ) where {T, NDim} - texparams = TextureParameters(T, NDim; parameters...) + texparams = TextureParameters(T, NDim; mipmap, parameters...) id = glGenTextures() glBindTexture(texturetype, id) set_packing_alignment(data) @@ -523,7 +524,8 @@ function TextureParameters(T, NDim; x_repeat = :clamp_to_edge, #wrap_s y_repeat = x_repeat, #wrap_t z_repeat = x_repeat, #wrap_r - anisotropic = 1f0 + anisotropic = 1f0, + mipmap = false ) T <: Integer && (minfilter === :linear || magfilter === :linear) && error("Wrong Texture Parameter: Integer texture can't interpolate. Try :nearest") repeat = (x_repeat, y_repeat, z_repeat) @@ -536,7 +538,7 @@ function TextureParameters(T, NDim; end TextureParameters( minfilter, magfilter, ntuple(i->repeat[i], NDim), - anisotropic, swizzle_mask + anisotropic, swizzle_mask, mipmap ) end function TextureParameters(t::Texture{T, NDim}; kw_args...) where {T, NDim} diff --git a/GLMakie/src/render_pipeline.jl b/GLMakie/src/render_pipeline.jl index 71eacb555ad..b87580963b5 100644 --- a/GLMakie/src/render_pipeline.jl +++ b/GLMakie/src/render_pipeline.jl @@ -4,22 +4,36 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF empty!(factory.children) # function barrier for types? - function get_buffer!(context, buffers, T, extras) + function get_buffer!(context, buffers, T, format) # reuse internalformat = GLAbstraction.default_internalcolorformat(T) + is_float_format = eltype(T) == N0f8 || eltype(T) <: AbstractFloat + default_filter = ifelse(is_float_format, :linear, :nearest) + minfilter = ifelse(format.minfilter === :any, ifelse(format.mipmap, :linear_mipmap_linear, default_filter), format.minfilter) + magfilter = ifelse(format.magfilter === :any, default_filter, format.magfilter) + for (i, buffer) in enumerate(buffers) if (buffer.id != 0) && (internalformat == buffer.internalformat) && - (get(extras, :minfilter, :nearest) == buffer.parameters.minfilter) + (minfilter == buffer.parameters.minfilter) && + (magfilter == buffer.parameters.magfilter) && + (format.repeat == buffer.parameters.repeat) && + (format.mipmap == buffer.parameters.mipmap) + return popat!(buffers, i) end end # create new - interp = get(extras, :minfilter, :nearest) - if !(eltype(T) == N0f8 || eltype(T) <: AbstractFloat) && (interp == :linear) - error("Cannot use :linear interpolation with non float types.") + if !is_float_format && (format.mipmap || minfilter != :nearest || magfilter != :nearest) + error("Cannot use :linear interpolation or mipmaps with non float buffers.") end - return Texture(context, T, size(factory), minfilter = interp, x_repeat = :clamp_to_edge) + format.mipmap && error("Mipmaps not yet implemented for Pipeline buffers") + # TODO: Figure out what we need to do for mipmaps. Do we need to call + # glGenerateMipmap before every stage using the buffer? Should + # Stages do that? Do we need anything extra for setup? + return Texture(context, T, size(factory), + minfilter = minfilter, magfilter = magfilter, + x_repeat = format.repeat[1], y_repeat = format.repeat[2]) end # reuse buffers that match formats (and make sure that factory.buffers @@ -28,7 +42,7 @@ function Makie.reset!(factory::FramebufferFactory, formats::Vector{Makie.BufferF empty!(factory.buffers) for format in formats T = Makie.format_to_type(format) - tex = get_buffer!(factory.fb.context, buffers, T, format.extras) + tex = get_buffer!(factory.fb.context, buffers, T, format) push!(factory.buffers, tex) end diff --git a/src/utilities/RenderPipeline.jl b/src/utilities/RenderPipeline.jl index b9869a59103..da8060df6c1 100644 --- a/src/utilities/RenderPipeline.jl +++ b/src/utilities/RenderPipeline.jl @@ -25,35 +25,51 @@ module BFT end -# TODO: try `BufferFormat{T} ... type::Type{T}` for better performance? -# TODO: consider adding a "immediately reusable" flag here or to Stage so that -# Stage can communicate that it allows output = input struct BufferFormat dims::Int type::BFT.BufferFormatType - extras::Dict{Symbol, Any} + + minfilter::Symbol + magfilter::Symbol + repeat::NTuple{2, Symbol} + mipmap::Bool + # anisotropy::Float32 # useless for this context? + # local_read::Bool # flag so stage can mark that it only reads the pixel it will write to, i.e. allows using input as output + # multisample # for MSAA end """ - BufferFormat([dims = 4, type = N0f8]; extras...) + BufferFormat([dims = 4, type = N0f8]; [texture_parameters...]) Creates a `BufferFormat` which encodes requirements for an input or output of a `Stage`. For example, a color output may require 3 (RGB) N0f8's (8 bit "float" normalized to a 0..1 range). -The `BufferFormat` may also include extra requirements such as -`x_minfilter = :nearest` or `mipmap = true`. +The `BufferFormat` may also specify texture parameters for the buffer: +- `minfilter = :any`: How are pixels combined (:linear, :nearest, :nearest_mipmap_nearest, :linear_mipmap_nearest, :nearest_mipmap_linear, :linear_mipmap_linear) +- `magfilter = :any`: How are pixels interpolated (:linear, :nearest) +- `repeat = :clamp_to_edge`: How are pixels sampled beyond the boundary? (:clamp_to_edge, :mirrored_repeat, :repeat) +- `mipmap = false`: Should mipmaps be used? """ -function BufferFormat(dims::Integer = 4, type::DataType = N0f8; extras...) - return BufferFormat(dims, type, Dict{Symbol, Any}(extras)) +function BufferFormat( + dims::Integer, type::BFT.BufferFormatType; + minfilter = :any, magfilter = :any, + repeat = (:clamp_to_edge, :clamp_to_edge), + mipmap = false + ) + _repeat = ifelse(repeat isa Symbol, (repeat, repeat), repeat) + return BufferFormat(dims, type, minfilter, magfilter, _repeat, mipmap) end -@generated function BufferFormat(dims::Integer, ::Type{T}, extras::Dict{Symbol, Any}) where {T} +BufferFormat(dims = 4, type = N0f8; kwargs...) = BufferFormat(dims, type; kwargs...) +@generated function BufferFormat(dims::Integer, ::Type{T}; kwargs...) where {T} type = BFT.BufferFormatType(UInt8(findfirst(x -> x === T, BFT.type_lookup)) - 0x01) - return :(BufferFormat(dims, $type, extras)) + return :(BufferFormat(dims, $type; kwargs...)) end function Base.:(==)(f1::BufferFormat, f2::BufferFormat) - return (f1.dims == f2.dims) && (f1.type == f2.type) && (f1.extras == f2.extras) + return (f1.dims == f2.dims) && (f1.type == f2.type) && + (f1.minfilter == f2.minfilter) && (f1.magfilter == f2.magfilter) && + (f1.repeat == f2.repeat) && (f1.mipmap == f2.mipmap) end """ @@ -66,30 +82,30 @@ Rules: - The output size is `dims = max(f1.dims, f2.dims)` - Types must come from the same base type (AbstractFloat, Signed, Unsigned) where N0f8 counts as a float. - The output type is the larger one of the two -- Extra requirements must match if both formats have them. -- If only one format contains a specific extra requirement it is accepted and copied to the output. +- minfilter and magfilter must match (if one is :any, the other is used) +- repeat must match +- mipmap = true takes precedence """ function BufferFormat(f1::BufferFormat, f2::BufferFormat) - if BFT.is_compatible(f1.type, f2.type) + if is_compatible(f1, f2) dims = max(f1.dims, f2.dims) type = BFT._promote(f1.type, f2.type) - extras = deepcopy(f1.extras) - for (k, v) in f2.extras - get!(extras, k, v) == v || error("Failed to merge BufferFormat: Extra requirement $(extras[k]) != $v.") - end - return BufferFormat(dims, type, extras) + # currently don't allow different min/magfilters + minfilter = ifelse(f1.minfilter == :any, f2.minfilter, f1.minfilter) + magfilter = ifelse(f1.magfilter == :any, f2.magfilter, f1.magfilter) + repeat = f1.repeat + mipmap = f1.mipmap || f2.mipmap + return BufferFormat(dims, type, minfilter, magfilter, repeat, mipmap) else error("Failed to merge BufferFormat: $f1 and $f2 are not compatible.") end end function is_compatible(f1::BufferFormat, f2::BufferFormat) - BFT.is_compatible(f1.type, f2.type) || return false - extras_compat = true - for (k, v) in f1.extras - extras_compat = extras_compat && (get(f2.extras, k, v) == v) - end - return extras_compat + return BFT.is_compatible(f1.type, f2.type) && + (f1.minfilter == :any || f2.minfilter == :any || f1.minfilter == f2.minfilter) && + (f1.magfilter == :any || f2.magfilter == :any || f1.magfilter == f2.magfilter) && + (f1.repeat == f2.repeat) end function format_to_type(format::BufferFormat) From 07ecca08d5839122250f884c6da547f4447280e3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 15:51:42 +0100 Subject: [PATCH 130/135] maybe fix docs? --- docs/src/explanations/render_pipeline.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/explanations/render_pipeline.md b/docs/src/explanations/render_pipeline.md index c6b9b8d2b96..803ef78025f 100644 --- a/docs/src/explanations/render_pipeline.md +++ b/docs/src/explanations/render_pipeline.md @@ -132,7 +132,7 @@ You may also want to look at "GLMakie/src/postprocessing.jl" and "GLMakie/assets As an example we will create a stage that applies a Sepia effect to our figure. Lets begin by setting up a pipeline that includes the stage. -```@example RenderPipeline backend=GLMakie +```@example RenderPipeline # Create an empty Pipeline pipeline = Makie.RenderPipeline() @@ -173,7 +173,7 @@ Each stage will get a framebuffer based on the connected outputs of the pipeline The names match the names of the pipeline stage with a `_buffer` postfix. What we now need to do is define a constructor for a `<: GLMakie.AbstractRenderStep` object which represents the stage in the `GLRenderPipeline`, and a `run_step()` method that executes the stage. -```@example RenderPipeline backend=GLMakie +```@example RenderPipeline using GLMakie function GLMakie.construct(::Val{:Tint}, screen, framebuffer, inputs, parent) From a92a2c7f0ad09424a647b3a8aea6eb278ffe0824 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 16:03:55 +0100 Subject: [PATCH 131/135] update tests --- test/render_pipeline.jl | 69 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/test/render_pipeline.jl b/test/render_pipeline.jl index 20af2b7d1cf..ef02590731b 100644 --- a/test/render_pipeline.jl +++ b/test/render_pipeline.jl @@ -11,14 +11,18 @@ using Makie: generate_buffers, default_pipeline f = BufferFormat() @test f.dims == 4 @test f.type == BFT.float8 - @test isempty(f.extras) + @test f.minfilter == :any + @test f.magfilter == :any + @test f.repeat == (:clamp_to_edge, :clamp_to_edge) + @test f.mipmap == false - f = BufferFormat(1, Float16, a = 1, b = 2) + f = BufferFormat(1, Float16, minfilter = :linear, magfilter = :nearest, mipmap = true, repeat = :repeat) @test f.dims == 1 @test f.type == BFT.float16 - @test haskey(f.extras, :a) && (f.extras[:a] == 1) - @test haskey(f.extras, :b) && (f.extras[:b] == 2) - @test length(keys(f.extras)) == 2 + @test f.minfilter == :linear + @test f.magfilter == :nearest + @test f.repeat == (:repeat, :repeat) + @test f.mipmap == true end types = [(N0f8, Float16, Float32), (Int8, Int16, Int32), (UInt8, UInt16, UInt32)] @@ -34,12 +38,22 @@ using Makie: generate_buffers, default_pipeline end # extras - @test is_compatible(BufferFormat(a = 1), BufferFormat()) - @test is_compatible(BufferFormat(a = 1), BufferFormat(a = 1)) - @test !is_compatible(BufferFormat(a = 1), BufferFormat(a = 2)) - @test is_compatible(BufferFormat(a = 1), BufferFormat(b = 2)) - @test !is_compatible(BufferFormat(2, Int8, a = 1), BufferFormat(b = 2)) - @test is_compatible(BufferFormat(2, Int8, a = 1), BufferFormat(1, Int32, b = 2, c = 3)) + @test is_compatible(BufferFormat(mipmap = true), BufferFormat(mipmap = false)) + + @test is_compatible(BufferFormat(repeat = :repeat), BufferFormat(repeat = :repeat)) + @test !is_compatible(BufferFormat(repeat = :repeat), BufferFormat(repeat = :clamp_to_egde)) + + @test is_compatible(BufferFormat(minfilter = :any), BufferFormat(minfilter = :any)) + @test is_compatible(BufferFormat(minfilter = :any), BufferFormat(minfilter = :linear)) + @test is_compatible(BufferFormat(minfilter = :linear), BufferFormat(minfilter = :linear)) + @test !is_compatible(BufferFormat(minfilter = :nearest), BufferFormat(minfilter = :linear)) + + @test is_compatible(BufferFormat(magfilter = :any), BufferFormat(magfilter = :any)) + @test is_compatible(BufferFormat(magfilter = :any), BufferFormat(magfilter = :linear)) + @test is_compatible(BufferFormat(magfilter = :linear), BufferFormat(magfilter = :linear)) + @test !is_compatible(BufferFormat(magfilter = :nearest), BufferFormat(magfilter = :linear)) + + @test is_compatible(BufferFormat(2, Int8, minfilter = :any, magfilter = :linear), BufferFormat(1, Int32, minfilter = :nearest)) end @testset "BufferFormat merging" begin @@ -57,16 +71,29 @@ using Makie: generate_buffers, default_pipeline end end - @test begin - B = BufferFormat(BufferFormat(a = :a), BufferFormat()) - haskey(B.extras, :a) && (B.extras[:a] == :a) - end - @test begin - B = BufferFormat(BufferFormat(2, N0f8, a = "s"), BufferFormat(3, Float16, a = "s")) - haskey(B.extras, :a) && (B.extras[:a] == "s") - end - @test_throws ErrorException BufferFormat(BufferFormat(a = :a), BufferFormat(a = :b)) - @test_throws ErrorException BufferFormat(BufferFormat(a = :a), BufferFormat(a = 1)) + a = BufferFormat(minfilter = :any, magfilter = :any, mipmap = false) + b = BufferFormat(minfilter = :linear, magfilter = :nearest, mipmap = true) + B = BufferFormat(a, b) + @test B.minfilter == :linear + @test B.magfilter == :nearest + @test B.mipmap + + B = BufferFormat(b, a) + @test B.minfilter == :linear + @test B.magfilter == :nearest + @test B.mipmap + + a = BufferFormat(minfilter = :nearest, magfilter = :linear, mipmap = true, repeat = :repeat) + b = BufferFormat(minfilter = :nearest, magfilter = :linear, mipmap = true, repeat = :repeat) + B = BufferFormat(a, b) + @test B.minfilter == :nearest + @test B.magfilter == :linear + @test B.mipmap + @test B.repeat == (:repeat, :repeat) + + @test_throws ErrorException BufferFormat(BufferFormat(repeat = :a), BufferFormat(repeat = :b)) + @test_throws ErrorException BufferFormat(BufferFormat(minfilter = :a), BufferFormat(minfilter = :b)) + @test_throws ErrorException BufferFormat(BufferFormat(magfilter = :a), BufferFormat(magfilter = :b)) end end From a01c2953324b59f8de09e56064ac5a1914328846 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 16:46:21 +0100 Subject: [PATCH 132/135] maybe with imports? --- docs/src/explanations/render_pipeline.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/explanations/render_pipeline.md b/docs/src/explanations/render_pipeline.md index 803ef78025f..c0f9166f1f1 100644 --- a/docs/src/explanations/render_pipeline.md +++ b/docs/src/explanations/render_pipeline.md @@ -133,6 +133,8 @@ As an example we will create a stage that applies a Sepia effect to our figure. Lets begin by setting up a pipeline that includes the stage. ```@example RenderPipeline +using Makie + # Create an empty Pipeline pipeline = Makie.RenderPipeline() @@ -239,7 +241,7 @@ end With that done we can now use our new pipeline for rendering: ```@figure RenderPipeline backend=GLMakie -using FileIO +using FileIO, GLMakie GLMakie.activate!(render_pipeline = pipeline) cow = load(Makie.assetpath("cow.png")) From 9df94c0e0e0cdc531d2cef2c42ba6cc7a01fcf3f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 25 Jan 2025 18:16:24 +0100 Subject: [PATCH 133/135] maybe no f,a,p? --- docs/src/explanations/render_pipeline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/explanations/render_pipeline.md b/docs/src/explanations/render_pipeline.md index c0f9166f1f1..c4c781c216e 100644 --- a/docs/src/explanations/render_pipeline.md +++ b/docs/src/explanations/render_pipeline.md @@ -245,7 +245,7 @@ using FileIO, GLMakie GLMakie.activate!(render_pipeline = pipeline) cow = load(Makie.assetpath("cow.png")) -f,a,p = image(rotr90(cow)) +image(rotr90(cow)) ``` ## WGLMakie From 36c19da662bc9ba0770388cda0f4fea722d05052 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 26 Jan 2025 00:51:21 +0100 Subject: [PATCH 134/135] rework output buffer handling --- GLMakie/assets/shader/fragment_output.frag | 50 +++++++++++--------- GLMakie/src/glshaders/image_like.jl | 8 +--- GLMakie/src/glshaders/lines.jl | 6 +-- GLMakie/src/glshaders/mesh.jl | 3 +- GLMakie/src/glshaders/particles.jl | 11 ++--- GLMakie/src/glshaders/surface.jl | 3 +- GLMakie/src/glshaders/visualize_interface.jl | 49 ++++++------------- GLMakie/src/postprocessing.jl | 8 ++-- 8 files changed, 55 insertions(+), 83 deletions(-) diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index 6dc06fce29e..7287d48781c 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -1,16 +1,20 @@ {{GLSL_VERSION}} +{{TARGET_STAGE}} + layout(location=0) out vec4 fragment_color; layout(location=1) out uvec2 fragment_groupid; -{{buffers}} -// resolves to: -// // if transparency == true -// layout(location=2) out float coverage; -// // if transparency == false && ssao = true -// layout(location=2) out vec3 fragment_position; -// layout(location=3) out vec3 fragment_normal_occlusion; +#ifdef SSAO_TARGET +layout(location=2) out vec3 fragment_position; +layout(location=3) out vec3 fragment_normal; +#endif + +#ifdef OIT_TARGET +layout(location=2) out float coverage; +uniform float oit_scale; +#endif in vec3 o_view_pos; in vec3 o_view_normal; @@ -22,20 +26,20 @@ void write2framebuffer(vec4 color, uvec2 id){ // For plot/sprite picking fragment_groupid = id; - {{buffer_writes}} - // resolves to: - - // if transparency == true - // float weight = color.a * max(0.01, $scale * pow((1 - gl_FragCoord.z), 3)); - // coverage = 1.0 - clamp(color.a, 0.0, 1.0); - // fragment_color.rgb = weight * color.rgb; - // fragment_color.a = weight; - - // // if transparency == false && ssao = true - // fragment_color = color; - // fragment_position.xyz = o_view_pos; - // fragment_normal_occlusion.xyz = o_view_normal; - - // // else - // fragment_color = color; +#ifdef DEFAULT_TARGET + fragment_color = color; +#endif + +#ifdef SSAO_TARGET + fragment_color = color; + fragment_position.xyz = o_view_pos; + fragment_normal.xyz = o_view_normal; +#endif + +#ifdef OIT_TARGET + float weight = color.a * max(0.01, oit_scale * pow((1 - gl_FragCoord.z), 3)); + coverage = 1.0 - clamp(color.a, 0.0, 1.0); + fragment_color.rgb = weight * color.rgb; + fragment_color.a = weight; +#endif } diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index df862391097..5302280d074 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -43,10 +43,7 @@ function draw_heatmap(screen, data::Dict) shader = GLVisualizeShader( screen, "fragment_output.frag", "heatmap.vert", "heatmap.frag", - view = Dict( - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) - ) + view = Dict("TARGET_STAGE" => target_stage(screen, data)) ) fxaa = false end @@ -85,8 +82,7 @@ function draw_volume(screen, main::VolumeTypes, data::Dict) "depth_default" => vol_depth_default(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), "depth_write" => vol_depth_write(to_value(enable_depth)), - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) prerender = VolumePrerender(data[:transparency], data[:overdraw]) diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 81bc6ec8141..76eabe0b98d 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -140,8 +140,7 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: screen, "fragment_output.frag", "lines.vert", "lines.geom", "lines.frag", view = Dict( - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)), + "TARGET_STAGE" => target_stage(screen, data), "define_fast_path" => to_value(fast) ? "#define FAST_PATH" : "", "stripped_color_type" => color_type ) @@ -187,8 +186,7 @@ function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where "fragment_output.frag", "line_segment.vert", "line_segment.geom", "lines.frag", view = Dict( - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)), + "TARGET_STAGE" => target_stage(screen, data), "stripped_color_type" => color_type ) ) diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 3cd14d5f7a3..ac4bf28cc76 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -62,8 +62,7 @@ function draw_mesh(screen, data::Dict) "picking_mode" => to_value(get(data, :picking_mode, "")), "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) end diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 36fd98e5131..c314832f9ea 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -120,8 +120,7 @@ function draw_mesh_particle(screen, p, data) "shading" => light_calc(shading), "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) end @@ -149,10 +148,7 @@ function draw_pixel_scatter(screen, position::VectorTypes, data::Dict) shader = GLVisualizeShader( screen, "fragment_output.frag", "dots.vert", "dots.frag", - view = Dict( - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) - ) + view = Dict("TARGET_STAGE" => target_stage(screen, data)) ) gl_primitive = GL_POINTS end @@ -271,8 +267,7 @@ function draw_scatter(screen, (marker, position), data) "sprites.vert", "distance_shape.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, GLBuffer), - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) scale_primitive = true diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 384c3e246ae..8f8b621ac9a 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -156,8 +156,7 @@ function draw_surface(screen, main, data::Dict) "picking_mode" => "#define PICKING_INDEX_FROM_UV", "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) end diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index d17bc0a5441..b8ec05a33be 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -150,39 +150,20 @@ to_index_buffer(ctx, x) = error( Please choose from Int, Vector{UnitRange{Int}}, Vector{Int} or a signal of either of them" ) -function output_buffers(screen::Screen, transparency = false) - pipeline = screen.config.render_pipeline - if transparency && any(stage -> stage.name == :OIT, pipeline.stages) - """ - layout(location=2) out float coverage; - """ - elseif any(stage -> stage.name == :SSAO1, pipeline.stages) - """ - layout(location=2) out vec3 fragment_position; - layout(location=3) out vec3 fragment_normal; - """ +function target_stage(screen, data) + idx = findfirst(step -> renders_in_stage(data, step), screen.render_pipeline.steps) + @assert !isnothing(idx) "Could not find a render stage compatible with the given settings." + + # Keep it simple for now + fb = screen.render_pipeline.steps[idx].framebuffer + if fb.counter == 2 + return "#define DEFAULT_TARGET" + elseif fb.counter == 3 + data[:oit_scale] = screen.config.transparency_weight_scale + return "#define OIT_TARGET" + elseif fb.counter == 4 + return "#define SSAO_TARGET" else - "" + error("Number of colorbuffers in render framebuffer does not match any known configurations ($(fb.counter))") end -end - -function output_buffer_writes(screen::Screen, transparency = false) - pipeline = screen.config.render_pipeline - if transparency && any(stage -> stage.name == :OIT, pipeline.stages) - scale = screen.config.transparency_weight_scale - """ - float weight = color.a * max(0.01, $scale * pow((1 - gl_FragCoord.z), 3)); - coverage = 1.0 - clamp(color.a, 0.0, 1.0); - fragment_color.rgb = weight * color.rgb; - fragment_color.a = weight; - """ - elseif any(stage -> stage.name == :SSAO1, pipeline.stages) - """ - fragment_color = color; - fragment_position.xyz = o_view_pos; - fragment_normal.xyz = o_view_normal; - """ - else - "fragment_color = color;" - end -end +end \ No newline at end of file diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 60446de4856..81e73f17832 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -141,9 +141,9 @@ function id2scene(screen, id1) return false, nothing end -function should_render(robj::RenderObject, step::RenderPlots) - return robj.visible && - compare(robj[:ssao][], step.ssao) && +renders_in_stage(robj, ::AbstractRenderStep) = false +function renders_in_stage(robj, step::RenderPlots) + return compare(robj[:ssao][], step.ssao) && compare(robj[:transparency][], step.transparency) && compare(robj[:fxaa][], step.fxaa) end @@ -164,7 +164,7 @@ function run_step(screen, glscene, step::RenderPlots) set_draw_buffers(step.framebuffer) for (zindex, screenid, elem) in screen.renderlist - should_render(elem, step) || continue + elem.visible && renders_in_stage(elem, step) || continue found, scene = id2scene(screen, screenid) (found && scene.visible[]) || continue From cd7bcc2d563f8904a8aef59f5cf91883eebc767a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 26 Jan 2025 02:08:44 +0100 Subject: [PATCH 135/135] fix errors --- GLMakie/src/glshaders/voxel.jl | 3 +-- GLMakie/src/postprocessing.jl | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GLMakie/src/glshaders/voxel.jl b/GLMakie/src/glshaders/voxel.jl index 58f9b7d383b..9d9ae9338f9 100644 --- a/GLMakie/src/glshaders/voxel.jl +++ b/GLMakie/src/glshaders/voxel.jl @@ -24,8 +24,7 @@ function draw_voxels(screen, main::VolumeTypes, data::Dict) "shading" => light_calc(shading), "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", - "buffers" => output_buffers(screen, to_value(transparency)), - "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + "TARGET_STAGE" => target_stage(screen, data) ) ) end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 81e73f17832..acb62c3e8c9 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -142,10 +142,11 @@ function id2scene(screen, id1) end renders_in_stage(robj, ::AbstractRenderStep) = false +renders_in_stage(robj::RenderObject, step::RenderPlots) = renders_in_stage(robj.uniforms, step) function renders_in_stage(robj, step::RenderPlots) - return compare(robj[:ssao][], step.ssao) && - compare(robj[:transparency][], step.transparency) && - compare(robj[:fxaa][], step.fxaa) + return compare(to_value(get(robj, :ssao, false)), step.ssao) && + compare(to_value(get(robj, :transparency, false)), step.transparency) && + compare(to_value(get(robj, :fxaa, false)), step.fxaa) end function run_step(screen, glscene, step::RenderPlots)