diff --git a/gfx-examples/src/cube.rs b/gfx-examples/src/cube.rs index 1cc540997e..13f839c6fc 100644 --- a/gfx-examples/src/cube.rs +++ b/gfx-examples/src/cube.rs @@ -88,9 +88,22 @@ struct Cube { index_buf: wgpu::Buffer, index_count: usize, bind_group: wgpu::BindGroup, + uniform_buf: wgpu::Buffer, pipeline: wgpu::RenderPipeline, } +impl Cube { + fn generate_matrix(aspect_ratio: f32) -> cgmath::Matrix4 { + let mx_projection = cgmath::perspective(cgmath::Deg(45f32), aspect_ratio, 1.0, 10.0); + let mx_view = cgmath::Matrix4::look_at( + cgmath::Point3::new(1.5f32, -5.0, 3.0), + cgmath::Point3::new(0f32, 0.0, 0.0), + cgmath::Vector3::unit_z(), + ); + mx_projection * mx_view + } +} + impl framework::Example for Cube { fn init(device: &mut wgpu::Device, sc_desc: &wgpu::SwapChainDescriptor) -> Self { use std::mem; @@ -196,18 +209,9 @@ impl framework::Example for Cube { size: 64, usage: wgpu::BufferUsageFlags::UNIFORM | wgpu::BufferUsageFlags::TRANSFER_DST, }); - { - let aspect_ratio = sc_desc.width as f32 / sc_desc.height as f32; - let mx_projection = cgmath::perspective(cgmath::Deg(45f32), aspect_ratio, 1.0, 10.0); - let mx_view = cgmath::Matrix4::look_at( - cgmath::Point3::new(1.5f32, -5.0, 3.0), - cgmath::Point3::new(0f32, 0.0, 0.0), - cgmath::Vector3::unit_z(), - ); - let mx_total = mx_projection * mx_view; - let mx_raw: &[f32; 16] = mx_total.as_ref(); - uniform_buf.set_sub_data(0, framework::cast_slice(&mx_raw[..])); - } + let mx_total = Self::generate_matrix(sc_desc.width as f32 / sc_desc.height as f32); + let mx_ref: &[f32; 16] = mx_total.as_ref(); + uniform_buf.set_sub_data(0, framework::cast_slice(&mx_ref[..])); // Create bind group let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -294,11 +298,17 @@ impl framework::Example for Cube { index_buf, index_count: index_data.len(), bind_group, + uniform_buf, pipeline, } } - fn update(&mut self, _event: wgpu::winit::WindowEvent) { + fn update(&mut self, event: wgpu::winit::WindowEvent) { + if let wgpu::winit::WindowEvent::Resized(size) = event { + let mx_total = Self::generate_matrix(size.width as f32 / size.height as f32); + let mx_ref: &[f32; 16] = mx_total.as_ref(); + self.uniform_buf.set_sub_data(0, framework::cast_slice(&mx_ref[..])); + } } fn render(&mut self, frame: &wgpu::SwapChainOutput, device: &mut wgpu::Device) { diff --git a/gfx-examples/src/framework.rs b/gfx-examples/src/framework.rs index a0a07479fd..ec0514ef74 100644 --- a/gfx-examples/src/framework.rs +++ b/gfx-examples/src/framework.rs @@ -70,7 +70,7 @@ pub fn run(title: &str) { .to_physical(window.get_hidpi_factor()); let surface = instance.create_surface(&window); - let sc_desc = wgpu::SwapChainDescriptor { + let mut sc_desc = wgpu::SwapChainDescriptor { usage: wgpu::TextureUsageFlags::OUTPUT_ATTACHMENT, format: wgpu::TextureFormat::B8g8r8a8Unorm, width: size.width as u32, @@ -91,7 +91,11 @@ pub fn run(title: &str) { .. } => { let physical = size.to_physical(window.get_hidpi_factor()); - info!("Resized to {:?}", physical); + info!("Resizing to {:?}", physical); + sc_desc.width = physical.width as u32; + sc_desc.height = physical.height as u32; + swap_chain = device.create_swap_chain(&surface, &sc_desc); + example.update(WindowEvent::Resized(size)); } Event::WindowEvent { event, .. } => match event { WindowEvent::KeyboardInput { diff --git a/wgpu-bindings/wgpu.h b/wgpu-bindings/wgpu.h index e21cb7e96d..db6a7ade9c 100644 --- a/wgpu-bindings/wgpu.h +++ b/wgpu-bindings/wgpu.h @@ -435,10 +435,10 @@ typedef struct { WGPUByteArray code; } WGPUShaderModuleDescriptor; -typedef WGPUId WGPUSwapChainId; - typedef WGPUId WGPUSurfaceId; +typedef WGPUSurfaceId WGPUSwapChainId; + typedef uint32_t WGPUTextureUsageFlags; typedef struct { diff --git a/wgpu-native/src/device.rs b/wgpu-native/src/device.rs index b2d84231bd..480ddd1ab6 100644 --- a/wgpu-native/src/device.rs +++ b/wgpu-native/src/device.rs @@ -98,9 +98,8 @@ unsafe impl Send for DestroyedResources {} unsafe impl Sync for DestroyedResources {} impl DestroyedResources { - fn add(&mut self, resource_id: ResourceId, life_guard: &LifeGuard) { - self.referenced - .push((resource_id, life_guard.ref_count.clone())); + fn add(&mut self, resource_id: ResourceId, ref_count: RefCount) { + self.referenced.push((resource_id, ref_count)); } /// Returns the last submission index that is done. @@ -139,7 +138,11 @@ impl DestroyedResources { } impl DestroyedResources { - fn triage_referenced(&mut self) { + fn triage_referenced( + &mut self, + buffer_tracker: &mut BufferTracker, + texture_tracker: &mut TextureTracker, + ) { for i in (0..self.referenced.len()).rev() { // one in resource itself, and one here in this list let num_refs = self.referenced[i].1.load(); @@ -148,11 +151,13 @@ impl DestroyedResources { let resource_id = self.referenced.swap_remove(i).0; let (submit_index, resource) = match resource_id { ResourceId::Buffer(id) => { + buffer_tracker.remove(id); let buf = HUB.buffers.unregister(id); let si = buf.life_guard.submission_index.load(Ordering::Acquire); (si, Resource::Buffer(buf)) } ResourceId::Texture(id) => { + texture_tracker.remove(id); let tex = HUB.textures.unregister(id); let si = tex.life_guard.submission_index.load(Ordering::Acquire); (si, Resource::Texture(tex)) @@ -369,7 +374,10 @@ pub extern "C" fn wgpu_buffer_destroy(buffer_id: BufferId) { .get(buffer.device_id.value) .destroyed .lock() - .add(ResourceId::Buffer(buffer_id), &buffer.life_guard); + .add( + ResourceId::Buffer(buffer_id), + buffer.life_guard.ref_count.clone(), + ); } @@ -579,7 +587,10 @@ pub extern "C" fn wgpu_texture_destroy(texture_id: TextureId) { .get(texture.device_id.value) .destroyed .lock() - .add(ResourceId::Texture(texture_id), &texture.life_guard); + .add( + ResourceId::Texture(texture_id), + texture.life_guard.ref_count.clone(), + ); } #[no_mangle] @@ -595,7 +606,10 @@ pub extern "C" fn wgpu_texture_view_destroy(texture_view_id: TextureViewId) { .get(device_id) .destroyed .lock() - .add(ResourceId::TextureView(texture_view_id), &view.life_guard); + .add( + ResourceId::TextureView(texture_view_id), + view.life_guard.ref_count.clone(), + ); } @@ -902,6 +916,8 @@ pub extern "C" fn wgpu_queue_submit( .life_guard .submission_index .fetch_add(1, Ordering::Relaxed); + let mut buffer_tracker = device.buffer_tracker.lock(); + let mut texture_tracker = device.texture_tracker.lock(); //TODO: if multiple command buffers are submitted, we can re-use the last // native command buffer of the previous chain instead of always creating @@ -910,8 +926,6 @@ pub extern "C" fn wgpu_queue_submit( let mut command_buffer_guard = HUB.command_buffers.write(); let buffer_guard = HUB.buffers.read(); let texture_guard = HUB.textures.read(); - let mut buffer_tracker = device.buffer_tracker.lock(); - let mut texture_tracker = device.texture_tracker.lock(); // finish all the command buffers first for &cmb_id in command_buffer_ids { @@ -965,17 +979,20 @@ pub extern "C" fn wgpu_queue_submit( let fence = device.raw.create_fence(false).unwrap(); { let command_buffer_guard = HUB.command_buffers.read(); - let swap_chain_guard = HUB.swap_chains.read(); + let surface_guard = HUB.surfaces.read(); let wait_semaphores = swap_chain_links .into_iter() - .map(|link| { + .flat_map(|link| { //TODO: check the epoch - let sem = &swap_chain_guard + surface_guard .get(link.swap_chain_id.0) - .frames[link.image_index as usize] - .sem_available; - (sem, hal::pso::PipelineStage::COLOR_ATTACHMENT_OUTPUT) + .swap_chain + .as_ref() + .map(|swap_chain| ( + &swap_chain.frames[link.image_index as usize].sem_available, + hal::pso::PipelineStage::COLOR_ATTACHMENT_OUTPUT, + )) }); let submission = @@ -997,7 +1014,7 @@ pub extern "C" fn wgpu_queue_submit( let last_done = { let mut destroyed = device.destroyed.lock(); - destroyed.triage_referenced(); + destroyed.triage_referenced(&mut *buffer_tracker, &mut *texture_tracker); let last_done = destroyed.cleanup(&device.raw); destroyed.active.push(ActiveSubmission { @@ -1294,12 +1311,12 @@ pub extern "C" fn wgpu_device_create_compute_pipeline( HUB.compute_pipelines.register(pipeline) } - pub fn device_create_swap_chain( device_id: DeviceId, surface_id: SurfaceId, desc: &swap_chain::SwapChainDescriptor, -) -> (swap_chain::SwapChain, Vec>) { + outdated: swap_chain::OutdatedFrame, +) -> Vec> { let device_guard = HUB.devices.read(); let device = device_guard.get(device_id); let mut surface_guard = HUB.surfaces.write(); @@ -1331,25 +1348,42 @@ pub fn device_create_swap_chain( "Requested size {}x{} is outside of the supported range: {:?}", desc.width, desc.height, caps.extents); + + let (old_raw, sem_available, command_pool) = match surface.swap_chain.take() { + Some(mut old) => { + assert_eq!(old.device_id.value, device_id); + let mut destroyed = device.destroyed.lock(); + destroyed.add(ResourceId::Texture(old.outdated.texture_id.value), old.outdated.texture_id.ref_count); + destroyed.add(ResourceId::TextureView(old.outdated.view_id.value), old.outdated.view_id.ref_count); + unsafe { + old.command_pool.reset() + }; + (Some(old.raw), old.sem_available, old.command_pool) + } + _ => unsafe { + let sem_available = device.raw + .create_semaphore() + .unwrap(); + let command_pool = device.raw + .create_command_pool_typed( + &device.queue_group, + hal::pool::CommandPoolCreateFlags::RESET_INDIVIDUAL, + ) + .unwrap(); + (None, sem_available, command_pool) + } + }; + let (raw, backbuffer) = unsafe { device.raw .create_swapchain( &mut surface.raw, config.with_image_usage(usage), - None, + old_raw, ) .unwrap() }; - let command_pool = unsafe { - device.raw - .create_command_pool_typed( - &device.queue_group, - hal::pool::CommandPoolCreateFlags::RESET_INDIVIDUAL, - ) - .unwrap() - }; - - let swap_chain = swap_chain::SwapChain { + surface.swap_chain = Some(swap_chain::SwapChain { raw, device_id: Stored { value: device_id, @@ -1357,16 +1391,17 @@ pub fn device_create_swap_chain( }, frames: Vec::with_capacity(num_frames as usize), acquired: Vec::with_capacity(1), //TODO: get it from gfx-hal? - sem_available: device.raw.create_semaphore().unwrap(), + sem_available, + outdated, command_pool, - }; + }); let images = match backbuffer { hal::Backbuffer::Images(images) => images, hal::Backbuffer::Framebuffer(_) => panic!("Deprecated API detected!"), }; - let textures = images + images .into_iter() .map(|raw| resource::Texture { raw, @@ -1384,9 +1419,7 @@ pub fn device_create_swap_chain( swap_chain_link: None, life_guard: LifeGuard::new(), }) - .collect(); - - (swap_chain, textures) + .collect() } #[cfg(feature = "local")] @@ -1394,8 +1427,12 @@ fn swap_chain_populate_textures( swap_chain_id: SwapChainId, textures: Vec>, ) { - let mut swap_chain_guard = HUB.swap_chains.write(); - let swap_chain = swap_chain_guard.get_mut(swap_chain_id); + let mut surface_guard = HUB.surfaces.write(); + let swap_chain = surface_guard + .get_mut(swap_chain_id) + .swap_chain + .as_mut() + .unwrap(); let device_guard = HUB.devices.read(); let device = device_guard.get(swap_chain.device_id.value); @@ -1457,10 +1494,28 @@ pub extern "C" fn wgpu_device_create_swap_chain( surface_id: SurfaceId, desc: &swap_chain::SwapChainDescriptor, ) -> SwapChainId { - let (swap_chain, textures) = device_create_swap_chain(device_id, surface_id, desc); - let id = HUB.swap_chains.register(swap_chain); - swap_chain_populate_textures(id, textures); - id + let outdated = { + let outdated_texture = device_create_texture(device_id, &desc.to_texture_desc()); + let texture_id = Stored { + ref_count: outdated_texture.life_guard.ref_count.clone(), + value: HUB.textures.register(outdated_texture), + }; + device_track_texture(device_id, texture_id.value, texture_id.ref_count.clone()); + + let outdated_view = texture_create_default_view(texture_id.value); + let view_id = Stored { + ref_count: outdated_view.life_guard.ref_count.clone(), + value: HUB.texture_views.register(outdated_view), + }; + swap_chain::OutdatedFrame { + texture_id, + view_id, + } + }; + + let textures = device_create_swap_chain(device_id, surface_id, desc, outdated); + swap_chain_populate_textures(surface_id, textures); + surface_id } diff --git a/wgpu-native/src/hub.rs b/wgpu-native/src/hub.rs index 75c8ba992b..202d0fe43b 100644 --- a/wgpu-native/src/hub.rs +++ b/wgpu-native/src/hub.rs @@ -4,7 +4,7 @@ use crate::{ RenderPassHandle, ComputePassHandle, PipelineLayoutHandle, RenderPipelineHandle, ComputePipelineHandle, ShaderModuleHandle, BufferHandle, SamplerHandle, TextureHandle, TextureViewHandle, - SurfaceHandle, SwapChainHandle, + SurfaceHandle, }; use hal::backend::FastHashMap; @@ -126,7 +126,6 @@ pub struct Hub { pub(crate) texture_views: Arc>, pub(crate) samplers: Arc>, pub(crate) surfaces: Arc>, - pub(crate) swap_chains: Arc>, } lazy_static! { diff --git a/wgpu-native/src/instance.rs b/wgpu-native/src/instance.rs index 6654fecc68..7fcb8021f5 100644 --- a/wgpu-native/src/instance.rs +++ b/wgpu-native/src/instance.rs @@ -54,10 +54,7 @@ pub extern "C" fn wgpu_instance_create_surface_from_winit( .read() .get(instance_id) .create_surface(window); - let surface = SurfaceHandle { - raw, - }; - + let surface = SurfaceHandle::new(raw); HUB.surfaces.register(surface) } @@ -71,12 +68,11 @@ pub fn instance_create_surface_from_xlib( unimplemented!(); #[cfg(all(unix, feature = "gfx-backend-vulkan"))] - SurfaceHandle { - raw: HUB.instances - .read() - .get(instance_id) - .create_surface_from_xlib(display, window), - } + SurfaceHandle::new(HUB.instances + .read() + .get(instance_id) + .create_surface_from_xlib(display, window) + ) } #[cfg(feature = "local")] @@ -99,12 +95,11 @@ pub fn instance_create_surface_from_macos_layer( unimplemented!(); #[cfg(feature = "gfx-backend-metal")] - SurfaceHandle { - raw: HUB.instances - .read() - .get(instance_id) - .create_surface_from_layer(layer as *mut _), - } + SurfaceHandle::new(HUB.instances + .read() + .get(instance_id) + .create_surface_from_layer(layer as *mut _) + ) } #[cfg(feature = "local")] @@ -139,9 +134,7 @@ pub fn instance_create_surface_from_windows_hwnd( .create_surface_from_hwnd(hinstance, hwnd); #[cfg_attr(not(target_os = "windows"), allow(unreachable_code))] - SurfaceHandle { - raw, - } + SurfaceHandle::new(raw) } #[cfg(feature = "local")] diff --git a/wgpu-native/src/lib.rs b/wgpu-native/src/lib.rs index c4255ad685..2667a414ea 100644 --- a/wgpu-native/src/lib.rs +++ b/wgpu-native/src/lib.rs @@ -219,5 +219,4 @@ type ComputePassHandle = ComputePass; // Swap chain pub type SurfaceId = hub::Id; type SurfaceHandle = Surface; -pub type SwapChainId = hub::Id; -type SwapChainHandle = SwapChain; +pub type SwapChainId = SurfaceId; diff --git a/wgpu-native/src/swap_chain.rs b/wgpu-native/src/swap_chain.rs index 53552fee71..16a027f32b 100644 --- a/wgpu-native/src/swap_chain.rs +++ b/wgpu-native/src/swap_chain.rs @@ -1,4 +1,5 @@ -use crate::{Stored, WeaklyStored, +use crate::{ + Extent3d, Stored, WeaklyStored, DeviceId, SwapChainId, TextureId, TextureViewId, }; use crate::{conv, resource}; @@ -8,7 +9,7 @@ use crate::track::{TrackPermit}; use hal; use hal::{Device as _Device, Swapchain as _Swapchain}; -use log::trace; +use log::{trace, warn}; use std::{iter, mem}; @@ -23,6 +24,16 @@ pub(crate) struct SwapChainLink { pub struct Surface { pub(crate) raw: B::Surface, + pub(crate) swap_chain: Option>, +} + +impl Surface { + pub(crate) fn new(raw: B::Surface) -> Self { + Surface { + raw, + swap_chain: None, + } + } } pub(crate) struct Frame { @@ -34,6 +45,12 @@ pub(crate) struct Frame { pub comb: hal::command::CommandBuffer, } +pub struct OutdatedFrame { + pub(crate) texture_id: Stored, + pub(crate) view_id: Stored, +} + +const OUTDATED_IMAGE_INDEX: u32 = !0; //TODO: does it need a ref-counted lifetime? pub struct SwapChain { @@ -42,6 +59,7 @@ pub struct SwapChain { pub(crate) frames: Vec>, pub(crate) acquired: Vec, pub(crate) sem_available: B::Semaphore, + pub(crate) outdated: OutdatedFrame, #[cfg_attr(not(feature = "local"), allow(dead_code))] //TODO: remove pub(crate) command_pool: hal::CommandPool, } @@ -54,6 +72,22 @@ pub struct SwapChainDescriptor { pub height: u32, } +impl SwapChainDescriptor { + pub fn to_texture_desc(&self) -> resource::TextureDescriptor { + resource::TextureDescriptor { + size: Extent3d { + width: self.width, + height: self.height, + depth: 1, + }, + array_size: 1, + dimension: resource::TextureDimension::D2, + format: self.format, + usage: self.usage, + } + } +} + #[repr(C)] pub struct SwapChainOutput { pub texture_id: TextureId, @@ -64,37 +98,51 @@ pub struct SwapChainOutput { pub extern "C" fn wgpu_swap_chain_get_next_texture( swap_chain_id: SwapChainId, ) -> SwapChainOutput { - let mut swap_chain_guard = HUB.swap_chains.write(); - let swap_chain = swap_chain_guard.get_mut(swap_chain_id); - assert_ne!(swap_chain.acquired.len(), swap_chain.acquired.capacity(), - "Unable to acquire any more swap chain images before presenting"); - + let mut surface_guard = HUB.surfaces.write(); + let swap_chain = surface_guard + .get_mut(swap_chain_id) + .swap_chain + .as_mut() + .unwrap(); let device_guard = HUB.devices.read(); let device = device_guard.get(swap_chain.device_id.value); - let image_index = unsafe { + assert_ne!(swap_chain.acquired.len(), swap_chain.acquired.capacity(), + "Unable to acquire any more swap chain images before presenting"); + + match { let sync = hal::FrameSync::Semaphore(&swap_chain.sem_available); - swap_chain.raw.acquire_image(!0, sync).unwrap() - }; + unsafe { swap_chain.raw.acquire_image(!0, sync) } + } { + Ok(image_index) => { + swap_chain.acquired.push(image_index); + let frame = &mut swap_chain.frames[image_index as usize]; + unsafe { + device.raw.wait_for_fence(&frame.fence, !0).unwrap(); + } - swap_chain.acquired.push(image_index); - let frame = &mut swap_chain.frames[image_index as usize]; - unsafe { - device.raw.wait_for_fence(&frame.fence, !0).unwrap(); - } + mem::swap(&mut frame.sem_available, &mut swap_chain.sem_available); - mem::swap(&mut frame.sem_available, &mut swap_chain.sem_available); + let texture_guard = HUB.textures.read(); + let texture = texture_guard.get(frame.texture_id.value); + match texture.swap_chain_link { + Some(ref link) => *link.epoch.lock() += 1, + None => unreachable!(), + } - let texture_guard = HUB.textures.read(); - let texture = texture_guard.get(frame.texture_id.value); - match texture.swap_chain_link { - Some(ref link) => *link.epoch.lock() += 1, - None => unreachable!(), - } - - SwapChainOutput { - texture_id: frame.texture_id.value, - view_id: frame.view_id.value, + SwapChainOutput { + texture_id: frame.texture_id.value, + view_id: frame.view_id.value, + } + } + Err(e) => { + warn!("acquire_image failed: {:?}", e); + swap_chain.acquired.push(OUTDATED_IMAGE_INDEX); + SwapChainOutput { + texture_id: swap_chain.outdated.texture_id.value, + view_id: swap_chain.outdated.view_id.value, + } + } } } @@ -102,13 +150,24 @@ pub extern "C" fn wgpu_swap_chain_get_next_texture( pub extern "C" fn wgpu_swap_chain_present( swap_chain_id: SwapChainId, ) { - let mut swap_chain_guard = HUB.swap_chains.write(); - let swap_chain = swap_chain_guard.get_mut(swap_chain_id); - let mut device_guard = HUB.devices.write(); - let device = device_guard.get_mut(swap_chain.device_id.value); + let mut surface_guard = HUB.surfaces.write(); + let swap_chain = surface_guard + .get_mut(swap_chain_id) + .swap_chain + .as_mut() + .unwrap(); let image_index = swap_chain.acquired.remove(0); - let frame = &mut swap_chain.frames[image_index as usize]; + let frame = match swap_chain.frames.get_mut(image_index as usize) { + Some(frame) => frame, + None => { + assert_eq!(image_index, OUTDATED_IMAGE_INDEX); + return + } + }; + + let mut device_guard = HUB.devices.write(); + let device = device_guard.get_mut(swap_chain.device_id.value); let texture_guard = HUB.textures.read(); let texture = texture_guard.get(frame.texture_id.value); @@ -138,7 +197,7 @@ pub extern "C" fn wgpu_swap_chain_present( range: texture.full_range.clone(), }); - unsafe { + let err = unsafe { frame.comb.begin(false); frame.comb.pipeline_barrier( all_image_stages() .. hal::pso::PipelineStage::COLOR_ATTACHMENT_OUTPUT, @@ -157,11 +216,13 @@ pub extern "C" fn wgpu_swap_chain_present( device.raw.reset_fence(&frame.fence).unwrap(); let queue = &mut device.queue_group.queues[0]; queue.submit(submission, Some(&frame.fence)); - queue - .present( - iter::once((&swap_chain.raw, image_index)), - iter::once(&frame.sem_present), - ) - .unwrap(); + queue.present( + iter::once((&swap_chain.raw, image_index)), + iter::once(&frame.sem_present), + ) + }; + + if let Err(e) = err { + warn!("present failed: {:?}", e); } } diff --git a/wgpu-native/src/track.rs b/wgpu-native/src/track.rs index 808f8b7858..f0bd086a04 100644 --- a/wgpu-native/src/track.rs +++ b/wgpu-native/src/track.rs @@ -87,6 +87,11 @@ impl + PartialE } } + /// Remove an id from the tracked map. + pub(crate) fn remove(&mut self, id: I) -> bool { + self.map.remove(&WeaklyStored(id)).is_some() + } + /// Get the last usage on a resource. pub(crate) fn query(&mut self, stored: &Stored, default: U) -> Query { match self.map.entry(WeaklyStored(stored.value.clone())) {