base texture sampling
This commit is contained in:
+2
-2
@@ -31,8 +31,8 @@
|
|||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.SPIRV_Interpreter = .{
|
.SPIRV_Interpreter = .{
|
||||||
.url = "git+https://git.kbz8.me/kbz_8/SPIRV-Interpreter#45453c1b9e93dc9ca096855d6219533f31e89f0f",
|
.url = "git+https://git.kbz8.me/kbz_8/SPIRV-Interpreter#bc84a9f5530c5d9c7a981126b0c1ee6405b90bf7",
|
||||||
.hash = "SPIRV_Interpreter-0.0.1-ajmpnydRBQBjwqmtraJk0vDJt-5npiV3atf423vS0D14",
|
.hash = "SPIRV_Interpreter-0.0.1-ajmpnyNmBQDv76iN4hKcjzO-2vT0JLCwkKhfWLjaYmDL",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
//.SPIRV_Interpreter = .{
|
//.SPIRV_Interpreter = .{
|
||||||
|
|||||||
@@ -829,14 +829,15 @@ pub fn pushConstants(interface: *Interface, stages: vk.ShaderStageFlags, offset:
|
|||||||
pub fn execute(context: *anyopaque, device: *ExecutionDevice) VkError!void {
|
pub fn execute(context: *anyopaque, device: *ExecutionDevice) VkError!void {
|
||||||
const impl: *Impl = @ptrCast(@alignCast(context));
|
const impl: *Impl = @ptrCast(@alignCast(context));
|
||||||
|
|
||||||
|
const state = &device.pipeline_states[
|
||||||
|
if (impl.stages.vertex_bit or impl.stages.fragment_bit)
|
||||||
|
ExecutionDevice.GRAPHICS_PIPELINE_STATE
|
||||||
|
else
|
||||||
|
ExecutionDevice.COMPUTE_PIPELINE_STATE
|
||||||
|
];
|
||||||
|
|
||||||
const size = @min(lib.PUSH_CONSTANT_SIZE - impl.offset, impl.blob.len);
|
const size = @min(lib.PUSH_CONSTANT_SIZE - impl.offset, impl.blob.len);
|
||||||
// TODO: pipeline layout offset
|
@memcpy(state.push_constant_blob[impl.offset .. impl.offset + size], impl.blob[0..size]);
|
||||||
if (impl.stages.vertex_bit or impl.stages.fragment_bit) {
|
|
||||||
@memcpy(device.pipeline_states[ExecutionDevice.GRAPHICS_PIPELINE_STATE].push_constant_blob[impl.offset..size], impl.blob[0..size]);
|
|
||||||
}
|
|
||||||
if (impl.stages.compute_bit) {
|
|
||||||
@memcpy(device.pipeline_states[ExecutionDevice.COMPUTE_PIPELINE_STATE].push_constant_blob[impl.offset..size], impl.blob[0..size]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+152
-17
@@ -7,6 +7,7 @@ const Device = base.Device;
|
|||||||
const Buffer = base.Buffer;
|
const Buffer = base.Buffer;
|
||||||
const BufferView = base.BufferView;
|
const BufferView = base.BufferView;
|
||||||
const ImageView = base.ImageView;
|
const ImageView = base.ImageView;
|
||||||
|
const Sampler = base.Sampler;
|
||||||
|
|
||||||
const SoftBuffer = @import("SoftBuffer.zig");
|
const SoftBuffer = @import("SoftBuffer.zig");
|
||||||
const SoftBufferView = @import("SoftBufferView.zig");
|
const SoftBufferView = @import("SoftBufferView.zig");
|
||||||
@@ -72,12 +73,18 @@ pub fn create(device: *base.Device, allocator: std.mem.Allocator, layout: *base.
|
|||||||
.storage_buffer,
|
.storage_buffer,
|
||||||
.storage_buffer_dynamic,
|
.storage_buffer_dynamic,
|
||||||
=> @sizeOf(DescriptorBuffer),
|
=> @sizeOf(DescriptorBuffer),
|
||||||
|
|
||||||
.storage_image,
|
.storage_image,
|
||||||
.input_attachment,
|
.input_attachment,
|
||||||
=> @sizeOf(DescriptorImage),
|
=> @sizeOf(DescriptorImage),
|
||||||
|
|
||||||
.storage_texel_buffer,
|
.storage_texel_buffer,
|
||||||
.uniform_texel_buffer,
|
.uniform_texel_buffer,
|
||||||
=> @sizeOf(DescriptorTexel),
|
=> @sizeOf(DescriptorTexel),
|
||||||
|
|
||||||
|
.combined_image_sampler,
|
||||||
|
=> @sizeOf(DescriptorTexture),
|
||||||
|
|
||||||
else => 0,
|
else => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +142,22 @@ pub fn create(device: *base.Device, allocator: std.mem.Allocator, layout: *base.
|
|||||||
}
|
}
|
||||||
break :blk desc;
|
break :blk desc;
|
||||||
},
|
},
|
||||||
else => {},
|
|
||||||
|
.combined_image_sampler,
|
||||||
|
=> descriptor.* = blk: {
|
||||||
|
const desc: Descriptor = .{
|
||||||
|
.texture = local_allocator.alloc(DescriptorTexture, binding.array_size) catch return VkError.OutOfHostMemory,
|
||||||
|
};
|
||||||
|
for (desc.texture[0..]) |*d| {
|
||||||
|
d.* = .{
|
||||||
|
.sampler = null,
|
||||||
|
.view = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break :blk desc;
|
||||||
|
},
|
||||||
|
|
||||||
|
else => descriptor.* = .{ .unsupported = .{} },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,33 +175,124 @@ pub fn destroy(interface: *Interface, allocator: std.mem.Allocator) void {
|
|||||||
allocator.destroy(self);
|
allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn descriptorLen(descriptor: Descriptor) usize {
|
||||||
|
return switch (descriptor) {
|
||||||
|
.buffer => |buffer| buffer.len,
|
||||||
|
.image => |image| image.len,
|
||||||
|
.texel_buffer => |texel_buffer| texel_buffer.len,
|
||||||
|
.texture => |texture| texture.len,
|
||||||
|
.unsupported => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advanceToAvailableDescriptor(descriptors: []const Descriptor, binding: *usize, array_element: *usize) bool {
|
||||||
|
while (binding.* < descriptors.len) {
|
||||||
|
switch (descriptors[binding.*]) {
|
||||||
|
.unsupported => return true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = descriptorLen(descriptors[binding.*]);
|
||||||
|
if (array_element.* < len) return true;
|
||||||
|
if (array_element.* > len) return false;
|
||||||
|
|
||||||
|
binding.* += 1;
|
||||||
|
array_element.* = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copyDescriptorRange(dst_desc: *Descriptor, dst_array_element: usize, src_desc: Descriptor, src_array_element: usize, descriptor_count: usize) bool {
|
||||||
|
switch (dst_desc.*) {
|
||||||
|
.buffer => |dst_buffer| {
|
||||||
|
const src_buffer = switch (src_desc) {
|
||||||
|
.buffer => |buffer| buffer,
|
||||||
|
else => return false,
|
||||||
|
};
|
||||||
|
@memcpy(
|
||||||
|
dst_buffer[dst_array_element .. dst_array_element + descriptor_count],
|
||||||
|
src_buffer[src_array_element .. src_array_element + descriptor_count],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
.image => |dst_image| {
|
||||||
|
const src_image = switch (src_desc) {
|
||||||
|
.image => |image| image,
|
||||||
|
else => return false,
|
||||||
|
};
|
||||||
|
@memcpy(
|
||||||
|
dst_image[dst_array_element .. dst_array_element + descriptor_count],
|
||||||
|
src_image[src_array_element .. src_array_element + descriptor_count],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
.texel_buffer => |dst_texel_buffer| {
|
||||||
|
const src_texel_buffer = switch (src_desc) {
|
||||||
|
.texel_buffer => |texel_buffer| texel_buffer,
|
||||||
|
else => return false,
|
||||||
|
};
|
||||||
|
@memcpy(
|
||||||
|
dst_texel_buffer[dst_array_element .. dst_array_element + descriptor_count],
|
||||||
|
src_texel_buffer[src_array_element .. src_array_element + descriptor_count],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
.texture => |dst_texture| {
|
||||||
|
const src_texture = switch (src_desc) {
|
||||||
|
.texture => |texture| texture,
|
||||||
|
else => return false,
|
||||||
|
};
|
||||||
|
@memcpy(
|
||||||
|
dst_texture[dst_array_element .. dst_array_element + descriptor_count],
|
||||||
|
src_texture[src_array_element .. src_array_element + descriptor_count],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
.unsupported => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn copy(interface: *Interface, src_interface: *const Interface, data: vk.CopyDescriptorSet) VkError!void {
|
pub fn copy(interface: *Interface, src_interface: *const Interface, data: vk.CopyDescriptorSet) VkError!void {
|
||||||
const self: *Self = @alignCast(@fieldParentPtr("interface", interface));
|
const self: *Self = @alignCast(@fieldParentPtr("interface", interface));
|
||||||
const src: *const Self = @alignCast(@fieldParentPtr("interface", src_interface));
|
const src: *const Self = @alignCast(@fieldParentPtr("interface", src_interface));
|
||||||
|
|
||||||
const dst_start = @min(@as(usize, @intCast(data.dst_binding)), self.descriptors.len);
|
var dst_binding: usize = @intCast(data.dst_binding);
|
||||||
const src_start = @min(@as(usize, @intCast(data.src_binding)), src.descriptors.len);
|
var src_binding: usize = @intCast(data.src_binding);
|
||||||
|
var dst_array_element: usize = @intCast(data.dst_array_element);
|
||||||
|
var src_array_element: usize = @intCast(data.src_array_element);
|
||||||
|
var descriptor_count: usize = @intCast(data.descriptor_count);
|
||||||
|
|
||||||
const descriptor_count: usize = @intCast(data.descriptor_count);
|
while (descriptor_count > 0) {
|
||||||
|
if (!advanceToAvailableDescriptor(self.descriptors, &dst_binding, &dst_array_element) or
|
||||||
|
!advanceToAvailableDescriptor(src.descriptors, &src_binding, &src_array_element))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dst_remaining = self.descriptors.len - dst_start;
|
const dst_desc = &self.descriptors[dst_binding];
|
||||||
const src_remaining = src.descriptors.len - src_start;
|
const src_desc = src.descriptors[src_binding];
|
||||||
|
const dst_len = descriptorLen(dst_desc.*);
|
||||||
|
const src_len = descriptorLen(src_desc);
|
||||||
|
if (dst_len == 0 or src_len == 0) {
|
||||||
|
base.unsupported("descriptor type for copy", .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dst_remaining = dst_len - dst_array_element;
|
||||||
|
const src_remaining = src_len - src_array_element;
|
||||||
const copy_count = @min(descriptor_count, dst_remaining, src_remaining);
|
const copy_count = @min(descriptor_count, dst_remaining, src_remaining);
|
||||||
|
|
||||||
const dst_slice = self.descriptors[dst_start .. dst_start + copy_count];
|
if (!copyDescriptorRange(dst_desc, dst_array_element, src_desc, src_array_element, copy_count)) {
|
||||||
const src_slice = src.descriptors[src_start .. src_start + copy_count];
|
|
||||||
|
|
||||||
for (dst_slice, src_slice) |*dst_desc, src_desc| {
|
|
||||||
switch (dst_desc.*) {
|
|
||||||
.buffer => |dst_buffer| @memcpy(dst_buffer[0..], src_desc.buffer[0..]),
|
|
||||||
.image => |dst_image| @memcpy(dst_image[0..], src_desc.image[0..]),
|
|
||||||
.texel_buffer => |dst_texel| @memcpy(dst_texel[0..], src_desc.texel_buffer[0..]),
|
|
||||||
else => {
|
|
||||||
dst_desc.* = .{ .unsupported = .{} };
|
|
||||||
base.unsupported("descriptor type for copy", .{});
|
base.unsupported("descriptor type for copy", .{});
|
||||||
},
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descriptor_count -= copy_count;
|
||||||
|
dst_array_element += copy_count;
|
||||||
|
src_array_element += copy_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +320,7 @@ pub fn write(interface: *Interface, write_data: vk.WriteDescriptorSet) VkError!v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.storage_image,
|
.storage_image,
|
||||||
.input_attachment,
|
.input_attachment,
|
||||||
=> {
|
=> {
|
||||||
@@ -219,6 +333,7 @@ pub fn write(interface: *Interface, write_data: vk.WriteDescriptorSet) VkError!v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.storage_texel_buffer,
|
.storage_texel_buffer,
|
||||||
.uniform_texel_buffer,
|
.uniform_texel_buffer,
|
||||||
=> {
|
=> {
|
||||||
@@ -231,6 +346,26 @@ pub fn write(interface: *Interface, write_data: vk.WriteDescriptorSet) VkError!v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.combined_image_sampler,
|
||||||
|
=> {
|
||||||
|
for (write_data.p_image_info, 0..write_data.descriptor_count) |image_info, i| {
|
||||||
|
const desc = &self.descriptors[write_data.dst_binding].texture[i];
|
||||||
|
desc.* = .{
|
||||||
|
.sampler = null,
|
||||||
|
.view = null,
|
||||||
|
};
|
||||||
|
if (image_info.image_view != .null_handle) {
|
||||||
|
const image_view = try NonDispatchable(ImageView).fromHandleObject(image_info.image_view);
|
||||||
|
desc.view = @as(*SoftImageView, @alignCast(@fieldParentPtr("interface", image_view)));
|
||||||
|
}
|
||||||
|
if (image_info.sampler != .null_handle) {
|
||||||
|
const sampler = try NonDispatchable(Sampler).fromHandleObject(image_info.sampler);
|
||||||
|
desc.sampler = @as(*SoftSampler, @alignCast(@fieldParentPtr("interface", sampler)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
else => {
|
else => {
|
||||||
self.descriptors[write_data.dst_binding] = .{ .unsupported = .{} };
|
self.descriptors[write_data.dst_binding] = .{ .unsupported = .{} };
|
||||||
base.unsupported("descriptor type {s} for writting", .{@tagName(write_data.descriptor_type)});
|
base.unsupported("descriptor type {s} for writting", .{@tagName(write_data.descriptor_type)});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const SoftBufferView = @import("SoftBufferView.zig");
|
|||||||
const SoftImage = @import("SoftImage.zig");
|
const SoftImage = @import("SoftImage.zig");
|
||||||
const SoftImageView = @import("SoftImageView.zig");
|
const SoftImageView = @import("SoftImageView.zig");
|
||||||
const SoftInstance = @import("SoftInstance.zig");
|
const SoftInstance = @import("SoftInstance.zig");
|
||||||
|
const SoftSampler = @import("SoftSampler.zig");
|
||||||
const SoftShaderModule = @import("SoftShaderModule.zig");
|
const SoftShaderModule = @import("SoftShaderModule.zig");
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
@@ -100,6 +101,7 @@ pub fn createCompute(device: *base.Device, allocator: std.mem.Allocator, cache:
|
|||||||
.readImageInt4 = readImageInt4,
|
.readImageInt4 = readImageInt4,
|
||||||
.writeImageFloat4 = writeImageFloat4,
|
.writeImageFloat4 = writeImageFloat4,
|
||||||
.writeImageInt4 = writeImageInt4,
|
.writeImageInt4 = writeImageInt4,
|
||||||
|
.sampleImageFloat4 = sampleImageFloat4,
|
||||||
},
|
},
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
std.log.scoped(.SpvRuntimeInit).err("SPIR-V Runtime failed to initialize, {s}", .{@errorName(err)});
|
std.log.scoped(.SpvRuntimeInit).err("SPIR-V Runtime failed to initialize, {s}", .{@errorName(err)});
|
||||||
@@ -184,6 +186,7 @@ pub fn createGraphics(device: *base.Device, allocator: std.mem.Allocator, cache:
|
|||||||
.readImageInt4 = readImageInt4,
|
.readImageInt4 = readImageInt4,
|
||||||
.writeImageFloat4 = writeImageFloat4,
|
.writeImageFloat4 = writeImageFloat4,
|
||||||
.writeImageInt4 = writeImageInt4,
|
.writeImageInt4 = writeImageInt4,
|
||||||
|
.sampleImageFloat4 = sampleImageFloat4,
|
||||||
},
|
},
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
std.log.scoped(.SpvRuntimeInit).err("SPIR-V Runtime failed to initialize, {s}", .{@errorName(err)});
|
std.log.scoped(.SpvRuntimeInit).err("SPIR-V Runtime failed to initialize, {s}", .{@errorName(err)});
|
||||||
@@ -368,3 +371,41 @@ fn writeImageInt4(context: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32,
|
|||||||
) catch return SpvRuntimeError.Unknown;
|
) catch return SpvRuntimeError.Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sampleImageFloat4(context: *anyopaque, context2: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32) SpvRuntimeError!spv.Runtime.Vec4(f32) {
|
||||||
|
var pixel = zm.f32x4s(0.0);
|
||||||
|
|
||||||
|
if (dim == .Buffer) {
|
||||||
|
const buffer_view: *SoftBufferView = @ptrCast(@alignCast(context));
|
||||||
|
const buffer: *SoftBuffer = @alignCast(@fieldParentPtr("interface", buffer_view.interface.buffer));
|
||||||
|
const map = buffer.mapAsSliceWithOffset(u8, buffer_view.interface.offset, buffer_view.interface.range) catch return SpvRuntimeError.Unknown;
|
||||||
|
_ = map;
|
||||||
|
} else {
|
||||||
|
const image_view: *SoftImageView = @ptrCast(@alignCast(context));
|
||||||
|
const image: *SoftImage = @alignCast(@fieldParentPtr("interface", image_view.interface.image));
|
||||||
|
|
||||||
|
const sampler: *SoftSampler = @ptrCast(@alignCast(context2));
|
||||||
|
_ = sampler;
|
||||||
|
|
||||||
|
pixel = image.readFloat4(
|
||||||
|
.{
|
||||||
|
.x = std.math.clamp(@as(i32, @intFromFloat(x * @as(f32, @floatFromInt(image.interface.extent.width)))), 0, image.interface.extent.width - 1),
|
||||||
|
.y = std.math.clamp(@as(i32, @intFromFloat(y * @as(f32, @floatFromInt(image.interface.extent.height)))), 0, image.interface.extent.height - 1),
|
||||||
|
.z = std.math.clamp(@as(i32, @intFromFloat(z * @as(f32, @floatFromInt(image.interface.extent.depth)))), 0, image.interface.extent.depth - 1),
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.aspect_mask = image_view.interface.subresource_range.aspect_mask,
|
||||||
|
.mip_level = image_view.interface.subresource_range.base_mip_level,
|
||||||
|
.array_layer = image_view.interface.subresource_range.base_array_layer,
|
||||||
|
},
|
||||||
|
image_view.interface.format,
|
||||||
|
) catch return SpvRuntimeError.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.x = pixel[0],
|
||||||
|
.y = pixel[1],
|
||||||
|
.z = pixel[2],
|
||||||
|
.w = pixel[3],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ pub fn writeDescriptorSets(state: *PipelineState, rt: *spv.Runtime) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.image => |image_data_array| for (image_data_array, 0..) |image_data, descriptor_index| {
|
.image => |image_data_array| for (image_data_array, 0..) |image_data, descriptor_index| {
|
||||||
if (image_data.object) |image_view| {
|
if (image_data.object) |image_view| {
|
||||||
const addr: usize = @intFromPtr(image_view);
|
const addr: usize = @intFromPtr(image_view);
|
||||||
@@ -91,6 +92,7 @@ pub fn writeDescriptorSets(state: *PipelineState, rt: *spv.Runtime) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
.texel_buffer => |texel_data_array| for (texel_data_array, 0..) |texel_data, descriptor_index| {
|
.texel_buffer => |texel_data_array| for (texel_data_array, 0..) |texel_data, descriptor_index| {
|
||||||
if (texel_data.object) |buffer_view| {
|
if (texel_data.object) |buffer_view| {
|
||||||
const addr: usize = @intFromPtr(buffer_view);
|
const addr: usize = @intFromPtr(buffer_view);
|
||||||
@@ -102,6 +104,32 @@ pub fn writeDescriptorSets(state: *PipelineState, rt: *spv.Runtime) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.texture => |texture_data_array| for (texture_data_array, 0..) |texture_data, descriptor_index| {
|
||||||
|
const SampledImage = packed struct {
|
||||||
|
image: usize,
|
||||||
|
sampler: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
var data: SampledImage = undefined;
|
||||||
|
|
||||||
|
if (texture_data.view) |image_view| {
|
||||||
|
const addr: usize = @intFromPtr(image_view);
|
||||||
|
data.image = addr;
|
||||||
|
}
|
||||||
|
if (texture_data.sampler) |sampler| {
|
||||||
|
const addr: usize = @intFromPtr(sampler);
|
||||||
|
data.sampler = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
try rt.writeDescriptorSet(
|
||||||
|
std.mem.asBytes(&data),
|
||||||
|
@as(u32, @intCast(set_index)),
|
||||||
|
@as(u32, @intCast(binding_index)),
|
||||||
|
@as(u32, @intCast(descriptor_index)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user