Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions .github/workflows/generated-bindings-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,7 @@ jobs:
cp vulkan.v vulkan_video.v loader.v v.mod VERSION REGISTRY_COMMIT VOLK_COMMIT "$module_root/"
cp c/loader_bridge.h c/volk.c.v c/volk_impl.h "$module_root/c/"
cp -R c/vendor "$module_root/c/"
cp ergonomic/ergonomic.v ergonomic/allocator.v ergonomic/discovery.v ergonomic/configuration.v \
ergonomic/presentation.v ergonomic/resources.v \
"$module_root/ergonomic/"
cp ergonomic/*.v "$module_root/ergonomic/"
echo "VMODULES=$RUNNER_TEMP/vmodules" >> "$GITHUB_ENV"
- name: Type-check with the strict V3 frontend
if: matrix.v-channel == 'v3'
Expand Down Expand Up @@ -285,6 +283,9 @@ jobs:
fi
v "${v_flags[@]}" -o "$RUNNER_TEMP/bundled-header-smoke" ci/bundled_header_smoke
v "${v_flags[@]}" test ergonomic
python3 scripts/check_docs.py
v "${v_flags[@]}" -o "$RUNNER_TEMP/vulkan-first-use" examples/getting_started
v "${v_flags[@]}" -o "$RUNNER_TEMP/vulkan-conveniences" examples/conveniences
if [[ "${{ runner.os }}" == Linux && "${{ matrix.v-channel }}" == stable ]]; then
v -d vulkan_xlib -cc "${{ matrix.compiler }}" \
-o "$RUNNER_TEMP/vulkan-xlib-smoke" ci/platform_xlib
Expand All @@ -294,9 +295,32 @@ jobs:
shell: bash
run: |
set -euo pipefail
python3 ci/test_setup_preserves_module.py
v -cc tcc test ergonomic
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
v -cc tcc run examples/ergonomic_lifecycle
- name: Check out presentation example dependency
if: runner.os == 'Linux' && matrix.v-channel == 'stable'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
repository: antono2/glfw
ref: v2.0.1
path: .ci-glfw
- name: Validate presentation and resize
env:
GLFW_INCLUDE: /usr/include
GLFW_LIB: /usr/lib/x86_64-linux-gnu
if: runner.os == 'Linux' && matrix.v-channel == 'stable'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libglfw3-dev xvfb xauth
mkdir -p "$VMODULES/antono2/glfw"
cp -R .ci-glfw/. "$VMODULES/antono2/glfw/"
v -cc gcc -o "$RUNNER_TEMP/presentation" examples/presentation
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation xvfb-run -a "$RUNNER_TEMP/presentation" > "$RUNNER_TEMP/presentation.log" 2>&1
cat "$RUNNER_TEMP/presentation.log"
if grep -Eiq 'Validation (Error|Warning)|VUID-' "$RUNNER_TEMP/presentation.log"; then exit 1; fi
- name: Report source compiler compatibility warning
if: always() && matrix.v-channel != 'stable' && (steps.checkout_v_source.outcome == 'failure' || steps.setup_v_source.outcome == 'failure' || steps.compile.outcome == 'failure')
shell: bash
Expand All @@ -311,6 +335,9 @@ jobs:
shell: bash
run: |
set -euo pipefail
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation v -cc "${{ matrix.compiler }}" run examples/conveniences > "$RUNNER_TEMP/conveniences.log" 2>&1
cat "$RUNNER_TEMP/conveniences.log"
if grep -Eiq 'Validation (Error|Warning)|VUID-' "$RUNNER_TEMP/conveniences.log"; then exit 1; fi
lifecycle_log="$RUNNER_TEMP/vulkan-lifecycle.log"
set +e
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ Thumbs.db
# setup.vsh outputs
setup
AvailableVulkanVersions.json

# Python validation scripts
__pycache__/
*.pyc
6 changes: 4 additions & 2 deletions API_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ allocations. The typed `submit()` call remains the convenient checked default.

`Queue.submit()` accepts a non-empty primary-command-buffer batch plus raw wait semaphore handles, signal semaphore handles and an optional raw fence handle in `SubmitOptions`. Every wait semaphore requires a pipeline-stage mask at the same array index; mismatched counts are rejected before Vulkan is called. Owners expose these handles publicly, so the options stay copyable while the owner wrappers remain `@[nocopy]`. The helper keeps the temporary command-handle array alive through `vkQueueSubmit`, returns non-negative Vulkan statuses unchanged and converts failures to `VulkanError`.

`OwnedImage` creates a simple exclusive-sharing 2D image with one mip level, one array layer and one sample. It exposes the raw image and memory handles plus its format, extent, tiling, usage, allocation size and selected memory type. Destruction releases the image before its bound allocation, and must happen before destroying the parent device. More specialized image creation remains available through the raw layer.
`OwnedImage` creates a simple exclusive-sharing 2D image with one array layer and one sample. `new_image_2d_mips` accepts an explicit mip count. It exposes the raw image and memory handles plus its format, extent, tiling, usage, allocation size and selected memory type. Destruction releases the image before its bound allocation, and must happen before destroying the parent device. More specialized image creation remains available through the raw layer.

The owned buffer and image helpers intentionally make dedicated allocations;
they optimize for transparent lifetime rules, not allocation density. Use
Expand Down Expand Up @@ -143,7 +143,7 @@ Shader modules and mappings must be destroyed or unmapped before their parent
resource or device. Owned-device and queue `wait_idle()` helpers preserve typed
Vulkan errors.

`ImageLayoutTransition` keeps the synchronization-1 source/destination stage masks, access masks, old/new layouts, dependency flags and aspect mask explicit. `PrimaryCommandBuffer.transition_image_layout()` records one image-only `vkCmdPipelineBarrier` over the owned image's single mip level and array layer. It does not infer synchronization, track layout state or perform queue-family ownership transfers; use the raw API for broader ranges, ownership transfers or synchronization-2 barriers.
`ImageLayoutTransition` keeps the synchronization-1 source/destination stage masks, access masks, old/new layouts, dependency flags and aspect mask explicit. `PrimaryCommandBuffer.transition_image_layout()` records one image-only `vkCmdPipelineBarrier` over the image subresource range selected by the transition. It does not infer synchronization, track layout state or perform queue-family ownership transfers; use the raw API for broader ranges, ownership transfers or synchronization-2 barriers.

`InstanceOptions` validates and owns instance layer/extension name pointers through creation. `DeviceOptions` accepts either its compatible single-queue fields or `DeviceQueueRequest` values for multiple queues and families, plus device extensions, core features and an application-owned feature `pNext` chain. `PhysicalDevice.find_present_queue_family()` layers surface support over the existing queue-flag selection. Optional host allocation callbacks are copied into the instance or device owner and passed through child creation, cleanup and destruction. The callback functions and `pUserData` state must outlive the affected owners. Concurrent-sharing resources remain in the raw layer.

Expand All @@ -159,3 +159,5 @@ Vulkan errors.
8. Surface-support snapshots and explicit swapchain-choice helpers. (Implemented.)
9. Checked host-visible buffer mappings, coherent byte uploads and owned SPIR-V shader modules. (Implemented.)
10. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.

See [convenience reference](docs/conveniences.md) for format queries, descriptor owners, swapchain outcomes, and pipeline caches.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ in `VERSION`.

## Unreleased

- Add explicit ergonomic owners for descriptor layouts/pools, swapchains and
image views, pipeline caches, and timestamp queries.
- Add format selection, debug labels, device UUID and external-resource
capability queries with raw Vulkan escape hatches.
- Add runnable headless and resize/presentation examples, parameter and lifetime
tables, README example checks, and installation that preserves existing pins.

## 3.2.2 - 2026-10-09

### Changed
Expand Down
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ default V module location:
v run "$HOME/.vmodules/antono2/vulkan/setup.vsh"
```

In PowerShell, use `v run "$HOME\.vmodules\antono2\vulkan\setup.vsh" --check`.
If `VMODULES` is configured, use its `antono2/vulkan` directory instead.
Setup preserves existing module installations, including pinned releases.
Use `--probe` to compile and run the first-use example through these bindings.

From a source checkout, use `v run setup.vsh`. The script supports Ubuntu and
Debian, Fedora, Arch, openSUSE, macOS and Windows with winget.
On Linux it installs a C compiler, Vulkan runtime tools and the VPM module;
Expand Down Expand Up @@ -61,22 +66,46 @@ upstream revisions recorded by this repository.

## First use

Applications using the binding directly must initialize Volk before the first
Vulkan call, then load instance- and device-level commands after creating the
corresponding handles:
Save this as `main.v` and run `v run main.v`. It prints the available device
names and reports an error if none are found. No window or shader tools are needed.

<!-- example: examples/getting_started/main.v -->
```v
import antono2.vulkan as vk
module main

import antono2.vulkan.ergonomic as vke

fn main() {
run() or {
eprintln(err)
exit(1)
}
}

if vk.initialize_loader() != .success {
panic('Vulkan loader initialization failed')
fn run() ! {
mut instance := vke.new_instance_with_options(vke.InstanceOptions{
application_name: 'Vulkan first use'
})!
defer { instance.destroy() }
devices := instance.physical_devices()!
if devices.len == 0 { return error('Vulkan runtime found, but no devices were enumerated') }
for device in devices {
println(device.name())
}
}
// create the Vulkan instance
vk.load_instance_commands(instance)
// create the Vulkan device
vk.load_device_commands(device)
```

The ergonomic layer initializes dispatch for you. Applications creating raw
handles must call `initialize_loader()`, then `load_instance_commands(instance)`
and `load_device_commands(device)` after their respective creation calls.

| Task | Guide or example |
| --- | --- |
| Query formats, manage descriptors or pipeline caches | [Convenience reference](docs/conveniences.md) |
| Create and recreate a presentation swapchain | [Presentation example](examples/presentation) |
| Validate headless resource operations | [Convenience smoke test](examples/conveniences) |
| Manage native resources and synchronization | [Ownership](OWNERSHIP.md) and [API design](API_DESIGN.md) |

### Platform extensions

Enable a platform extension when building code that uses its raw types or
Expand Down
41 changes: 41 additions & 0 deletions ci/test_setup_preserves_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Exercise installation with fake package commands and an existing release pin."""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import tempfile

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--v', default='v')
args = parser.parse_args()
if not __import__('sys').platform.startswith('linux'):
raise SystemExit('Linux package-command fixture only')
root = Path(__file__).resolve().parents[1]
compiler = shutil.which(args.v) or str(Path(args.v).resolve())
with tempfile.TemporaryDirectory(prefix='vulkan pinned module ') as temporary:
work = Path(temporary)
modules = work / 'modules'
package = modules / 'antono2/vulkan'
package.mkdir(parents=True)
pin = "Module { name: 'antono2.vulkan' version: '1.2.3' }\n"
(package / 'v.mod').write_text(pin)
source = work / 'fixture.v'
source.write_text('''import os
fn main() {
if os.file_name(os.executable()) == 'v' {
os.write_file(os.getenv('INSTALL_MARKER'), os.args.join(' ')) or { panic(err) }
exit(88)
}
}
''')
stub = work / 'sudo'
subprocess.run([compiler, '-cc', 'gcc', '-o', str(stub), str(source)], check=True)
shutil.copy2(stub, work / 'v')
marker = work / 'unexpected-install.txt'
env = dict(os.environ, VMODULES=str(modules), PATH=str(work) + os.pathsep + os.environ['PATH'], INSTALL_MARKER=str(marker))
result = subprocess.run([compiler, 'run', str(root / 'setup.vsh'), '--install'], env=env, capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr
assert not marker.exists(), 'setup attempted an unpinned module install'
assert (package / 'v.mod').read_text() == pin
print('Existing module pin preserved; package commands were mocked.')
91 changes: 91 additions & 0 deletions docs/conveniences.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Rendering conveniences

Import `antono2.vulkan.ergonomic`. Native handles remain exposed, owners use
explicit `destroy()`, and child resources must be destroyed before their device.
The complete [headless example](../examples/conveniences/main.v) validates formats,
descriptors and pipeline-cache round trips.

## Formats

| Method | Inputs | Result |
| --- | --- | --- |
| `format_properties(format)` | Physical-device format | Linear/optimal/buffer feature masks |
| `select_format(preferred, tiling, required)` | Ordered formats and complete required mask | First matching format, or error |
| `image_format_properties(format, type, tiling, usage, flags)` | Exact image configuration | `supported` and native limits; other failures remain typed errors |

A feature-mask match alone does not validate an image's dimensions or sample
count. Query the exact configuration and check the returned limits as needed.
Format order is application policy, including depth precision and HDR choices.

## Descriptors

| Owner or method | Contract |
| --- | --- |
| `new_descriptor_layout(bindings)` | Unique binding numbers, nonzero counts/stages; immutable samplers use the raw API |
| `new_descriptor_pool(max_sets, sizes)` | Positive capacity; pool owns all allocated sets |
| `pool.allocate(layouts)` | Batch allocation; layouts must belong to this device |
| `set.write_buffers(binding, first, infos)` | Checks binding type and array bounds; offsets/ranges are bytes |
| `set.write_images(binding, first, infos)` | Checks binding type, array bounds and required handles |
| `pool.reset()` | Invalidates all borrowed sets and releases their allocations |

Sets are borrowed values with pool-generation checks. They do not need individual
cleanup. The pool owner must outlive the borrowed values. Reset/destruction is
rejected on subsequent helper writes, but raw handle use bypasses these checks.
Descriptor resources and their layouts remain caller responsibilities. Complete
GPU use before updating descriptors, resetting pools or destroying resources;
helpers do not infer synchronization or validate native resource lifetimes.

## Presentation

See [presentation](../examples/presentation/main.v) for a complete windowed
clear/present loop and resize recreation. It requires GLFW and a surface that
supports transfer-destination and color-attachment usage.

| Acquire result | `image_index` | Application action |
| --- | --- | --- |
| `success` | Present | Render and present |
| `suboptimal_khr` | Present | Finish the acquired image's synchronization cycle; schedule recreation |
| `timeout`, `not_ready` | Absent | Pump events/retry according to application policy |
| `error_out_of_date_khr` | Absent | Recreate when the framebuffer extent is nonzero |
| Other failures | Error | Handle the native Vulkan error |

`new_swapchain(info)` requires the device's `VK_KHR_swapchain` extension. It owns
its image views and exposes borrowed swapchain images. It supports one array
layer. Enumeration retries `VK_INCOMPLETE`; partial construction cleans up views
and the swapchain. `Queue.present()` preserves success, suboptimal and out-of-date
results. It does not reset fences, run an event loop or recreate automatically.

Keep a render-finished semaphore per swapchain image, and wait for frame work
before reusing command buffers. Reset a frame fence only immediately before the
submission that will signal it. Destroy dependent framebuffers before the
swapchain. If `oldSwapchain` is provided, Vulkan retirement rules still apply,
including creation failure. Do not assume the old swapchain remains acquirable.

## Diagnostics and caches

| API | Requirements and lifetime |
| --- | --- |
| `device.debug_labels()` | Enable instance `VK_EXT_debug_utils`; returns loaded begin/end label commands |
| `new_timestamp_queries(count)` | Selected queue family must support timestamps; explicit reset/write/read |
| `queries.read()` | Nonblocking; inspect each sample's `available` field |
| `queries.elapsed_ns(start, end)` | Available samples from one clock domain; accounts for one counter wrap |
| `new_pipeline_cache(bytes)` | Empty bytes start a cache; supplied blobs must match device/vendor/UUID |
| `cache.bytes()` | Owned byte copy; caller externally synchronizes cache access |

Use the public cache handle when creating pipelines. Cache import errors are
explicit; applications can choose an empty-cache fallback. These APIs do not
choose filenames or perform disk writes. Reuse/cache benefits depend on workload
and driver and should be measured separately from correctness checks.

## Interoperability queries

Create an instance with `api_version: vk.api_version_1_1` or newer to use
`instance.device_identity(physical)`, `external_buffer_properties(...)` and
`external_semaphore_properties(...)`. Query the exact buffer usage/flags and
external handle type. The returned native properties include compatible handle
types, import/export support and dedicated-allocation requirements.

Use the device UUID with OpenCL's `DeviceRequirements.required_uuid` to select
the same physical device. Matching UUIDs alone never establishes zero-copy
support: validate both APIs' memory and semaphore capabilities and ownership
rules. These Vulkan queries have no OpenCL dependency.
20 changes: 20 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Local validation — 2026-10-14

Validated on Linux with V 0.5.2, GCC and TinyCC, and an NVIDIA GeForce GTX 1060
6GB. These results describe this checkout, not a cross-platform certification.
Windows and macOS compilation checks are configured in CI but were not run locally.

- All six ergonomic unit-test files passed.
- The convenience example exercised format queries, descriptor reset
invalidation, cache roundtrip/rejection, labels, timestamps and external-resource
capability queries. It passed with Vulkan validation enabled.
- The GLFW presentation example rendered, resized, recreated its swapchain and
rendered again under Xvfb with Vulkan validation enabled.
- Strict V3 frontend checks passed for convenience and presentation examples.
- Setup probes and mocked package-command tests passed, including preservation
of an existing installed module version.
- README code matches the executable first-use example.

The handwritten ergonomic layer owns these changes; generated Vulkan bindings
and the Vulkan registry generator do not need regeneration for these helpers.
Applications still choose synchronization, resource lifetimes and backend policy.
Loading
Loading