Skip to content

update to 1.4 - #52

Open
SimonDanisch wants to merge 15 commits into
masterfrom
sd/vk1.4
Open

SimonDanisch wants to merge 15 commits into
masterfrom
sd/vk1.4

Conversation

@SimonDanisch

Copy link
Copy Markdown
Member

Vulkan 1.4 has lots of nice features e.g. to use hardware accelerated matmul.

@serenity4

Copy link
Copy Markdown
Member

I remember attempting to update to even just a more recent 1.3 patch version about a year ago, and was faced with nontrivial issues in the generated code that required adjustments on the generator side (and possibly on VulkanSpec.jl to adapt to a changing representation of the contents of the specification). Support for 1.4 might require quite some thought to get it to a working state.

I have essentially moved on from Julia/Vulkan work but I'm happy to review changes in the generator parts (I will trust that you check tests pass in Vulkan.jl and most importantly in your downstream packages before we merge). If updating the generator proves to be too complex/requires to be too intimately familiar with how it works internally then feel free to let me know and I'll see if I can find some time to do it.

@serenity4

Copy link
Copy Markdown
Member

Oh wait, I thought this was a PR to Vulkan.jl, not to VulkanCore.jl, my mistake. My previous comment applies to the former.

Seems like CI has issues fetching the SDK, would be nice to get that working for at least Linux CI.

Comment thread gen/Manifest.toml
Comment on lines 24 to +28
[[deps.Clang]]
deps = ["CEnum", "Clang_jll", "Downloads", "Pkg", "TOML"]
git-tree-sha1 = "d78c2973d7a752be377fe173bc9ff2dc2d9c3ed6"
deps = ["CEnum", "Clang_unified_jll", "Downloads", "Pkg", "TOML"]
git-tree-sha1 = "abf4d804064811b1948978a4aee478ccc76d6c9d"
uuid = "40e3b903-d033-50b4-a0cc-940c62c95e31"
version = "0.17.6"
version = "0.19.3"

@serenity4 serenity4 Jul 30, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Updating to Clang 0.19.3 seems to have a fairly different representation for structs (they are generated with a single .data field that is an NTuple with getproperty/propertynames overloaded to represent the different "fields"), as in

struct VkRenderingAttachmentInfo
    sType::VkStructureType
    pNext::Ptr{Cvoid}
    imageView::VkImageView
    imageLayout::VkImageLayout
    resolveMode::VkResolveModeFlagBits
    resolveImageView::VkImageView
    resolveImageLayout::VkImageLayout
    loadOp::VkAttachmentLoadOp
    storeOp::VkAttachmentStoreOp
    clearValue::VkClearValue
end

previously and now this:

struct VkRenderingAttachmentInfo
    data::NTuple{72, UInt8}
end

function Base.getproperty(x::Ptr{VkRenderingAttachmentInfo}, f::Symbol)
    f === :sType && return Ptr{VkStructureType}(x + 0)
    f === :pNext && return Ptr{Ptr{Cvoid}}(x + 8)
    f === :imageView && return Ptr{VkImageView}(x + 16)
    f === :imageLayout && return Ptr{VkImageLayout}(x + 24)
    f === :resolveMode && return Ptr{VkResolveModeFlagBits}(x + 28)
    f === :resolveImageView && return Ptr{VkImageView}(x + 32)
    f === :resolveImageLayout && return Ptr{VkImageLayout}(x + 40)
    f === :loadOp && return Ptr{VkAttachmentLoadOp}(x + 44)
    f === :storeOp && return Ptr{VkAttachmentStoreOp}(x + 48)
    f === :clearValue && return Ptr{VkClearValue}(x + 52)
    return getfield(x, f)
end

function Base.getproperty(x::VkRenderingAttachmentInfo, f::Symbol)
    r = Ref{VkRenderingAttachmentInfo}(x)
    ptr = Base.unsafe_convert(Ptr{VkRenderingAttachmentInfo}, r)
    fptr = getproperty(ptr, f)
    GC.@preserve r unsafe_load(fptr)
end

function Base.setproperty!(x::Ptr{VkRenderingAttachmentInfo}, f::Symbol, v)
    unsafe_store!(getproperty(x, f), v)
end

function Base.propertynames(x::VkRenderingAttachmentInfo, private::Bool = false)
    (:sType, :pNext, :imageView, :imageLayout, :resolveMode, :resolveImageView, :resolveImageLayout, :loadOp, :storeOp, :clearValue, if private
            fieldnames(typeof(x))
        else
            ()
        end...)
end

function VkRenderingAttachmentInfo(sType::VkStructureType, pNext::Ptr{Cvoid}, imageView::VkImageView, imageLayout::VkImageLayout, resolveMode::VkResolveModeFlagBits, resolveImageView::VkImageView, resolveImageLayout::VkImageLayout, loadOp::VkAttachmentLoadOp, storeOp::VkAttachmentStoreOp, clearValue::VkClearValue)
    ref = Ref{VkRenderingAttachmentInfo}()
    ptr = Base.unsafe_convert(Ptr{VkRenderingAttachmentInfo}, ref)
    ptr.sType = sType
    ptr.pNext = pNext
    ptr.imageView = imageView
    ptr.imageLayout = imageLayout
    ptr.resolveMode = resolveMode
    ptr.resolveImageView = resolveImageView
    ptr.resolveImageLayout = resolveImageLayout
    ptr.loadOp = loadOp
    ptr.storeOp = storeOp
    ptr.clearValue = clearValue
    ref[]
end

It feels like it might be working fine but at the same time it could easily break code that used to introspect into e.g. fieldnames instead of the user-facing propertynames.

It might be just fine but I'd prefer to have confirmation downstream, outside the VulkanCore.jl test suite, that the changes from updating to latest Clang don't behave unexpectedly.

Otherwise it might be preferable to keep this PR limited to updating to 1.4 bindings and keep the same version for Clang.jl as we used before. We can always do a PR later that bumps Clang.jl to the latest version if needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I remembered that there were alignment bugs in 0.17.x.

I summoned Claude to review this now.

SimonDanisch and others added 6 commits September 11, 2026 12:47
A package that cannot work on this machine should not cost anything to depend
on. VulkanCore compiled 34,000 generated lines of ccall wrappers, Vulkan
127,000 more plus a dispatch table and an 8,573-name export list, and Lava a
whole Julia->SPIR-V compiler — all on every Mac in this tree, for code Mantle
declares and, under its own `@static if Sys.isapple()`, never imports.

VulkanCore probes the loader once at precompile time and exposes the answer as
HAS_LOADER; Vulkan and Lava gate their bodies on it, and Vulkan re-exposes it
outside its own gate so a dependent can ask without first checking whether
there is anything to ask. The extensions are gated too — Vk.Format does not
exist when the bindings do not, and those were the last thing still failing.

The gated text is UNCHANGED and unindented on purpose: with a loader present
each module is byte-for-byte the one it was, so the only case this can break is
the empty one.

Measured on macOS with no libvulkan: VulkanCore 3670 -> 554 ms, Vulkan 13098 ->
363 ms, and Lava now precompiles at all — gated out, it no longer evaluates the
`NativeEmitter{Out, Flats}` its body wants and this KernelAbstractions checkout
does not have. All three load in 0.4 s. `using Metal, RayMakie` takes 3.1 s
from a cold RayMakie cache, where it used to error.

A precompile-time answer, so installing a driver later needs
Pkg.precompile(; force = true).

NOT verified with a loader present — there is none on this machine.
The version tracks the Vulkan API version, and the bindings have been generated
against Vulkan_Headers_jll 1.4.321 since "update to 1.4".
"update to 1.4" regenerated thirteen platform files and added a fourteenth,
lib/x86_64-unknown-freebsd13.2.jl -- the target Clang names now -- but left
lib/x86_64-unknown-freebsd.jl untouched at 1.3.240, and that is the one this file
still included. FreeBSD therefore loaded 1.3 bindings under a 1.4 wrapper: of the
3000 Vk names Vulkan.jl's bsd.jl mentions, around a hundred did not exist.

Now 2, both Wayland, and those are a separate and older problem: the wrapper
generator enables PLATFORM_WAYLAND for BSD while no FreeBSD binding here has ever
wrapped Wayland, in this version or the previous one.

The 1.3 file is deleted rather than left beside its replacement.
`JULIA_VULKAN_LIBNAME` is read here, while the package precompiles, and an `ENV`
read is not part of the precompile hash. So setting it takes effect only if
something else happens to invalidate the cache. Observed directly on a driverless
Mac: set the variable, restart Julia, and the cache built WITHOUT it was reused --
`HAS_LOADER` stayed false on a machine that by then had a perfectly good driver
available. Only editing a source file forced the rebuild.

A preference is hashed, so changing it rebuilds the bindings against the new
library on the next load. `Vulkan.set_driver` writes it.

The environment variable still works and still comes before the system default,
but it is the fallback now rather than the mechanism.
The SDK download was pinned to LunarG 1.3.239.0, which LunarG no longer
serves: every job failed on a 404 before Julia was installed. Mesa's
lavapipe comes from apt instead, through the same action Vulkan.jl uses,
so a runner with no GPU still has something to create an instance
against.

That alone leaves CI red. `test/glfw.jl` asserted `@test_broken result ==
VK_SUCCESS` whenever `JULIA_GITHUB_ACTIONS_CI` was ON, because the runner
had no driver -- with one present that is an Unexpected Pass, which is an
error. The whole env-var gate is gone: the tests ask `HAS_LOADER`, the
same question the package itself gates on, so CI and a developer machine
now run the same suite, `old_tests.jl` included. `using GLFW` moves under
that gate too, so the no-driver job needs no window toolkit.

Verified with lavapipe as the only ICD, which is what the runner will
have: 10 of 10. Same on this machine's real drivers.

Also: macOS keeps a job with no driver on purpose, since that is where
the `HAS_LOADER` case is actually met; the stale commented-out Windows
job goes; and `julia-uploadcodecov` is archived, replaced by
`julia-processcoverage` plus `codecov-action`.
@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.02%. Comparing base (a7eb842) to head (1d02829).

Files with missing lines Patch % Lines
src/LibVulkan.jl 66.66% 1 Missing ⚠️
src/blob_constructors.jl 92.30% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master      #52       +/-   ##
===========================================
+ Coverage   33.87%   61.02%   +27.15%     
===========================================
  Files           3        5        +2     
  Lines          62      136       +74     
===========================================
+ Hits           21       83       +62     
- Misses         41       53       +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Skipping the API is right on a machine that has no Vulkan loader, and a
silent pass on a machine that is supposed to have one: if the lavapipe
install ever breaks, the Linux job would quietly drop from 10 tests to 2
and stay green.

The job that installs the driver now says so, and a missing loader there
is a failure rather than a skip. This makes CI assert MORE than a dev
machine, never less -- the opposite of the gate it replaced.

Verified both ways: 11 of 11 with the requirement set and lavapipe as the
only ICD, and the guard fails when the loader is missing.
The env var of the previous commit was patching around the wrong thing.
The suite does not need to know whether it may skip: a machine with no
Vulkan loader is a supported state, but not one this suite can conclude
anything from, so it simply requires one. No gate, no skip, no second job.

macOS gets MoltenVK, the loader and the validation layers from Homebrew,
which is how Vulkan is reached there anyway and what the old LunarG SDK
job was aiming at. The loader is named through a preference rather than
JULIA_VULKAN_LIBNAME, since only the preference is part of the precompile
hash and `julia-actions/cache` restores package caches; Pkg.test copies it
into the test sandbox.

Linux verified locally with lavapipe as the only ICD: 11 of 11. macOS is
unverified from here -- no Mac.
The macOS runner found both of these. MoltenVK is a portability driver, and
since loader 1.3.216 an instance that does not ask for portability
enumeration is not allowed to see one: `vkCreateInstance` answers
VK_ERROR_INCOMPATIBLE_DRIVER. `with_portability` asks the loader whether it
offers the extension rather than asking what platform this is, so there is
one code path everywhere -- the Linux loader advertises it too, and the flag
is set there as well.

Second, `old_tests.jl` then carried on with the handle vkCreateInstance had
never written and segfaulted inside the loader, because `@test` inside a
testset records a failure and continues. It stops there now.

Linux with lavapipe as the only ICD: 11 of 11. Forcing the failure with a
bogus driver file raises rather than crashing.
The previous commit's edit took the LocalPreferences.toml step out along
with the comment block above it, so VulkanCore precompiled without being
told which loader to bind to.

It surfaced as `UndefVarError: VkExtensionProperties` from vkhelper.jl,
which is a bad way to say "no loader": the helpers name types the gate did
not compile. The suite says it outright now, before including anything.

Linux with lavapipe: 11 of 11.
@SimonDanisch

Copy link
Copy Markdown
Member Author

I spend some time to get macOS CI running, which found two bugs: MoltenVK needs portability enumeration, and old_tests.jl segfaulted on the handle.
Anything else we should test before merging and tagging this?

@serenity4

Copy link
Copy Markdown
Member

Did you read and consider this comment? I still don't see why we would want to change the current generated code. I am not sure about its implications and whether it is even desirable. I believe the "opaque struct everywhere" generation mode can be turned off in the Generator.toml to keep the previous behavior.

@serenity4

serenity4 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Also, given the number of changes, before merging/releasing it would be preferable to test that in a downstream package that relies on VulkanCore, such as https://github.com/SimonDanisch/Lava.jl. Did you try it with JuliaGPU/Vulkan.jl#62?

@SimonDanisch

Copy link
Copy Markdown
Member Author

Did you read and consider #52 (comment)?
Ah, actually missed that.
I just double checked: a typedef'd union is emitted as a byte blob with alignment 1, so containers laid out field-wise got wrong offsets. Reverting Clang brings back wrong field offsets. I guess we'll need to deal with this ugliness going forward.

Also, given the number of changes, before merging/releasing it would be preferable to test that in a downstream package that relies on VulkanCore

Yes this has been developed as part of testing Vulkan and Lava on ~5 platforms (windows x linux x apple x nvidia x amd), with pretty heavy Vulkan applications (raytracing, a new makie vulkan backend, and lots of compute).

@serenity4

Copy link
Copy Markdown
Member

Ah, actually missed that.
I just double checked: a typedef'd union is emitted as a byte blob with alignment 1, so containers laid out field-wise got wrong offsets. Reverting Clang brings back wrong field offsets. I guess we'll need to deal with this ugliness going forward.

C unions were already handled as binary blobs with the right getproperty/setproperty! indirections, which AFAIK would take care of alignment issues, and we had 27 such unions. From what I understand here, some structures now get that treatment too (totalling 124 cases while there is only a ~2x struct increase globally between 1.3 and 1.4). Did something in the C headers change?
For instance I do not see why VkRenderingAttachmentInfo (as mentioned in #52 (comment)) is now emitted as a binary blob (or needs to be emitted as such). I see most structs are left untouched, but I'd be nice to avoid any breakages besides Vulkan.jl (which is already going to be updated accordingly), for other users that may have relied on fieldtypes or other internal type API considerations that are affected. Even though we may not have so many users outside of that... it's hard to tell.

Yes this has been developed as part of testing Vulkan and Lava on ~5 platforms (windows x linux x apple x nvidia x amd), with pretty heavy Vulkan applications (raytracing, a new makie vulkan backend, and lots of compute).

It's good to have this confirmation. I would have also tested myself with my own experimental Vulkan applications but they're not in a functional state at the moment (some bugs popping out apparently due to various changes in packages and/or in system Vulkan drivers) and I'm not planning to resume work on them in the foreseeable future.

Before merging I'd ideally prefer to have another perspective on this given that it's a bunch of changes. I remember that @Gnimuc used VulkanCore.jl without going through Vulkan.jl (AFAIK), if he'd be keen to share his thoughts/review and make sure that doesn't break his code. Other reviewer suggestions are welcome too of course, as I'm not sure who is actively depending on this/maintaining this at the moment (I'm just around to look at the code and provide feedback but no longer use it actively).

@Gnimuc

Gnimuc commented Sep 24, 2026

Copy link
Copy Markdown
Member

🤖 AI-generated (Claude Code)

Review of the current head (01a6ff0). Everything below was checked on Linux x86_64 with Julia 1.13 unless noted, and is ordered by severity.

High

1. The blob structs' constructors are strictly typed, which breaks the released Vulkan.jl (lib/x86_64-linux-gnu.jl:4325)

This adds to the open thread on gen/Manifest.toml: that thread is about fieldnames, this is about the constructors. With Clang.jl 0.19, 33 structs that had named fields on master (counted on x86_64-linux-gnu) are now data::NTuple blobs. Their only positional constructor has typed arguments, where the old default constructor converted each one:

VkClearAttachment(VK_IMAGE_ASPECT_COLOR_BIT, 0, clear_value)
# master:  works
# this PR: MethodError: no method matching VkClearAttachment(::VkImageAspectFlagBits, ::Int64, ::VkClearValue)

The registered Vulkan.jl 0.6.28–0.6.30 have VulkanCore compat 1.3.1 - 1, so they will resolve to 1.4.0. Their wrappers pass Vulkan.jl's own enum and bitmask types, and plain Integers, into these constructors. So _ClearAttachment, _RenderingAttachmentInfo, _SamplerCustomBorderColorCreateInfoEXT, _AccelerationStructureGeometryTrianglesDataKHR and others throw a MethodError at runtime.

The layout argument holds for some structs: on master, VkAccelerationStructureGeometryTrianglesDataKHR.vertexData is at offset 20, but C puts it at 24. VkClearAttachment and VkRenderingAttachmentInfo, though, have the same offsets on master as here, so for them only the API changed. add_record_constructors = true was already set on master; the Clang.jl version is what changed.

Possible fixes:

  • tag this as 2.0.0;
  • cap the VulkanCore compat of the existing Vulkan.jl versions in General;
  • also generate converting constructors for the blob structs.

2. A cache built without a loader stays empty after one is installed (src/LibVulkan.jl:85)

HAS_LOADER is decided at precompile time, and the library's presence isn't part of the cache key. I precompiled with the libvulkan preference naming a library that wasn't findable, then made it findable via LD_LIBRARY_PATH. The next session still had HAS_LOADER == false, and vkCreateInstance was undefined.

On master the same sequence fixes itself:

  1. A package's own __init__ doesn't run while it precompiles, so precompile succeeds.
  2. using fails with the actionable "Failed to retrieve a valid Vulkan library" error.
  3. Once the library is findable, the same cache has all the bindings.

A typical way to hit this: run Pkg.add("Vulkan") on a Mac, or in a container, before the loader is installed or on the default search path. After that, Vulkan.jl fails with UndefVarError: VkInstance in every session.

The non-fatal __init__ already fixes the original problem. Always including the bindings would remove this state entirely.

Medium

3. Pkg.precompile(; force = true) doesn't exist (src/LibVulkan.jl:44)

This is the documented recovery for item 2, but Pkg has no force keyword: FieldError: type Pkg.Types.Context has no field force. This command does rebuild the cache (verified HAS_LOADER == true afterwards):

Base.compilecache(Base.identify_package("VulkanCore"))

4. ppEnabledExtensionNames points into an array nothing keeps alive (test/old_tests.jl:38)

GC.@preserve … extensions keeps the Vector{String} alive. It doesn't keep alive the char* array that unsafe_strings2pp builds with Base.cconvert (vkhelper.jl:9).

This test used to pass 0, C_NULL. Now the list is never empty, because the Linux loader also offers VK_KHR_portability_enumeration. After one GC.gc() plus some allocation, ppEnabledExtensionNames[0] changed from the string's address to 0xdead70ad. If that happens before the call, the loader reads garbage, which shows up as an intermittent segfault or a spurious VK_ERROR_EXTENSION_NOT_PRESENT.

test/glfw.jl:31-35 has the same problem for layers and extensions; that part predates this PR. The with_portability docstring (vkhelper.jl:122) also states the incomplete rule.

Fix: keep the Base.cconvert(Ptr{Cstring}, extensions) result and GC.@preserve that too.

5. loaded() returns true when no bindings were compiled (src/LibVulkan.jl:56)

In item 2's stale state, __init__ still dlopens the library. So loaded() is true while every binding is undefined (reproduced), and code that follows its docstring then hits an UndefVarError. Fix: loaded() = HAS_LOADER && libvulkan_handle[] != C_NULL.

Low

6. An empty JULIA_VULKAN_LIBNAME now removes all bindings (src/LibVulkan.jl:33)

Master ignored a set-but-empty variable. Here libvulkan becomes "", and Libdl.find_library("") is "", so HAS_LOADER is false even when libvulkan.so.1 loads fine. ENV isn't in the cache key, so unsetting the variable later doesn't help.

7. The released Vulkan.set_driver doesn't set this preference (src/LibVulkan.jl:22)

In Vulkan.jl 0.6.30, set_driver(:SwiftShader) sets ENV["JULIA_VULKAN_LIBNAME"] and the ICD variables at runtime, after VulkanCore is loaded. It never sets a VulkanCore preference. I didn't check JuliaGPU/Vulkan.jl#62; if it changes set_driver, naming that Vulkan.jl version here would help.

8. Output file names still follow Clang's versioned triples (gen/generator.jl:74)

strip_triple_version is applied to the flag lookup, not to output_file_path. That already caused a bug in this PR, fixed in 66e24c2: the regen wrote x86_64-unknown-freebsd13.2.jl, but LibVulkan.jl kept including the stale 1.3.240 file, and nothing errored. The next triple bump will do the same.

Two ways to catch it:

  • normalize the output name. Don't reuse that regex as-is, though: it would also turn mingw32 into mingw.
  • make the generator error when it writes a file that LibVulkan.jl doesn't include.

9. Nit: a test that can't fail (test/runtests.jl:11)

VulkanCore.HAS_LOADER === LibVulkan.HAS_LOADER compares a constant with itself. Lines 12, 13 and 16 then check the same fact again.


How this was checked:

  • loaded master's and this PR's lib/x86_64-linux-gnu.jl side by side;
  • reproduced the stale cache in a scratch depot;
  • ran a GC stress test on the create-info helper;
  • read Vulkan.jl 0.6.30's source from the package server.

Not checked: JuliaGPU/Vulkan.jl#62, or any platform other than Linux x86_64. CI is green on the head commit.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants