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
9 changes: 9 additions & 0 deletions .github/workflows/generated-bindings-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,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 "$module_root/c/"
cp ergonomic/ergonomic.v ergonomic/discovery.v ergonomic/configuration.v \
ergonomic/presentation.v ergonomic/resources.v \
"$module_root/ergonomic/"
echo "VMODULES=$RUNNER_TEMP/vmodules" >> "$GITHUB_ENV"
- name: Compile smoke test and ergonomic unit tests
Expand All @@ -175,6 +176,14 @@ jobs:
v -cc "${{ matrix.compiler }}" run generator/test
fi
v -cc "${{ matrix.compiler }}" test ergonomic
- name: Validate TinyCC loader and ergonomic API
if: runner.os == 'Linux' && matrix.v-channel == 'stable'
shell: bash
run: |
set -euo pipefail
v -cc tcc test ergonomic
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
v -cc tcc run examples/ergonomic_lifecycle
- name: Report V master compatibility warning
if: always() && matrix.v-channel == 'master' && (steps.checkout_v_master.outcome == 'failure' || steps.setup_v_master.outcome == 'failure' || steps.compile.outcome == 'failure')
run: echo "::warning::The live V master compatibility lane failed; stable V 0.5.2 remains authoritative."
Expand Down
23 changes: 22 additions & 1 deletion API_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ println('${physical_device.name()}: queue family ${device.queue.family_index}')

`OwnedImageView` creates an identity-swizzled 2D view using the image's format and explicit aspect mask. It exposes the raw view and parent-image handles, view type, format, and complete subresource range. Every view must be destroyed before its image.

`PhysicalDevice.surface_support()` owns the three presentation queries needed
for swapchain negotiation. The accompanying selection helpers keep policy
explicit through ordered preference arrays while correctly handling Vulkan's
undefined-format sentinel, FIFO fallback, fixed versus variable extents,
bounded image counts, and composite-alpha masks.

`OwnedBuffer.map()` validates the range and host-visible property before
returning a borrowed `MappedBufferMemory`. Its `write_bytes()` helper is
restricted to host-coherent memory so a successful return means the bytes are
visible without a separate flush. Use the public pointer plus raw flush and
invalidate commands for non-coherent memory. `upload_bytes()` provides the
common one-shot map, copy, and unmap sequence.

`OwnedShaderModule` validates SPIR-V size, alignment, and magic before calling
Vulkan. Byte input is copied to aligned words for the duration of creation.
Shader modules and mappings must be destroyed or unmapped before their parent
resource or device. 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.

`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. Custom allocation callbacks and concurrent-sharing resources remain in the raw layer. A future allocator-aware owning wrapper must retain the allocator used at creation so the same callbacks are supplied during destruction.
Expand All @@ -125,4 +144,6 @@ println('${physical_device.name()}: queue family ${device.queue.family_index}')
5. Owned 2D images with explicit parent ownership and destruction ordering. (Implemented.)
6. Checked primary command-buffer queue submission with explicit synchronization. (Implemented.)
7. Owned 2D image views and focused synchronization-1 layout-transition recording. (Implemented.)
8. Builders only where they eliminate unsafe pointer/count bookkeeping; Vulkan synchronization and memory choices should remain explicit.
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.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ This changelog tracks the semantic version of the V module in `v.mod`.
Generated binding snapshots continue to use the Vulkan registry version stored
in `VERSION`.

## Unreleased

### Added

- Owned SPIR-V shader modules with byte-count, alignment, and magic validation.
- Checked host-visible buffer mappings, persistent coherent byte writes, and
one-shot map/copy/unmap uploads.
- Owned surface-support discovery plus reusable format, present-mode, extent,
image-count, and composite-alpha selection helpers.
- Typed device and queue idle waits.
- TinyCC loader and lifecycle coverage on Linux.

### Fixed

- Use a deep-bound Vulkan loader handle under TinyCC so Volk's exported global
dispatch variables cannot shadow loader entry points.

## 1.6.0 - 2026-09-09

### Added
Expand Down
12 changes: 9 additions & 3 deletions OWNERSHIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ in the reverse order of creation.
V structs are values and can be copied. A copied `Instance`, `Device`,
`CommandPool`, `OwnedBuffer`, `OwnedImage`, or `OwnedImageView` refers to the
same native allocation; destroying more than one copy is invalid. `Fence`,
`Semaphore`, and `PrimaryCommandBuffer` clear their handle when a mutable value
is destroyed or freed, but a previously made copy is still independent and can
retain the old handle.
`Semaphore`, `PrimaryCommandBuffer`, `MappedBufferMemory`, and
`OwnedShaderModule` clear their handle or pointer when a mutable value is
destroyed, freed, or unmapped, but a previously made copy is still independent
and can retain the old value.

A mapped range borrows its `OwnedBuffer`; unmap it before destroying the buffer.
An `OwnedShaderModule` must be destroyed before its parent device. A successful
`new_shader_module*` call copies or consumes SPIR-V only during creation, so the
input slice need not outlive the call.

Until a breaking ownership redesign, follow these rules:

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ multi-queue logical-device ownership. It also provides explicit memory-type
selection and owned buffer/device-memory allocation, owned command pools and
primary command-buffer lifecycle helpers, synchronization objects, checked queue
submission, owned 2D images and views, and explicit image-layout transition
recording without modifying generated files.
recording without modifying generated files. Presentation helpers collect and
select surface formats, present modes, extents, image counts, and composite
alpha modes. Host-visible buffers support checked persistent mappings and
coherent uploads, while owned shader modules accept validated SPIR-V words or
bytes.
See [the ergonomic API design](API_DESIGN.md).

Instance and device configuration can validate requested names before Vulkan is
Expand Down
26 changes: 26 additions & 0 deletions c/loader_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,32 @@
// Keep initialization and validation in the translation unit that owns Volk's
// global dispatch table. Some Linux loader/SDK combinations report success from
// volkInitialize() while leaving vkCreateInstance unresolved.
#if defined(__TINYC__) && defined(__linux__) && !defined(RTLD_DEEPBIND)
#define RTLD_DEEPBIND 0x00008
#endif

static VkResult v_vulkan_initialize_loader(void) {
#if defined(__TINYC__) && defined(__linux__) && defined(RTLD_DEEPBIND)
// TinyCC exports Volk's global function-pointer variables from the main
// executable. Without deep binding, the Vulkan loader can resolve names
// such as vkCreateInstance back to those variables instead of its own
// entry points. Load the ICD-facing loader in its own symbol scope and give
// Volk the unambiguous vkGetInstanceProcAddr address.
void *loader = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
if (loader == NULL) {
loader = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
}
if (loader == NULL) {
return VK_ERROR_INITIALIZATION_FAILED;
}
PFN_vkGetInstanceProcAddr get_instance_proc_addr =
(PFN_vkGetInstanceProcAddr)dlsym(loader, "vkGetInstanceProcAddr");
if (get_instance_proc_addr == NULL) {
return VK_ERROR_INITIALIZATION_FAILED;
}
volkInitializeCustom(get_instance_proc_addr);
return vkCreateInstance != NULL ? VK_SUCCESS : VK_ERROR_INITIALIZATION_FAILED;
#else
VkResult result = volkInitialize();
if (result != VK_SUCCESS || vkCreateInstance != NULL) {
return result;
Expand All @@ -20,4 +45,5 @@ static VkResult v_vulkan_initialize_loader(void) {
#endif

return vkCreateInstance != NULL ? VK_SUCCESS : VK_ERROR_INITIALIZATION_FAILED;
#endif
}
2 changes: 1 addition & 1 deletion ergonomic/configuration.v
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn new_instance_with_options(options InstanceOptions) !Instance {
for extension in options.extensions {
extension_pointers << extension.str
}
application := vk.ApplicationInfo{
mut application := vk.ApplicationInfo{
pApplicationName: options.application_name.str
applicationVersion: options.application_version
pEngineName: options.engine_name.str
Expand Down
5 changes: 4 additions & 1 deletion ergonomic/ergonomic.v
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ pub:
size vk.DeviceSize
allocation_size vk.DeviceSize
memory_type_index u32
memory_properties vk.MemoryPropertyFlags
}

// new_buffer creates an exclusive-sharing buffer, selects a compatible memory
Expand All @@ -344,7 +345,8 @@ pub fn (device Device) new_buffer(size vk.DeviceSize, usage vk.BufferUsageFlags,

mut requirements := vk.MemoryRequirements{}
vk.get_buffer_memory_requirements(device.handle, handle, mut requirements)
memory_type_index := device.physical_device.find_memory_type(requirements.memoryTypeBits, required_memory_properties) or {
physical_memory := device.physical_device.memory_properties()
memory_type_index := select_memory_type(physical_memory, requirements.memoryTypeBits, required_memory_properties) or {
vk.destroy_buffer(device.handle, handle, unsafe { nil })
return error('no compatible memory type for buffer')
}
Expand Down Expand Up @@ -372,6 +374,7 @@ pub fn (device Device) new_buffer(size vk.DeviceSize, usage vk.BufferUsageFlags,
size: size
allocation_size: requirements.size
memory_type_index: memory_type_index
memory_properties: physical_memory.memoryTypes[memory_type_index].propertyFlags
}
}

Expand Down
175 changes: 175 additions & 0 deletions ergonomic/presentation.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
module ergonomic

import antono2.vulkan as vk

// SurfaceSupport is an owned snapshot of the presentation capabilities used
// when choosing a swapchain configuration.
pub struct SurfaceSupport {
pub:
capabilities vk.SurfaceCapabilitiesKHR
formats []vk.SurfaceFormatKHR
present_modes []vk.PresentModeKHR
}

// surface_capabilities queries the fixed and variable limits for surface.
pub fn (device PhysicalDevice) surface_capabilities(surface vk.SurfaceKHR) !vk.SurfaceCapabilitiesKHR {
if isnil(surface) {
return error('presentation surface must not be null')
}
mut capabilities := vk.SurfaceCapabilitiesKHR{}
require_success(vk.get_physical_device_surface_capabilities_khr(device.handle, surface, mut capabilities), 'vkGetPhysicalDeviceSurfaceCapabilitiesKHR')!
return capabilities
}

// surface_formats performs Vulkan's count/fill enumeration and retries if the
// surface changes while it is being queried.
pub fn (device PhysicalDevice) surface_formats(surface vk.SurfaceKHR) ![]vk.SurfaceFormatKHR {
if isnil(surface) {
return error('presentation surface must not be null')
}
for {
mut count := u32(0)
mut no_formats := unsafe { nil }
result := vk.get_physical_device_surface_formats_khr(device.handle, surface, &count, mut no_formats)
if result == .incomplete {
continue
}
require_success(result, 'vkGetPhysicalDeviceSurfaceFormatsKHR(count)')!
if count == 0 {
return []vk.SurfaceFormatKHR{}
}
mut formats := []vk.SurfaceFormatKHR{len: int(count)}
fill_result := vk.get_physical_device_surface_formats_khr(device.handle, surface, &count, mut formats[0])
if fill_result == .incomplete {
continue
}
require_success(fill_result, 'vkGetPhysicalDeviceSurfaceFormatsKHR(values)')!
return formats[..int(count)].clone()
}
return []vk.SurfaceFormatKHR{}
}

// surface_present_modes performs Vulkan's count/fill enumeration and retries
// if the surface changes while it is being queried.
pub fn (device PhysicalDevice) surface_present_modes(surface vk.SurfaceKHR) ![]vk.PresentModeKHR {
if isnil(surface) {
return error('presentation surface must not be null')
}
for {
mut count := u32(0)
result := vk.get_physical_device_surface_present_modes_khr(device.handle, surface, &count, unsafe { nil })
if result == .incomplete {
continue
}
require_success(result, 'vkGetPhysicalDeviceSurfacePresentModesKHR(count)')!
if count == 0 {
return []vk.PresentModeKHR{}
}
mut modes := []vk.PresentModeKHR{len: int(count)}
fill_result := vk.get_physical_device_surface_present_modes_khr(device.handle, surface, &count, modes.data)
if fill_result == .incomplete {
continue
}
require_success(fill_result, 'vkGetPhysicalDeviceSurfacePresentModesKHR(values)')!
return modes[..int(count)].clone()
}
return []vk.PresentModeKHR{}
}

// surface_support obtains the capabilities, formats, and present modes needed
// to configure or recreate a swapchain.
pub fn (device PhysicalDevice) surface_support(surface vk.SurfaceKHR) !SurfaceSupport {
return SurfaceSupport{
capabilities: device.surface_capabilities(surface)!
formats: device.surface_formats(surface)!
present_modes: device.surface_present_modes(surface)!
}
}

// select_surface_format picks the first available exact match in preferred
// order, falling back to the first advertised format. A lone undefined format
// means the surface accepts the caller's first preference.
pub fn select_surface_format(available []vk.SurfaceFormatKHR,
preferred []vk.SurfaceFormatKHR) !vk.SurfaceFormatKHR {
if available.len == 0 {
return error('surface exposes no formats')
}
if available.len == 1 && available[0].format == .undefined && preferred.len > 0 {
return preferred[0]
}
for wanted in preferred {
for candidate in available {
if candidate.format == wanted.format && candidate.colorSpace == wanted.colorSpace {
return candidate
}
}
}
return available[0]
}

// select_present_mode picks the first available mode in preferred order. FIFO
// is the portable fallback; malformed empty input is rejected.
pub fn select_present_mode(available []vk.PresentModeKHR,
preferred []vk.PresentModeKHR) !vk.PresentModeKHR {
if available.len == 0 {
return error('surface exposes no present modes')
}
for wanted in preferred {
if wanted in available {
return wanted
}
}
if vk.PresentModeKHR.fifo in available {
return .fifo
}
return available[0]
}

// select_surface_extent returns the surface's fixed extent when one is set,
// otherwise clamps the requested framebuffer extent to the advertised range.
pub fn select_surface_extent(capabilities vk.SurfaceCapabilitiesKHR,
requested vk.Extent2D) vk.Extent2D {
if capabilities.currentExtent.width != max_u32 {
return capabilities.currentExtent
}
return vk.Extent2D{
width: clamp_u32(requested.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width)
height: clamp_u32(requested.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)
}
}

fn clamp_u32(value u32, minimum u32, maximum u32) u32 {
return if value < minimum {
minimum
} else if value > maximum {
maximum
} else {
value
}
}

// select_surface_image_count requests minImageCount plus additional_images and
// clamps the result to maxImageCount when the surface defines a maximum.
pub fn select_surface_image_count(capabilities vk.SurfaceCapabilitiesKHR,
additional_images u32) u32 {
mut count := if additional_images > max_u32 - capabilities.minImageCount {
max_u32
} else {
capabilities.minImageCount + additional_images
}
if capabilities.maxImageCount > 0 && count > capabilities.maxImageCount {
count = capabilities.maxImageCount
}
return count
}

// select_composite_alpha picks the first supported mode in preferred order.
pub fn select_composite_alpha(supported vk.CompositeAlphaFlagsKHR,
preferred []vk.CompositeAlphaFlagBitsKHR) !vk.CompositeAlphaFlagBitsKHR {
for wanted in preferred {
if supported & u32(wanted) != 0 {
return wanted
}
}
return error('surface exposes none of the preferred composite-alpha modes')
}
Loading