2020-01-23 08:31:56 +00:00
|
|
|
use crate::{
|
|
|
|
legion::{
|
2020-01-24 07:39:56 +00:00
|
|
|
borrow::RefMap,
|
2020-01-23 08:31:56 +00:00
|
|
|
prelude::{Entity, World},
|
|
|
|
},
|
|
|
|
math::Vec4,
|
2020-02-04 05:00:00 +00:00
|
|
|
render::render_graph_2::{
|
|
|
|
wgpu_renderer::DynamicUniformBufferInfo, BindType, ResourceProvider, UniformPropertyType,
|
|
|
|
},
|
2020-01-23 08:31:56 +00:00
|
|
|
};
|
2020-02-04 05:00:00 +00:00
|
|
|
use legion::{prelude::*, storage::Component};
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use std::marker::PhantomData;
|
2020-01-27 09:13:38 +00:00
|
|
|
use zerocopy::AsBytes;
|
2020-01-18 22:09:53 +00:00
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
pub type ShaderUniformSelector = fn(Entity, &World) -> Option<RefMap<&dyn AsUniforms>>;
|
|
|
|
pub struct ShaderUniforms {
|
|
|
|
// used for distinguishing
|
|
|
|
pub uniform_selectors: Vec<ShaderUniformSelector>,
|
|
|
|
}
|
|
|
|
|
2020-01-27 09:13:38 +00:00
|
|
|
impl ShaderUniforms {
|
2020-01-23 08:31:56 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
ShaderUniforms {
|
|
|
|
uniform_selectors: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add(&mut self, selector: ShaderUniformSelector) {
|
|
|
|
self.uniform_selectors.push(selector);
|
|
|
|
}
|
2020-01-27 09:13:38 +00:00
|
|
|
|
|
|
|
pub fn get_uniform_info<'a>(
|
|
|
|
&'a self,
|
|
|
|
world: &'a World,
|
|
|
|
entity: Entity,
|
|
|
|
uniform_name: &str,
|
|
|
|
) -> Option<&'a UniformInfo> {
|
|
|
|
for uniform_selector in self.uniform_selectors.iter().rev() {
|
|
|
|
let uniforms = uniform_selector(entity, world).unwrap_or_else(|| {
|
|
|
|
panic!(
|
|
|
|
"ShaderUniform selector points to a missing component. Uniform: {}",
|
|
|
|
uniform_name
|
|
|
|
)
|
|
|
|
});
|
|
|
|
|
|
|
|
let info = uniforms.get_uniform_info(uniform_name);
|
|
|
|
if let Some(_) = info {
|
2020-02-04 05:00:00 +00:00
|
|
|
return info;
|
2020-01-27 09:13:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_uniform_bytes<'a>(
|
|
|
|
&'a self,
|
|
|
|
world: &'a World,
|
|
|
|
entity: Entity,
|
|
|
|
uniform_name: &str,
|
|
|
|
) -> Option<Vec<u8>> {
|
|
|
|
for uniform_selector in self.uniform_selectors.iter().rev() {
|
|
|
|
let uniforms = uniform_selector(entity, world).unwrap_or_else(|| {
|
|
|
|
panic!(
|
|
|
|
"ShaderUniform selector points to a missing component. Uniform: {}",
|
|
|
|
uniform_name
|
|
|
|
)
|
|
|
|
});
|
|
|
|
|
|
|
|
let bytes = uniforms.get_uniform_bytes(uniform_name);
|
|
|
|
if let Some(_) = bytes {
|
2020-02-04 05:00:00 +00:00
|
|
|
return bytes;
|
2020-01-27 09:13:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
2020-01-23 08:31:56 +00:00
|
|
|
}
|
2020-01-18 22:09:53 +00:00
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
pub struct StandardMaterial {
|
|
|
|
pub albedo: Vec4,
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
pub trait GetBytes {
|
|
|
|
fn get_bytes(&self) -> Vec<u8>;
|
|
|
|
fn get_bytes_ref(&self) -> Option<&[u8]>;
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-24 07:39:56 +00:00
|
|
|
// TODO: might need to add zerocopy to this crate to impl AsBytes for external crates
|
2020-01-27 05:44:01 +00:00
|
|
|
// impl<T> GetBytes for T where T : AsBytes {
|
2020-01-24 07:39:56 +00:00
|
|
|
// fn get_bytes(&self) -> Vec<u8> {
|
|
|
|
// self.as_bytes().into()
|
|
|
|
// }
|
2020-01-18 22:09:53 +00:00
|
|
|
|
2020-01-24 07:39:56 +00:00
|
|
|
// fn get_bytes_ref(&self) -> Option<&[u8]> {
|
|
|
|
// Some(self.as_bytes())
|
|
|
|
// }
|
|
|
|
// }
|
2020-01-18 22:09:53 +00:00
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
impl GetBytes for Vec4 {
|
|
|
|
fn get_bytes(&self) -> Vec<u8> {
|
|
|
|
let vec4_array: [f32; 4] = (*self).into();
|
|
|
|
vec4_array.as_bytes().into()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_bytes_ref(&self) -> Option<&[u8]> {
|
|
|
|
None
|
|
|
|
}
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
pub trait AsUniforms {
|
2020-01-27 09:13:38 +00:00
|
|
|
fn get_uniform_infos(&self) -> &[UniformInfo];
|
|
|
|
fn get_uniform_info(&self, name: &str) -> Option<&UniformInfo>;
|
2020-01-23 08:31:56 +00:00
|
|
|
fn get_uniform_layouts(&self) -> &[&[UniformPropertyType]];
|
2020-01-27 05:44:01 +00:00
|
|
|
fn get_uniform_bytes(&self, name: &str) -> Option<Vec<u8>>;
|
2020-01-23 08:31:56 +00:00
|
|
|
// TODO: support zero-copy uniforms
|
2020-01-27 09:13:38 +00:00
|
|
|
// fn get_uniform_bytes_ref(&self, name: &str) -> Option<&[u8]>;
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 08:31:56 +00:00
|
|
|
// pub struct UniformInfo<'a> {
|
|
|
|
// pub name: &'a str,
|
2020-01-27 09:13:38 +00:00
|
|
|
// pub
|
2020-01-23 08:31:56 +00:00
|
|
|
// }
|
|
|
|
|
2020-01-27 05:44:01 +00:00
|
|
|
pub struct UniformInfo<'a> {
|
2020-01-27 09:13:38 +00:00
|
|
|
pub name: &'a str,
|
|
|
|
pub bind_type: BindType,
|
2020-01-27 05:44:01 +00:00
|
|
|
}
|
|
|
|
|
2020-01-27 09:13:38 +00:00
|
|
|
pub fn uniform_selector<T>(entity: Entity, world: &World) -> Option<RefMap<&dyn AsUniforms>>
|
|
|
|
where
|
|
|
|
T: AsUniforms + Component,
|
|
|
|
{
|
|
|
|
world
|
|
|
|
.get_component::<T>(entity)
|
|
|
|
.map(|c| c.map_into(|s| s as &dyn AsUniforms))
|
2020-01-27 05:44:01 +00:00
|
|
|
}
|
2020-01-23 08:31:56 +00:00
|
|
|
|
2020-01-18 22:09:53 +00:00
|
|
|
// create this from a derive macro
|
2020-01-27 09:13:38 +00:00
|
|
|
const STANDARD_MATERIAL_UNIFORM_INFO: &[UniformInfo] = &[UniformInfo {
|
2020-01-23 08:31:56 +00:00
|
|
|
name: "StandardMaterial",
|
|
|
|
bind_type: BindType::Uniform {
|
2020-01-27 09:13:38 +00:00
|
|
|
dynamic: false,
|
|
|
|
// TODO: fill this in with properties
|
|
|
|
properties: Vec::new(),
|
2020-01-23 08:31:56 +00:00
|
|
|
},
|
2020-01-27 09:13:38 +00:00
|
|
|
}];
|
2020-01-23 08:31:56 +00:00
|
|
|
|
|
|
|
// these are separate from BindType::Uniform{properties} because they need to be const
|
|
|
|
const STANDARD_MATERIAL_UNIFORM_LAYOUTS: &[&[UniformPropertyType]] = &[&[]];
|
|
|
|
|
2020-02-04 05:00:00 +00:00
|
|
|
// const
|
2020-01-23 08:31:56 +00:00
|
|
|
impl AsUniforms for StandardMaterial {
|
2020-01-27 09:13:38 +00:00
|
|
|
fn get_uniform_infos(&self) -> &[UniformInfo] {
|
2020-01-23 08:31:56 +00:00
|
|
|
STANDARD_MATERIAL_UNIFORM_INFO
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
2020-01-23 08:31:56 +00:00
|
|
|
|
|
|
|
fn get_uniform_layouts(&self) -> &[&[UniformPropertyType]] {
|
|
|
|
STANDARD_MATERIAL_UNIFORM_LAYOUTS
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
2020-01-23 08:31:56 +00:00
|
|
|
|
2020-01-27 05:44:01 +00:00
|
|
|
fn get_uniform_bytes(&self, name: &str) -> Option<Vec<u8>> {
|
2020-01-27 09:13:38 +00:00
|
|
|
match name {
|
|
|
|
"StandardMaterial" => Some(self.albedo.get_bytes()),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn get_uniform_info(&self, name: &str) -> Option<&UniformInfo> {
|
|
|
|
match name {
|
|
|
|
"StandardMaterial" => Some(&STANDARD_MATERIAL_UNIFORM_INFO[0]),
|
|
|
|
_ => None,
|
|
|
|
}
|
2020-01-27 05:44:01 +00:00
|
|
|
}
|
2020-01-27 09:13:38 +00:00
|
|
|
|
2020-01-27 05:44:01 +00:00
|
|
|
// fn iter_properties(&self) -> std::slice::Iter<&'static str> {
|
|
|
|
// STANDARD_MATERIAL_PROPERTIES.iter()
|
|
|
|
// }
|
|
|
|
// fn get_property(&self, name: &str) -> Option<ShaderValue> {
|
|
|
|
// match name {
|
|
|
|
// "albedo" => Some(match self.albedo {
|
|
|
|
// Albedo::Color(color) => ShaderValue::Vec4(color),
|
|
|
|
// Albedo::Texture(ref texture) => ShaderValue::Texture(texture)
|
|
|
|
// }),
|
|
|
|
// _ => None,
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
|
|
|
// create this from a derive macro
|
2020-01-27 09:13:38 +00:00
|
|
|
const LOCAL_TO_WORLD_UNIFORM_INFO: &[UniformInfo] = &[UniformInfo {
|
2020-01-27 05:44:01 +00:00
|
|
|
name: "Object",
|
|
|
|
bind_type: BindType::Uniform {
|
2020-01-27 09:13:38 +00:00
|
|
|
dynamic: false,
|
2020-02-04 05:00:00 +00:00
|
|
|
// TODO: maybe fill this in with properties (vec.push cant be const though)
|
2020-01-27 09:13:38 +00:00
|
|
|
properties: Vec::new(),
|
2020-01-27 05:44:01 +00:00
|
|
|
},
|
2020-01-27 09:13:38 +00:00
|
|
|
}];
|
2020-01-27 05:44:01 +00:00
|
|
|
|
|
|
|
// these are separate from BindType::Uniform{properties} because they need to be const
|
|
|
|
const LOCAL_TO_WORLD_UNIFORM_LAYOUTS: &[&[UniformPropertyType]] = &[&[]];
|
|
|
|
|
|
|
|
// const ST
|
|
|
|
impl AsUniforms for bevy_transform::prelude::LocalToWorld {
|
2020-01-27 09:13:38 +00:00
|
|
|
fn get_uniform_infos(&self) -> &[UniformInfo] {
|
2020-01-27 05:44:01 +00:00
|
|
|
LOCAL_TO_WORLD_UNIFORM_INFO
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_uniform_layouts(&self) -> &[&[UniformPropertyType]] {
|
|
|
|
LOCAL_TO_WORLD_UNIFORM_LAYOUTS
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_uniform_bytes(&self, name: &str) -> Option<Vec<u8>> {
|
2020-01-27 09:13:38 +00:00
|
|
|
match name {
|
|
|
|
"Object" => Some(self.0.to_cols_array_2d().as_bytes().into()),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn get_uniform_info(&self, name: &str) -> Option<&UniformInfo> {
|
|
|
|
match name {
|
|
|
|
"Object" => Some(&LOCAL_TO_WORLD_UNIFORM_INFO[0]),
|
|
|
|
_ => None,
|
|
|
|
}
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
2020-01-23 08:31:56 +00:00
|
|
|
// fn iter_properties(&self) -> std::slice::Iter<&'static str> {
|
|
|
|
// STANDARD_MATERIAL_PROPERTIES.iter()
|
|
|
|
// }
|
|
|
|
// fn get_property(&self, name: &str) -> Option<ShaderValue> {
|
|
|
|
// match name {
|
|
|
|
// "albedo" => Some(match self.albedo {
|
|
|
|
// Albedo::Color(color) => ShaderValue::Vec4(color),
|
|
|
|
// Albedo::Texture(ref texture) => ShaderValue::Texture(texture)
|
|
|
|
// }),
|
|
|
|
// _ => None,
|
|
|
|
// }
|
|
|
|
// }
|
2020-01-18 22:09:53 +00:00
|
|
|
}
|
2020-02-04 05:00:00 +00:00
|
|
|
|
|
|
|
pub struct UniformResourceProvider<T>
|
|
|
|
where
|
|
|
|
T: AsUniforms + Send + Sync,
|
|
|
|
{
|
|
|
|
_marker: PhantomData<T>,
|
2020-02-04 17:39:23 +00:00
|
|
|
uniform_buffer_info_names: Vec<String>,
|
2020-02-04 05:00:00 +00:00
|
|
|
// dynamic_uniform_buffer_infos: HashMap<String, DynamicUniformBufferInfo>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> UniformResourceProvider<T>
|
|
|
|
where
|
|
|
|
T: AsUniforms + Send + Sync,
|
|
|
|
{
|
|
|
|
pub fn new() -> Self {
|
|
|
|
UniformResourceProvider {
|
|
|
|
// dynamic_uniform_buffer_infos: HashMap::new(),
|
2020-02-04 17:39:23 +00:00
|
|
|
uniform_buffer_info_names: Vec::new(),
|
2020-02-04 05:00:00 +00:00
|
|
|
_marker: PhantomData,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> ResourceProvider for UniformResourceProvider<T>
|
|
|
|
where
|
|
|
|
T: AsUniforms + Send + Sync + 'static,
|
|
|
|
{
|
2020-02-04 08:06:17 +00:00
|
|
|
fn initialize(&mut self, renderer: &mut dyn super::Renderer, world: &mut World) {}
|
2020-02-04 05:00:00 +00:00
|
|
|
|
|
|
|
fn update(&mut self, renderer: &mut dyn super::Renderer, world: &mut World) {
|
|
|
|
let query = <Read<T>>::query();
|
|
|
|
// retrieve all uniforms buffers that aren't aleady set. these are "dynamic" uniforms, which are set by the user in ShaderUniforms
|
|
|
|
// TODO: this breaks down in multiple ways:
|
2020-02-04 17:39:23 +00:00
|
|
|
// (SOLVED 1) resource_info will be set after the first run so this won't update.
|
2020-02-04 05:00:00 +00:00
|
|
|
// (2) if we create new buffers, the old bind groups will be invalid
|
2020-02-04 08:06:17 +00:00
|
|
|
|
|
|
|
// reset all uniform buffer info counts
|
|
|
|
for name in self.uniform_buffer_info_names.iter() {
|
2020-02-04 17:39:23 +00:00
|
|
|
renderer
|
|
|
|
.get_dynamic_uniform_buffer_info_mut(name)
|
|
|
|
.unwrap()
|
|
|
|
.count = 0;
|
2020-02-04 08:06:17 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 17:39:23 +00:00
|
|
|
let mut sizes = Vec::new();
|
|
|
|
let mut counts = Vec::new();
|
2020-02-04 05:00:00 +00:00
|
|
|
for uniforms in query.iter(world) {
|
|
|
|
let uniform_layouts = uniforms.get_uniform_layouts();
|
|
|
|
for (i, uniform_info) in uniforms.get_uniform_infos().iter().enumerate() {
|
2020-02-04 17:39:23 +00:00
|
|
|
// only add the first time a uniform info is processed
|
|
|
|
if self.uniform_buffer_info_names.len() <= i {
|
2020-02-04 05:00:00 +00:00
|
|
|
let uniform_layout = uniform_layouts[i];
|
2020-02-04 17:39:23 +00:00
|
|
|
let size = uniform_layout
|
2020-02-04 08:06:17 +00:00
|
|
|
.iter()
|
|
|
|
.map(|u| u.get_size())
|
|
|
|
.fold(0, |total, current| total + current);
|
2020-02-04 17:39:23 +00:00
|
|
|
sizes.push(size);
|
|
|
|
|
2020-02-04 08:06:17 +00:00
|
|
|
self.uniform_buffer_info_names
|
2020-02-04 17:39:23 +00:00
|
|
|
.push(uniform_info.name.to_string());
|
|
|
|
}
|
|
|
|
|
|
|
|
if counts.len() <= i {
|
|
|
|
counts.push(0);
|
2020-02-04 05:00:00 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 17:39:23 +00:00
|
|
|
counts[i] += 1;
|
2020-02-04 05:00:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-04 17:39:23 +00:00
|
|
|
// create and update uniform buffer info. this is separate from the last block to avoid
|
|
|
|
// the expense of hashing for large numbers of entities
|
|
|
|
for (i, name) in self.uniform_buffer_info_names.iter().enumerate() {
|
|
|
|
if let None = renderer.get_dynamic_uniform_buffer_info(name) {
|
|
|
|
let mut info = DynamicUniformBufferInfo::new();
|
|
|
|
info.size = sizes[i];
|
|
|
|
renderer.add_dynamic_uniform_buffer_info(name, info);
|
|
|
|
}
|
|
|
|
|
|
|
|
let info = renderer.get_dynamic_uniform_buffer_info_mut(name).unwrap();
|
|
|
|
info.count = counts[i];
|
|
|
|
}
|
|
|
|
|
2020-02-04 05:00:00 +00:00
|
|
|
// allocate uniform buffers
|
2020-02-04 08:06:17 +00:00
|
|
|
for name in self.uniform_buffer_info_names.iter() {
|
|
|
|
if let Some(_) = renderer.get_resource_info(name) {
|
|
|
|
continue;
|
|
|
|
}
|
2020-02-04 05:00:00 +00:00
|
|
|
|
2020-02-04 08:06:17 +00:00
|
|
|
let info = renderer.get_dynamic_uniform_buffer_info_mut(name).unwrap();
|
|
|
|
|
|
|
|
// allocate enough space for twice as many entities as there are currently;
|
|
|
|
info.capacity = info.count * 2;
|
|
|
|
let size = wgpu::BIND_BUFFER_ALIGNMENT * info.capacity;
|
|
|
|
renderer.create_buffer(
|
|
|
|
name,
|
|
|
|
size,
|
|
|
|
wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::UNIFORM,
|
|
|
|
);
|
|
|
|
}
|
2020-02-04 05:00:00 +00:00
|
|
|
|
|
|
|
// copy entity uniform data to buffers
|
|
|
|
for name in self.uniform_buffer_info_names.iter() {
|
2020-02-04 08:06:17 +00:00
|
|
|
let size = {
|
|
|
|
let info = renderer.get_dynamic_uniform_buffer_info(name).unwrap();
|
|
|
|
wgpu::BIND_BUFFER_ALIGNMENT * info.count
|
|
|
|
};
|
2020-02-04 05:00:00 +00:00
|
|
|
|
|
|
|
let alignment = wgpu::BIND_BUFFER_ALIGNMENT as usize;
|
|
|
|
let mut offset = 0usize;
|
2020-02-04 17:39:23 +00:00
|
|
|
let info = renderer.get_dynamic_uniform_buffer_info_mut(name).unwrap();
|
2020-02-04 08:06:17 +00:00
|
|
|
for (i, (entity, _uniforms)) in query.iter_entities(world).enumerate() {
|
2020-02-04 05:00:00 +00:00
|
|
|
// TODO: check if index has changed. if it has, then entity should be updated
|
|
|
|
// TODO: only mem-map entities if their data has changed
|
2020-02-05 02:48:42 +00:00
|
|
|
// PERF: These hashmap inserts are pretty expensive (10 fps for 10000 entities)
|
2020-02-04 05:00:00 +00:00
|
|
|
info.offsets.insert(entity, offset as u64);
|
|
|
|
info.indices.insert(i, entity);
|
|
|
|
// TODO: try getting ref first
|
2020-02-04 08:06:17 +00:00
|
|
|
offset += alignment;
|
2020-02-04 05:00:00 +00:00
|
|
|
}
|
2020-02-04 08:06:17 +00:00
|
|
|
|
|
|
|
// let mut data = vec![Default::default(); size as usize];
|
|
|
|
renderer.create_buffer_mapped(
|
|
|
|
"tmp_uniform_mapped",
|
|
|
|
size as usize,
|
|
|
|
wgpu::BufferUsage::COPY_SRC,
|
|
|
|
&mut |mapped| {
|
|
|
|
let alignment = wgpu::BIND_BUFFER_ALIGNMENT as usize;
|
|
|
|
let mut offset = 0usize;
|
|
|
|
for uniforms in query.iter(world) {
|
|
|
|
// TODO: check if index has changed. if it has, then entity should be updated
|
|
|
|
// TODO: only mem-map entities if their data has changed
|
|
|
|
// TODO: try getting ref first
|
|
|
|
if let Some(uniform_bytes) = uniforms.get_uniform_bytes(name) {
|
|
|
|
mapped[offset..(offset + uniform_bytes.len())]
|
|
|
|
.copy_from_slice(uniform_bytes.as_slice());
|
|
|
|
offset += alignment;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
renderer.copy_buffer_to_buffer("tmp_uniform_mapped", 0, name, 0, size);
|
2020-02-04 05:00:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn resize(
|
|
|
|
&mut self,
|
|
|
|
renderer: &mut dyn super::Renderer,
|
|
|
|
world: &mut World,
|
|
|
|
width: u32,
|
|
|
|
height: u32,
|
|
|
|
) {
|
|
|
|
}
|
|
|
|
}
|