diff --git a/CHANGELOG.md b/CHANGELOG.md index 403e5f64..a5e513b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog for Vulkan.jl +## Version `v0.6.16` +- ![Feature][badge-feature] Dependencies between handles may be specified via `Vk.depends_on(x, handle)`, to ensure that a given handle is not destroyed before anything that depends on it. This leverages the reference counting system already implemented, which itself encodes such dependencies from a given parent handle and its children. See the docstring of `Vk.depends_on` for more details. + ## Version `v0.6.14` - ![Feature][badge-feature] New mappings between Julia types and Vulkan formats are available, via `Vk.Format` constructors and `Vk.format_type` functions. diff --git a/Project.toml b/Project.toml index bedbbec1..21ac72e6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Vulkan" uuid = "9f14b124-c50e-4008-a7d4-969b3a6cd68a" authors = ["Cédric Belmant"] -version = "0.6.15" +version = "0.6.16" [deps] Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" diff --git a/src/preferences.jl b/src/preferences.jl index bde9aac2..4178d11e 100644 --- a/src/preferences.jl +++ b/src/preferences.jl @@ -1,5 +1,6 @@ using Preferences: Preferences, @load_preference set_preferences!(args...; kwargs...) = Preferences.set_preferences!(@__MODULE__, args...; kwargs...) +load_preference(args...; kwargs...) = Preferences.load_preference(@__MODULE__, args...; kwargs...) macro pref_log_destruction(handle, ex) if @load_preference("LOG_DESTRUCTION", "false") == "true" diff --git a/src/prewrap/handles.jl b/src/prewrap/handles.jl index 19cfdfc7..b7ce9048 100644 --- a/src/prewrap/handles.jl +++ b/src/prewrap/handles.jl @@ -28,7 +28,9 @@ function try_destroy(f, handle::Handle, parent) if !isnothing(parent) && !isa(parent.destructor, UndefInitializer) parent.destructor() end + return true end + handle.refcount[] end function init_handle!(handle::Handle, destructor, parent=nothing) @@ -46,6 +48,45 @@ function (T::Type{<:Handle})(ptr::Ptr{Cvoid}, destructor, parent) init_handle!(T(ptr, parent, RefCounter(UInt(1))), destructor, parent) end +""" + depends_on(x, handle::Handle) + +Make reference counting aware that `x` depends on `handle`. + +This ensures that `handle` is destroyed *after* `x`, and not the other way around. + +This may notably be used to encode dependencies that fall out of Vulkan's handle hierarchy, +such as between a `SurfaceKHR` and a `SwapchainKHR`. + +If `x` is not a `Handle`, it must be a mutable object; in this case, a finalizer will be added +which decrements the `handle`'s reference count (and destroys them if it reaches zero). + +`depends_on(x, handle)` is idempotent: multiple calls to it will simply incur needless incrementing/decrementing and finalizer registrations, possibly harming performance, but will not cause bugs. + +If one is a parent handle of the other (i.e. `Vk.parent(x) === handle`), `depends_on(x, handle)` is already implicit, and needs not be used. + +!!! warning + `depends_on` must not be used in a circular manner: using both `depends_on(x, y)` and `depends_on(y, x)` will prevent both `x` and `y` from ever being destroyed. Same for `depends_on(x, y)`, `depends_on(y, z)`, `depends_on(z, x)` and so on. +""" +function depends_on end + +function depends_on(x::Vk.Handle, handle::Vk.Handle) + Vk.increment_refcount!(handle) + prev_destructor = x.destructor + x.destructor = () -> begin + prev_destructor() + iszero(x.refcount[]) && handle.destructor() + end + nothing +end + +function depends_on(x, handle::Vk.Handle) + T = typeof(x) + ismutabletype(T) || error("`x` must be a mutable object or a `Vk.Handle`") + finalizer(_ -> handle.destructor(), x) + nothing +end + macro dispatch(handle, expr) if @load_preference("USE_DISPATCH_TABLE", "true") == "true" @match expr begin diff --git a/test/api.jl b/test/api.jl index e91ff2f0..223cda71 100644 --- a/test/api.jl +++ b/test/api.jl @@ -38,7 +38,7 @@ const WITH_DEBUG = let available_extensions = unwrap(enumerate_instance_extensio end end -@testset "Vulkan tests" begin +@testset "Vulkan API usage" begin include("init.jl") @testset "Utilities" begin diff --git a/test/handles.jl b/test/handles.jl new file mode 100644 index 00000000..7ee8a222 --- /dev/null +++ b/test/handles.jl @@ -0,0 +1,105 @@ +using Vulkan, Test +using Vulkan: depends_on + +mutable struct TestHandleNoParent <: Handle + vks::Ptr{Cvoid} + refcount::Vk.RefCounter + destructor +end +TestHandleNoParent(vks::Ptr{Cvoid}, refcount::Vk.RefCounter) = TestHandleNoParent(vks, refcount, undef) +TestHandleNoParent() = TestHandleNoParent(Ptr{Cvoid}(rand(UInt)), signal_destroyed) + +mutable struct TestHandleWithParent <: Handle + vks::Ptr{Cvoid} + parent::Handle + refcount::Vk.RefCounter + destructor +end +TestHandleWithParent(vks::Ptr{Cvoid}, parent::Handle, refcount::Vk.RefCounter) = TestHandleWithParent(vks, parent, refcount, undef) +TestHandleWithParent(parent) = TestHandleWithParent(Ptr{Cvoid}(rand(UInt)), signal_destroyed, parent) + +destroyed = IdDict{Union{TestHandleNoParent,TestHandleWithParent}, Nothing}() +signal_destroyed(x) = setindex!(destroyed, nothing, x) + +@testset "Handles" begin + function test_no_dependency(x, handle) + @test !haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(x) + @test haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(handle) + @test haskey(destroyed, handle) + end + + # Test that `handle` being finalized before `x` doesn't destroy `handle`. + function test_dependency_respected(x, handle) + @test !haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(handle) + @test !haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(x) + @test haskey(destroyed, x) + @test haskey(destroyed, handle) + end + + # Test that `x` being finalized acts as if there were no dependency. + function test_dependency_nonintrusive(x, handle) + @test !haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(x) + @test haskey(destroyed, x) + @test !haskey(destroyed, handle) + finalize(handle) + @test haskey(destroyed, handle) + end + + handle = TestHandleNoParent() + x = TestHandleNoParent() + test_no_dependency(x, handle) + + handle = TestHandleNoParent() + x = TestHandleNoParent() + test_no_dependency(handle, x) + + handle = TestHandleNoParent() + x = TestHandleWithParent(handle) + test_dependency_respected(x, handle) + + handle = TestHandleNoParent() + x = TestHandleWithParent(handle) + test_dependency_nonintrusive(x, handle) + + handle = TestHandleNoParent() + x = TestHandleNoParent() + depends_on(x, handle) + test_dependency_respected(x, handle) + + handle = TestHandleNoParent() + x = TestHandleNoParent() + depends_on(x, handle) + test_dependency_nonintrusive(x, handle) + + handle = TestHandleNoParent() + x = TestHandleNoParent() + depends_on(x, handle) + depends_on(x, handle) + test_dependency_respected(x, handle) + + handle = TestHandleNoParent() + x = TestHandleNoParent() + depends_on(x, handle) + depends_on(x, handle) + test_dependency_nonintrusive(x, handle) + + # Circular dependency: no handle in a given dependency chain will ever be destroyed. + handle = TestHandleNoParent() + x = TestHandleNoParent() + depends_on(handle, x) + depends_on(x, handle) + finalize(x) + finalize(handle) + @test !haskey(destroyed, x) + @test !haskey(destroyed, handle) +end; diff --git a/test/runtests.jl b/test/runtests.jl index 7238dbd9..ea34d5f5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,6 +11,7 @@ using Accessors: @set set_driver(:SwiftShader) @testset "Vulkan.jl" begin + include("handles.jl") include("api.jl") include("dispatch.jl") include("formats.jl")