From 6659547abd7869abd4f5851f8a81bea4c0a66e86 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 12:16:04 +0200 Subject: [PATCH 1/3] Add Vulkan resource conveniences and verified getting-started examples --- .github/workflows/generated-bindings-ci.yml | 30 +++- .gitignore | 4 + API_DESIGN.md | 6 +- CHANGELOG.md | 7 + README.md | 49 +++-- ci/test_setup_preserves_module.py | 41 +++++ docs/conveniences.md | 91 ++++++++++ docs/validation.md | 20 +++ ergonomic/conveniences_test.v | 41 +++++ ergonomic/descriptors.v | 190 ++++++++++++++++++++ ergonomic/diagnostics.v | 125 +++++++++++++ ergonomic/ergonomic.v | 54 +++--- ergonomic/formats.v | 46 +++++ ergonomic/interop.v | 55 ++++++ ergonomic/pipeline_cache.v | 72 ++++++++ ergonomic/swapchain.v | 142 +++++++++++++++ examples/conveniences/main.v | 111 ++++++++++++ examples/getting_started/main.v | 22 +++ examples/presentation/README.md | 18 ++ examples/presentation/main.v | 167 +++++++++++++++++ examples/presentation/v.mod | 4 + scripts/check_docs.py | 13 ++ setup.vsh | 31 +++- 23 files changed, 1296 insertions(+), 43 deletions(-) create mode 100644 ci/test_setup_preserves_module.py create mode 100644 docs/conveniences.md create mode 100644 docs/validation.md create mode 100644 ergonomic/conveniences_test.v create mode 100644 ergonomic/descriptors.v create mode 100644 ergonomic/diagnostics.v create mode 100644 ergonomic/formats.v create mode 100644 ergonomic/interop.v create mode 100644 ergonomic/pipeline_cache.v create mode 100644 ergonomic/swapchain.v create mode 100644 examples/conveniences/main.v create mode 100644 examples/getting_started/main.v create mode 100644 examples/presentation/README.md create mode 100644 examples/presentation/main.v create mode 100644 examples/presentation/v.mod create mode 100644 scripts/check_docs.py diff --git a/.github/workflows/generated-bindings-ci.yml b/.github/workflows/generated-bindings-ci.yml index 753738e..2fe8a23 100644 --- a/.github/workflows/generated-bindings-ci.yml +++ b/.github/workflows/generated-bindings-ci.yml @@ -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' @@ -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 @@ -294,9 +295,29 @@ 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 + 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 @@ -311,6 +332,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 \ diff --git a/.gitignore b/.gitignore index 1439849..66cf6df 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ Thumbs.db # setup.vsh outputs setup AvailableVulkanVersions.json + +# Python validation scripts +__pycache__/ +*.pyc diff --git a/API_DESIGN.md b/API_DESIGN.md index ab42d54..73c8839 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -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 @@ -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. @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index fd3574e..5867cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index b803420..2b0c20e 100644 --- a/README.md +++ b/README.md @@ -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; @@ -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. + ```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 diff --git a/ci/test_setup_preserves_module.py b/ci/test_setup_preserves_module.py new file mode 100644 index 0000000..7fada8c --- /dev/null +++ b/ci/test_setup_preserves_module.py @@ -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.') diff --git a/docs/conveniences.md b/docs/conveniences.md new file mode 100644 index 0000000..f4668c8 --- /dev/null +++ b/docs/conveniences.md @@ -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. diff --git a/docs/validation.md b/docs/validation.md new file mode 100644 index 0000000..47e5bdd --- /dev/null +++ b/docs/validation.md @@ -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. diff --git a/ergonomic/conveniences_test.v b/ergonomic/conveniences_test.v new file mode 100644 index 0000000..5b8d5f9 --- /dev/null +++ b/ergonomic/conveniences_test.v @@ -0,0 +1,41 @@ +module ergonomic + +import antono2.vulkan as vk + +fn test_format_mask_and_acquire_outcomes() ! { + properties := vk.FormatProperties{ + optimalTilingFeatures: 3 + linearTilingFeatures: 1 + } + assert format_supports(properties, .optimal, 3) + assert !format_supports(properties, .linear, 3) + assert !format_supports(properties, .optimal, 4) + for status in [vk.Result.timeout, .not_ready, .error_out_of_date_khr] { + outcome := acquire_outcome(status, 999)! + assert outcome.status == status + assert outcome.image_index == none + } + for status in [vk.Result.success, .suboptimal_khr] { + outcome := acquire_outcome(status, 2)! + assert outcome.image_index? == 2 + } + mut rejected := false + acquire_outcome(.error_device_lost, 2) or { + rejected = true + assert err is VulkanError + } + assert rejected + assert !pipeline_cache_compatible([u8(0), 1], vk.PhysicalDeviceProperties{}) +} + +fn test_descriptor_validation_without_driver() { + device := OwnedDevice{} + mut rejected := false + device.new_descriptor_layout([ + vk.DescriptorSetLayoutBinding{ binding: 0, descriptorCount: 0 }, + ]) or { rejected = true } + assert rejected + rejected = false + device.new_descriptor_pool(0, []) or { rejected = true } + assert rejected +} diff --git a/ergonomic/descriptors.v b/ergonomic/descriptors.v new file mode 100644 index 0000000..20401b9 --- /dev/null +++ b/ergonomic/descriptors.v @@ -0,0 +1,190 @@ +// Owned layouts and pools; descriptor sets borrow their pool and layout metadata. +module ergonomic + +import antono2.vulkan as vk + +@[nocopy] +pub struct OwnedDescriptorLayout { + device vk.Device + allocator &HostAllocator = unsafe { nil } + bindings []vk.DescriptorSetLayoutBinding +pub mut: + handle vk.DescriptorSetLayout +} + +pub fn (device &OwnedDevice) new_descriptor_layout(bindings []vk.DescriptorSetLayoutBinding) !&OwnedDescriptorLayout { + mut seen := map[u32]bool{} + for binding in bindings { + if binding.descriptorCount == 0 || binding.stageFlags == 0 || binding.binding in seen { + return error('descriptor bindings need unique numbers, nonzero counts and shader stages') + } + if !isnil(binding.pImmutableSamplers) { + return error('immutable samplers require the raw descriptor layout API') + } + seen[binding.binding] = true + } + info := vk.DescriptorSetLayoutCreateInfo{ + bindingCount: u32(bindings.len) + pBindings: bindings.data + } + mut handle := vk.DescriptorSetLayout(unsafe { nil }) + require_success(vk.create_descriptor_set_layout(device.handle, &info, + allocator_ptr(device.allocator), &handle), 'vkCreateDescriptorSetLayout')! + return &OwnedDescriptorLayout{ + device: device.handle + allocator: device.allocator + bindings: bindings.clone() + handle: handle + } +} + +pub fn (mut layout OwnedDescriptorLayout) destroy() { + if !isnil(layout.handle) { + vk.destroy_descriptor_set_layout(layout.device, layout.handle, + allocator_ptr(layout.allocator)) + layout.handle = unsafe { nil } + } +} + +@[heap; nocopy] +pub struct OwnedDescriptorPool { + device vk.Device + allocator &HostAllocator = unsafe { nil } +mut: + generation u64 +pub mut: + handle vk.DescriptorPool +} + +pub fn (device &OwnedDevice) new_descriptor_pool(max_sets u32, sizes []vk.DescriptorPoolSize) !&OwnedDescriptorPool { + if max_sets == 0 { return error('descriptor pool max_sets must be positive') } + for size in sizes { + if size.descriptorCount == 0 { return error('descriptor pool sizes must be positive') } + } + info := vk.DescriptorPoolCreateInfo{ + maxSets: max_sets + poolSizeCount: u32(sizes.len) + pPoolSizes: sizes.data + } + mut handle := vk.DescriptorPool(unsafe { nil }) + require_success(vk.create_descriptor_pool(device.handle, &info, + allocator_ptr(device.allocator), &handle), 'vkCreateDescriptorPool')! + return &OwnedDescriptorPool{ + device: device.handle + allocator: device.allocator + handle: handle + } +} + +// Pool reset/destruction invalidates every set. Complete GPU use before either. +pub fn (mut pool OwnedDescriptorPool) reset() ! { + if isnil(pool.handle) { return error('descriptor pool is destroyed') } + require_success(vk.reset_descriptor_pool(pool.device, pool.handle, 0), 'vkResetDescriptorPool')! + pool.generation++ +} + +pub fn (mut pool OwnedDescriptorPool) destroy() { + if !isnil(pool.handle) { + vk.destroy_descriptor_pool(pool.device, pool.handle, allocator_ptr(pool.allocator)) + pool.handle = unsafe { nil } + pool.generation++ + } +} + +pub struct BorrowedDescriptorSet { + pool &OwnedDescriptorPool + generation u64 + bindings []vk.DescriptorSetLayoutBinding +pub: + handle vk.DescriptorSet +} + +pub fn (pool &OwnedDescriptorPool) allocate(layouts []&OwnedDescriptorLayout) ![]BorrowedDescriptorSet { + if isnil(pool.handle) { return error('descriptor pool is destroyed') } + if layouts.len == 0 { return []BorrowedDescriptorSet{} } + mut handles := []vk.DescriptorSetLayout{cap: layouts.len} + for layout in layouts { + if isnil(layout.handle) || layout.device != pool.device { + return error('descriptor layout belongs to another device or is destroyed') + } + handles << layout.handle + } + info := vk.DescriptorSetAllocateInfo{ + descriptorPool: pool.handle + descriptorSetCount: u32(handles.len) + pSetLayouts: handles.data + } + mut sets := []vk.DescriptorSet{len: handles.len, init: vk.DescriptorSet(unsafe { nil })} + require_success(vk.allocate_descriptor_sets(pool.device, &info, sets.data), + 'vkAllocateDescriptorSets')! + mut result := []BorrowedDescriptorSet{cap: sets.len} + for i, handle in sets { + result << BorrowedDescriptorSet{ + pool: pool + generation: pool.generation + bindings: layouts[i].bindings.clone() + handle: handle + } + } + return result +} + +fn (set BorrowedDescriptorSet) binding_type(binding u32, first u32, count int) !vk.DescriptorType { + if isnil(set.pool.handle) || set.generation != set.pool.generation { + return error('descriptor set invalidated by pool reset or destruction') + } + for entry in set.bindings { + if entry.binding == binding { + if count < 1 || u64(first) + u64(count) > entry.descriptorCount { + return error('descriptor write exceeds binding array') + } + return entry.descriptorType + } + } + return error('descriptor binding does not exist') +} + +pub fn (set BorrowedDescriptorSet) write_buffers(binding u32, first u32, infos []vk.DescriptorBufferInfo) ! { + kind := set.binding_type(binding, first, infos.len)! + if kind !in [.uniform_buffer, .storage_buffer, .uniform_buffer_dynamic, .storage_buffer_dynamic] { + return error('binding does not accept buffer descriptors') + } + for info in infos { + if isnil(info.buffer) || info.range == 0 { + return error('buffer descriptor needs a buffer and nonzero range') + } + } + write := vk.WriteDescriptorSet{ + dstSet: set.handle + dstBinding: binding + dstArrayElement: first + descriptorCount: u32(infos.len) + descriptorType: kind + pBufferInfo: infos.data + } + vk.update_descriptor_sets(set.pool.device, 1, &write, 0, unsafe { nil }) +} + +pub fn (set BorrowedDescriptorSet) write_images(binding u32, first u32, infos []vk.DescriptorImageInfo) ! { + kind := set.binding_type(binding, first, infos.len)! + if kind !in [.sampler, .combined_image_sampler, .sampled_image, .storage_image, .input_attachment] { + return error('binding does not accept image descriptors') + } + for info in infos { + if kind != .sampler && isnil(info.imageView) { + return error('image descriptor needs a view') + } + if kind in [.sampler, .combined_image_sampler] && isnil(info.sampler) { + return error('sampler descriptor needs a sampler') + } + } + write := vk.WriteDescriptorSet{ + dstSet: set.handle + dstBinding: binding + dstArrayElement: first + descriptorCount: u32(infos.len) + descriptorType: kind + pImageInfo: infos.data + } + vk.update_descriptor_sets(set.pool.device, 1, &write, 0, unsafe { nil }) +} diff --git a/ergonomic/diagnostics.v b/ergonomic/diagnostics.v new file mode 100644 index 0000000..ac465ee --- /dev/null +++ b/ergonomic/diagnostics.v @@ -0,0 +1,125 @@ +// Optional debug labels and timestamp queries. Synchronization remains explicit. +module ergonomic + +import antono2.vulkan as vk + +pub struct DebugLabels { + begin vk.PFN_vkCmdBeginDebugUtilsLabelEXT @[required] + end vk.PFN_vkCmdEndDebugUtilsLabelEXT @[required] +} + +// Enable VK_EXT_debug_utils on the instance before loading these device commands. +pub fn (device &OwnedDevice) debug_labels() !DebugLabels { + begin := unsafe { + vk.PFN_vkCmdBeginDebugUtilsLabelEXT(vk.get_device_proc_addr(device.handle, + c'vkCmdBeginDebugUtilsLabelEXT')) + } + end := unsafe { + vk.PFN_vkCmdEndDebugUtilsLabelEXT(vk.get_device_proc_addr(device.handle, + c'vkCmdEndDebugUtilsLabelEXT')) + } + if isnil(begin) || isnil(end) { return error('enable VK_EXT_debug_utils to use command labels') } + return DebugLabels{ + begin: begin + end: end + } +} + +pub fn (labels DebugLabels) begin_label(command vk.CommandBuffer, name string, color [4]f32) { + info := vk.DebugUtilsLabelEXT{ + pLabelName: name.str + color: color + } + labels.begin(command, &info) +} + +pub fn (labels DebugLabels) end_label(command vk.CommandBuffer) { + labels.end(command) +} + +@[nocopy] +pub struct TimestampQueries { + device vk.Device + allocator &HostAllocator = unsafe { nil } +pub: + count u32 + valid_bits u32 + period_ns f32 +pub mut: + handle vk.QueryPool +} + +pub struct TimestampSample { +pub: + ticks u64 + available bool +} + +// Query support is checked on the selected device queue family. +pub fn (device &OwnedDevice) new_timestamp_queries(count u32) !&TimestampQueries { + families := device.physical_device.queue_families() + bits := families[int(device.queue.family_index)].properties.timestampValidBits + if count == 0 || count > u32(max_int / 2) || bits == 0 { + return error('invalid timestamp count or queue lacks timestamp support') + } + info := vk.QueryPoolCreateInfo{ + queryType: .timestamp + queryCount: count + } + mut handle := vk.QueryPool(unsafe { nil }) + require_success(vk.create_query_pool(device.handle, &info, allocator_ptr(device.allocator), + &handle), 'vkCreateQueryPool(timestamps)')! + return &TimestampQueries{ + device: device.handle + allocator: device.allocator + handle: handle + count: count + valid_bits: bits + period_ns: device.physical_device.properties.limits.timestampPeriod + } +} + +// Record reset before writes; don't reset/reuse queries still in flight. +pub fn (queries &TimestampQueries) reset(command vk.CommandBuffer) ! { + if isnil(queries.handle) { return error('timestamp queries are destroyed') } + vk.cmd_reset_query_pool(command, queries.handle, 0, queries.count) +} + +pub fn (queries &TimestampQueries) write(command vk.CommandBuffer, stage vk.PipelineStageFlagBits, index u32) ! { + if isnil(queries.handle) || index >= queries.count { + return error('invalid timestamp query index') + } + vk.cmd_write_timestamp(command, stage, queries.handle, index) +} + +// Nonblocking result retrieval. Unavailable samples must not be used for timing. +pub fn (queries &TimestampQueries) read() ![]TimestampSample { + if isnil(queries.handle) { return error('timestamp queries are destroyed') } + mut data := []u64{len: int(queries.count) * 2} + flags := u32(vk.QueryResultFlagBits._64) | u32(vk.QueryResultFlagBits.with_availability) + status := vk.get_query_pool_results(queries.device, queries.handle, 0, queries.count, + usize(data.len) * 8, data.data, 16, flags) + if status !in [.success, .not_ready] { require_success(status, 'vkGetQueryPoolResults')! } + mut result := []TimestampSample{cap: int(queries.count)} + for i in 0 .. int(queries.count) { + result << TimestampSample{ + ticks: data[i * 2] + available: data[i * 2 + 1] != 0 + } + } + return result +} + +// Handles one counter wrap. Samples must come from this pool and one clock domain. +pub fn (queries &TimestampQueries) elapsed_ns(start TimestampSample, end TimestampSample) !f64 { + if !start.available || !end.available { return error('timestamp samples are not available') } + mask := if queries.valid_bits == 64 { ~u64(0) } else { (u64(1) << queries.valid_bits) - 1 } + return f64((end.ticks - start.ticks) & mask) * f64(queries.period_ns) +} + +pub fn (mut queries TimestampQueries) destroy() { + if !isnil(queries.handle) { + vk.destroy_query_pool(queries.device, queries.handle, allocator_ptr(queries.allocator)) + queries.handle = unsafe { nil } + } +} diff --git a/ergonomic/ergonomic.v b/ergonomic/ergonomic.v index 584c218..a3f7acc 100644 --- a/ergonomic/ergonomic.v +++ b/ergonomic/ergonomic.v @@ -51,7 +51,8 @@ pub fn require_success(result vk.Result, operation string) ! { // destroy before its storage goes out of scope. @[nocopy] pub struct OwnedInstance { - allocator &HostAllocator = unsafe { nil } + api_version u32 = vk.api_version_1_0 + allocator &HostAllocator = unsafe { nil } pub mut: handle vk.Instance } @@ -67,11 +68,17 @@ pub fn new_instance_with_allocator(create_info &vk.InstanceCreateInfo, allocator require_success(vk.initialize_loader(), 'volkInitialize')! mut handle := vk.Instance(unsafe { nil }) host_allocator := new_host_allocator(allocator) - require_success(vk.create_instance(create_info, allocator_ptr(host_allocator), &handle), 'vkCreateInstance')! + require_success(vk.create_instance(create_info, allocator_ptr(host_allocator), &handle), + 'vkCreateInstance')! vk.load_instance_commands(handle) return &OwnedInstance{ - allocator: host_allocator - handle: handle + api_version: if isnil(create_info.pApplicationInfo) { + vk.api_version_1_0 + } else { + create_info.pApplicationInfo.apiVersion + } + allocator: host_allocator + handle: handle } } @@ -252,8 +259,8 @@ pub fn (device &OwnedDevice) new_command_pool_for_queue(queue Queue, flags vk.Co queueFamilyIndex: queue.family_index } mut handle := vk.CommandPool(unsafe { nil }) - require_success(vk.create_command_pool(device.handle, &create_info, allocator_ptr(device.allocator), &handle), - 'vkCreateCommandPool')! + require_success(vk.create_command_pool(device.handle, &create_info, + allocator_ptr(device.allocator), &handle), 'vkCreateCommandPool')! return &OwnedCommandPool{ device: device.handle allocator: device.allocator @@ -379,8 +386,8 @@ pub fn (device &OwnedDevice) new_buffer(size vk.DeviceSize, usage vk.BufferUsage sharingMode: .exclusive } mut handle := vk.Buffer(unsafe { nil }) - require_success(vk.create_buffer(device.handle, &create_info, allocator_ptr(device.allocator), &handle), - 'vkCreateBuffer')! + require_success(vk.create_buffer(device.handle, &create_info, allocator_ptr(device.allocator), + &handle), 'vkCreateBuffer')! mut requirements := vk.MemoryRequirements{} vk.get_buffer_memory_requirements(device.handle, handle, mut requirements) @@ -396,8 +403,8 @@ pub fn (device &OwnedDevice) new_buffer(size vk.DeviceSize, usage vk.BufferUsage memoryTypeIndex: memory_type_index } mut memory := vk.DeviceMemory(unsafe { nil }) - require_success(vk.allocate_memory(device.handle, &allocate_info, allocator_ptr(device.allocator), &memory), - 'vkAllocateMemory') or { + require_success(vk.allocate_memory(device.handle, &allocate_info, + allocator_ptr(device.allocator), &memory), 'vkAllocateMemory') or { vk.destroy_buffer(device.handle, handle, allocator_ptr(device.allocator)) return err } @@ -456,7 +463,8 @@ pub: // all required properties, allocates it and binds it at offset zero. pub fn (device &OwnedDevice) new_image_2d(width u32, height u32, format vk.Format, tiling vk.ImageTiling, usage vk.ImageUsageFlags, required_memory_properties vk.MemoryPropertyFlags) !&OwnedImage { - return device.new_image_2d_mips(width, height, 1, format, tiling, usage, required_memory_properties) + return device.new_image_2d_mips(width, height, 1, format, tiling, usage, + required_memory_properties) } // new_image_2d_mips creates an image with a complete or partial mip chain. @@ -490,8 +498,8 @@ pub fn (device &OwnedDevice) new_image_2d_mips(width u32, height u32, mip_levels initialLayout: .undefined } mut handle := vk.Image(unsafe { nil }) - require_success(vk.create_image(device.handle, &create_info, allocator_ptr(device.allocator), &handle), - 'vkCreateImage')! + require_success(vk.create_image(device.handle, &create_info, allocator_ptr(device.allocator), + &handle), 'vkCreateImage')! mut requirements := vk.MemoryRequirements{} vk.get_image_memory_requirements(device.handle, handle, mut requirements) @@ -505,8 +513,8 @@ pub fn (device &OwnedDevice) new_image_2d_mips(width u32, height u32, mip_levels memoryTypeIndex: memory_type_index } mut memory := vk.DeviceMemory(unsafe { nil }) - require_success(vk.allocate_memory(device.handle, &allocate_info, allocator_ptr(device.allocator), &memory), - 'vkAllocateMemory') or { + require_success(vk.allocate_memory(device.handle, &allocate_info, + allocator_ptr(device.allocator), &memory), 'vkAllocateMemory') or { vk.destroy_image(device.handle, handle, allocator_ptr(device.allocator)) return err } @@ -616,8 +624,8 @@ pub fn (image &OwnedImage) new_view_mips(aspect_mask vk.ImageAspectFlags, base_m subresourceRange: subresource_range } mut handle := vk.ImageView(unsafe { nil }) - require_success(vk.create_image_view(image.device, &create_info, allocator_ptr(image.allocator), &handle), - 'vkCreateImageView')! + require_success(vk.create_image_view(image.device, &create_info, + allocator_ptr(image.allocator), &handle), 'vkCreateImageView')! return &OwnedImageView{ device: image.device allocator: image.allocator @@ -672,8 +680,8 @@ pub fn (transition ImageLayoutTransition) image_memory_barrier(image &OwnedImage srcQueueFamilyIndex: vk.queue_family_ignored dstQueueFamilyIndex: vk.queue_family_ignored image: image.handle - subresourceRange: image_subresource_range(transition.aspect_mask, transition.base_mip_level, - level_count) + subresourceRange: image_subresource_range(transition.aspect_mask, + transition.base_mip_level, level_count) } } @@ -710,8 +718,8 @@ pub fn (device &OwnedDevice) new_fence(signaled bool) !&OwnedFence { flags: flags } mut handle := vk.Fence(unsafe { nil }) - require_success(vk.create_fence(device.handle, &create_info, allocator_ptr(device.allocator), &handle), - 'vkCreateFence')! + require_success(vk.create_fence(device.handle, &create_info, allocator_ptr(device.allocator), + &handle), 'vkCreateFence')! return &OwnedFence{ device: device.handle allocator: device.allocator @@ -765,8 +773,8 @@ pub mut: pub fn (device &OwnedDevice) new_semaphore() !&OwnedSemaphore { create_info := vk.SemaphoreCreateInfo{} mut handle := vk.Semaphore(unsafe { nil }) - require_success(vk.create_semaphore(device.handle, &create_info, allocator_ptr(device.allocator), &handle), - 'vkCreateSemaphore')! + require_success(vk.create_semaphore(device.handle, &create_info, + allocator_ptr(device.allocator), &handle), 'vkCreateSemaphore')! return &OwnedSemaphore{ device: device.handle allocator: device.allocator diff --git a/ergonomic/formats.v b/ergonomic/formats.v new file mode 100644 index 0000000..b1ec863 --- /dev/null +++ b/ergonomic/formats.v @@ -0,0 +1,46 @@ +// Format queries with application-supplied preference order and requirements. +module ergonomic + +import antono2.vulkan as vk + +pub fn (device PhysicalDevice) format_properties(format vk.Format) vk.FormatProperties { + mut properties := vk.FormatProperties{} + vk.get_physical_device_format_properties(device.handle, format, mut properties) + return properties +} + +pub fn format_supports(properties vk.FormatProperties, tiling vk.ImageTiling, required vk.FormatFeatureFlags) bool { + available := match tiling { + .linear { properties.linearTilingFeatures } + .optimal { properties.optimalTilingFeatures } + else { return false } + } + + return available & required == required +} + +pub fn (device PhysicalDevice) select_format(preferred []vk.Format, tiling vk.ImageTiling, required vk.FormatFeatureFlags) !vk.Format { + for format in preferred { + if format_supports(device.format_properties(format), tiling, required) { return format } + } + return error('none of the preferred formats supports the requested tiling and features') +} + +pub struct ImageFormatSupport { +pub: + supported bool + properties vk.ImageFormatProperties +} + +// Unsupported formats are reported separately from native query failures. +pub fn (device PhysicalDevice) image_format_properties(format vk.Format, image_type vk.ImageType, tiling vk.ImageTiling, usage vk.ImageUsageFlags, flags vk.ImageCreateFlags) !ImageFormatSupport { + mut properties := vk.ImageFormatProperties{} + status := vk.get_physical_device_image_format_properties(device.handle, format, image_type, + tiling, usage, flags, mut properties) + if status == .error_format_not_supported { return ImageFormatSupport{} } + require_success(status, 'vkGetPhysicalDeviceImageFormatProperties')! + return ImageFormatSupport{ + supported: true + properties: properties + } +} diff --git a/ergonomic/interop.v b/ergonomic/interop.v new file mode 100644 index 0000000..a9518a6 --- /dev/null +++ b/ergonomic/interop.v @@ -0,0 +1,55 @@ +// Cross-API identity and exact external-buffer/semaphore capability queries. +// No dependency on OpenCL or an OS-specific handle import implementation. +module ergonomic + +import antono2.vulkan as vk + +pub struct DeviceIdentity { +pub: + device_uuid [16]u8 + driver_uuid [16]u8 +} + +fn (instance &OwnedInstance) require_interop_queries() ! { + if isnil(instance.handle) || instance.api_version < vk.api_version_1_1 { + return error('interop queries require a live Vulkan 1.1+ instance') + } +} + +// physical must come from this instance. UUID matching is necessary but does +// not establish external resource compatibility; query the exact usage too. +pub fn (instance &OwnedInstance) device_identity(physical PhysicalDevice) !DeviceIdentity { + instance.require_interop_queries()! + mut id := vk.PhysicalDeviceIDProperties{} + mut properties := vk.PhysicalDeviceProperties2{ + pNext: &id + } + vk.get_physical_device_properties2(physical.handle, mut properties) + return DeviceIdentity{ + device_uuid: id.deviceUUID + driver_uuid: id.driverUUID + } +} + +pub fn (instance &OwnedInstance) external_buffer_properties(physical PhysicalDevice, usage vk.BufferUsageFlags, flags vk.BufferCreateFlags, handle_type vk.ExternalMemoryHandleTypeFlagBits) !vk.ExternalMemoryProperties { + instance.require_interop_queries()! + if usage == 0 { return error('external buffer usage must be nonzero') } + info := vk.PhysicalDeviceExternalBufferInfo{ + flags: flags + usage: usage + handleType: handle_type + } + mut properties := vk.ExternalBufferProperties{} + vk.get_physical_device_external_buffer_properties(physical.handle, &info, mut properties) + return properties.externalMemoryProperties +} + +pub fn (instance &OwnedInstance) external_semaphore_properties(physical PhysicalDevice, handle_type vk.ExternalSemaphoreHandleTypeFlagBits) !vk.ExternalSemaphoreProperties { + instance.require_interop_queries()! + info := vk.PhysicalDeviceExternalSemaphoreInfo{ + handleType: handle_type + } + mut properties := vk.ExternalSemaphoreProperties{} + vk.get_physical_device_external_semaphore_properties(physical.handle, &info, mut properties) + return properties +} diff --git a/ergonomic/pipeline_cache.v b/ergonomic/pipeline_cache.v new file mode 100644 index 0000000..2641567 --- /dev/null +++ b/ergonomic/pipeline_cache.v @@ -0,0 +1,72 @@ +// Pipeline cache blobs are driver/device-specific; callers own storage policy. +module ergonomic + +import antono2.vulkan as vk +import encoding.binary + +@[nocopy] +pub struct OwnedPipelineCache { + device vk.Device + allocator &HostAllocator = unsafe { nil } +pub mut: + handle vk.PipelineCache +} + +pub fn pipeline_cache_compatible(data []u8, properties vk.PhysicalDeviceProperties) bool { + if data.len < 32 { return false } + if binary.little_endian_u32(data[..4]) != 32 || binary.little_endian_u32(data[4..8]) != 1 { + return false + } + if binary.little_endian_u32(data[8..12]) != properties.vendorID + || binary.little_endian_u32(data[12..16]) != properties.deviceID { + return false + } + for i in 0 .. 16 { + if data[16 + i] != properties.pipelineCacheUUID[i] { return false } + } + return true +} + +// Rejected data returns an error; pass [] to explicitly start with an empty cache. +pub fn (device &OwnedDevice) new_pipeline_cache(data []u8) !&OwnedPipelineCache { + if data.len > 0 && !pipeline_cache_compatible(data, device.physical_device.properties) { + return error('pipeline cache header does not match this device') + } + info := vk.PipelineCacheCreateInfo{ + initialDataSize: usize(data.len) + pInitialData: data.data + } + mut handle := vk.PipelineCache(unsafe { nil }) + require_success(vk.create_pipeline_cache(device.handle, &info, allocator_ptr(device.allocator), + &handle), 'vkCreatePipelineCache')! + return &OwnedPipelineCache{ + device: device.handle + allocator: device.allocator + handle: handle + } +} + +// Caller externally synchronizes cache use, including pipeline creation. +pub fn (cache &OwnedPipelineCache) bytes() ![]u8 { + if isnil(cache.handle) { return error('pipeline cache is destroyed') } + for { + mut size := usize(0) + require_success(vk.get_pipeline_cache_data(cache.device, cache.handle, &size, + unsafe { nil }), 'vkGetPipelineCacheData(size)')! + if size > usize(max_int) { return error('pipeline cache exceeds V array capacity') } + mut data := []u8{len: int(size)} + status := vk.get_pipeline_cache_data(cache.device, cache.handle, &size, data.data) + if status == .incomplete { continue + } + require_success(status, 'vkGetPipelineCacheData(bytes)')! + return data[..int(size)].clone() + } + return []u8{} +} + +pub fn (mut cache OwnedPipelineCache) destroy() { + if !isnil(cache.handle) { + vk.destroy_pipeline_cache(cache.device, cache.handle, allocator_ptr(cache.allocator)) + cache.handle = unsafe { nil } + } +} diff --git a/ergonomic/swapchain.v b/ergonomic/swapchain.v new file mode 100644 index 0000000..e23d895 --- /dev/null +++ b/ergonomic/swapchain.v @@ -0,0 +1,142 @@ +// Explicit swapchain ownership. Images are borrowed; owned views die with the swapchain. +module ergonomic + +import antono2.vulkan as vk + +@[nocopy] +pub struct OwnedSwapchain { + device vk.Device + allocator &HostAllocator = unsafe { nil } +pub: + images []vk.Image + views []vk.ImageView + format vk.Format + extent vk.Extent2D +pub mut: + handle vk.SwapchainKHR +} + +pub fn swapchain_images(device vk.Device, swapchain vk.SwapchainKHR) ![]vk.Image { + for { + mut count := u32(0) + status := vk.get_swapchain_images_khr(device, swapchain, &count, unsafe { nil }) + if status == .incomplete { continue + } + require_success(status, 'vkGetSwapchainImagesKHR(count)')! + if count == 0 { return error('swapchain contains no images') } + mut images := []vk.Image{len: int(count), init: vk.Image(unsafe { nil })} + filled := vk.get_swapchain_images_khr(device, swapchain, &count, images.data) + if filled == .incomplete { continue + } + require_success(filled, 'vkGetSwapchainImagesKHR(images)')! + return images[..int(count)].clone() + } + return []vk.Image{} +} + +// Device must have enabled VK_KHR_swapchain. On replacement Vulkan may retire +// oldSwapchain even if later creation fails. Caller handles recreation policy. +pub fn (device &OwnedDevice) new_swapchain(info &vk.SwapchainCreateInfoKHR) !&OwnedSwapchain { + if info.imageExtent.width == 0 || info.imageExtent.height == 0 { + return error('defer swapchain creation while framebuffer extent is zero') + } + if info.imageArrayLayers != 1 { + return error('convenience swapchain views require one image layer') + } + mut handle := vk.SwapchainKHR(unsafe { nil }) + require_success(vk.create_swapchain_khr(device.handle, info, allocator_ptr(device.allocator), + &handle), 'vkCreateSwapchainKHR')! + mut success := false + mut views := []vk.ImageView{} + defer { + if !success { + for view in views { + vk.destroy_image_view(device.handle, view, allocator_ptr(device.allocator)) + } + vk.destroy_swapchain_khr(device.handle, handle, allocator_ptr(device.allocator)) + } + } + images := swapchain_images(device.handle, handle)! + for image in images { + view_info := vk.ImageViewCreateInfo{ + image: image + viewType: ._2d + format: info.imageFormat + subresourceRange: vk.ImageSubresourceRange{ + aspectMask: u32(vk.ImageAspectFlagBits.color) + levelCount: 1 + layerCount: 1 + } + } + mut view := vk.ImageView(unsafe { nil }) + require_success(vk.create_image_view(device.handle, &view_info, + allocator_ptr(device.allocator), &view), 'vkCreateImageView(swapchain)')! + views << view + } + success = true + return &OwnedSwapchain{ + device: device.handle + allocator: device.allocator + handle: handle + images: images + views: views + format: info.imageFormat + extent: info.imageExtent + } +} + +pub struct AcquireOutcome { +pub: + status vk.Result + image_index ?u32 +} + +pub fn acquire_outcome(status vk.Result, index u32) !AcquireOutcome { + if status in [.success, .suboptimal_khr] { return AcquireOutcome{ + status: status + image_index: index + } } + if status in [.timeout, .not_ready, .error_out_of_date_khr] { return AcquireOutcome{ + status: status + } } + require_success(status, 'vkAcquireNextImageKHR')! + return AcquireOutcome{ + status: status + } +} + +pub fn (swapchain &OwnedSwapchain) acquire(timeout u64, semaphore vk.Semaphore, fence vk.Fence) !AcquireOutcome { + if isnil(swapchain.handle) { return error('swapchain is destroyed') } + if isnil(semaphore) && isnil(fence) { return error('acquire requires a semaphore or fence') } + mut index := u32(0) + status := vk.acquire_next_image_khr(swapchain.device, swapchain.handle, timeout, semaphore, + fence, &index) + return acquire_outcome(status, index) +} + +pub fn (queue Queue) present(swapchain &OwnedSwapchain, index u32, waits []vk.Semaphore) !vk.Result { + if isnil(swapchain.handle) || index >= u32(swapchain.images.len) { + return error('invalid swapchain or image index') + } + if queue.device != swapchain.device { return error('present queue belongs to another device') } + info := vk.PresentInfoKHR{ + waitSemaphoreCount: u32(waits.len) + pWaitSemaphores: waits.data + swapchainCount: 1 + pSwapchains: &swapchain.handle + pImageIndices: &index + } + status := vk.queue_present_khr(queue.handle, &info) + if status in [.success, .suboptimal_khr, .error_out_of_date_khr] { return status } + return check(status, 'vkQueuePresentKHR') +} + +// Complete GPU/presentation use and destroy framebuffers before destruction. +pub fn (mut swapchain OwnedSwapchain) destroy() { + if isnil(swapchain.handle) { return } + for view in swapchain.views { + vk.destroy_image_view(swapchain.device, view, allocator_ptr(swapchain.allocator)) + } + vk.destroy_swapchain_khr(swapchain.device, swapchain.handle, allocator_ptr(swapchain.allocator)) + swapchain.handle = unsafe { nil } +} diff --git a/examples/conveniences/main.v b/examples/conveniences/main.v new file mode 100644 index 0000000..1625133 --- /dev/null +++ b/examples/conveniences/main.v @@ -0,0 +1,111 @@ +// Headless integration check for descriptors, format queries and cache round trips. +module main + +import antono2.vulkan as vk +import antono2.vulkan.ergonomic as vke + +fn main() { + run() or { + eprintln(err) + exit(1) + } +} + +fn run() ! { + mut instance := vke.new_instance_with_options(vke.InstanceOptions{ + api_version: vk.api_version_1_1 + extensions: [ + 'VK_EXT_debug_utils', + ] + })! + defer { instance.destroy() } + devices := instance.physical_devices()! + if devices.len == 0 { return error('no Vulkan device') } + physical := devices[0] + identity := instance.device_identity(physical)! + assert identity.device_uuid.len == 16 + $if linux { + memory := instance.external_buffer_properties(physical, + u32(vk.BufferUsageFlagBits.vertex_buffer), 0, .opaque_fd)! + semaphore := instance.external_semaphore_properties(physical, .opaque_fd)! + println('external buffer flags=${memory.externalMemoryFeatures}, semaphore flags=${semaphore.externalSemaphoreFeatures}') + } + family := physical.find_queue_family(u32(vk.QueueFlagBits.graphics)) or { + return error('no graphics queue') + } + mut device := physical.new_device(family)! + defer { device.destroy() } + format := physical.select_format([vk.Format.d32_sfloat, .d24_unorm_s8_uint], .optimal, + u32(vk.FormatFeatureFlagBits.depth_stencil_attachment))! + assert format != .undefined + support := physical.image_format_properties(.r8g8b8a8_unorm, ._2d, .optimal, + u32(vk.ImageUsageFlagBits.sampled), 0)! + assert support.supported + mut layout := device.new_descriptor_layout([ + vk.DescriptorSetLayoutBinding{ + binding: 0 + descriptorType: .uniform_buffer + descriptorCount: 1 + stageFlags: u32(vk.ShaderStageFlagBits.vertex) + }, + ])! + defer { layout.destroy() } + mut pool := device.new_descriptor_pool(2, [ + vk.DescriptorPoolSize{ type: .uniform_buffer, descriptorCount: 2 }, + ])! + defer { pool.destroy() } + mut buffer := device.new_buffer(64, u32(vk.BufferUsageFlagBits.uniform_buffer), + u32(vk.MemoryPropertyFlagBits.host_visible) | u32(vk.MemoryPropertyFlagBits.host_coherent))! + defer { buffer.destroy() } + sets := pool.allocate([layout, layout])! + for set in sets { + set.write_buffers(0, 0, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + ])! + } + mut rejected := false + sets[0].write_buffers(0, 1, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + ]) or { rejected = true } + assert rejected + pool.reset()! + rejected = false + sets[0].write_buffers(0, 0, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + ]) or { rejected = true } + assert rejected + assert pool.allocate([layout])!.len == 1 + mut cache := device.new_pipeline_cache([])! + defer { cache.destroy() } + data := cache.bytes()! + assert data.len >= 32 + mut restored := device.new_pipeline_cache(data)! + defer { restored.destroy() } + mut corrupt := data.clone() + corrupt[8] ^= 255 + rejected = false + device.new_pipeline_cache(corrupt) or { rejected = true } + assert rejected + labels := device.debug_labels()! + mut queries := device.new_timestamp_queries(2)! + defer { queries.destroy() } + mut commands := device.new_command_pool(0)! + defer { commands.destroy() } + mut command := commands.allocate_primary(1)![0] + mut completed := device.new_fence(false)! + defer { completed.destroy() } + defer { device.wait_idle() or {} } + command.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))! + queries.reset(command.handle)! + labels.begin_label(command.handle, 'timestamp probe', [f32(0.1), 0.4, 0.8, 1.0]!) + queries.write(command.handle, .top_of_pipe, 0)! + queries.write(command.handle, .bottom_of_pipe, 1)! + labels.end_label(command.handle) + command.end()! + device.queue.submit([command], vke.SubmitOptions{ fence: completed.handle })! + vke.require_success(completed.wait(10_000_000_000)!, 'wait timestamp probe')! + samples := queries.read()! + assert samples.len == 2 && samples[0].available && samples[1].available + assert queries.elapsed_ns(samples[0], samples[1])! >= 0 + println('Vulkan format, descriptor, cache and timestamp checks passed on ${physical.name()}') +} diff --git a/examples/getting_started/main.v b/examples/getting_started/main.v new file mode 100644 index 0000000..b28dd1d --- /dev/null +++ b/examples/getting_started/main.v @@ -0,0 +1,22 @@ +module main + +import antono2.vulkan.ergonomic as vke + +fn main() { + run() or { + eprintln(err) + exit(1) + } +} + +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()) + } +} diff --git a/examples/presentation/README.md b/examples/presentation/README.md new file mode 100644 index 0000000..2defcd0 --- /dev/null +++ b/examples/presentation/README.md @@ -0,0 +1,18 @@ +# Clear, present, resize + +Install GLFW's native development files and the dependency in this example's +`v.mod`. Use the Vulkan checkout containing the new ergonomic helpers as your +`antono2.vulkan` module. From the Vulkan repository root: + +```sh +v run examples/presentation +``` + +The example clears a window, presents six frames, resizes it, then creates a new +swapchain and presents six more frames. It exits after reporting both generations. +Run under validation with `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation`. +On headless Linux, use `xvfb-run -a` around the compiled executable. + +The example requires transfer-destination and color-attachment swapchain usage, +and a graphics queue that supports presentation. Applications can use other +rendering paths through the same swapchain owner. Runtime failures remain visible. diff --git a/examples/presentation/main.v b/examples/presentation/main.v new file mode 100644 index 0000000..668b8d5 --- /dev/null +++ b/examples/presentation/main.v @@ -0,0 +1,167 @@ +// A finite clear-and-present example that recreates its swapchain after resize. +module main + +import antono2.glfw +import antono2.vulkan as vk +import antono2.vulkan.ergonomic as vke + +fn C.glfwSetWindowSize(window &glfw.Window, width int, height int) +fn main() { + run() or { + eprintln(err) + exit(1) + } +} + +fn run() ! { + if !glfw.initialize() { return error('GLFW initialization failed') } + defer { glfw.terminate() } + glfw.window_hint(glfw.client_api, glfw.no_api) + window := glfw.create_window(640, 360, 'Vulkan presentation', unsafe { nil }, unsafe { nil }) + if isnil(window) { return error('window creation failed') } + defer { glfw.destroy_window(window) } + mut count := u32(0) + names := glfw.get_required_instance_extensions(&count) + mut extensions := []string{} + for i in 0 .. int(count) { + extensions << unsafe { cstring_to_vstring(names[i]) } + } + mut instance := vke.new_instance_with_options(vke.InstanceOptions{ extensions: extensions })! + defer { instance.destroy() } + mut surface := vk.SurfaceKHR(unsafe { nil }) + vke.require_success(glfw.create_window_surface(instance.handle, window, unsafe { nil }, + &surface), 'glfwCreateWindowSurface')! + defer { vk.destroy_surface_khr(instance.handle, surface, unsafe { nil }) } + devices := instance.physical_devices()! + if devices.len == 0 { return error('no Vulkan device') } + physical := devices[0] + family := physical.find_present_queue_family(u32(vk.QueueFlagBits.graphics), surface)! + mut device := physical.new_device_with_options(vke.DeviceOptions{ + queue_family: family + extensions: [ + 'VK_KHR_swapchain', + ] + })! + defer { device.destroy() } + render_generation(physical, device, surface, window)! + C.glfwSetWindowSize(window, 800, 450) + glfw.poll_events() + render_generation(physical, device, surface, window)! + println('Presented two swapchain generations, including resize.') +} + +fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surface vk.SurfaceKHR, window &glfw.Window) ! { + support := physical.surface_support(surface)! + required_usage := u32(vk.ImageUsageFlagBits.transfer_dst) | u32(vk.ImageUsageFlagBits.color_attachment) + if support.capabilities.supportedUsageFlags & required_usage != required_usage { + return error('this clear example requires transfer-destination and color-attachment swapchain images') + } + format := vke.select_surface_format(support.formats, [ + vk.SurfaceFormatKHR{ format: .b8g8r8a8_srgb, colorSpace: .srgb_nonlinear }, + ])! + mut width := i32(0) + mut height := i32(0) + glfw.get_framebuffer_size(window, &width, &height) + if width <= 0 || height <= 0 { + return error('window is minimized; resume when extent is nonzero') + } + extent := vke.select_surface_extent(support.capabilities, vk.Extent2D{ + width: u32(width) + height: u32(height) + }) + info := vk.SwapchainCreateInfoKHR{ + surface: surface + minImageCount: vke.select_surface_image_count(support.capabilities, 1) + imageFormat: format.format + imageColorSpace: format.colorSpace + imageExtent: extent + imageArrayLayers: 1 + imageUsage: required_usage + imageSharingMode: .exclusive + preTransform: support.capabilities.currentTransform + compositeAlpha: vke.select_composite_alpha(support.capabilities.supportedCompositeAlpha, [ + .opaque, + ])! + presentMode: .fifo + clipped: vk._true + } + mut swapchain := device.new_swapchain(&info)! + defer { swapchain.destroy() } + mut pool := device.new_command_pool(u32(vk.CommandPoolCreateFlagBits.reset_command_buffer))! + defer { pool.destroy() } + mut command := pool.allocate_primary(1)![0] + mut ready := device.new_semaphore()! + defer { ready.destroy() } + mut done := []&vke.OwnedSemaphore{} + defer { + for mut semaphore in done { + semaphore.destroy() + } + } + for _ in swapchain.images { + done << device.new_semaphore()! + } + mut fence := device.new_fence(true)! + defer { fence.destroy() } + // Also complete submitted work before cleanup on an error path. + defer { device.wait_idle() or {} } + for _ in 0 .. 6 { + glfw.poll_events() + vke.require_success(fence.wait(10_000_000_000)!, 'wait frame')! + acquired := swapchain.acquire(1_000_000_000, ready.handle, unsafe { nil })! + index := acquired.image_index or { + if acquired.status == .error_out_of_date_khr { return } + return error('image acquisition did not complete: ${acquired.status}') + } + + command.reset(0)! + command.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))! + range := vk.ImageSubresourceRange{ + aspectMask: u32(vk.ImageAspectFlagBits.color) + levelCount: 1 + layerCount: 1 + } + before := vk.ImageMemoryBarrier{ + oldLayout: .undefined + newLayout: .transfer_dst_optimal + dstAccessMask: u32(vk.AccessFlagBits.transfer_write) + srcQueueFamilyIndex: vk.queue_family_ignored + dstQueueFamilyIndex: vk.queue_family_ignored + image: swapchain.images[index] + subresourceRange: range + } + vk.cmd_pipeline_barrier(command.handle, u32(vk.PipelineStageFlagBits.top_of_pipe), + u32(vk.PipelineStageFlagBits.transfer), 0, 0, unsafe { nil }, 0, unsafe { nil }, 1, + &before) + color := vk.ClearColorValue{ + float32: [f32(0.05), 0.15, 0.35, 1.0]! + } + vk.cmd_clear_color_image(command.handle, swapchain.images[index], .transfer_dst_optimal, + &color, 1, &range) + after := vk.ImageMemoryBarrier{ + oldLayout: .transfer_dst_optimal + newLayout: .present_src_khr + srcAccessMask: u32(vk.AccessFlagBits.transfer_write) + srcQueueFamilyIndex: vk.queue_family_ignored + dstQueueFamilyIndex: vk.queue_family_ignored + image: swapchain.images[index] + subresourceRange: range + } + vk.cmd_pipeline_barrier(command.handle, u32(vk.PipelineStageFlagBits.transfer), + u32(vk.PipelineStageFlagBits.bottom_of_pipe), 0, 0, unsafe { nil }, 0, unsafe { nil }, + 1, &after) + command.end()! + fence.reset()! + device.queue.submit([command], vke.SubmitOptions{ + wait_semaphores: [ready.handle] + wait_stage_masks: [ + u32(vk.PipelineStageFlagBits.transfer), + ] + signal_semaphores: [done[index].handle] + fence: fence.handle + })! + status := device.queue.present(swapchain, index, [done[index].handle])! + if status == .error_out_of_date_khr { return } + } + device.wait_idle()! +} diff --git a/examples/presentation/v.mod b/examples/presentation/v.mod new file mode 100644 index 0000000..3f3bacc --- /dev/null +++ b/examples/presentation/v.mod @@ -0,0 +1,4 @@ +Module { + name: 'vulkan_presentation_example' + dependencies: ['antono2.glfw@v2.0.1'] +} diff --git a/scripts/check_docs.py b/scripts/check_docs.py new file mode 100644 index 0000000..77201ca --- /dev/null +++ b/scripts/check_docs.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +"""Ensure first-use snippets are exact copies of the examples exercised by CI.""" +from pathlib import Path +import re +root = Path(__file__).resolve().parents[1] +readme = root / 'packaging/published_README.md' +if not readme.exists(): + readme = root / 'README.md' +blocks = re.findall(r'\s*```v\n(.*?)\n```', readme.read_text(), re.S) +assert blocks, 'README needs a tested first-use example' +for path, snippet in blocks: + assert (root / path).read_text().strip() == snippet.strip(), f'Readme example drift: {path}' +print(f'{len(blocks)} README example(s) match tested source.') diff --git a/setup.vsh b/setup.vsh index 9f80013..295ab0b 100644 --- a/setup.vsh +++ b/setup.vsh @@ -5,7 +5,7 @@ // read-only diagnostic pass suitable for support requests and CI. import os -const usage = 'Usage: v run setup.vsh [--install|--check]\n\n' + +const usage = 'Usage: v run setup.vsh [--install|--check|--probe]\n\n' + ' --install Install native Vulkan tools and this V module (default).\n' + ' --check Report whether the compiler, bundled headers, loader and device work.\n' fn command_exists(name string) bool { @@ -263,14 +263,34 @@ fn check() bool { return ok } +fn probe() ! { + root := os.dir(os.real_path(@FILE)) + mut example := os.join_path(root, 'examples', 'getting_started') + if !os.is_dir(example) && os.base(root) == 'packaging' { + example = os.join_path(os.dir(root), 'examples', 'getting_started') + } + if !os.is_dir(example) { return error('first-use example is missing from this installation') } + run('v run ${os.quoted_path(example)}')! + println('[ok] Binding runtime probe completed successfully.') +} + fn main() { if os.args.len > 2 - || (os.args.len == 2 && os.args[1] !in ['--install', '--check', '-h', '--help']) { - eprintln(usage) + || (os.args.len == 2 && os.args[1] !in ['--install', '--check', '--probe', '-h', '--help']) { + eprintln(usage + + '\n --probe Compile and execute the first-use example; does not install packages.') exit(2) } if os.args.len == 2 && os.args[1] in ['-h', '--help'] { - println(usage) + println(usage + + '\n --probe Compile and execute the first-use example; does not install packages.') + return + } + if os.args.len == 2 && os.args[1] == '--probe' { + probe() or { + eprintln(err) + exit(1) + } return } install := os.args.len == 1 || os.args[1] == '--install' @@ -279,7 +299,8 @@ fn main() { eprintln('Setup failed: ${err}') exit(1) } - if command_exists('v') { + if command_exists('v') + && !os.is_file(os.join_path(os.vmodules_dir(), 'antono2', 'vulkan', 'v.mod')) { run('v install antono2.vulkan') or { eprintln('Could not install the V module: ${err}') exit(1) From c1f54d44ec1c5dbce63002f3adc66d5004d27456 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 12:56:46 +0200 Subject: [PATCH 2/3] Clarify Vulkan example constants and API-specific values --- examples/conveniences/main.v | 39 ++++++++++++++++++----------- examples/ergonomic_lifecycle/main.v | 22 ++++++++++------ examples/presentation/main.v | 34 +++++++++++++++++++------ 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/examples/conveniences/main.v b/examples/conveniences/main.v index 1625133..8381864 100644 --- a/examples/conveniences/main.v +++ b/examples/conveniences/main.v @@ -12,7 +12,12 @@ fn main() { } fn run() ! { + uniform_binding := u32(0) + set_count := u32(2) // Two sets, each with one uniform-buffer descriptor. + uniform_bytes := u64(16 * sizeof(f32)) // Space for a 4x4 float matrix. + probe_timeout_ns := u64(10_000_000_000) // Ten seconds; Vulkan timeouts use nanoseconds. mut instance := vke.new_instance_with_options(vke.InstanceOptions{ + // Device UUID and external-resource capability queries require Vulkan 1.1. api_version: vk.api_version_1_1 extensions: [ 'VK_EXT_debug_utils', @@ -23,7 +28,7 @@ fn run() ! { if devices.len == 0 { return error('no Vulkan device') } physical := devices[0] identity := instance.device_identity(physical)! - assert identity.device_uuid.len == 16 + assert identity.device_uuid.len == 16 // Vulkan device UUIDs contain 16 bytes. $if linux { memory := instance.external_buffer_properties(physical, u32(vk.BufferUsageFlagBits.vertex_buffer), 0, .opaque_fd)! @@ -43,50 +48,53 @@ fn run() ! { assert support.supported mut layout := device.new_descriptor_layout([ vk.DescriptorSetLayoutBinding{ - binding: 0 + binding: uniform_binding descriptorType: .uniform_buffer descriptorCount: 1 stageFlags: u32(vk.ShaderStageFlagBits.vertex) }, ])! defer { layout.destroy() } - mut pool := device.new_descriptor_pool(2, [ - vk.DescriptorPoolSize{ type: .uniform_buffer, descriptorCount: 2 }, + mut pool := device.new_descriptor_pool(set_count, [ + vk.DescriptorPoolSize{ type: .uniform_buffer, descriptorCount: set_count }, ])! defer { pool.destroy() } - mut buffer := device.new_buffer(64, u32(vk.BufferUsageFlagBits.uniform_buffer), + mut buffer := device.new_buffer(uniform_bytes, u32(vk.BufferUsageFlagBits.uniform_buffer), u32(vk.MemoryPropertyFlagBits.host_visible) | u32(vk.MemoryPropertyFlagBits.host_coherent))! defer { buffer.destroy() } sets := pool.allocate([layout, layout])! for set in sets { - set.write_buffers(0, 0, [ - vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + set.write_buffers(uniform_binding, 0, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: uniform_bytes }, ])! } mut rejected := false - sets[0].write_buffers(0, 1, [ - vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + // Array element 1 is invalid: this binding contains only element 0. + sets[0].write_buffers(uniform_binding, 1, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: uniform_bytes }, ]) or { rejected = true } assert rejected pool.reset()! rejected = false - sets[0].write_buffers(0, 0, [ - vk.DescriptorBufferInfo{ buffer: buffer.handle, range: 64 }, + sets[0].write_buffers(uniform_binding, 0, [ + vk.DescriptorBufferInfo{ buffer: buffer.handle, range: uniform_bytes }, ]) or { rejected = true } assert rejected assert pool.allocate([layout])!.len == 1 mut cache := device.new_pipeline_cache([])! defer { cache.destroy() } data := cache.bytes()! - assert data.len >= 32 + assert data.len >= 32 // Version-one cache headers occupy at least 32 bytes. mut restored := device.new_pipeline_cache(data)! defer { restored.destroy() } mut corrupt := data.clone() - corrupt[8] ^= 255 + // Byte 8 begins vendorID in a version-one header; flip it to test rejection. + corrupt[8] ^= 0xff rejected = false device.new_pipeline_cache(corrupt) or { rejected = true } assert rejected labels := device.debug_labels()! + // Two timestamps bracket the command buffer: start at 0, end at 1. mut queries := device.new_timestamp_queries(2)! defer { queries.destroy() } mut commands := device.new_command_pool(0)! @@ -97,13 +105,14 @@ fn run() ! { defer { device.wait_idle() or {} } command.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))! queries.reset(command.handle)! - labels.begin_label(command.handle, 'timestamp probe', [f32(0.1), 0.4, 0.8, 1.0]!) + label_rgba := [f32(0.1), 0.4, 0.8, 1.0]! // Opaque blue in GPU debugging tools. + labels.begin_label(command.handle, 'timestamp probe', label_rgba) queries.write(command.handle, .top_of_pipe, 0)! queries.write(command.handle, .bottom_of_pipe, 1)! labels.end_label(command.handle) command.end()! device.queue.submit([command], vke.SubmitOptions{ fence: completed.handle })! - vke.require_success(completed.wait(10_000_000_000)!, 'wait timestamp probe')! + vke.require_success(completed.wait(probe_timeout_ns)!, 'wait timestamp probe')! samples := queries.read()! assert samples.len == 2 && samples[0].available && samples[1].available assert queries.elapsed_ns(samples[0], samples[1])! >= 0 diff --git a/examples/ergonomic_lifecycle/main.v b/examples/ergonomic_lifecycle/main.v index 7c5ffb0..916fd17 100644 --- a/examples/ergonomic_lifecycle/main.v +++ b/examples/ergonomic_lifecycle/main.v @@ -24,6 +24,7 @@ fn main() { queue_family := physical_device.find_queue_family(required_queue_flags) or { panic('no graphics/compute queue family was found') } + // Request queue index 1 when available to exercise nonzero queue selection. if queue_family.properties.queueCount >= 2 { mut legacy_device := physical_device.new_device_with_options(vke.DeviceOptions{ queue_family: queue_family @@ -54,26 +55,30 @@ fn main() { memory_properties := u32(vk.MemoryPropertyFlagBits.device_local) buffer_usage := u32(vk.BufferUsageFlagBits.vertex_buffer) | u32(vk.BufferUsageFlagBits.transfer_dst) - mut buffer := device.new_buffer(4096, buffer_usage, memory_properties) or { panic(err) } + vertex_capacity_bytes := u64(4096) // Small 4 KiB allocation for the lifecycle check. + mut buffer := device.new_buffer(vertex_capacity_bytes, buffer_usage, memory_properties) or { + panic(err) + } defer { buffer.destroy() } host_memory_properties := u32(vk.MemoryPropertyFlagBits.host_visible) | u32(vk.MemoryPropertyFlagBits.host_coherent) - mut host_buffer := device.new_buffer(64, u32(vk.BufferUsageFlagBits.transfer_src), host_memory_properties) or { - panic(err) - } + host_capacity_bytes := u64(64) // Enough room for both four-byte writes below. + mut host_buffer := device.new_buffer(host_capacity_bytes, + u32(vk.BufferUsageFlagBits.transfer_src), host_memory_properties) or { panic(err) } defer { host_buffer.destroy() } host_buffer.upload_bytes(0, [u8(1), 2, 3, 4]) or { panic(err) } + // Map four bytes after the first write; flags 0 selects default mapping behavior. mut mapped := host_buffer.map(4, 4, 0) or { panic(err) } mapped.write_bytes(0, [u8(5), 6, 7, 8]) or { panic(err) } mapped.unmap() image_usage := u32(vk.ImageUsageFlagBits.sampled) | u32(vk.ImageUsageFlagBits.transfer_dst) - mut image := device.new_image_2d(64, 64, .r8g8b8a8_unorm, .optimal, image_usage, memory_properties) or { - panic(err) - } + image_side_pixels := u32(64) // Small square texture used only for a layout transition. + mut image := device.new_image_2d(image_side_pixels, image_side_pixels, .r8g8b8a8_unorm, + .optimal, image_usage, memory_properties) or { panic(err) } defer { image.destroy() } @@ -114,7 +119,8 @@ fn main() { work_queue.submit([command_buffer], vke.SubmitOptions{ fence: fence.handle }) or { panic(err) } - if fence.wait(5_000_000_000) or { panic(err) } != .success { + submission_timeout_ns := u64(5_000_000_000) // Five seconds, expressed in nanoseconds. + if fence.wait(submission_timeout_ns) or { panic(err) } != .success { panic('queue submission did not complete before the smoke-test timeout') } work_queue.wait_idle() or { panic(err) } diff --git a/examples/presentation/main.v b/examples/presentation/main.v index 668b8d5..263bfef 100644 --- a/examples/presentation/main.v +++ b/examples/presentation/main.v @@ -17,7 +17,11 @@ fn run() ! { if !glfw.initialize() { return error('GLFW initialization failed') } defer { glfw.terminate() } glfw.window_hint(glfw.client_api, glfw.no_api) - window := glfw.create_window(640, 360, 'Vulkan presentation', unsafe { nil }, unsafe { nil }) + // A small 16:9 window, enlarged below to exercise swapchain recreation. + initial_width := 640 + initial_height := 360 + window := glfw.create_window(initial_width, initial_height, 'Vulkan presentation', + unsafe { nil }, unsafe { nil }) if isnil(window) { return error('window creation failed') } defer { glfw.destroy_window(window) } mut count := u32(0) @@ -44,13 +48,18 @@ fn run() ! { })! defer { device.destroy() } render_generation(physical, device, surface, window)! - C.glfwSetWindowSize(window, 800, 450) + resized_width := 800 + resized_height := 450 + C.glfwSetWindowSize(window, resized_width, resized_height) glfw.poll_events() render_generation(physical, device, surface, window)! println('Presented two swapchain generations, including resize.') } fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surface vk.SurfaceKHR, window &glfw.Window) ! { + frames_per_generation := 6 // Keep the example finite while reusing synchronization objects. + frame_timeout_ns := u64(10_000_000_000) // Ten seconds for submitted GPU work. + acquire_timeout_ns := u64(1_000_000_000) // One second to obtain a presentation image. support := physical.surface_support(surface)! required_usage := u32(vk.ImageUsageFlagBits.transfer_dst) | u32(vk.ImageUsageFlagBits.color_attachment) if support.capabilities.supportedUsageFlags & required_usage != required_usage { @@ -69,20 +78,23 @@ fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surfa width: u32(width) height: u32(height) }) + // Request one image above the surface minimum, clamped to its supported limit. + extra_swapchain_images := u32(1) info := vk.SwapchainCreateInfoKHR{ surface: surface - minImageCount: vke.select_surface_image_count(support.capabilities, 1) + minImageCount: vke.select_surface_image_count(support.capabilities, + extra_swapchain_images) imageFormat: format.format imageColorSpace: format.colorSpace imageExtent: extent - imageArrayLayers: 1 + imageArrayLayers: 1 // A single 2D presentation layer, not a stereo array. imageUsage: required_usage imageSharingMode: .exclusive preTransform: support.capabilities.currentTransform compositeAlpha: vke.select_composite_alpha(support.capabilities.supportedCompositeAlpha, [ .opaque, ])! - presentMode: .fifo + presentMode: .fifo // Portable, display-synchronized presentation. clipped: vk._true } mut swapchain := device.new_swapchain(&info)! @@ -101,14 +113,15 @@ fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surfa for _ in swapchain.images { done << device.new_semaphore()! } + // Start signaled so the first frame does not wait for a nonexistent submission. mut fence := device.new_fence(true)! defer { fence.destroy() } // Also complete submitted work before cleanup on an error path. defer { device.wait_idle() or {} } - for _ in 0 .. 6 { + for _ in 0 .. frames_per_generation { glfw.poll_events() - vke.require_success(fence.wait(10_000_000_000)!, 'wait frame')! - acquired := swapchain.acquire(1_000_000_000, ready.handle, unsafe { nil })! + vke.require_success(fence.wait(frame_timeout_ns)!, 'wait frame')! + acquired := swapchain.acquire(acquire_timeout_ns, ready.handle, unsafe { nil })! index := acquired.image_index or { if acquired.status == .error_out_of_date_khr { return } return error('image acquisition did not complete: ${acquired.status}') @@ -116,11 +129,13 @@ fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surfa command.reset(0)! command.begin(u32(vk.CommandBufferUsageFlagBits.one_time_submit))! + // Clear the only mip level and array layer of this swapchain image. range := vk.ImageSubresourceRange{ aspectMask: u32(vk.ImageAspectFlagBits.color) levelCount: 1 layerCount: 1 } + // Undefined discards old pixels: each frame replaces the entire image. before := vk.ImageMemoryBarrier{ oldLayout: .undefined newLayout: .transfer_dst_optimal @@ -130,9 +145,11 @@ fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surfa image: swapchain.images[index] subresourceRange: range } + // No memory/buffer barriers; the final count of 1 refers to this image barrier. vk.cmd_pipeline_barrier(command.handle, u32(vk.PipelineStageFlagBits.top_of_pipe), u32(vk.PipelineStageFlagBits.transfer), 0, 0, unsafe { nil }, 0, unsafe { nil }, 1, &before) + // Opaque dark blue, expressed as linear RGBA components in [0, 1]. color := vk.ClearColorValue{ float32: [f32(0.05), 0.15, 0.35, 1.0]! } @@ -147,6 +164,7 @@ fn render_generation(physical vke.PhysicalDevice, device &vke.OwnedDevice, surfa image: swapchain.images[index] subresourceRange: range } + // No memory/buffer barriers; the final count of 1 refers to this image barrier. vk.cmd_pipeline_barrier(command.handle, u32(vk.PipelineStageFlagBits.transfer), u32(vk.PipelineStageFlagBits.bottom_of_pipe), 0, 0, unsafe { nil }, 0, unsafe { nil }, 1, &after) From cf65c6645a07170cb444db7b0aeb93d175a5d3f8 Mon Sep 17 00:00:00 2001 From: antono2 Date: Wed, 14 Oct 2026 13:15:13 +0200 Subject: [PATCH 3/3] Set native GLFW paths for the presentation CI example --- .github/workflows/generated-bindings-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/generated-bindings-ci.yml b/.github/workflows/generated-bindings-ci.yml index 2fe8a23..7a08802 100644 --- a/.github/workflows/generated-bindings-ci.yml +++ b/.github/workflows/generated-bindings-ci.yml @@ -307,6 +307,9 @@ jobs: 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: |