mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 14:08:32 +00:00
batched draw target works! embrace the "log" crate
This commit is contained in:
parent
2d0bff97a8
commit
5db5f6de9c
11 changed files with 210 additions and 75 deletions
|
@ -14,7 +14,7 @@ bitflags = "1.0"
|
|||
glam = "0.8.6"
|
||||
winit = { version = "0.22.0", optional = true }
|
||||
zerocopy = "0.3"
|
||||
log = "0.4"
|
||||
log = { version = "0.4", features = ["release_max_level_info"] }
|
||||
env_logger = "0.7"
|
||||
rand = "0.7.2"
|
||||
gltf = "0.14.0"
|
||||
|
|
|
@ -11,7 +11,6 @@ pub fn get_winit_run() -> Box<dyn Fn(App)> {
|
|||
Box::new(|mut app: App| {
|
||||
env_logger::init();
|
||||
let event_loop = EventLoop::new();
|
||||
log::info!("Initializing the window...");
|
||||
let winit_window = {
|
||||
let window = app.resources.get::<Window>().unwrap();
|
||||
let winit_window = winit::window::Window::new(&event_loop).unwrap();
|
||||
|
@ -22,7 +21,7 @@ pub fn get_winit_run() -> Box<dyn Fn(App)> {
|
|||
|
||||
app.resources.insert(winit_window);
|
||||
|
||||
log::info!("Entering render loop...");
|
||||
log::debug!("Entering render loop");
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = if cfg!(feature = "metal-auto-capture") {
|
||||
ControlFlow::Exit
|
||||
|
@ -45,8 +44,6 @@ pub fn get_winit_run() -> Box<dyn Fn(App)> {
|
|||
&mut app.world,
|
||||
&mut app.resources,
|
||||
);
|
||||
} else {
|
||||
println!("no renderer {} {}", size.width, size.height);
|
||||
}
|
||||
}
|
||||
event::Event::WindowEvent { event, .. } => match event {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{
|
||||
asset::Handle,
|
||||
asset::{AssetStorage, Handle},
|
||||
legion::prelude::*,
|
||||
prelude::Renderable,
|
||||
render::{
|
||||
draw_target::DrawTarget,
|
||||
pipeline::PipelineDescriptor,
|
||||
render_resource::{resource_name, AssetBatchers},
|
||||
render_resource::{resource_name, AssetBatchers, RenderResourceAssignments},
|
||||
renderer::{RenderPass, Renderer},
|
||||
},
|
||||
};
|
||||
|
@ -15,32 +16,90 @@ pub struct AssignedBatchesDrawTarget;
|
|||
impl DrawTarget for AssignedBatchesDrawTarget {
|
||||
fn draw(
|
||||
&self,
|
||||
_world: &World,
|
||||
world: &World,
|
||||
resources: &Resources,
|
||||
_render_pass: &mut dyn RenderPass,
|
||||
_pipeline_handle: Handle<PipelineDescriptor>,
|
||||
render_pass: &mut dyn RenderPass,
|
||||
pipeline_handle: Handle<PipelineDescriptor>,
|
||||
) {
|
||||
log::debug!("drawing batches for pipeline {:?}", pipeline_handle);
|
||||
let asset_batches = resources.get::<AssetBatchers>().unwrap();
|
||||
// let renderer = render_pass.get_renderer();
|
||||
// println!("Drawing batches");
|
||||
for _batch in asset_batches.get_batches() {
|
||||
// println!("{:?}", batch);
|
||||
// render_pass.set_bind_groups(batch.render_resource_assignments.as_ref());
|
||||
// render_pass.draw_indexed(0..1, 0, 0..1);
|
||||
}
|
||||
let global_render_resource_assignments =
|
||||
resources.get::<RenderResourceAssignments>().unwrap();
|
||||
render_pass.set_render_resources(&global_render_resource_assignments);
|
||||
for batch in asset_batches.get_batches() {
|
||||
let indices = render_pass.set_render_resources(&batch.render_resource_assignments);
|
||||
log::debug!("drawing batch {:?}", batch.render_resource_assignments.id);
|
||||
log::trace!("{:#?}", batch);
|
||||
for batched_entity in batch.entities.iter() {
|
||||
let renderable = world.get_component::<Renderable>(*batched_entity).unwrap();
|
||||
if !renderable.is_visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
// println!();
|
||||
// println!();
|
||||
// println!();
|
||||
log::trace!("start drawing batched entity: {:?}", batched_entity);
|
||||
log::trace!("{:#?}", renderable.render_resource_assignments);
|
||||
let entity_indices =
|
||||
render_pass.set_render_resources(&renderable.render_resource_assignments);
|
||||
let mut draw_indices = &indices;
|
||||
if entity_indices.is_some() {
|
||||
if indices.is_some() {
|
||||
// panic!("entities overriding their batch's vertex buffer is not currently supported");
|
||||
log::trace!("using batch vertex indices");
|
||||
draw_indices = &entity_indices;
|
||||
} else {
|
||||
log::trace!("using entity vertex indices");
|
||||
draw_indices = &entity_indices;
|
||||
}
|
||||
}
|
||||
|
||||
if draw_indices.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
render_pass.draw_indexed(draw_indices.as_ref().unwrap().clone(), 0, 0..1);
|
||||
log::trace!("finish drawing batched entity: {:?}", batched_entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(
|
||||
&mut self,
|
||||
_world: &World,
|
||||
_resources: &Resources,
|
||||
_renderer: &mut dyn Renderer,
|
||||
_pipeline_handle: Handle<PipelineDescriptor>,
|
||||
world: &mut World,
|
||||
resources: &Resources,
|
||||
renderer: &mut dyn Renderer,
|
||||
pipeline_handle: Handle<PipelineDescriptor>,
|
||||
) {
|
||||
let mut asset_batches = resources.get_mut::<AssetBatchers>().unwrap();
|
||||
let pipeline_storage = resources.get::<AssetStorage<PipelineDescriptor>>().unwrap();
|
||||
let pipeline_descriptor = pipeline_storage.get(&pipeline_handle).unwrap();
|
||||
|
||||
let mut global_render_resource_assignments =
|
||||
resources.get_mut::<RenderResourceAssignments>().unwrap();
|
||||
|
||||
log::debug!("setting up batch bind groups for pipeline: {:?}", pipeline_handle);
|
||||
log::trace!("setting up global bind groups");
|
||||
renderer.setup_bind_groups(&mut global_render_resource_assignments, pipeline_descriptor);
|
||||
|
||||
for batch in asset_batches.get_batches_mut() {
|
||||
log::debug!("setting up batch bind groups: {:?}", batch.render_resource_assignments.id);
|
||||
log::trace!("{:#?}", batch);
|
||||
renderer.setup_bind_groups(
|
||||
&mut batch.render_resource_assignments,
|
||||
pipeline_descriptor,
|
||||
);
|
||||
for batched_entity in batch.entities.iter() {
|
||||
let mut renderable = world.get_component_mut::<Renderable>(*batched_entity).unwrap();
|
||||
if !renderable.is_visible || renderable.is_instanced {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::trace!("setting up entity bind group {:?} for batch {:?}", batched_entity, batch.render_resource_assignments.id);
|
||||
renderer.setup_bind_groups(
|
||||
&mut renderable.render_resource_assignments,
|
||||
pipeline_descriptor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_name(&self) -> String {
|
||||
|
|
|
@ -55,8 +55,7 @@ impl<'a> ForwardPipelineBuilder for RenderGraphBuilder<'a> {
|
|||
},
|
||||
write_mask: ColorWrite::ALL,
|
||||
})
|
||||
.add_draw_target(resource_name::draw_target::ASSIGNED_MESHES);
|
||||
// .add_draw_target(resource_name::draw_target::ASSIGNED_BATCHES);
|
||||
.add_draw_target(resource_name::draw_target::ASSIGNED_BATCHES);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,6 +110,13 @@ impl AssetBatchers {
|
|||
.flatten()
|
||||
}
|
||||
|
||||
pub fn get_batches_mut(&mut self) -> impl Iterator<Item = &mut Batch> {
|
||||
self.asset_batchers
|
||||
.iter_mut()
|
||||
.map(|a| a.get_batches_mut())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn get_handle_batches<T>(&self) -> Option<impl Iterator<Item = &Batch>>
|
||||
where
|
||||
T: 'static,
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::RenderResourceAssignmentsId;
|
|||
use crate::render::render_resource::BufferUsage;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct BufferArrayInfo {
|
||||
pub item_count: usize,
|
||||
pub item_size: usize,
|
||||
|
@ -32,6 +32,7 @@ impl BufferArrayInfo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BufferInfo {
|
||||
pub size: usize,
|
||||
pub buffer_usage: BufferUsage,
|
||||
|
@ -50,6 +51,7 @@ impl Default for BufferInfo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ResourceInfo {
|
||||
Buffer(BufferInfo),
|
||||
Texture,
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::{
|
|||
render::{
|
||||
mesh::Mesh,
|
||||
render_graph::RenderGraph,
|
||||
render_resource::{AssetBatchers, BufferInfo, BufferUsage, ResourceProvider},
|
||||
render_resource::{
|
||||
AssetBatchers, BufferInfo, BufferUsage, RenderResourceAssignments, ResourceProvider,
|
||||
},
|
||||
renderer::Renderer,
|
||||
shader::AsUniforms,
|
||||
Vertex,
|
||||
|
@ -39,6 +41,38 @@ impl MeshResourceProvider {
|
|||
.filter(changed::<Handle<Mesh>>()),
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_mesh_resources(renderer: &mut dyn Renderer, mesh_storage: &mut AssetStorage<Mesh>, handle: Handle<Mesh>, render_resource_assignments: &mut RenderResourceAssignments) {
|
||||
let (vertex_buffer, index_buffer) = if let Some(vertex_buffer) = renderer
|
||||
.get_render_resources()
|
||||
.get_mesh_vertices_resource(handle)
|
||||
{
|
||||
(vertex_buffer, renderer.get_render_resources().get_mesh_indices_resource(handle))
|
||||
} else {
|
||||
let mesh_asset = mesh_storage.get(&handle).unwrap();
|
||||
let vertex_buffer = renderer.create_buffer_with_data(
|
||||
BufferInfo {
|
||||
buffer_usage: BufferUsage::VERTEX,
|
||||
..Default::default()
|
||||
},
|
||||
mesh_asset.vertices.as_bytes(),
|
||||
);
|
||||
let index_buffer = renderer.create_buffer_with_data(
|
||||
BufferInfo {
|
||||
buffer_usage: BufferUsage::INDEX,
|
||||
..Default::default()
|
||||
},
|
||||
mesh_asset.indices.as_bytes(),
|
||||
);
|
||||
|
||||
let render_resources = renderer.get_render_resources_mut();
|
||||
render_resources.set_mesh_vertices_resource(handle, vertex_buffer);
|
||||
render_resources.set_mesh_indices_resource(handle, index_buffer);
|
||||
(vertex_buffer, Some(index_buffer))
|
||||
};
|
||||
|
||||
render_resource_assignments.set_vertex_buffer("Vertex", vertex_buffer, index_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceProvider for MeshResourceProvider {
|
||||
|
@ -53,48 +87,31 @@ impl ResourceProvider for MeshResourceProvider {
|
|||
.set_vertex_buffer_descriptor(Vertex::get_vertex_buffer_descriptor().cloned().unwrap());
|
||||
}
|
||||
|
||||
fn update(&mut self, renderer: &mut dyn Renderer, world: &mut World, resources: &Resources) {
|
||||
let mesh_storage = resources.get_mut::<AssetStorage<Mesh>>().unwrap();
|
||||
fn update(&mut self, _renderer: &mut dyn Renderer, world: &mut World, resources: &Resources) {
|
||||
let mut asset_batchers = resources.get_mut::<AssetBatchers>().unwrap();
|
||||
for (entity, (mesh_handle, _renderable)) in self.mesh_query.iter_entities(world) {
|
||||
for (entity, (mesh_handle, _renderable)) in self.mesh_query.iter_entities_mut(world) {
|
||||
asset_batchers.set_entity_handle(entity, *mesh_handle);
|
||||
if let None = renderer
|
||||
.get_render_resources()
|
||||
.get_mesh_vertices_resource(*mesh_handle)
|
||||
{
|
||||
let mesh_asset = mesh_storage.get(&mesh_handle).unwrap();
|
||||
let vertex_buffer = renderer.create_buffer_with_data(
|
||||
BufferInfo {
|
||||
buffer_usage: BufferUsage::VERTEX,
|
||||
..Default::default()
|
||||
},
|
||||
mesh_asset.vertices.as_bytes(),
|
||||
);
|
||||
let index_buffer = renderer.create_buffer_with_data(
|
||||
BufferInfo {
|
||||
buffer_usage: BufferUsage::INDEX,
|
||||
..Default::default()
|
||||
},
|
||||
mesh_asset.indices.as_bytes(),
|
||||
);
|
||||
|
||||
let render_resources = renderer.get_render_resources_mut();
|
||||
render_resources.set_mesh_vertices_resource(*mesh_handle, vertex_buffer);
|
||||
render_resources.set_mesh_indices_resource(*mesh_handle, index_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_update(
|
||||
&mut self,
|
||||
_renderer: &mut dyn Renderer,
|
||||
renderer: &mut dyn Renderer,
|
||||
_world: &mut World,
|
||||
_resources: &Resources,
|
||||
resources: &Resources,
|
||||
) {
|
||||
// TODO: assign vertex buffers
|
||||
// let mesh_storage = resources.get_mut::<AssetStorage<Mesh>>().unwrap();
|
||||
// let mut asset_batchers = resources.get_mut::<AssetBatchers>().unwrap();
|
||||
// for batch in asset_batchers.get_handle_batches::<Mesh>() {
|
||||
// }
|
||||
let mut mesh_storage = resources.get_mut::<AssetStorage<Mesh>>().unwrap();
|
||||
let mut asset_batchers = resources.get_mut::<AssetBatchers>().unwrap();
|
||||
|
||||
// this scope is necessary because the Fetch<AssetBatchers> pointer behaves weirdly
|
||||
{
|
||||
if let Some(batches) = asset_batchers.get_handle_batches_mut::<Mesh>() {
|
||||
for batch in batches {
|
||||
let handle = batch.get_handle::<Mesh>().unwrap();
|
||||
log::trace!("setup mesh for {:?}", batch.render_resource_assignments.id);
|
||||
Self::setup_mesh_resources(renderer, &mut mesh_storage, handle, &mut batch.render_resource_assignments);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -512,12 +512,13 @@ where
|
|||
};
|
||||
|
||||
if let Some(new_capacity) = new_capacity {
|
||||
println!("creating buffer {}", new_capacity);
|
||||
let mut item_size = buffer_array_status.item_size;
|
||||
if align {
|
||||
item_size = Self::get_aligned_dynamic_uniform_size(item_size);
|
||||
}
|
||||
|
||||
let total_size = item_size * new_capacity;
|
||||
|
||||
let buffer = renderer.create_buffer(BufferInfo {
|
||||
array_info: Some(BufferArrayInfo {
|
||||
item_capacity: new_capacity,
|
||||
|
@ -525,11 +526,19 @@ where
|
|||
item_size,
|
||||
..Default::default()
|
||||
}),
|
||||
size: item_size * new_capacity,
|
||||
size: total_size,
|
||||
buffer_usage: BufferUsage::COPY_DST | BufferUsage::UNIFORM,
|
||||
is_dynamic: true,
|
||||
});
|
||||
|
||||
log::trace!(
|
||||
"creating buffer for uniform {}. size: {} item_capacity: {} item_size: {}",
|
||||
std::any::type_name::<T>(),
|
||||
total_size,
|
||||
new_capacity,
|
||||
item_size
|
||||
);
|
||||
|
||||
buffer_array_status.buffer = Some(buffer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use super::{WgpuRenderer, WgpuResources};
|
||||
use crate::render::{
|
||||
pipeline::PipelineDescriptor,
|
||||
render_resource::{RenderResource, RenderResourceAssignments},
|
||||
pipeline::{BindGroupDescriptorId, PipelineDescriptor},
|
||||
render_resource::{RenderResource, RenderResourceAssignments, ResourceInfo, RenderResourceSetId},
|
||||
renderer::{RenderPass, Renderer},
|
||||
};
|
||||
use std::ops::Range;
|
||||
use std::{collections::HashMap, ops::Range};
|
||||
|
||||
pub struct WgpuRenderPass<'a, 'b, 'c, 'd> {
|
||||
pub render_pass: &'b mut wgpu::RenderPass<'a>,
|
||||
pub pipeline_descriptor: &'c PipelineDescriptor,
|
||||
pub wgpu_resources: &'a WgpuResources,
|
||||
pub renderer: &'d WgpuRenderer,
|
||||
pub bound_bind_groups: HashMap<u32, (RenderResourceSetId)>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c, 'd> RenderPass for WgpuRenderPass<'a, 'b, 'c, 'd> {
|
||||
|
@ -43,6 +44,30 @@ impl<'a, 'b, 'c, 'd> RenderPass for WgpuRenderPass<'a, 'b, 'c, 'd> {
|
|||
render_resource_assignments: &RenderResourceAssignments,
|
||||
) -> Option<Range<u32>> {
|
||||
let pipeline_layout = self.pipeline_descriptor.get_layout().unwrap();
|
||||
// PERF: vertex buffer lookup comes at a cost when vertex buffers aren't in render_resource_assignments. iterating over render_resource_assignment vertex buffers
|
||||
// would likely be faster
|
||||
let mut indices = None;
|
||||
for (i, vertex_buffer_descriptor) in
|
||||
pipeline_layout.vertex_buffer_descriptors.iter().enumerate()
|
||||
{
|
||||
if let Some((vertex_buffer, index_buffer)) =
|
||||
render_resource_assignments.get_vertex_buffer(&vertex_buffer_descriptor.name)
|
||||
{
|
||||
log::trace!("set vertex buffer {}: {} ({:?})", i, vertex_buffer_descriptor.name, vertex_buffer);
|
||||
self.set_vertex_buffer(i as u32, vertex_buffer, 0);
|
||||
if let Some(index_buffer) = index_buffer {
|
||||
log::trace!("set index buffer: {} ({:?})", vertex_buffer_descriptor.name, index_buffer);
|
||||
self.set_index_buffer(index_buffer, 0);
|
||||
match self.renderer.get_resource_info(index_buffer).unwrap() {
|
||||
ResourceInfo::Buffer(buffer_info) => {
|
||||
indices = Some(0..(buffer_info.size / 2) as u32)
|
||||
}
|
||||
_ => panic!("expected a buffer type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for bind_group in pipeline_layout.bind_groups.iter() {
|
||||
if let Some((render_resource_set_id, dynamic_uniform_indices)) =
|
||||
render_resource_assignments.get_render_resource_set_id(bind_group.id)
|
||||
|
@ -51,20 +76,31 @@ impl<'a, 'b, 'c, 'd> RenderPass for WgpuRenderPass<'a, 'b, 'c, 'd> {
|
|||
.wgpu_resources
|
||||
.get_bind_group(bind_group.id, *render_resource_set_id)
|
||||
{
|
||||
// TODO: check to see if bind group is already set
|
||||
let empty = &[];
|
||||
const EMPTY: &'static [u32] = &[];
|
||||
let dynamic_uniform_indices =
|
||||
if let Some(dynamic_uniform_indices) = dynamic_uniform_indices {
|
||||
dynamic_uniform_indices.as_slice()
|
||||
} else {
|
||||
empty
|
||||
EMPTY
|
||||
};
|
||||
|
||||
// TODO: remove this
|
||||
// if dynamic_uniform_indices.len() == 0 && bind_group.index > 0 {
|
||||
// continue;
|
||||
// }
|
||||
// don't bind bind groups if they are already set
|
||||
// TODO: these checks come at a performance cost. make sure its worth it!
|
||||
if let Some(bound_render_resource_set) = self.bound_bind_groups.get(&bind_group.index) {
|
||||
if *bound_render_resource_set == *render_resource_set_id && dynamic_uniform_indices.len() == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if dynamic_uniform_indices.len() == 0 {
|
||||
self.bound_bind_groups
|
||||
.insert(bind_group.index, *render_resource_set_id);
|
||||
} else {
|
||||
self.bound_bind_groups.remove(&bind_group.index);
|
||||
}
|
||||
|
||||
log::trace!("set bind group {} {:?}: {:?}", bind_group.index, dynamic_uniform_indices, render_resource_set_id);
|
||||
self.render_pass.set_bind_group(
|
||||
bind_group.index,
|
||||
&wgpu_bind_group,
|
||||
|
@ -74,6 +110,6 @@ impl<'a, 'b, 'c, 'd> RenderPass for WgpuRenderPass<'a, 'b, 'c, 'd> {
|
|||
}
|
||||
}
|
||||
|
||||
None
|
||||
indices
|
||||
}
|
||||
}
|
||||
|
|
|
@ -490,6 +490,7 @@ impl Renderer for WgpuRenderer {
|
|||
pipeline_descriptor,
|
||||
wgpu_resources: &self.wgpu_resources,
|
||||
renderer: &self,
|
||||
bound_bind_groups: HashMap::default(),
|
||||
};
|
||||
|
||||
for draw_target_name in pipeline_descriptor.draw_targets.iter() {
|
||||
|
@ -614,6 +615,8 @@ impl Renderer for WgpuRenderer {
|
|||
bind_group,
|
||||
render_resource_assignments,
|
||||
);
|
||||
} else {
|
||||
log::trace!("reusing RenderResourceSet {:?} for bind group {}", render_resource_set_id, bind_group.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,12 +57,15 @@ impl WgpuResources {
|
|||
if let Some((render_resource_set_id, _indices)) =
|
||||
render_resource_assignments.get_render_resource_set_id(bind_group_descriptor.id)
|
||||
{
|
||||
log::debug!("start creating bind group for RenderResourceSet {:?}", render_resource_set_id);
|
||||
let bindings = bind_group_descriptor
|
||||
.bindings
|
||||
.iter()
|
||||
.map(|binding| {
|
||||
if let Some(resource) = render_resource_assignments.get(&binding.name) {
|
||||
|
||||
let resource_info = self.resource_info.get(&resource).unwrap();
|
||||
log::trace!("found binding {} ({}) resource: {:?} {:?}", binding.index, binding.name, resource, resource_info);
|
||||
wgpu::Binding {
|
||||
binding: binding.index,
|
||||
resource: match &binding.bind_type {
|
||||
|
@ -122,6 +125,9 @@ impl WgpuResources {
|
|||
bind_group_info
|
||||
.bind_groups
|
||||
.insert(*render_resource_set_id, bind_group);
|
||||
|
||||
log::debug!("created bind group for RenderResourceSet {:?}", render_resource_set_id);
|
||||
log::trace!("{:#?}", bind_group_descriptor);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
|
Loading…
Add table
Reference in a new issue