Skip to content

Commit

Permalink
Support specifying external dependencies to refcounting via depends_on
Browse files Browse the repository at this point in the history
  • Loading branch information
serenity4 committed Jul 6, 2024
1 parent e41d3b5 commit 29e253a
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
1 change: 1 addition & 0 deletions src/preferences.jl
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
41 changes: 41 additions & 0 deletions src/prewrap/handles.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/api.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 105 additions & 0 deletions test/handles.jl
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

2 comments on commit 29e253a

@serenity4
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/110558

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.6.16 -m "<description of version>" 29e253aa15074f591d26e9687f56880c1217a134
git push origin v0.6.16

Please sign in to comment.