typed primitive buffers + macro for creation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ActiveSensors, SensorMap, UiRegion, WidgetId, Widgets,
|
ActiveSensors, SensorMap, UiRegion, WidgetId, Widgets,
|
||||||
primitive::{PrimitiveData, PrimitiveInstance, Primitives},
|
primitive::{Primitive, Primitives},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Painter<'a, Ctx: 'static> {
|
pub struct Painter<'a, Ctx: 'static> {
|
||||||
@@ -28,16 +28,8 @@ impl<'a, Ctx> Painter<'a, Ctx> {
|
|||||||
region: UiRegion::full(),
|
region: UiRegion::full(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn write<Data: PrimitiveData>(&mut self, data: Data) {
|
pub fn write<P: Primitive>(&mut self, data: P) {
|
||||||
let ptr = self.primitives.data.len() as u32;
|
self.primitives.write(data, self.region);
|
||||||
let region = self.region;
|
|
||||||
self.primitives
|
|
||||||
.instances
|
|
||||||
.push(PrimitiveInstance { region, ptr });
|
|
||||||
self.primitives.data.push(Data::DISCRIM);
|
|
||||||
self.primitives
|
|
||||||
.data
|
|
||||||
.extend_from_slice(bytemuck::cast_slice::<_, u32>(&[data]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw<W>(&mut self, id: &WidgetId<W>)
|
pub fn draw<W>(&mut self, id: &WidgetId<W>)
|
||||||
|
|||||||
@@ -13,16 +13,18 @@ pub struct WindowUniform {
|
|||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct PrimitiveInstance {
|
pub struct PrimitiveInstance {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub ptr: u32,
|
pub binding: u32,
|
||||||
|
pub idx: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveInstance {
|
impl PrimitiveInstance {
|
||||||
const ATTRIBS: [VertexAttribute; 5] = wgpu::vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 6] = wgpu::vertex_attr_array![
|
||||||
0 => Float32x2,
|
0 => Float32x2,
|
||||||
1 => Float32x2,
|
1 => Float32x2,
|
||||||
2 => Float32x2,
|
2 => Float32x2,
|
||||||
3 => Float32x2,
|
3 => Float32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
|
5 => Uint32,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
primitive::{PrimitiveInstance, Primitives},
|
primitive::{PrimitiveBuffers, Primitives},
|
||||||
render::util::ArrBuf,
|
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||||
};
|
};
|
||||||
use data::WindowUniform;
|
use data::WindowUniform;
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
@@ -16,40 +16,34 @@ mod util;
|
|||||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||||
|
|
||||||
pub struct UIRenderNode {
|
pub struct UIRenderNode {
|
||||||
bind_group_layout: BindGroupLayout,
|
layout0: BindGroupLayout,
|
||||||
bind_group: BindGroup,
|
group0: BindGroup,
|
||||||
|
primitive_layout: BindGroupLayout,
|
||||||
|
primitive_group: BindGroup,
|
||||||
pipeline: RenderPipeline,
|
pipeline: RenderPipeline,
|
||||||
|
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
instance: ArrBuf<PrimitiveInstance>,
|
instance: ArrBuf<PrimitiveInstance>,
|
||||||
data: ArrBuf<u32>,
|
primitives: PrimitiveBuffers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UIRenderNode {
|
impl UIRenderNode {
|
||||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||||
pass.set_pipeline(&self.pipeline);
|
|
||||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
|
||||||
if self.instance.len() != 0 {
|
if self.instance.len() != 0 {
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_bind_group(0, &self.group0, &[]);
|
||||||
|
pass.set_bind_group(1, &self.primitive_group, &[]);
|
||||||
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
|
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
|
||||||
pass.draw(0..4, 0..self.instance.len() as u32);
|
pass.draw(0..4, 0..self.instance.len() as u32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(&mut self, device: &Device, queue: &Queue, primitives: Option<&Primitives>) {
|
||||||
&mut self,
|
|
||||||
device: &Device,
|
|
||||||
queue: &Queue,
|
|
||||||
primitives: Option<&Primitives>,
|
|
||||||
) {
|
|
||||||
if let Some(primitives) = primitives {
|
if let Some(primitives) = primitives {
|
||||||
self.instance.update(device, queue, &primitives.instances);
|
self.instance.update(device, queue, &primitives.instances);
|
||||||
self.data.update(device, queue, &primitives.data);
|
self.primitives.update(device, queue, &primitives.data);
|
||||||
self.bind_group = Self::bind_group(
|
self.primitive_group =
|
||||||
device,
|
Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers())
|
||||||
&self.bind_group_layout,
|
|
||||||
&self.window_buffer,
|
|
||||||
&self.data.buffer,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +63,7 @@ impl UIRenderNode {
|
|||||||
|
|
||||||
let window_uniform = WindowUniform::default();
|
let window_uniform = WindowUniform::default();
|
||||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||||
label: Some("Camera Buffer"),
|
label: Some("window"),
|
||||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||||
});
|
});
|
||||||
@@ -79,26 +73,28 @@ impl UIRenderNode {
|
|||||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||||
"instance",
|
"instance",
|
||||||
);
|
);
|
||||||
let data = ArrBuf::new(
|
let primitives = PrimitiveBuffers::new(device);
|
||||||
device,
|
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
||||||
"data",
|
|
||||||
);
|
|
||||||
|
|
||||||
let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
let layout0 = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[BindGroupLayoutEntry {
|
||||||
BindGroupLayoutEntry {
|
binding: 0,
|
||||||
binding: 0,
|
visibility: ShaderStages::VERTEX,
|
||||||
visibility: ShaderStages::VERTEX,
|
ty: BindingType::Buffer {
|
||||||
ty: BindingType::Buffer {
|
ty: BufferBindingType::Uniform,
|
||||||
ty: BufferBindingType::Uniform,
|
has_dynamic_offset: false,
|
||||||
has_dynamic_offset: false,
|
min_binding_size: None,
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
label: Some("window"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let group0 = Self::bind_group_0(device, &layout0, &window_buffer);
|
||||||
|
|
||||||
|
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
|
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: i as u32,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
@@ -106,16 +102,17 @@ impl UIRenderNode {
|
|||||||
min_binding_size: None,
|
min_binding_size: None,
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
}
|
||||||
],
|
}),
|
||||||
label: Some("camera_bind_group_layout"),
|
label: Some("primitive"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let bind_group = Self::bind_group(device, &bind_group_layout, &window_buffer, &data.buffer);
|
let primitive_group =
|
||||||
|
Self::primitive_group(device, &primitive_layout, primitives.buffers());
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
label: Some("UI Shape Pipeline Layout"),
|
label: Some("UI Shape Pipeline Layout"),
|
||||||
bind_group_layouts: &[&bind_group_layout],
|
bind_group_layouts: &[&layout0, &primitive_layout],
|
||||||
push_constant_ranges: &[],
|
push_constant_ranges: &[],
|
||||||
});
|
});
|
||||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
@@ -157,34 +154,44 @@ impl UIRenderNode {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
bind_group_layout,
|
layout0,
|
||||||
bind_group,
|
group0,
|
||||||
|
primitive_layout,
|
||||||
|
primitive_group,
|
||||||
pipeline,
|
pipeline,
|
||||||
window_buffer,
|
window_buffer,
|
||||||
instance,
|
instance,
|
||||||
data,
|
primitives,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_group(
|
pub fn bind_group_0(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
window_buffer: &Buffer,
|
window_buffer: &Buffer,
|
||||||
data: &Buffer,
|
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
entries: &[
|
entries: &[BindGroupEntry {
|
||||||
BindGroupEntry {
|
binding: 0,
|
||||||
binding: 0,
|
resource: window_buffer.as_entire_binding(),
|
||||||
resource: window_buffer.as_entire_binding(),
|
}],
|
||||||
},
|
label: Some("ui window"),
|
||||||
BindGroupEntry {
|
})
|
||||||
binding: 1,
|
}
|
||||||
resource: data.as_entire_binding(),
|
|
||||||
},
|
pub fn primitive_group(
|
||||||
],
|
device: &Device,
|
||||||
label: Some("ui_bind_group"),
|
layout: &BindGroupLayout,
|
||||||
|
buffers: [&Buffer; PrimitiveBuffers::LEN],
|
||||||
|
) -> BindGroup {
|
||||||
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
layout,
|
||||||
|
entries: &buffers.each_ref().map(|b| BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: b.as_entire_binding(),
|
||||||
|
}),
|
||||||
|
label: Some("ui primitives"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,93 @@
|
|||||||
use crate::Color;
|
use crate::{
|
||||||
|
Color, UiRegion,
|
||||||
pub use super::data::PrimitiveInstance;
|
render::{ArrBuf, data::PrimitiveInstance},
|
||||||
|
};
|
||||||
|
use bytemuck::Pod;
|
||||||
|
use wgpu::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Primitives {
|
pub struct Primitives {
|
||||||
pub instances: Vec<PrimitiveInstance>,
|
pub(super) instances: Vec<PrimitiveInstance>,
|
||||||
pub data: Vec<u32>,
|
pub(super) data: PrimitiveData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: Self must have at least u32 alignment
|
pub trait Primitive: Pod {
|
||||||
pub trait PrimitiveData: bytemuck::Pod {
|
const BINDING: u32;
|
||||||
const DISCRIM: u32;
|
fn vec(data: &mut PrimitiveData) -> &mut Vec<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! primitives {
|
||||||
|
($($name:ident: $ty:ty => $binding:expr,)*) => {
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PrimitiveData {
|
||||||
|
$($name: Vec<$ty>,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PrimitiveBuffers {
|
||||||
|
$($name: ArrBuf<$ty>,)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveBuffers {
|
||||||
|
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
|
||||||
|
$(self.$name.update(device, queue, &data.$name);)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveBuffers {
|
||||||
|
pub const LEN: usize = primitives!(@count $($name)*);
|
||||||
|
pub fn buffers(&self) -> [&Buffer; Self::LEN] {
|
||||||
|
[
|
||||||
|
$(&self.$name.buffer)*
|
||||||
|
]
|
||||||
|
}
|
||||||
|
pub fn new(device: &Device) -> Self {
|
||||||
|
Self {
|
||||||
|
$($name: ArrBuf::new(
|
||||||
|
device,
|
||||||
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
|
stringify!($name),
|
||||||
|
))*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(
|
||||||
|
unsafe impl bytemuck::Pod for $ty {}
|
||||||
|
unsafe impl bytemuck::Zeroable for $ty {}
|
||||||
|
impl Primitive for $ty {
|
||||||
|
const BINDING: u32 = $binding;
|
||||||
|
fn vec(data: &mut PrimitiveData) -> &mut Vec<Self> {
|
||||||
|
&mut data.$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
(@count $t1:tt, $($t:tt),+) => { 1 + gen!(@count $($t),+) };
|
||||||
|
(@count $t:tt) => { 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
primitives!(
|
||||||
|
rects: RoundedRectData => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Primitives {
|
||||||
|
pub fn write<P: Primitive>(&mut self, data: P, region: UiRegion) {
|
||||||
|
let vec = P::vec(&mut self.data);
|
||||||
|
let i = vec.len() as u32;
|
||||||
|
self.instances.push(PrimitiveInstance {
|
||||||
|
region,
|
||||||
|
idx: i,
|
||||||
|
binding: P::BINDING,
|
||||||
|
});
|
||||||
|
vec.push(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone)]
|
||||||
pub struct RoundedRectData {
|
pub struct RoundedRectData {
|
||||||
pub color: Color<u8>,
|
pub color: Color<u8>,
|
||||||
pub radius: f32,
|
pub radius: f32,
|
||||||
pub thickness: f32,
|
pub thickness: f32,
|
||||||
pub inner_radius: f32,
|
pub inner_radius: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveData for RoundedRectData {
|
|
||||||
const DISCRIM: u32 = 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
const RECT: u32 = 0;
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> window: WindowUniform;
|
var<uniform> window: WindowUniform;
|
||||||
@group(0) @binding(1)
|
@group(1) @binding(RECT)
|
||||||
var<storage> data: array<u32>;
|
var<storage> rects: array<RoundedRect>;
|
||||||
|
|
||||||
struct WindowUniform {
|
struct WindowUniform {
|
||||||
dim: vec2<f32>,
|
dim: vec2<f32>,
|
||||||
@@ -12,7 +14,8 @@ struct InstanceInput {
|
|||||||
@location(1) top_left_offset: vec2<f32>,
|
@location(1) top_left_offset: vec2<f32>,
|
||||||
@location(2) bottom_right_anchor: vec2<f32>,
|
@location(2) bottom_right_anchor: vec2<f32>,
|
||||||
@location(3) bottom_right_offset: vec2<f32>,
|
@location(3) bottom_right_offset: vec2<f32>,
|
||||||
@location(4) pointer: u32,
|
@location(4) binding: u32,
|
||||||
|
@location(5) idx: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoundedRect {
|
struct RoundedRect {
|
||||||
@@ -23,9 +26,10 @@ struct RoundedRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@location(0) pointer: u32,
|
@location(0) top_left: vec2<f32>,
|
||||||
@location(1) top_left: vec2<f32>,
|
@location(1) bot_right: vec2<f32>,
|
||||||
@location(2) bot_right: vec2<f32>,
|
@location(2) binding: u32,
|
||||||
|
@location(3) idx: u32,
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +56,8 @@ fn vs_main(
|
|||||||
) * size;
|
) * size;
|
||||||
pos = pos / window.dim * 2.0 - 1.0;
|
pos = pos / window.dim * 2.0 - 1.0;
|
||||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||||
out.pointer = in.pointer;
|
out.binding = in.binding;
|
||||||
|
out.idx = in.idx;
|
||||||
out.top_left = top_left;
|
out.top_left = top_left;
|
||||||
out.bot_right = bot_right;
|
out.bot_right = bot_right;
|
||||||
|
|
||||||
@@ -64,25 +69,20 @@ fn fs_main(
|
|||||||
in: VertexOutput
|
in: VertexOutput
|
||||||
) -> @location(0) vec4<f32> {
|
) -> @location(0) vec4<f32> {
|
||||||
let pos = in.clip_position.xy;
|
let pos = in.clip_position.xy;
|
||||||
let ty = data[in.pointer];
|
|
||||||
let dp = in.pointer + 1u;
|
|
||||||
let region = Region(pos, in.top_left, in.bot_right);
|
let region = Region(pos, in.top_left, in.bot_right);
|
||||||
switch ty {
|
let i = in.idx;
|
||||||
case 0u: {
|
switch in.binding {
|
||||||
return draw_rounded_rect(region, RoundedRect(
|
case RECT: {
|
||||||
data[dp + 0u],
|
return draw_rounded_rect(region, rects[i]);
|
||||||
bitcast<f32>(data[dp + 1u]),
|
}
|
||||||
bitcast<f32>(data[dp + 2u]),
|
default: {
|
||||||
bitcast<f32>(data[dp + 3u]),
|
return vec4(1.0, 0.0, 1.0, 1.0);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
default: {}
|
|
||||||
}
|
}
|
||||||
return vec4(1.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
|
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
|
||||||
var color = unpack4x8unorm(rect.color);
|
var color = read_color(rect.color);
|
||||||
|
|
||||||
let edge = 0.5;
|
let edge = 0.5;
|
||||||
|
|
||||||
@@ -108,3 +108,21 @@ fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner:
|
|||||||
let q = abs(p) - (rect_corner - radius);
|
let q = abs(p) - (rect_corner - radius);
|
||||||
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
|
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_color(c: u32) -> vec4<f32> {
|
||||||
|
let color = unpack4x8unorm(c);
|
||||||
|
return vec4(
|
||||||
|
srgb_to_linear(color.r),
|
||||||
|
srgb_to_linear(color.g),
|
||||||
|
srgb_to_linear(color.b),
|
||||||
|
color.a,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn srgb_to_linear(c: f32) -> f32 {
|
||||||
|
if c <= 0.04045 {
|
||||||
|
return c / 12.92;
|
||||||
|
} else {
|
||||||
|
return pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user