Changed ray batch data access to be through methods.
This is (potentially) just temporary. It's to make it a bit easier to play with data layout to see how that affects performance.
This commit is contained in:
parent
eef29c2b2f
commit
5dd8eb919b
|
@ -95,7 +95,7 @@ impl<'a> BVH4<'a> {
|
||||||
let mut node_tests: u64 = 0;
|
let mut node_tests: u64 = 0;
|
||||||
|
|
||||||
let traversal_table =
|
let traversal_table =
|
||||||
&TRAVERSAL_TABLE[ray_code(rays.dir_inv_accel[ray_stack.next_task_ray_idx(0)])];
|
&TRAVERSAL_TABLE[ray_code(rays.dir_inv_local(ray_stack.next_task_ray_idx(0)))];
|
||||||
|
|
||||||
// +2 of max depth for root and last child
|
// +2 of max depth for root and last child
|
||||||
let mut node_stack = [self.root.unwrap(); (BVH_MAX_DEPTH * 3) + 2];
|
let mut node_stack = [self.root.unwrap(); (BVH_MAX_DEPTH * 3) + 2];
|
||||||
|
@ -117,10 +117,10 @@ impl<'a> BVH4<'a> {
|
||||||
let mut hit_count = 0;
|
let mut hit_count = 0;
|
||||||
ray_stack.pop_do_next_task(children.len(), |ray_idx| {
|
ray_stack.pop_do_next_task(children.len(), |ray_idx| {
|
||||||
let hit = (!rays.is_done(ray_idx))
|
let hit = (!rays.is_done(ray_idx))
|
||||||
&& lerp_slice(bounds, rays.time[ray_idx]).intersect_ray(
|
&& lerp_slice(bounds, rays.time(ray_idx)).intersect_ray(
|
||||||
rays.orig_accel[ray_idx],
|
rays.orig_local(ray_idx),
|
||||||
rays.dir_inv_accel[ray_idx],
|
rays.dir_inv_local(ray_idx),
|
||||||
rays.max_t[ray_idx],
|
rays.max_t(ray_idx),
|
||||||
);
|
);
|
||||||
|
|
||||||
if hit {
|
if hit {
|
||||||
|
@ -194,10 +194,10 @@ impl<'a> BVH4<'a> {
|
||||||
|
|
||||||
ray_stack.pop_do_next_task(object_count, |ray_idx| {
|
ray_stack.pop_do_next_task(object_count, |ray_idx| {
|
||||||
let hit = (!rays.is_done(ray_idx))
|
let hit = (!rays.is_done(ray_idx))
|
||||||
&& lerp_slice(bounds, rays.time[ray_idx]).intersect_ray(
|
&& lerp_slice(bounds, rays.time(ray_idx)).intersect_ray(
|
||||||
rays.orig_accel[ray_idx],
|
rays.orig_local(ray_idx),
|
||||||
rays.dir_inv_accel[ray_idx],
|
rays.dir_inv_local(ray_idx),
|
||||||
rays.max_t[ray_idx],
|
rays.max_t(ray_idx),
|
||||||
);
|
);
|
||||||
if hit {
|
if hit {
|
||||||
hit_count += 1;
|
hit_count += 1;
|
||||||
|
|
|
@ -266,10 +266,10 @@ impl<'a> Surface for RectangleLight<'a> {
|
||||||
let _ = shader; // Silence 'unused' warning
|
let _ = shader; // Silence 'unused' warning
|
||||||
|
|
||||||
ray_stack.pop_do_next_task(0, |ray_idx| {
|
ray_stack.pop_do_next_task(0, |ray_idx| {
|
||||||
let time = rays.time[ray_idx];
|
let time = rays.time(ray_idx);
|
||||||
let orig = rays.orig_world[ray_idx];
|
let orig = rays.orig(ray_idx);
|
||||||
let dir = rays.dir_world[ray_idx];
|
let dir = rays.dir(ray_idx);
|
||||||
let max_t = rays.max_t[ray_idx];
|
let max_t = rays.max_t(ray_idx);
|
||||||
|
|
||||||
// Calculate time interpolated values
|
// Calculate time interpolated values
|
||||||
let dim = lerp_slice(self.dimensions, time);
|
let dim = lerp_slice(self.dimensions, time);
|
||||||
|
@ -307,7 +307,7 @@ impl<'a> Surface for RectangleLight<'a> {
|
||||||
orig,
|
orig,
|
||||||
dir,
|
dir,
|
||||||
pos,
|
pos,
|
||||||
rays.wavelength[ray_idx],
|
rays.wavelength(ray_idx),
|
||||||
time,
|
time,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -325,7 +325,7 @@ impl<'a> Surface for RectangleLight<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set ray's max t
|
// Set ray's max t
|
||||||
rays.max_t[ray_idx] = t;
|
rays.set_max_t(ray_idx, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -215,7 +215,7 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
let _ = shader; // Silence 'unused' warning
|
let _ = shader; // Silence 'unused' warning
|
||||||
|
|
||||||
ray_stack.pop_do_next_task(0, |ray_idx| {
|
ray_stack.pop_do_next_task(0, |ray_idx| {
|
||||||
let time = rays.time[ray_idx];
|
let time = rays.time(ray_idx);
|
||||||
|
|
||||||
// Get the transform space
|
// Get the transform space
|
||||||
let xform = lerp_slice(space, time);
|
let xform = lerp_slice(space, time);
|
||||||
|
@ -224,8 +224,8 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
let radius = lerp_slice(self.radii, time); // Radius of the sphere
|
let radius = lerp_slice(self.radii, time); // Radius of the sphere
|
||||||
|
|
||||||
// Get the ray origin and direction in local space
|
// Get the ray origin and direction in local space
|
||||||
let orig = rays.orig_accel[ray_idx].into_vector();
|
let orig = rays.orig(ray_idx).into_vector();
|
||||||
let dir = rays.dir_world[ray_idx] * xform;
|
let dir = rays.dir(ray_idx) * xform;
|
||||||
|
|
||||||
// Code adapted to Rust from https://github.com/Tecla/Rayito
|
// Code adapted to Rust from https://github.com/Tecla/Rayito
|
||||||
// Ray-sphere intersection can result in either zero, one or two points
|
// Ray-sphere intersection can result in either zero, one or two points
|
||||||
|
@ -257,7 +257,7 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
|
|
||||||
// Get our final parametric values
|
// Get our final parametric values
|
||||||
let mut t0 = q / a;
|
let mut t0 = q / a;
|
||||||
let mut t1 = if q != 0.0 { c / q } else { rays.max_t[ray_idx] };
|
let mut t1 = if q != 0.0 { c / q } else { rays.max_t(ray_idx) };
|
||||||
|
|
||||||
// Swap them so they are ordered right
|
// Swap them so they are ordered right
|
||||||
if t0 > t1 {
|
if t0 > t1 {
|
||||||
|
@ -266,14 +266,14 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check our intersection for validity against this ray's extents
|
// Check our intersection for validity against this ray's extents
|
||||||
if t0 > rays.max_t[ray_idx] || t1 <= 0.0 {
|
if t0 > rays.max_t(ray_idx) || t1 <= 0.0 {
|
||||||
// Didn't hit because sphere is entirely outside of ray's extents
|
// Didn't hit because sphere is entirely outside of ray's extents
|
||||||
return ([0, 0, 0, 0, 0, 0, 0, 0], 0);
|
return ([0, 0, 0, 0, 0, 0, 0, 0], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = if t0 > 0.0 {
|
let t = if t0 > 0.0 {
|
||||||
t0
|
t0
|
||||||
} else if t1 <= rays.max_t[ray_idx] {
|
} else if t1 <= rays.max_t(ray_idx) {
|
||||||
t1
|
t1
|
||||||
} else {
|
} else {
|
||||||
// Didn't hit because ray is entirely within the sphere, and
|
// Didn't hit because ray is entirely within the sphere, and
|
||||||
|
@ -300,7 +300,7 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
let normal = unit_pos.into_normal() * inv_xform;
|
let normal = unit_pos.into_normal() * inv_xform;
|
||||||
|
|
||||||
let intersection_data = SurfaceIntersectionData {
|
let intersection_data = SurfaceIntersectionData {
|
||||||
incoming: rays.dir_world[ray_idx],
|
incoming: rays.dir(ray_idx),
|
||||||
t: t,
|
t: t,
|
||||||
pos: pos,
|
pos: pos,
|
||||||
pos_err: pos_err,
|
pos_err: pos_err,
|
||||||
|
@ -309,11 +309,11 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
local_space: xform,
|
local_space: xform,
|
||||||
sample_pdf: self.sample_pdf(
|
sample_pdf: self.sample_pdf(
|
||||||
&xform,
|
&xform,
|
||||||
rays.orig_world[ray_idx],
|
rays.orig(ray_idx),
|
||||||
rays.dir_world[ray_idx],
|
rays.dir(ray_idx),
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
rays.wavelength[ray_idx],
|
rays.wavelength(ray_idx),
|
||||||
time,
|
time,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -332,7 +332,7 @@ impl<'a> Surface for SphereLight<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set ray's max t
|
// Set ray's max t
|
||||||
rays.max_t[ray_idx] = t;
|
rays.set_max_t(ray_idx, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
([0, 0, 0, 0, 0, 0, 0, 0], 0)
|
([0, 0, 0, 0, 0, 0, 0, 0], 0)
|
||||||
|
|
105
src/ray.rs
105
src/ray.rs
|
@ -23,14 +23,14 @@ pub struct Ray {
|
||||||
/// A batch of rays, stored in SoA layout.
|
/// A batch of rays, stored in SoA layout.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RayBatch {
|
pub struct RayBatch {
|
||||||
pub orig_world: Vec<Point>,
|
orig_world: Vec<Point>,
|
||||||
pub dir_world: Vec<Vector>,
|
dir_world: Vec<Vector>,
|
||||||
pub orig_accel: Vec<Point>,
|
orig_accel: Vec<Point>,
|
||||||
pub dir_inv_accel: Vec<Vector>,
|
dir_inv_accel: Vec<Vector>,
|
||||||
pub max_t: Vec<f32>,
|
max_t: Vec<f32>,
|
||||||
pub time: Vec<f32>,
|
time: Vec<f32>,
|
||||||
pub wavelength: Vec<f32>,
|
wavelength: Vec<f32>,
|
||||||
pub flags: Vec<FlagType>,
|
flags: Vec<FlagType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RayBatch {
|
impl RayBatch {
|
||||||
|
@ -135,37 +135,84 @@ impl RayBatch {
|
||||||
self.orig_world.len()
|
self.orig_world.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the given ray (at index `idx`) is an occlusion ray.
|
|
||||||
pub fn is_occlusion(&self, idx: usize) -> bool {
|
|
||||||
(self.flags[idx] & OCCLUSION_FLAG) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the given ray (at index `idx`) has finished traversal.
|
|
||||||
pub fn is_done(&self, idx: usize) -> bool {
|
|
||||||
(self.flags[idx] & DONE_FLAG) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks the given ray (at index `idx`) as an occlusion ray.
|
|
||||||
pub fn mark_occlusion(&mut self, idx: usize) {
|
|
||||||
self.flags[idx] |= OCCLUSION_FLAG
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks the given ray (at index `idx`) as having finished traversal.
|
|
||||||
pub fn mark_done(&mut self, idx: usize) {
|
|
||||||
self.flags[idx] |= DONE_FLAG
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the accel data of the given ray (at index `idx`) with the
|
/// Updates the accel data of the given ray (at index `idx`) with the
|
||||||
/// given world-to-local-space transform matrix.
|
/// given world-to-local-space transform matrix.
|
||||||
///
|
///
|
||||||
/// This should be called when entering (and exiting) traversal of a
|
/// This should be called when entering (and exiting) traversal of a
|
||||||
/// new transform space.
|
/// new transform space.
|
||||||
pub fn update_accel(&mut self, idx: usize, xform: &Matrix4x4) {
|
pub fn update_local(&mut self, idx: usize, xform: &Matrix4x4) {
|
||||||
self.orig_accel[idx] = self.orig_world[idx] * *xform;
|
self.orig_accel[idx] = self.orig_world[idx] * *xform;
|
||||||
self.dir_inv_accel[idx] = Vector {
|
self.dir_inv_accel[idx] = Vector {
|
||||||
co: Float4::splat(1.0) / (self.dir_world[idx] * *xform).co,
|
co: Float4::splat(1.0) / (self.dir_world[idx] * *xform).co,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//==========================================================
|
||||||
|
// Data access
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn orig(&self, idx: usize) -> Point {
|
||||||
|
self.orig_world[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn dir(&self, idx: usize) -> Vector {
|
||||||
|
self.dir_world[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn orig_local(&self, idx: usize) -> Point {
|
||||||
|
self.orig_accel[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn dir_inv_local(&self, idx: usize) -> Vector {
|
||||||
|
self.dir_inv_accel[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn time(&self, idx: usize) -> f32 {
|
||||||
|
self.time[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn max_t(&self, idx: usize) -> f32 {
|
||||||
|
self.max_t[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn set_max_t(&mut self, idx: usize, new_max_t: f32) {
|
||||||
|
self.max_t[idx] = new_max_t;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn wavelength(&self, idx: usize) -> f32 {
|
||||||
|
self.wavelength[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the given ray (at index `idx`) is an occlusion ray.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_occlusion(&self, idx: usize) -> bool {
|
||||||
|
(self.flags[idx] & OCCLUSION_FLAG) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the given ray (at index `idx`) has finished traversal.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_done(&self, idx: usize) -> bool {
|
||||||
|
(self.flags[idx] & DONE_FLAG) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the given ray (at index `idx`) as an occlusion ray.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn mark_occlusion(&mut self, idx: usize) {
|
||||||
|
self.flags[idx] |= OCCLUSION_FLAG
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the given ray (at index `idx`) as having finished traversal.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn mark_done(&mut self, idx: usize) {
|
||||||
|
self.flags[idx] |= DONE_FLAG
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure used for tracking traversal of a ray batch through a scene.
|
/// A structure used for tracking traversal of a ray batch through a scene.
|
||||||
|
|
|
@ -503,7 +503,7 @@ impl LightPath {
|
||||||
// Distant light
|
// Distant light
|
||||||
SceneLightSample::Distant { direction, .. } => {
|
SceneLightSample::Distant { direction, .. } => {
|
||||||
let (attenuation, closure_pdf) = closure.evaluate(
|
let (attenuation, closure_pdf) = closure.evaluate(
|
||||||
rays.dir_world[ray_idx],
|
rays.dir(ray_idx),
|
||||||
direction,
|
direction,
|
||||||
idata.nor,
|
idata.nor,
|
||||||
idata.nor_g,
|
idata.nor_g,
|
||||||
|
@ -533,7 +533,7 @@ impl LightPath {
|
||||||
SceneLightSample::Surface { sample_geo, .. } => {
|
SceneLightSample::Surface { sample_geo, .. } => {
|
||||||
let dir = sample_geo.0 - idata.pos;
|
let dir = sample_geo.0 - idata.pos;
|
||||||
let (attenuation, closure_pdf) = closure.evaluate(
|
let (attenuation, closure_pdf) = closure.evaluate(
|
||||||
rays.dir_world[ray_idx],
|
rays.dir(ray_idx),
|
||||||
dir,
|
dir,
|
||||||
idata.nor,
|
idata.nor,
|
||||||
idata.nor_g,
|
idata.nor_g,
|
||||||
|
|
|
@ -159,7 +159,7 @@ impl<'a> Surface for TriangleMesh<'a> {
|
||||||
// Test each ray against the current triangle.
|
// Test each ray against the current triangle.
|
||||||
ray_stack.pop_do_next_task(0, |ray_idx| {
|
ray_stack.pop_do_next_task(0, |ray_idx| {
|
||||||
let ray_idx = ray_idx as usize;
|
let ray_idx = ray_idx as usize;
|
||||||
let ray_time = rays.time[ray_idx];
|
let ray_time = rays.time(ray_idx);
|
||||||
|
|
||||||
// Get triangle if necessary
|
// Get triangle if necessary
|
||||||
if !is_cached {
|
if !is_cached {
|
||||||
|
@ -215,9 +215,9 @@ impl<'a> Surface for TriangleMesh<'a> {
|
||||||
|
|
||||||
// Test ray against triangle
|
// Test ray against triangle
|
||||||
if let Some((t, b0, b1, b2)) = triangle::intersect_ray(
|
if let Some((t, b0, b1, b2)) = triangle::intersect_ray(
|
||||||
rays.orig_world[ray_idx],
|
rays.orig(ray_idx),
|
||||||
rays.dir_world[ray_idx],
|
rays.dir(ray_idx),
|
||||||
rays.max_t[ray_idx],
|
rays.max_t(ray_idx),
|
||||||
tri,
|
tri,
|
||||||
) {
|
) {
|
||||||
if rays.is_occlusion(ray_idx) {
|
if rays.is_occlusion(ray_idx) {
|
||||||
|
@ -257,7 +257,7 @@ impl<'a> Surface for TriangleMesh<'a> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let intersection_data = SurfaceIntersectionData {
|
let intersection_data = SurfaceIntersectionData {
|
||||||
incoming: rays.dir_world[ray_idx],
|
incoming: rays.dir(ray_idx),
|
||||||
t: t,
|
t: t,
|
||||||
pos: pos,
|
pos: pos,
|
||||||
pos_err: pos_err,
|
pos_err: pos_err,
|
||||||
|
@ -272,7 +272,7 @@ impl<'a> Surface for TriangleMesh<'a> {
|
||||||
intersection_data: intersection_data,
|
intersection_data: intersection_data,
|
||||||
closure: shader.shade(&intersection_data, ray_time),
|
closure: shader.shade(&intersection_data, ray_time),
|
||||||
};
|
};
|
||||||
rays.max_t[ray_idx] = t;
|
rays.set_max_t(ray_idx, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,14 +58,14 @@ impl<'a> TracerInner<'a> {
|
||||||
{
|
{
|
||||||
let ident = Matrix4x4::new();
|
let ident = Matrix4x4::new();
|
||||||
for i in 0..rays.len() {
|
for i in 0..rays.len() {
|
||||||
rays.update_accel(i, &ident);
|
rays.update_local(i, &ident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divide the rays into 8 different lanes by direction.
|
// Divide the rays into 8 different lanes by direction.
|
||||||
ray_stack.ensure_lane_count(8);
|
ray_stack.ensure_lane_count(8);
|
||||||
for i in 0..rays.len() {
|
for i in 0..rays.len() {
|
||||||
ray_stack.push_ray_index(i, ray_code(rays.dir_world[i]));
|
ray_stack.push_ray_index(i, ray_code(rays.dir(i)));
|
||||||
}
|
}
|
||||||
ray_stack.push_lanes_to_tasks(&[0, 1, 2, 3, 4, 5, 6, 7]);
|
ray_stack.push_lanes_to_tasks(&[0, 1, 2, 3, 4, 5, 6, 7]);
|
||||||
|
|
||||||
|
@ -97,8 +97,8 @@ impl<'a> TracerInner<'a> {
|
||||||
// TODO: re-divide rays based on direction (maybe?).
|
// TODO: re-divide rays based on direction (maybe?).
|
||||||
let xforms = self.xform_stack.top();
|
let xforms = self.xform_stack.top();
|
||||||
ray_stack.pop_do_next_task(2, |ray_idx| {
|
ray_stack.pop_do_next_task(2, |ray_idx| {
|
||||||
let t = rays.time[ray_idx];
|
let t = rays.time(ray_idx);
|
||||||
rays.update_accel(ray_idx, &lerp_slice(xforms, t));
|
rays.update_local(ray_idx, &lerp_slice(xforms, t));
|
||||||
([0, 1, 2, 3, 4, 5, 6, 7], 2)
|
([0, 1, 2, 3, 4, 5, 6, 7], 2)
|
||||||
});
|
});
|
||||||
ray_stack.push_lanes_to_tasks(&[0, 1]);
|
ray_stack.push_lanes_to_tasks(&[0, 1]);
|
||||||
|
@ -130,14 +130,14 @@ impl<'a> TracerInner<'a> {
|
||||||
let xforms = self.xform_stack.top();
|
let xforms = self.xform_stack.top();
|
||||||
if !xforms.is_empty() {
|
if !xforms.is_empty() {
|
||||||
ray_stack.pop_do_next_task(0, |ray_idx| {
|
ray_stack.pop_do_next_task(0, |ray_idx| {
|
||||||
let t = rays.time[ray_idx];
|
let t = rays.time(ray_idx);
|
||||||
rays.update_accel(ray_idx, &lerp_slice(xforms, t));
|
rays.update_local(ray_idx, &lerp_slice(xforms, t));
|
||||||
([0, 1, 2, 3, 4, 5, 6, 7], 0)
|
([0, 1, 2, 3, 4, 5, 6, 7], 0)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let ident = Matrix4x4::new();
|
let ident = Matrix4x4::new();
|
||||||
ray_stack.pop_do_next_task(0, |ray_idx| {
|
ray_stack.pop_do_next_task(0, |ray_idx| {
|
||||||
rays.update_accel(ray_idx, &ident);
|
rays.update_local(ray_idx, &ident);
|
||||||
([0, 1, 2, 3, 4, 5, 6, 7], 0)
|
([0, 1, 2, 3, 4, 5, 6, 7], 0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user