Skip to content

Commit

Permalink
Ray tracing fixes (#2625)
Browse files Browse the repository at this point in the history
* Fix shader files' inconsistent file extensions

* Fix shader files' inconsistent formatting

* Fix examples' code inconsistencies

* Fix library code inconsistencies

* Fix `get_ray_tracing_pipeline_shader_group_handles` scoping
marc0246 authored Jan 29, 2025
1 parent 6ae10df commit 938de01
Showing 22 changed files with 641 additions and 633 deletions.
157 changes: 78 additions & 79 deletions examples/ray-tracing-auto/main.rs
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ use vulkano::{
acquire_next_image, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo,
},
sync::{self, GpuFuture},
Version, VulkanLibrary,
Validated, Version, VulkanError, VulkanLibrary,
};
use winit::{
application::ApplicationHandler,
@@ -46,8 +46,8 @@ struct App {
instance: Arc<Instance>,
device: Arc<Device>,
queue: Arc<Queue>,
rcx: Option<RenderContext>,
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
rcx: Option<RenderContext>,
}

pub struct RenderContext {
@@ -84,11 +84,21 @@ impl App {
khr_acceleration_structure: true,
..DeviceExtensions::empty()
};
let device_features = DeviceFeatures {
acceleration_structure: true,
ray_tracing_pipeline: true,
buffer_device_address: true,
synchronization2: true,
..Default::default()
};
let (physical_device, queue_family_index) = instance
.enumerate_physical_devices()
.unwrap()
.filter(|p| p.api_version() >= Version::V1_3)
.filter(|p| p.supported_extensions().contains(&device_extensions))
.filter(|p| {
p.supported_extensions().contains(&device_extensions)
&& p.supported_features().contains(&device_features)
})
.filter_map(|p| {
p.queue_family_properties()
.iter()
@@ -110,6 +120,12 @@ impl App {
})
.unwrap();

println!(
"Using device: {} (type: {:?})",
physical_device.properties().device_name,
physical_device.properties().device_type,
);

let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
@@ -118,13 +134,7 @@ impl App {
queue_family_index,
..Default::default()
}],
enabled_features: DeviceFeatures {
acceleration_structure: true,
ray_tracing_pipeline: true,
buffer_device_address: true,
synchronization2: true,
..Default::default()
},
enabled_features: device_features,
..Default::default()
},
)
@@ -141,8 +151,8 @@ impl App {
instance,
device,
queue,
rcx: None,
command_buffer_allocator,
rcx: None,
}
}
}
@@ -155,55 +165,43 @@ impl ApplicationHandler for App {
.unwrap(),
);
let surface = Surface::from_window(self.instance.clone(), window.clone()).unwrap();

let physical_device = self.device.physical_device();
let supported_surface_formats = physical_device
.surface_formats(&surface, Default::default())
.unwrap();

// For each supported format, check if it is supported for storage images
let supported_storage_formats = supported_surface_formats
.into_iter()
.filter(|(format, _)| {
physical_device
.image_format_properties(ImageFormatInfo {
format: *format,
usage: ImageUsage::STORAGE,
..Default::default()
})
.unwrap()
.is_some()
})
.collect::<Vec<_>>();

println!(
"Using device: {} (type: {:?})",
physical_device.properties().device_name,
physical_device.properties().device_type,
);
let window_size = window.inner_size();

let (swapchain, images) = {
let surface_capabilities = self
.device
.physical_device()
.surface_capabilities(&surface, Default::default())
.unwrap();

let (swapchain_format, swapchain_color_space) = supported_storage_formats
.first()
.map(|(format, color_space)| (*format, *color_space))
let (image_format, image_color_space) = self
.device
.physical_device()
.surface_formats(&surface, Default::default())
.unwrap()
.into_iter()
.find(|(format, _)| {
self.device
.physical_device()
.image_format_properties(ImageFormatInfo {
format: *format,
usage: ImageUsage::STORAGE,
..Default::default()
})
.unwrap()
.is_some()
})
.unwrap();

Swapchain::new(
self.device.clone(),
surface.clone(),
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count.max(2),
image_format: swapchain_format,
image_color_space: swapchain_color_space,
image_extent: window.inner_size().into(),
// To simplify the example, we will directly write to the swapchain images
// from the ray tracing shader. This requires the images to support storage
// usage.
image_format,
image_color_space,
image_extent: window_size.into(),
// To simplify the example, we will directly write to the swapchain images from
// the ray tracing shader. This requires the images to support storage usage.
image_usage: ImageUsage::STORAGE,
composite_alpha: surface_capabilities
.supported_composite_alpha
@@ -273,13 +271,12 @@ impl ApplicationHandler for App {
)
.unwrap();

let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(self.device.clone()));
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
self.device.clone(),
Default::default(),
));

let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(self.device.clone()));

let scene = Scene::new(
self,
&images,
@@ -288,12 +285,15 @@ impl ApplicationHandler for App {
memory_allocator.clone(),
self.command_buffer_allocator.clone(),
);

let previous_frame_end = Some(sync::now(self.device.clone()).boxed());

self.rcx = Some(RenderContext {
window,
swapchain,
recreate_swapchain: false,
previous_frame_end: None,
scene,
previous_frame_end,
});
}

@@ -319,37 +319,35 @@ impl ApplicationHandler for App {
return;
}

// Cleanup previous frame
if let Some(previous_frame_end) = rcx.previous_frame_end.as_mut() {
previous_frame_end.cleanup_finished();
}
rcx.previous_frame_end.as_mut().unwrap().cleanup_finished();

// Recreate swapchain if needed
if rcx.recreate_swapchain {
let (new_swapchain, new_images) =
match rcx.swapchain.recreate(SwapchainCreateInfo {
let (new_swapchain, new_images) = rcx
.swapchain
.recreate(SwapchainCreateInfo {
image_extent: window_size.into(),
..rcx.swapchain.create_info()
}) {
Ok(r) => r,
Err(e) => panic!("Failed to recreate swapchain: {e:?}"),
};
})
.expect("failed to recreate swapchain");

rcx.swapchain = new_swapchain;
rcx.scene.handle_resize(&new_images);
rcx.recreate_swapchain = false;
}

// Acquire next image
let (image_index, suboptimal, acquire_future) =
match acquire_next_image(rcx.swapchain.clone(), None) {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to acquire next image: {e:?}");
rcx.recreate_swapchain = true;
return;
}
};
let (image_index, suboptimal, acquire_future) = match acquire_next_image(
rcx.swapchain.clone(),
None,
)
.map_err(Validated::unwrap)
{
Ok(r) => r,
Err(VulkanError::OutOfDate) => {
rcx.recreate_swapchain = true;
return;
}
Err(e) => panic!("failed to acquire next image: {e}"),
};

if suboptimal {
rcx.recreate_swapchain = true;
@@ -365,13 +363,10 @@ impl ApplicationHandler for App {
rcx.scene.record_commands(image_index, &mut builder);

let command_buffer = builder.build().unwrap();

let future = rcx
.previous_frame_end
.take()
.unwrap_or_else(|| {
Box::new(sync::now(self.device.clone())) as Box<dyn GpuFuture>
})
.unwrap()
.join(acquire_future)
.then_execute(self.queue.clone(), command_buffer)
.unwrap()
@@ -384,13 +379,17 @@ impl ApplicationHandler for App {
)
.then_signal_fence_and_flush();

match future {
match future.map_err(Validated::unwrap) {
Ok(future) => {
rcx.previous_frame_end = Some(Box::new(future) as Box<dyn GpuFuture>);
rcx.previous_frame_end = Some(future.boxed());
}
Err(VulkanError::OutOfDate) => {
rcx.recreate_swapchain = true;
rcx.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
Err(e) => {
println!("Failed to flush future: {e:?}");
rcx.previous_frame_end = Some(Box::new(sync::now(self.device.clone())));
println!("failed to flush future: {e}");
rcx.previous_frame_end = Some(sync::now(self.device.clone()).boxed());
}
}
}
6 changes: 0 additions & 6 deletions examples/ray-tracing-auto/raytrace.miss

This file was deleted.

10 changes: 0 additions & 10 deletions examples/ray-tracing-auto/raytrace.rchit

This file was deleted.

43 changes: 0 additions & 43 deletions examples/ray-tracing-auto/raytrace.rgen

This file was deleted.

10 changes: 10 additions & 0 deletions examples/ray-tracing-auto/rchit.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadInEXT vec3 hit_value;
hitAttributeEXT vec2 attribs;

void main() {
vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);
hit_value = barycentrics;
}
41 changes: 41 additions & 0 deletions examples/ray-tracing-auto/rgen.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadEXT vec3 hit_value;

layout(set = 0, binding = 0) uniform accelerationStructureEXT top_level_as;
layout(set = 0, binding = 1) uniform Camera {
mat4 view_proj; // Camera view * projection
mat4 view_inverse; // Camera inverse view matrix
mat4 proj_inverse; // Camera inverse projection matrix
} camera;
layout(set = 1, binding = 0, rgba32f) uniform image2D image;

void main() {
const vec2 pixel_center = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 in_uv = pixel_center / vec2(gl_LaunchSizeEXT.xy);
vec2 d = in_uv * 2.0 - 1.0;

vec4 origin = camera.view_inverse * vec4(0, 0, 0, 1);
vec4 target = camera.proj_inverse * vec4(d.x, d.y, 1, 1);
vec4 direction = camera.view_inverse * vec4(normalize(target.xyz), 0);

uint ray_flags = gl_RayFlagsOpaqueEXT;
float t_min = 0.001;
float t_max = 10000.0;

traceRayEXT(
top_level_as, // acceleration structure
ray_flags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin.xyz, // ray origin
t_min, // ray min range
direction.xyz, // ray direction
t_max, // ray max range
0); // payload (location = 0)

imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hit_value, 1.0));
}
8 changes: 8 additions & 0 deletions examples/ray-tracing-auto/rmiss.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadInEXT vec3 hit_value;

void main() {
hit_value = vec3(0.0, 0.0, 0.2);
}
120 changes: 54 additions & 66 deletions examples/ray-tracing-auto/scene.rs
Original file line number Diff line number Diff line change
@@ -33,39 +33,8 @@ use vulkano::{
sync::GpuFuture,
};

mod raygen {
vulkano_shaders::shader! {
ty: "raygen",
path: "raytrace.rgen",
vulkan_version: "1.2"
}
}

mod closest_hit {
vulkano_shaders::shader! {
ty: "closesthit",
path: "raytrace.rchit",
vulkan_version: "1.2"
}
}

mod miss {
vulkano_shaders::shader! {
ty: "miss",
path: "raytrace.miss",
vulkan_version: "1.2"
}
}

#[derive(BufferContents, Vertex)]
#[repr(C)]
struct MyVertex {
#[format(R32G32B32_SFLOAT)]
position: [f32; 3],
}

pub struct Scene {
descriptor_set_0: Arc<DescriptorSet>,
descriptor_set: Arc<DescriptorSet>,
swapchain_image_sets: Vec<(Arc<ImageView>, Arc<DescriptorSet>)>,
pipeline_layout: Arc<PipelineLayout>,
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
@@ -95,7 +64,6 @@ impl Scene {
.unwrap()
.entry_point("main")
.unwrap();

let miss = miss::load(app.device.clone())
.unwrap()
.entry_point("main")
@@ -126,7 +94,6 @@ impl Scene {
stages: stages.into_iter().collect(),
groups: groups.into_iter().collect(),
max_pipeline_ray_recursion_depth: 1,

..RayTracingPipelineCreateInfo::layout(pipeline_layout.clone())
},
)
@@ -162,11 +129,10 @@ impl Scene {
.unwrap();

// Build the bottom-level acceleration structure and then the top-level acceleration
// structure. Acceleration structures are used to accelerate ray tracing.
// The bottom-level acceleration structure contains the geometry data.
// The top-level acceleration structure contains the instances of the bottom-level
// acceleration structures. In our shader, we will trace rays against the top-level
// acceleration structure.
// structure. Acceleration structures are used to accelerate ray tracing. The bottom-level
// acceleration structure contains the geometry data. The top-level acceleration structure
// contains the instances of the bottom-level acceleration structures. In our shader, we
// will trace rays against the top-level acceleration structure.
let blas = unsafe {
build_acceleration_structure_triangles(
vertex_buffer,
@@ -176,7 +142,6 @@ impl Scene {
app.queue.clone(),
)
};

let tlas = unsafe {
build_top_level_acceleration_structure(
vec![AccelerationStructureInstance {
@@ -209,14 +174,14 @@ impl Scene {
..Default::default()
},
raygen::Camera {
viewInverse: view.inverse().to_cols_array_2d(),
projInverse: proj.inverse().to_cols_array_2d(),
viewProj: (proj * view).to_cols_array_2d(),
view_proj: (proj * view).to_cols_array_2d(),
view_inverse: view.inverse().to_cols_array_2d(),
proj_inverse: proj.inverse().to_cols_array_2d(),
},
)
.unwrap();

let descriptor_set_0 = DescriptorSet::new(
let descriptor_set = DescriptorSet::new(
descriptor_set_allocator.clone(),
pipeline_layout.set_layouts()[0].clone(),
[
@@ -234,7 +199,7 @@ impl Scene {
ShaderBindingTable::new(memory_allocator.clone(), &pipeline).unwrap();

Scene {
descriptor_set_0,
descriptor_set,
swapchain_image_sets,
descriptor_set_allocator,
pipeline_layout,
@@ -264,31 +229,52 @@ impl Scene {
self.pipeline_layout.clone(),
0,
vec![
self.descriptor_set_0.clone(),
self.descriptor_set.clone(),
self.swapchain_image_sets[image_index as usize].1.clone(),
],
)
.unwrap();

builder
.unwrap()
.bind_pipeline_ray_tracing(self.pipeline.clone())
.unwrap();

let extent = self.swapchain_image_sets[0].0.image().extent();

unsafe {
builder
.trace_rays(
self.shader_binding_table.addresses().clone(),
extent[0],
extent[1],
1,
)
.unwrap();
}
unsafe { builder.trace_rays(self.shader_binding_table.addresses().clone(), extent) }
.unwrap();
}
}

mod raygen {
vulkano_shaders::shader! {
ty: "raygen",
path: "rgen.glsl",
vulkan_version: "1.2",
}
}

mod closest_hit {
vulkano_shaders::shader! {
ty: "closesthit",
path: "rchit.glsl",
vulkan_version: "1.2",
}
}

mod miss {
vulkano_shaders::shader! {
ty: "miss",
path: "rmiss.glsl",
vulkan_version: "1.2",
}
}

#[derive(BufferContents, Vertex)]
#[repr(C)]
struct MyVertex {
#[format(R32G32B32_SFLOAT)]
position: [f32; 3],
}

/// This function is called once during initialization, then again whenever the window is resized.
fn window_size_dependent_setup(
images: &[Arc<Image>],
@@ -306,6 +292,7 @@ fn window_size_dependent_setup(
[],
)
.unwrap();

(image_view, descriptor_set)
})
.collect();
@@ -314,7 +301,9 @@ fn window_size_dependent_setup(
}

/// A helper function to build a acceleration structure and wait for its completion.
/// # SAFETY
///
/// # Safety
///
/// - If you are referencing a bottom-level acceleration structure in a top-level acceleration
/// structure, you must ensure that the bottom-level acceleration structure is kept alive.
unsafe fn build_acceleration_structure_common(
@@ -340,8 +329,8 @@ unsafe fn build_acceleration_structure_common(
)
.unwrap();

// We build a new scratch buffer for each acceleration structure for simplicity.
// You may want to reuse scratch buffers if you need to build many acceleration structures.
// We create a new scratch buffer for each acceleration structure for simplicity. You may want
// to reuse scratch buffers if you need to build many acceleration structures.
let scratch_buffer = Buffer::new_slice::<u8>(
memory_allocator.clone(),
BufferCreateInfo {
@@ -370,7 +359,7 @@ unsafe fn build_acceleration_structure_common(
)
};

let acceleration = unsafe { AccelerationStructure::new(device, as_create_info).unwrap() };
let acceleration = unsafe { AccelerationStructure::new(device, as_create_info) }.unwrap();

as_build_geometry_info.dst_acceleration_structure = Some(acceleration.clone());
as_build_geometry_info.scratch_data = Some(scratch_buffer);
@@ -380,9 +369,8 @@ unsafe fn build_acceleration_structure_common(
..Default::default()
};

// For simplicity, we build a single command buffer
// that builds the acceleration structure, then waits
// for its execution to complete.
// For simplicity, we build a single command buffer that builds the acceleration structure,
// then waits for its execution to complete.
let mut builder = AutoCommandBufferBuilder::primary(
command_buffer_allocator,
queue.queue_family_index(),
93 changes: 45 additions & 48 deletions examples/ray-tracing/main.rs
Original file line number Diff line number Diff line change
@@ -73,7 +73,6 @@ impl App {
InstanceCreateInfo {
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
enabled_extensions: InstanceExtensions {
ext_debug_utils: true,
ext_swapchain_colorspace: true,
..required_extensions
},
@@ -91,11 +90,21 @@ impl App {
khr_acceleration_structure: true,
..DeviceExtensions::empty()
};
let device_features = DeviceFeatures {
acceleration_structure: true,
ray_tracing_pipeline: true,
buffer_device_address: true,
synchronization2: true,
..Default::default()
};
let (physical_device, queue_family_index) = instance
.enumerate_physical_devices()
.unwrap()
.filter(|p| p.api_version() >= Version::V1_3)
.filter(|p| p.supported_extensions().contains(&device_extensions))
.filter(|p| {
p.supported_extensions().contains(&device_extensions)
&& p.supported_features().contains(&device_features)
})
.filter_map(|p| {
p.queue_family_properties()
.iter()
@@ -117,6 +126,12 @@ impl App {
})
.unwrap();

println!(
"Using device: {} (type: {:?})",
physical_device.properties().device_name,
physical_device.properties().device_type,
);

let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
@@ -125,13 +140,7 @@ impl App {
queue_family_index,
..Default::default()
}],
enabled_features: DeviceFeatures {
acceleration_structure: true,
ray_tracing_pipeline: true,
buffer_device_address: true,
synchronization2: true,
..Default::default()
},
enabled_features: device_features,
..Default::default()
},
)
@@ -162,43 +171,31 @@ impl ApplicationHandler for App {
.unwrap(),
);
let surface = Surface::from_window(self.instance.clone(), window.clone()).unwrap();

let physical_device = self.device.physical_device();
let supported_surface_formats = physical_device
.surface_formats(&surface, Default::default())
.unwrap();

// For each supported format, check if it is supported for storage images
let supported_storage_formats = supported_surface_formats
.into_iter()
.filter(|(format, _)| {
physical_device
.image_format_properties(ImageFormatInfo {
format: *format,
usage: ImageUsage::STORAGE,
..Default::default()
})
.unwrap()
.is_some()
})
.collect::<Vec<_>>();

println!(
"Using device: {} (type: {:?})",
physical_device.properties().device_name,
physical_device.properties().device_type,
);
let window_size = window.inner_size();

let swapchain_id = {
let surface_capabilities = self
.device
.physical_device()
.surface_capabilities(&surface, Default::default())
.unwrap();

let (swapchain_format, swapchain_color_space) = supported_storage_formats
.first()
.map(|(format, color_space)| (*format, *color_space))
let (image_format, image_color_space) = self
.device
.physical_device()
.surface_formats(&surface, Default::default())
.unwrap()
.into_iter()
.find(|(format, _)| {
self.device
.physical_device()
.image_format_properties(ImageFormatInfo {
format: *format,
usage: ImageUsage::STORAGE,
..Default::default()
})
.unwrap()
.is_some()
})
.unwrap();

self.resources
@@ -207,10 +204,13 @@ impl ApplicationHandler for App {
surface,
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count.max(3),
image_format: swapchain_format,
image_extent: window.inner_size().into(),
image_usage: ImageUsage::STORAGE | ImageUsage::COLOR_ATTACHMENT,
image_color_space: swapchain_color_space,
image_format,
image_extent: window_size.into(),
// To simplify the example, we will directly write to the swapchain images
// from the ray tracing shader. This requires the images to support storage
// usage.
image_usage: ImageUsage::STORAGE,
image_color_space,
composite_alpha: surface_capabilities
.supported_composite_alpha
.into_iter()
@@ -274,19 +274,16 @@ impl ApplicationHandler for App {
)
.unwrap(),
],
push_constant_ranges: vec![],
..Default::default()
},
)
.unwrap();

let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(self.device.clone()));
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
self.device.clone(),
Default::default(),
));

let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(self.device.clone()));

let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
self.device.clone(),
Default::default(),
@@ -305,8 +302,8 @@ impl ApplicationHandler for App {
pipeline_layout.clone(),
swapchain_id,
virtual_swapchain_id,
descriptor_set_allocator,
memory_allocator,
descriptor_set_allocator,
command_buffer_allocator,
),
)
6 changes: 0 additions & 6 deletions examples/ray-tracing/raytrace.miss

This file was deleted.

10 changes: 0 additions & 10 deletions examples/ray-tracing/raytrace.rchit

This file was deleted.

43 changes: 0 additions & 43 deletions examples/ray-tracing/raytrace.rgen

This file was deleted.

10 changes: 10 additions & 0 deletions examples/ray-tracing/rchit.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadInEXT vec3 hit_value;
hitAttributeEXT vec2 attribs;

void main() {
vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);
hit_value = barycentrics;
}
41 changes: 41 additions & 0 deletions examples/ray-tracing/rgen.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadEXT vec3 hit_value;

layout(set = 0, binding = 0) uniform accelerationStructureEXT top_level_as;
layout(set = 0, binding = 1) uniform Camera {
mat4 view_proj; // Camera view * projection
mat4 view_inverse; // Camera inverse view matrix
mat4 proj_inverse; // Camera inverse projection matrix
} camera;
layout(set = 1, binding = 0, rgba32f) uniform image2D image;

void main() {
const vec2 pixel_center = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 in_uv = pixel_center / vec2(gl_LaunchSizeEXT.xy);
vec2 d = in_uv * 2.0 - 1.0;

vec4 origin = camera.view_inverse * vec4(0, 0, 0, 1);
vec4 target = camera.proj_inverse * vec4(d.x, d.y, 1, 1);
vec4 direction = camera.view_inverse * vec4(normalize(target.xyz), 0);

uint ray_flags = gl_RayFlagsOpaqueEXT;
float t_min = 0.001;
float t_max = 10000.0;

traceRayEXT(
top_level_as, // acceleration structure
ray_flags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin.xyz, // ray origin
t_min, // ray min range
direction.xyz, // ray direction
t_max, // ray max range
0); // payload (location = 0)

imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hit_value, 1.0));
}
8 changes: 8 additions & 0 deletions examples/ray-tracing/rmiss.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadInEXT vec3 hit_value;

void main() {
hit_value = vec3(0.0, 0.0, 0.2);
}
168 changes: 85 additions & 83 deletions examples/ray-tracing/scene.rs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ use vulkano::{
PrimaryCommandBufferAbstract,
},
descriptor_set::{
allocator::StandardDescriptorSetAllocator, sys::RawDescriptorSet, WriteDescriptorSet,
allocator::StandardDescriptorSetAllocator, DescriptorSet, WriteDescriptorSet,
},
device::{Device, Queue},
format::Format,
@@ -37,40 +37,9 @@ use vulkano_taskgraph::{
command_buffer::RecordingCommandBuffer, resource::Resources, Id, Task, TaskContext, TaskResult,
};

mod raygen {
vulkano_shaders::shader! {
ty: "raygen",
path: "raytrace.rgen",
vulkan_version: "1.2"
}
}

mod closest_hit {
vulkano_shaders::shader! {
ty: "closesthit",
path: "raytrace.rchit",
vulkan_version: "1.2"
}
}

mod miss {
vulkano_shaders::shader! {
ty: "miss",
path: "raytrace.miss",
vulkan_version: "1.2"
}
}

#[derive(BufferContents, Vertex)]
#[repr(C)]
struct MyVertex {
#[format(R32G32B32_SFLOAT)]
position: [f32; 3],
}

pub struct SceneTask {
descriptor_set_0: Arc<RawDescriptorSet>,
swapchain_image_sets: Vec<(Arc<ImageView>, Arc<RawDescriptorSet>)>,
descriptor_set: Arc<DescriptorSet>,
swapchain_image_sets: Vec<(Arc<ImageView>, Arc<DescriptorSet>)>,
pipeline_layout: Arc<PipelineLayout>,
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
virtual_swapchain_id: Id<Swapchain>,
@@ -87,8 +56,8 @@ impl SceneTask {
pipeline_layout: Arc<PipelineLayout>,
swapchain_id: Id<Swapchain>,
virtual_swapchain_id: Id<Swapchain>,
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
memory_allocator: Arc<dyn MemoryAllocator>,
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
command_buffer_allocator: Arc<dyn CommandBufferAllocator>,
) -> Self {
let pipeline = {
@@ -100,7 +69,6 @@ impl SceneTask {
.unwrap()
.entry_point("main")
.unwrap();

let miss = miss::load(app.device.clone())
.unwrap()
.entry_point("main")
@@ -113,6 +81,8 @@ impl SceneTask {
PipelineShaderStageCreateInfo::new(closest_hit),
];

// Define the shader groups that will eventually turn into the shader binding table.
// The numbers are the indices of the stages in the `stages` array.
let groups = [
RayTracingShaderGroupCreateInfo::General { general_shader: 0 },
RayTracingShaderGroupCreateInfo::General { general_shader: 1 },
@@ -129,7 +99,6 @@ impl SceneTask {
stages: stages.into_iter().collect(),
groups: groups.into_iter().collect(),
max_pipeline_ray_recursion_depth: 1,

..RayTracingPipelineCreateInfo::layout(pipeline_layout.clone())
},
)
@@ -164,6 +133,11 @@ impl SceneTask {
)
.unwrap();

// Build the bottom-level acceleration structure and then the top-level acceleration
// structure. Acceleration structures are used to accelerate ray tracing. The bottom-level
// acceleration structure contains the geometry data. The top-level acceleration structure
// contains the instances of the bottom-level acceleration structures. In our shader, we
// will trace rays against the top-level acceleration structure.
let blas = unsafe {
build_acceleration_structure_triangles(
vertex_buffer,
@@ -173,7 +147,6 @@ impl SceneTask {
app.queue.clone(),
)
};

let tlas = unsafe {
build_top_level_acceleration_structure(
vec![AccelerationStructureInstance {
@@ -206,27 +179,23 @@ impl SceneTask {
..Default::default()
},
raygen::Camera {
viewInverse: view.inverse().to_cols_array_2d(),
projInverse: proj.inverse().to_cols_array_2d(),
viewProj: (proj * view).to_cols_array_2d(),
view_proj: (proj * view).to_cols_array_2d(),
view_inverse: view.inverse().to_cols_array_2d(),
proj_inverse: proj.inverse().to_cols_array_2d(),
},
)
.unwrap();

let descriptor_set_0 = RawDescriptorSet::new(
let descriptor_set = DescriptorSet::new(
descriptor_set_allocator.clone(),
&pipeline_layout.set_layouts()[0],
0,
)
.unwrap();

unsafe {
let writes = &[
pipeline_layout.set_layouts()[0].clone(),
[
WriteDescriptorSet::acceleration_structure(0, tlas.clone()),
WriteDescriptorSet::buffer(1, uniform_buffer.clone()),
];
descriptor_set_0.update(writes, &[]).unwrap();
}
],
[],
)
.unwrap();

let swapchain_image_sets = window_size_dependent_setup(
&app.resources,
@@ -239,7 +208,7 @@ impl SceneTask {
ShaderBindingTable::new(memory_allocator.clone(), &pipeline).unwrap();

SceneTask {
descriptor_set_0: Arc::new(descriptor_set_0),
descriptor_set,
swapchain_image_sets,
descriptor_set_allocator,
pipeline_layout,
@@ -279,67 +248,96 @@ impl Task for SceneTask {
&self.pipeline_layout,
0,
&[
&self.descriptor_set_0,
&self.swapchain_image_sets[image_index as usize].1,
self.descriptor_set.as_raw(),
self.swapchain_image_sets[image_index as usize].1.as_raw(),
],
&[],
)?;

cbf.bind_pipeline_ray_tracing(&self.pipeline)?;

let extent = self.swapchain_image_sets[0].0.image().extent();

unsafe {
cbf.trace_rays(
self.shader_binding_table.addresses(),
extent[0],
extent[1],
1,
)
}?;
unsafe { cbf.trace_rays(self.shader_binding_table.addresses(), extent) }?;

for (image_view, descriptor_set) in self.swapchain_image_sets.iter() {
for (_, descriptor_set) in self.swapchain_image_sets.iter() {
cbf.destroy_object(descriptor_set.clone());
cbf.destroy_object(image_view.clone());
}

cbf.destroy_object(self.blas.clone());
cbf.destroy_object(self.tlas.clone());
cbf.destroy_object(self.uniform_buffer.clone().into());
cbf.destroy_object(self.descriptor_set_0.clone());
cbf.destroy_object(self.descriptor_set.clone());

Ok(())
}
}

mod raygen {
vulkano_shaders::shader! {
ty: "raygen",
path: "rgen.glsl",
vulkan_version: "1.2",
}
}

mod closest_hit {
vulkano_shaders::shader! {
ty: "closesthit",
path: "rchit.glsl",
vulkan_version: "1.2",
}
}

mod miss {
vulkano_shaders::shader! {
ty: "miss",
path: "rmiss.glsl",
vulkan_version: "1.2",
}
}

#[derive(BufferContents, Vertex)]
#[repr(C)]
struct MyVertex {
#[format(R32G32B32_SFLOAT)]
position: [f32; 3],
}

/// This function is called once during initialization, then again whenever the window is resized.
fn window_size_dependent_setup(
resources: &Resources,
swapchain_id: Id<Swapchain>,
pipeline_layout: &Arc<PipelineLayout>,
descriptor_set_allocator: &Arc<StandardDescriptorSetAllocator>,
) -> Vec<(Arc<ImageView>, Arc<RawDescriptorSet>)> {
) -> Vec<(Arc<ImageView>, Arc<DescriptorSet>)> {
let swapchain_state = resources.swapchain(swapchain_id).unwrap();
let images = swapchain_state.images();

let swapchain_image_sets = images
.iter()
.map(|image| {
let descriptor_set = RawDescriptorSet::new(
let image_view = ImageView::new_default(image.clone()).unwrap();
let descriptor_set = DescriptorSet::new(
descriptor_set_allocator.clone(),
&pipeline_layout.set_layouts()[1],
0,
pipeline_layout.set_layouts()[1].clone(),
[WriteDescriptorSet::image_view(0, image_view.clone())],
[],
)
.unwrap();
let image_view = ImageView::new_default(image.clone()).unwrap();
let writes = &[WriteDescriptorSet::image_view(0, image_view.clone())];
unsafe { descriptor_set.update(writes, &[]) }.unwrap();
(image_view, Arc::new(descriptor_set))

(image_view, descriptor_set)
})
.collect();

swapchain_image_sets
}

/// A helper function to build a acceleration structure and wait for its completion.
///
/// # Safety
///
/// - If you are referencing a bottom-level acceleration structure in a top-level acceleration
/// structure, you must ensure that the bottom-level acceleration structure is kept alive.
unsafe fn build_acceleration_structure_common(
geometries: AccelerationStructureGeometries,
primitive_count: u32,
@@ -349,13 +347,6 @@ unsafe fn build_acceleration_structure_common(
device: Arc<Device>,
queue: Arc<Queue>,
) -> Arc<AccelerationStructure> {
let mut builder = AutoCommandBufferBuilder::primary(
command_buffer_allocator,
queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();

let mut as_build_geometry_info = AccelerationStructureBuildGeometryInfo {
mode: BuildAccelerationStructureMode::Build,
flags: BuildAccelerationStructureFlags::PREFER_FAST_TRACE,
@@ -370,6 +361,8 @@ unsafe fn build_acceleration_structure_common(
)
.unwrap();

// We create a new scratch buffer for each acceleration structure for simplicity. You may want
// to reuse scratch buffers if you need to build many acceleration structures.
let scratch_buffer = Buffer::new_slice::<u8>(
memory_allocator.clone(),
BufferCreateInfo {
@@ -398,7 +391,7 @@ unsafe fn build_acceleration_structure_common(
)
};

let acceleration = unsafe { AccelerationStructure::new(device, as_create_info).unwrap() };
let acceleration = unsafe { AccelerationStructure::new(device, as_create_info) }.unwrap();

as_build_geometry_info.dst_acceleration_structure = Some(acceleration.clone());
as_build_geometry_info.scratch_data = Some(scratch_buffer);
@@ -408,6 +401,15 @@ unsafe fn build_acceleration_structure_common(
..Default::default()
};

// For simplicity, we build a single command buffer that builds the acceleration structure,
// then waits for its execution to complete.
let mut builder = AutoCommandBufferBuilder::primary(
command_buffer_allocator,
queue.queue_family_index(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();

builder
.build_acceleration_structure(
as_build_geometry_info,
1 change: 1 addition & 0 deletions vulkano-taskgraph/src/command_buffer/commands/bind_push.rs
Original file line number Diff line number Diff line change
@@ -117,6 +117,7 @@ impl RecordingCommandBuffer<'_> {
self
}

/// Binds a ray tracing pipeline for future ray tracing calls.
pub unsafe fn bind_pipeline_ray_tracing(
&mut self,
pipeline: &Arc<RayTracingPipeline>,
38 changes: 22 additions & 16 deletions vulkano-taskgraph/src/command_buffer/commands/pipeline.rs
Original file line number Diff line number Diff line change
@@ -662,43 +662,49 @@ impl RecordingCommandBuffer<'_> {
self
}

/// Performs a single ray tracing operation using a ray tracing pipeline.
///
/// A ray tracing pipeline must have been bound using [`bind_pipeline_ray_tracing`]. Any
/// resources used by the ray tracing pipeline, such as descriptor sets, must have been set
/// beforehand.
///
/// # Safety
///
/// - The general [shader safety requirements] apply.
///
/// [`bind_pipeline_ray_tracing`]: Self::bind_pipeline_ray_tracing
/// [shader safety requirements]: vulkano::shader#safety
pub unsafe fn trace_rays(
&mut self,
shader_binding_table_addresses: &ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> Result<&mut Self> {
Ok(unsafe {
self.trace_rays_unchecked(shader_binding_table_addresses, width, height, depth)
})
Ok(unsafe { self.trace_rays_unchecked(shader_binding_table_addresses, dimensions) })
}

pub unsafe fn trace_rays_unchecked(
&mut self,
shader_binding_table_addresses: &ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> &mut Self {
let fns = self.device().fns();

let raygen = shader_binding_table_addresses.raygen.to_vk();
let miss = shader_binding_table_addresses.miss.to_vk();
let hit = shader_binding_table_addresses.hit.to_vk();
let callable = shader_binding_table_addresses.callable.to_vk();

let fns = self.device().fns();
unsafe {
(fns.khr_ray_tracing_pipeline.cmd_trace_rays_khr)(
self.handle(),
&raygen,
&miss,
&hit,
&callable,
width,
height,
depth,
);
}
dimensions[0],
dimensions[1],
dimensions[2],
)
};

self
}
1 change: 1 addition & 0 deletions vulkano/src/command_buffer/commands/bind_push.rs
Original file line number Diff line number Diff line change
@@ -378,6 +378,7 @@ impl<L> AutoCommandBufferBuilder<L> {
self
}

/// Binds a ray tracing pipeline for future ray tracing calls.
pub fn bind_pipeline_ray_tracing(
&mut self,
pipeline: Arc<RayTracingPipeline>,
71 changes: 39 additions & 32 deletions vulkano/src/command_buffer/commands/pipeline.rs
Original file line number Diff line number Diff line change
@@ -1593,34 +1593,42 @@ impl<L> AutoCommandBufferBuilder<L> {
self
}

/// Performs a single ray tracing operation using a ray tracing pipeline.
///
/// A ray tracing pipeline must have been bound using [`bind_pipeline_ray_tracing`]. Any
/// resources used by the ray tracing pipeline, such as descriptor sets, must have been set
/// beforehand.
///
/// # Safety
///
/// - The general [shader safety requirements] apply.
///
/// [`bind_pipeline_ray_tracing`]: Self::bind_pipeline_ray_tracing
/// [shader safety requirements]: vulkano::shader#safety
pub unsafe fn trace_rays(
&mut self,
shader_binding_table_addresses: ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> Result<&mut Self, Box<ValidationError>> {
self.inner
.validate_trace_rays(&shader_binding_table_addresses, width, height, depth)?;
.validate_trace_rays(&shader_binding_table_addresses, dimensions)?;

Ok(self.trace_rays_unchecked(shader_binding_table_addresses, width, height, depth))
Ok(self.trace_rays_unchecked(shader_binding_table_addresses, dimensions))
}

#[cfg_attr(not(feature = "document_unchecked"), doc(hidden))]
pub unsafe fn trace_rays_unchecked(
&mut self,
shader_binding_table_addresses: ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> &mut Self {
let pipeline = self.builder_state.pipeline_ray_tracing.as_deref().unwrap();

let mut used_resources = Vec::new();
self.add_descriptor_sets_resources(&mut used_resources, pipeline);

self.add_command("trace_rays", used_resources, move |out| {
out.trace_rays_unchecked(&shader_binding_table_addresses, width, height, depth);
out.trace_rays_unchecked(&shader_binding_table_addresses, dimensions);
});

self
@@ -4985,21 +4993,17 @@ impl RecordingCommandBuffer {
pub unsafe fn trace_rays(
&mut self,
shader_binding_table_addresses: &ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> Result<&mut Self, Box<ValidationError>> {
self.validate_trace_rays(shader_binding_table_addresses, width, height, depth)?;
self.validate_trace_rays(shader_binding_table_addresses, dimensions)?;

Ok(self.trace_rays_unchecked(shader_binding_table_addresses, width, height, depth))
Ok(self.trace_rays_unchecked(shader_binding_table_addresses, dimensions))
}

fn validate_trace_rays(
&self,
_shader_binding_table_addresses: &ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> Result<(), Box<ValidationError>> {
if !self.device().enabled_features().ray_tracing_pipeline {
return Err(Box::new(ValidationError {
@@ -5026,17 +5030,19 @@ impl RecordingCommandBuffer {

let device_properties = self.device().physical_device().properties();

let width = width as u64;
let height = height as u64;
let depth = depth as u64;
let width = dimensions[0] as u64;
let height = dimensions[1] as u64;
let depth = dimensions[2] as u64;

let max_width = device_properties.max_compute_work_group_count[0] as u64
* device_properties.max_compute_work_group_size[0] as u64;

if width > max_width {
return Err(Box::new(ValidationError {
context: "width".into(),
problem: "exceeds maxComputeWorkGroupCount[0] * maxComputeWorkGroupSize[0]".into(),
problem: "exceeds `max_compute_work_group_count[0] * \
max_compute_work_group_size[0]`"
.into(),
vuids: &["VUID-vkCmdTraceRaysKHR-width-03638"],
..Default::default()
}));
@@ -5048,7 +5054,9 @@ impl RecordingCommandBuffer {
if height > max_height {
return Err(Box::new(ValidationError {
context: "height".into(),
problem: "exceeds maxComputeWorkGroupCount[1] * maxComputeWorkGroupSize[1]".into(),
problem: "exceeds `max_compute_work_group_count[1] * \
max_compute_work_group_size[1]`"
.into(),
vuids: &["VUID-vkCmdTraceRaysKHR-height-03639"],
..Default::default()
}));
@@ -5060,7 +5068,9 @@ impl RecordingCommandBuffer {
if depth > max_depth {
return Err(Box::new(ValidationError {
context: "depth".into(),
problem: "exceeds maxComputeWorkGroupCount[2] * maxComputeWorkGroupSize[2]".into(),
problem: "exceeds `max_compute_work_group_count[2] * \
max_compute_work_group_size[2]`"
.into(),
vuids: &["VUID-vkCmdTraceRaysKHR-depth-03640"],
..Default::default()
}));
@@ -5072,7 +5082,7 @@ impl RecordingCommandBuffer {
if total_invocations > max_invocations {
return Err(Box::new(ValidationError {
context: "width * height * depth".into(),
problem: "exceeds maxRayDispatchInvocationCount".into(),
problem: "exceeds `max_ray_dispatch_invocation_count`".into(),
vuids: &["VUID-vkCmdTraceRaysKHR-width-03641"],
..Default::default()
}));
@@ -5085,26 +5095,23 @@ impl RecordingCommandBuffer {
pub unsafe fn trace_rays_unchecked(
&mut self,
shader_binding_table_addresses: &ShaderBindingTableAddresses,
width: u32,
height: u32,
depth: u32,
dimensions: [u32; 3],
) -> &mut Self {
let fns = self.device().fns();

let raygen = shader_binding_table_addresses.raygen.to_vk();
let miss = shader_binding_table_addresses.miss.to_vk();
let hit = shader_binding_table_addresses.hit.to_vk();
let callable = shader_binding_table_addresses.callable.to_vk();

let fns = self.device().fns();
(fns.khr_ray_tracing_pipeline.cmd_trace_rays_khr)(
self.handle(),
&raygen,
&miss,
&hit,
&callable,
width,
height,
depth,
dimensions[0],
dimensions[1],
dimensions[2],
);

self
113 changes: 0 additions & 113 deletions vulkano/src/device/mod.rs
Original file line number Diff line number Diff line change
@@ -114,7 +114,6 @@ use crate::{
instance::{Instance, InstanceOwned, InstanceOwnedDebugWrapper},
macros::{impl_id_counter, vulkan_bitflags},
memory::{ExternalMemoryHandleType, MemoryFdProperties, MemoryRequirements},
pipeline::ray_tracing::RayTracingPipeline,
Requires, RequiresAllOf, RequiresOneOf, Validated, ValidationError, Version, VulkanError,
VulkanObject,
};
@@ -1305,94 +1304,6 @@ impl Device {

Ok(())
}

pub fn ray_tracing_shader_group_handles(
&self,
ray_tracing_pipeline: &RayTracingPipeline,
first_group: u32,
group_count: u32,
) -> Result<ShaderGroupHandlesData, Validated<VulkanError>> {
self.validate_ray_tracing_pipeline_properties(
ray_tracing_pipeline,
first_group,
group_count,
)?;

unsafe {
Ok(self.ray_tracing_shader_group_handles_unchecked(
ray_tracing_pipeline,
first_group,
group_count,
)?)
}
}

fn validate_ray_tracing_pipeline_properties(
&self,
ray_tracing_pipeline: &RayTracingPipeline,
first_group: u32,
group_count: u32,
) -> Result<(), Box<ValidationError>> {
if !self.enabled_features().ray_tracing_pipeline
|| self
.physical_device()
.properties()
.shader_group_handle_size
.is_none()
{
Err(Box::new(ValidationError {
problem: "device property `shader_group_handle_size` is empty".into(),
requires_one_of: RequiresOneOf(&[RequiresAllOf(&[Requires::DeviceFeature(
"ray_tracing_pipeline",
)])]),
..Default::default()
}))?;
};

if (first_group + group_count) as usize > ray_tracing_pipeline.groups().len() {
Err(Box::new(ValidationError {
problem: "the sum of `first_group` and `group_count` must be less than or equal\
to the number of shader groups in pipeline"
.into(),
vuids: &["VUID-vkGetRayTracingShaderGroupHandlesKHR-firstGroup-02419"],
..Default::default()
}))?
}
// TODO: VUID-vkGetRayTracingShaderGroupHandlesKHR-pipeline-07828

Ok(())
}

#[cfg_attr(not(feature = "document_unchecked"), doc(hidden))]
pub unsafe fn ray_tracing_shader_group_handles_unchecked(
&self,
ray_tracing_pipeline: &RayTracingPipeline,
first_group: u32,
group_count: u32,
) -> Result<ShaderGroupHandlesData, VulkanError> {
let handle_size = self
.physical_device()
.properties()
.shader_group_handle_size
.unwrap();

let mut data = vec![0u8; (handle_size * group_count) as usize];
let fns = self.fns();
unsafe {
(fns.khr_ray_tracing_pipeline
.get_ray_tracing_shader_group_handles_khr)(
self.handle,
ray_tracing_pipeline.handle(),
first_group,
group_count,
data.len(),
data.as_mut_ptr().cast(),
)
.result()
.map_err(VulkanError::from)?;
}
Ok(ShaderGroupHandlesData { data, handle_size })
}
}

impl Debug for Device {
@@ -2223,30 +2134,6 @@ impl<T> Deref for DeviceOwnedDebugWrapper<T> {
}
}

/// Holds the data returned by [`Device::ray_tracing_shader_group_handles`].
#[derive(Clone, Debug)]
pub struct ShaderGroupHandlesData {
data: Vec<u8>,
handle_size: u32,
}

impl ShaderGroupHandlesData {
pub fn data(&self) -> &[u8] {
&self.data
}

pub fn handle_size(&self) -> u32 {
self.handle_size
}
}

impl ShaderGroupHandlesData {
/// Returns an iterator over the handles in the data.
pub fn iter(&self) -> impl ExactSizeIterator<Item = &[u8]> {
self.data().chunks_exact(self.handle_size as usize)
}
}

#[cfg(test)]
mod tests {
use crate::device::{
276 changes: 198 additions & 78 deletions vulkano/src/pipeline/ray_tracing.rs

Large diffs are not rendered by default.

0 comments on commit 938de01

Please sign in to comment.