improving draw line
This commit is contained in:
@@ -1016,7 +1016,7 @@ pub fn writeFloat4(c: F32x4, map: []u8, dst_format: vk.Format) void {
|
|||||||
(@as(u32, a) << 30);
|
(@as(u32, a) << 30);
|
||||||
},
|
},
|
||||||
|
|
||||||
.r32g32b32a32_uint => std.mem.bytesAsValue(U32x4, map).* = @intFromFloat(@round(@as(@Vector(4, f64), color) * @as(@Vector(4, f64), @splat(std.math.maxInt(u32))))),
|
.r32g32b32a32_uint => std.mem.bytesAsValue(U32x4, map).* = @intFromFloat(@round(@as(@Vector(4, f64), color))),
|
||||||
|
|
||||||
.r32g32b32a32_sfloat => std.mem.bytesAsValue(F32x4, map).* = color,
|
.r32g32b32a32_sfloat => std.mem.bytesAsValue(F32x4, map).* = color,
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ const ClipPlane = enum {
|
|||||||
|
|
||||||
const MAX_CLIPPED_POLYGON_VERTICES = 16;
|
const MAX_CLIPPED_POLYGON_VERTICES = 16;
|
||||||
|
|
||||||
|
pub const ClippedLine = struct {
|
||||||
|
v0: Vertex,
|
||||||
|
v1: Vertex,
|
||||||
|
};
|
||||||
|
|
||||||
const ClippedPolygon = struct {
|
const ClippedPolygon = struct {
|
||||||
vertices: [MAX_CLIPPED_POLYGON_VERTICES]Vertex = undefined,
|
vertices: [MAX_CLIPPED_POLYGON_VERTICES]Vertex = undefined,
|
||||||
len: usize = 0,
|
len: usize = 0,
|
||||||
@@ -59,6 +64,46 @@ pub fn clipTriangle(allocator: std.mem.Allocator, v0: *const Vertex, v1: *const
|
|||||||
return polygon;
|
return polygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clipLine(allocator: std.mem.Allocator, v0: *const Vertex, v1: *const Vertex) VkError!?ClippedLine {
|
||||||
|
var line: ClippedLine = .{
|
||||||
|
.v0 = v0.*,
|
||||||
|
.v1 = v1.*,
|
||||||
|
};
|
||||||
|
|
||||||
|
const planes = [_]ClipPlane{
|
||||||
|
.Left,
|
||||||
|
.Right,
|
||||||
|
.Bottom,
|
||||||
|
.Top,
|
||||||
|
.Near,
|
||||||
|
.Far,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (planes) |plane| {
|
||||||
|
const v0_distance = clipDistance(line.v0.position, plane);
|
||||||
|
const v1_distance = clipDistance(line.v1.position, plane);
|
||||||
|
const v0_inside = v0_distance >= 0.0;
|
||||||
|
const v1_inside = v1_distance >= 0.0;
|
||||||
|
|
||||||
|
if (!v0_inside and !v1_inside)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (v0_inside and v1_inside)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const t = v0_distance / (v0_distance - v1_distance);
|
||||||
|
const clipped_vertex = try interpolateVertexForClipping(allocator, &line.v0, &line.v1, t);
|
||||||
|
|
||||||
|
if (v0_inside) {
|
||||||
|
line.v1 = clipped_vertex;
|
||||||
|
} else {
|
||||||
|
line.v0 = clipped_vertex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn viewportTransformVertex(viewport: vk.Viewport, vertex: *Vertex) void {
|
pub fn viewportTransformVertex(viewport: vk.Viewport, vertex: *Vertex) void {
|
||||||
const x, const y, const z, const w = vertex.position;
|
const x, const y, const z, const w = vertex.position;
|
||||||
|
|
||||||
|
|||||||
@@ -133,12 +133,67 @@ pub fn processThenFragmentStage(renderer: *Renderer, allocator: std.mem.Allocato
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
.line_list => for (0..@divTrunc(draw_call.vertices.len, 2)) |line_index| {
|
||||||
|
const first_vertex = line_index * 2;
|
||||||
|
const v0 = &draw_call.vertices[first_vertex + 0];
|
||||||
|
const v1 = &draw_call.vertices[first_vertex + 1];
|
||||||
|
|
||||||
|
try clipTransformAndRasterizeLine(
|
||||||
|
allocator,
|
||||||
|
draw_call,
|
||||||
|
v0,
|
||||||
|
v1,
|
||||||
|
&color_attachment_access,
|
||||||
|
if (depth_attachment_access) |*access| access else null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
.line_strip => if (draw_call.vertices.len >= 2) {
|
||||||
|
for (0..(draw_call.vertices.len - 1)) |vertex_index| {
|
||||||
|
const v0 = &draw_call.vertices[vertex_index + 0];
|
||||||
|
const v1 = &draw_call.vertices[vertex_index + 1];
|
||||||
|
|
||||||
|
try clipTransformAndRasterizeLine(
|
||||||
|
allocator,
|
||||||
|
draw_call,
|
||||||
|
v0,
|
||||||
|
v1,
|
||||||
|
&color_attachment_access,
|
||||||
|
if (depth_attachment_access) |*access| access else null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
else => base.unsupported("primitive topology {any}", .{topology}),
|
else => base.unsupported("primitive topology {any}", .{topology}),
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_call.rasterizer_wait_group.await(io) catch return VkError.DeviceLost;
|
draw_call.rasterizer_wait_group.await(io) catch return VkError.DeviceLost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clipTransformAndRasterizeLine(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
draw_call: *DrawCall,
|
||||||
|
v0: *Vertex,
|
||||||
|
v1: *Vertex,
|
||||||
|
color_attachment_access: *const common.RenderTargetAccess,
|
||||||
|
depth_attachment_access: ?*common.RenderTargetAccess,
|
||||||
|
) VkError!void {
|
||||||
|
const clipped_line = (try clip.clipLine(allocator, v0, v1)) orelse return;
|
||||||
|
|
||||||
|
var tv0 = clipped_line.v0;
|
||||||
|
var tv1 = clipped_line.v1;
|
||||||
|
|
||||||
|
clip.viewportTransformVertex(draw_call.viewport, &tv0);
|
||||||
|
clip.viewportTransformVertex(draw_call.viewport, &tv1);
|
||||||
|
|
||||||
|
try bresenham.drawLine(
|
||||||
|
allocator,
|
||||||
|
draw_call,
|
||||||
|
&tv0,
|
||||||
|
&tv1,
|
||||||
|
color_attachment_access,
|
||||||
|
depth_attachment_access,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn clipTransformAndRasterizeTriangle(
|
fn clipTransformAndRasterizeTriangle(
|
||||||
renderer: *Renderer,
|
renderer: *Renderer,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
@@ -193,19 +248,11 @@ fn rasterizeTriangle(
|
|||||||
|
|
||||||
const pipeline_data = (renderer.state.pipeline orelse return VkError.InvalidHandleDrv).interface.mode.graphics;
|
const pipeline_data = (renderer.state.pipeline orelse return VkError.InvalidHandleDrv).interface.mode.graphics;
|
||||||
switch (pipeline_data.rasterization.polygon_mode) {
|
switch (pipeline_data.rasterization.polygon_mode) {
|
||||||
.fill => try edge_function.drawTriangle(
|
.fill => try edge_function.drawTriangle(allocator, draw_call, v0, v1, v2, color_attachment_access, depth_attachment_access),
|
||||||
allocator,
|
|
||||||
draw_call,
|
|
||||||
v0,
|
|
||||||
v1,
|
|
||||||
v2,
|
|
||||||
color_attachment_access,
|
|
||||||
depth_attachment_access,
|
|
||||||
),
|
|
||||||
.line => {
|
.line => {
|
||||||
try bresenham.drawLine(allocator, draw_call, v0, v1);
|
try bresenham.drawLine(allocator, draw_call, v0, v1, color_attachment_access, depth_attachment_access);
|
||||||
try bresenham.drawLine(allocator, draw_call, v1, v2);
|
try bresenham.drawLine(allocator, draw_call, v1, v2, color_attachment_access, depth_attachment_access);
|
||||||
try bresenham.drawLine(allocator, draw_call, v2, v0);
|
try bresenham.drawLine(allocator, draw_call, v2, v0, color_attachment_access, depth_attachment_access);
|
||||||
},
|
},
|
||||||
.point => {}, // TODO
|
.point => {}, // TODO
|
||||||
else => base.unsupported("polygon mode {any}", .{pipeline_data.rasterization.polygon_mode}),
|
else => base.unsupported("polygon mode {any}", .{pipeline_data.rasterization.polygon_mode}),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const base = @import("base");
|
|||||||
const spv = @import("spv");
|
const spv = @import("spv");
|
||||||
const zm = base.zm;
|
const zm = base.zm;
|
||||||
|
|
||||||
|
const blitter = @import("../blitter.zig");
|
||||||
const common = @import("common.zig");
|
const common = @import("common.zig");
|
||||||
const fragment = @import("../fragment.zig");
|
const fragment = @import("../fragment.zig");
|
||||||
|
|
||||||
@@ -27,9 +28,18 @@ const RunData = struct {
|
|||||||
end_vertex: *Renderer.Vertex,
|
end_vertex: *Renderer.Vertex,
|
||||||
start_step: usize,
|
start_step: usize,
|
||||||
end_step: usize,
|
end_step: usize,
|
||||||
|
color_attachment_access: *const common.RenderTargetAccess,
|
||||||
|
depth_attachment_access: ?*common.RenderTargetAccess,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn drawLine(allocator: std.mem.Allocator, draw_call: *Renderer.DrawCall, v0: *Renderer.Vertex, v1: *Renderer.Vertex) VkError!void {
|
pub fn drawLine(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
draw_call: *Renderer.DrawCall,
|
||||||
|
v0: *Renderer.Vertex,
|
||||||
|
v1: *Renderer.Vertex,
|
||||||
|
color_attachment_access: *const common.RenderTargetAccess,
|
||||||
|
depth_attachment_access: ?*common.RenderTargetAccess,
|
||||||
|
) VkError!void {
|
||||||
const io = draw_call.renderer.device.interface.io();
|
const io = draw_call.renderer.device.interface.io();
|
||||||
|
|
||||||
var x0: i32 = @intFromFloat(v0.position[0]);
|
var x0: i32 = @intFromFloat(v0.position[0]);
|
||||||
@@ -60,7 +70,6 @@ pub fn drawLine(allocator: std.mem.Allocator, draw_call: *Renderer.DrawCall, v0:
|
|||||||
|
|
||||||
const pipeline = draw_call.renderer.state.pipeline orelse return;
|
const pipeline = draw_call.renderer.state.pipeline orelse return;
|
||||||
|
|
||||||
var wg: std.Io.Group = .init;
|
|
||||||
const runtimes_count = (pipeline.stages.getPtr(.fragment) orelse return).runtimes.len;
|
const runtimes_count = (pipeline.stages.getPtr(.fragment) orelse return).runtimes.len;
|
||||||
if (runtimes_count == 0)
|
if (runtimes_count == 0)
|
||||||
return;
|
return;
|
||||||
@@ -93,11 +102,17 @@ pub fn drawLine(allocator: std.mem.Allocator, draw_call: *Renderer.DrawCall, v0:
|
|||||||
.end_vertex = end_vertex,
|
.end_vertex = end_vertex,
|
||||||
.start_step = start_step,
|
.start_step = start_step,
|
||||||
.end_step = end_step,
|
.end_step = end_step,
|
||||||
|
.color_attachment_access = color_attachment_access,
|
||||||
|
.depth_attachment_access = depth_attachment_access,
|
||||||
};
|
};
|
||||||
|
|
||||||
wg.async(io, runWrapper, .{run_data});
|
draw_call.rasterizer_wait_group.async(io, runWrapper, .{run_data});
|
||||||
}
|
}
|
||||||
wg.await(io) catch return VkError.DeviceLost;
|
|
||||||
|
// Not syncing workers between triangles when rendering without depth buffer
|
||||||
|
// will lead to pixel rendering order issues between triangles.
|
||||||
|
if (depth_attachment_access == null)
|
||||||
|
draw_call.rasterizer_wait_group.await(io) catch return VkError.DeviceLost;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bresenhamYAtStep(y0: i32, d_x: i32, d_err: i32, y_step: i32, step: usize) i32 {
|
fn bresenhamYAtStep(y0: i32, d_x: i32, d_err: i32, y_step: i32, step: usize) i32 {
|
||||||
@@ -121,10 +136,6 @@ fn runWrapper(data: RunData) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fn run(data: RunData) !void {
|
inline fn run(data: RunData) !void {
|
||||||
const color_attachment = if (data.draw_call.render_pass.interface.subpasses[data.draw_call.renderer.subpass_index].color_attachments) |attachments| attachments[0].attachment else return VkError.InvalidAttachmentDrv;
|
|
||||||
const render_target_view: *base.ImageView = data.draw_call.color_attachments[color_attachment];
|
|
||||||
const render_target: *SoftImage = @alignCast(@fieldParentPtr("interface", render_target_view.image));
|
|
||||||
|
|
||||||
var step = data.start_step;
|
var step = data.start_step;
|
||||||
while (step <= data.end_step) : (step += 1) {
|
while (step <= data.end_step) : (step += 1) {
|
||||||
const x = data.x0 + @as(i32, @intCast(step));
|
const x = data.x0 + @as(i32, @intCast(step));
|
||||||
@@ -140,7 +151,15 @@ inline fn run(data: RunData) !void {
|
|||||||
const t = @as(f32, @floatFromInt(step)) / @as(f32, @floatFromInt(@max(data.d_x, 1)));
|
const t = @as(f32, @floatFromInt(step)) / @as(f32, @floatFromInt(@max(data.d_x, 1)));
|
||||||
const z = ((1.0 - t) * data.start_vertex.position[2]) + (t * data.end_vertex.position[2]);
|
const z = ((1.0 - t) * data.start_vertex.position[2]) + (t * data.end_vertex.position[2]);
|
||||||
|
|
||||||
const pixel = fragment.shaderInvocation(
|
// Early depth test to avoid unnecesary computations
|
||||||
|
if (data.depth_attachment_access) |depth| {
|
||||||
|
const offset = @as(usize, @intCast(pixel_x)) * depth.texel_size + @as(usize, @intCast(pixel_y)) * depth.row_pitch;
|
||||||
|
const depth_value = blitter.readFloat4(depth.base[offset..], depth.format);
|
||||||
|
if (z >= depth_value[0])
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs = fragment.shaderInvocation(
|
||||||
data.allocator,
|
data.allocator,
|
||||||
data.draw_call,
|
data.draw_call,
|
||||||
data.batch_id,
|
data.batch_id,
|
||||||
@@ -157,22 +176,6 @@ inline fn run(data: RunData) !void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = pixel;
|
try common.writeToTargets(outputs, data.draw_call, data.color_attachment_access, data.depth_attachment_access, @intCast(pixel_x), @intCast(pixel_y), z);
|
||||||
_ = render_target;
|
|
||||||
|
|
||||||
//try render_target.writeFloat4(
|
|
||||||
// .{
|
|
||||||
// .x = pixel_x,
|
|
||||||
// .y = pixel_y,
|
|
||||||
// .z = 0, // FIXME
|
|
||||||
// },
|
|
||||||
// .{
|
|
||||||
// .aspect_mask = render_target_view.subresource_range.aspect_mask,
|
|
||||||
// .mip_level = render_target_view.subresource_range.base_mip_level,
|
|
||||||
// .array_layer = render_target_view.subresource_range.base_array_layer,
|
|
||||||
// },
|
|
||||||
// render_target_view.format,
|
|
||||||
// pixel,
|
|
||||||
//);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ const base = @import("base");
|
|||||||
const zm = base.zm;
|
const zm = base.zm;
|
||||||
const spv = @import("spv");
|
const spv = @import("spv");
|
||||||
|
|
||||||
|
const blitter = @import("../blitter.zig");
|
||||||
const Renderer = @import("../Renderer.zig");
|
const Renderer = @import("../Renderer.zig");
|
||||||
|
|
||||||
const VkError = base.VkError;
|
const VkError = base.VkError;
|
||||||
const F32x4 = zm.F32x4;
|
const F32x4 = zm.F32x4;
|
||||||
|
const U32x4 = @Vector(4, u32);
|
||||||
|
|
||||||
pub const RenderTargetAccess = struct {
|
pub const RenderTargetAccess = struct {
|
||||||
mutex: std.Io.Mutex,
|
mutex: std.Io.Mutex,
|
||||||
@@ -98,3 +100,43 @@ pub fn interpolateLineOutputs(
|
|||||||
inline fn interpolateF32x4(value0: F32x4, value1: F32x4, value2: F32x4, b0: f32, b1: f32, b2: f32) F32x4 {
|
inline fn interpolateF32x4(value0: F32x4, value1: F32x4, value2: F32x4, b0: f32, b1: f32, b2: f32) F32x4 {
|
||||||
return (value0 * zm.f32x4s(b0)) + (value1 * zm.f32x4s(b1)) + (value2 * zm.f32x4s(b2));
|
return (value0 * zm.f32x4s(b0)) + (value1 * zm.f32x4s(b1)) + (value2 * zm.f32x4s(b2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn writeToTargets(
|
||||||
|
outputs: [spv.SPIRV_MAX_OUTPUT_LOCATIONS][@sizeOf(F32x4)]u8,
|
||||||
|
draw_call: *Renderer.DrawCall,
|
||||||
|
color_attachment_access: *const RenderTargetAccess,
|
||||||
|
depth_attachment_access: ?*RenderTargetAccess,
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
z: f32,
|
||||||
|
) VkError!void {
|
||||||
|
const io = draw_call.renderer.device.interface.io();
|
||||||
|
|
||||||
|
const color_offset = @as(usize, @intCast(x)) * color_attachment_access.texel_size + @as(usize, @intCast(y)) * color_attachment_access.row_pitch;
|
||||||
|
|
||||||
|
// After work depth test to avoid overwritten depth pixels during fragment invocations
|
||||||
|
if (depth_attachment_access) |depth| {
|
||||||
|
const depth_offset = @as(usize, @intCast(x)) * depth.texel_size + @as(usize, @intCast(y)) * depth.row_pitch;
|
||||||
|
|
||||||
|
depth.mutex.lock(io) catch return VkError.DeviceLost;
|
||||||
|
defer depth.mutex.unlock(io);
|
||||||
|
|
||||||
|
const depth_value = blitter.readFloat4(depth.base[depth_offset..], depth.format);
|
||||||
|
if (z >= depth_value[0])
|
||||||
|
return;
|
||||||
|
blitter.writeFloat4(zm.f32x4s(z), depth.base[depth_offset..], depth.format);
|
||||||
|
|
||||||
|
// Doubled line to stay inside the critical section
|
||||||
|
if (base.format.isUnnormalizedInteger(color_attachment_access.format)) {
|
||||||
|
blitter.writeInt4(std.mem.bytesToValue(U32x4, &outputs[0]), color_attachment_access.base[color_offset..], color_attachment_access.format);
|
||||||
|
} else {
|
||||||
|
blitter.writeFloat4(std.mem.bytesToValue(F32x4, &outputs[0]), color_attachment_access.base[color_offset..], color_attachment_access.format);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (base.format.isUnnormalizedInteger(color_attachment_access.format)) {
|
||||||
|
blitter.writeInt4(std.mem.bytesToValue(U32x4, &outputs[0]), color_attachment_access.base[color_offset..], color_attachment_access.format);
|
||||||
|
} else {
|
||||||
|
blitter.writeFloat4(std.mem.bytesToValue(F32x4, &outputs[0]), color_attachment_access.base[color_offset..], color_attachment_access.format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,8 +125,6 @@ fn runWrapper(data: RunData) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fn run(data: RunData) !void {
|
inline fn run(data: RunData) !void {
|
||||||
const io = data.draw_call.renderer.device.interface.io();
|
|
||||||
|
|
||||||
var y = data.min_y;
|
var y = data.min_y;
|
||||||
while (y <= data.max_y) : (y += 1) {
|
while (y <= data.max_y) : (y += 1) {
|
||||||
var x = data.min_x;
|
var x = data.min_x;
|
||||||
@@ -178,31 +176,7 @@ inline fn run(data: RunData) !void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const color_offset = @as(usize, @intCast(x)) * data.color_attachment_access.texel_size + @as(usize, @intCast(y)) * data.color_attachment_access.row_pitch;
|
try common.writeToTargets(outputs, data.draw_call, data.color_attachment_access, data.depth_attachment_access, @intCast(x), @intCast(y), z);
|
||||||
|
|
||||||
// After work depth test to avoid overwritten depth pixels during fragment invocations
|
|
||||||
if (data.depth_attachment_access) |depth| {
|
|
||||||
const depth_offset = @as(usize, @intCast(x)) * depth.texel_size + @as(usize, @intCast(y)) * depth.row_pitch;
|
|
||||||
|
|
||||||
depth.mutex.lock(io) catch return VkError.DeviceLost;
|
|
||||||
defer depth.mutex.unlock(io);
|
|
||||||
|
|
||||||
const depth_value = blitter.readFloat4(depth.base[depth_offset..], depth.format);
|
|
||||||
if (z >= depth_value[0])
|
|
||||||
continue;
|
|
||||||
blitter.writeFloat4(zm.f32x4s(z), depth.base[depth_offset..], depth.format);
|
|
||||||
|
|
||||||
// Doubled line to stay inside the critical section
|
|
||||||
if (base.format.isUnnormalizedInteger(data.color_attachment_access.format))
|
|
||||||
blitter.writeInt4(std.mem.bytesToValue(@Vector(4, u32), &outputs[0]), data.color_attachment_access.base[color_offset..], data.color_attachment_access.format)
|
|
||||||
else
|
|
||||||
blitter.writeFloat4(std.mem.bytesToValue(@Vector(4, f32), &outputs[0]), data.color_attachment_access.base[color_offset..], data.color_attachment_access.format);
|
|
||||||
} else {
|
|
||||||
if (base.format.isUnnormalizedInteger(data.color_attachment_access.format))
|
|
||||||
blitter.writeInt4(std.mem.bytesToValue(@Vector(4, u32), &outputs[0]), data.color_attachment_access.base[color_offset..], data.color_attachment_access.format)
|
|
||||||
else
|
|
||||||
blitter.writeFloat4(std.mem.bytesToValue(@Vector(4, f32), &outputs[0]), data.color_attachment_access.base[color_offset..], data.color_attachment_access.format);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user