BLI: refactor IndexMask for better performance and memory usage #104629
|
@ -274,7 +274,7 @@ void CurvesGeometry::fill_curve_types(const IndexMask selection, const CurveType
|
|||
}
|
||||
}
|
||||
/* A potential performance optimization is only counting the changed indices. */
|
||||
this->curve_types_for_write().fill_indices(selection, type);
|
||||
index_mask::masked_fill<int8_t>(this->curve_types_for_write(), type, selection);
|
||||
this->update_curve_types();
|
||||
this->tag_topology_changed();
|
||||
}
|
||||
|
@ -581,9 +581,9 @@ void CurvesGeometry::ensure_nurbs_basis_cache() const
|
|||
const VArray<int8_t> orders = this->nurbs_orders();
|
||||
const VArray<int8_t> knots_modes = this->nurbs_knots_modes();
|
||||
|
||||
threading::parallel_for(nurbs_mask.index_range(), 64, [&](const IndexRange range) {
|
||||
nurbs_mask.foreach_span(GrainSize(64), [&](const auto sliced_mask) {
|
||||
Vector<float, 32> knots;
|
||||
for (const int curve_index : nurbs_mask.slice(range)) {
|
||||
for (const int curve_index : sliced_mask) {
|
||||
const IndexRange points = points_by_curve[curve_index];
|
||||
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
|
||||
|
||||
|
@ -1222,7 +1222,7 @@ static CurvesGeometry copy_with_removed_curves(
|
|||
const OffsetIndices old_points_by_curve = curves.points_by_curve();
|
||||
const Span<int> old_offsets = curves.offsets();
|
||||
const Vector<IndexRange> old_curve_ranges = curves_to_delete.to_ranges_invert(
|
||||
curves.curves_range(), nullptr);
|
||||
curves.curves_range());
|
||||
Vector<IndexRange> new_curve_ranges;
|
||||
Vector<IndexRange> old_point_ranges;
|
||||
Vector<IndexRange> new_point_ranges;
|
||||
|
@ -1338,10 +1338,8 @@ static void reverse_curve_point_data(const CurvesGeometry &curves,
|
|||
MutableSpan<T> data)
|
||||
{
|
||||
const OffsetIndices points_by_curve = curves.points_by_curve();
|
||||
threading::parallel_for(curve_selection.index_range(), 256, [&](IndexRange range) {
|
||||
for (const int curve_i : curve_selection.slice(range)) {
|
||||
data.slice(points_by_curve[curve_i]).reverse();
|
||||
}
|
||||
curve_selection.foreach_index([&](const int curve_i) {
|
||||
data.slice(points_by_curve[curve_i]).reverse();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1352,20 +1350,18 @@ static void reverse_swap_curve_point_data(const CurvesGeometry &curves,
|
|||
MutableSpan<T> data_b)
|
||||
{
|
||||
const OffsetIndices points_by_curve = curves.points_by_curve();
|
||||
threading::parallel_for(curve_selection.index_range(), 256, [&](IndexRange range) {
|
||||
for (const int curve_i : curve_selection.slice(range)) {
|
||||
const IndexRange points = points_by_curve[curve_i];
|
||||
MutableSpan<T> a = data_a.slice(points);
|
||||
MutableSpan<T> b = data_b.slice(points);
|
||||
for (const int i : IndexRange(points.size() / 2)) {
|
||||
const int end_index = points.size() - 1 - i;
|
||||
std::swap(a[end_index], b[i]);
|
||||
std::swap(b[end_index], a[i]);
|
||||
}
|
||||
if (points.size() % 2) {
|
||||
const int64_t middle_index = points.size() / 2;
|
||||
std::swap(a[middle_index], b[middle_index]);
|
||||
}
|
||||
curve_selection.foreach_index(GrainSize(256), [&](const int curve_i) {
|
||||
const IndexRange points = points_by_curve[curve_i];
|
||||
MutableSpan<T> a = data_a.slice(points);
|
||||
MutableSpan<T> b = data_b.slice(points);
|
||||
for (const int i : IndexRange(points.size() / 2)) {
|
||||
const int end_index = points.size() - 1 - i;
|
||||
std::swap(a[end_index], b[i]);
|
||||
std::swap(b[end_index], a[i]);
|
||||
}
|
||||
if (points.size() % 2) {
|
||||
const int64_t middle_index = points.size() / 2;
|
||||
std::swap(a[middle_index], b[middle_index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -137,11 +137,11 @@ VArray<float3> mesh_normals_varray(const Mesh &mesh,
|
|||
Span<float3> vert_normals = mesh.vert_normals();
|
||||
const Span<MEdge> edges = mesh.edges();
|
||||
Array<float3> edge_normals(mask.min_array_size());
|
||||
for (const int i : mask) {
|
||||
mask.foreach_index([&](const int i) {
|
||||
const MEdge &edge = edges[i];
|
||||
edge_normals[i] = math::normalize(
|
||||
math::interpolate(vert_normals[edge.v1], vert_normals[edge.v2], 0.5f));
|
||||
}
|
||||
});
|
||||
|
||||
return VArray<float3>::ForContainer(std::move(edge_normals));
|
||||
}
|
||||
|
@ -980,14 +980,12 @@ class VArrayImpl_For_VertexWeights final : public VMutableArrayImpl<float> {
|
|||
if (dverts_ == nullptr) {
|
||||
mask.foreach_index([&](const int i) { dst[i] = 0.0f; });
|
||||
}
|
||||
threading::parallel_for(mask.index_range(), 4096, [&](const IndexRange range) {
|
||||
for (const int64_t i : mask.slice(range)) {
|
||||
if (const MDeformWeight *weight = this->find_weight_at_index(i)) {
|
||||
dst[i] = weight->weight;
|
||||
}
|
||||
else {
|
||||
dst[i] = 0.0f;
|
||||
}
|
||||
mask.foreach_index(GrainSize(4096), [&](const int64_t i) {
|
||||
if (const MDeformWeight *weight = this->find_weight_at_index(i)) {
|
||||
dst[i] = weight->weight;
|
||||
}
|
||||
else {
|
||||
dst[i] = 0.0f;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -109,7 +109,8 @@ void Instances::remove(const IndexMask mask,
|
|||
const AnonymousAttributePropagationInfo &propagation_info)
|
||||
{
|
||||
using namespace blender;
|
||||
if (mask.is_range() && mask.as_range().start() == 0) {
|
||||
const std::optional<IndexRange> masked_range = mask.to_range();
|
||||
if (masked_range.has_value() && masked_range->start() == 0) {
|
||||
/* Deleting from the end of the array can be much faster since no data has to be shifted. */
|
||||
this->resize(mask.size());
|
||||
this->remove_unused_references();
|
||||
|
@ -118,12 +119,12 @@ void Instances::remove(const IndexMask mask,
|
|||
|
||||
const Span<int> old_handles = this->reference_handles();
|
||||
Vector<int> new_handles(mask.size());
|
||||
array_utils::gather(old_handles, mask.indices(), new_handles.as_mutable_span());
|
||||
array_utils::gather(old_handles, mask, new_handles.as_mutable_span());
|
||||
reference_handles_ = std::move(new_handles);
|
||||
|
||||
const Span<float4x4> old_tansforms = this->transforms();
|
||||
Vector<float4x4> new_transforms(mask.size());
|
||||
array_utils::gather(old_tansforms, mask.indices(), new_transforms.as_mutable_span());
|
||||
array_utils::gather(old_tansforms, mask, new_transforms.as_mutable_span());
|
||||
transforms_ = std::move(new_transforms);
|
||||
|
||||
const bke::CustomDataAttributes &src_attributes = attributes_;
|
||||
|
@ -140,7 +141,7 @@ void Instances::remove(const IndexMask mask,
|
|||
GSpan src = *src_attributes.get_for_read(id);
|
||||
dst_attributes.create(id, meta_data.data_type);
|
||||
GMutableSpan dst = *dst_attributes.get_for_write(id);
|
||||
array_utils::gather(src, mask.indices(), dst);
|
||||
array_utils::gather(src, mask, dst);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ BLI_NOINLINE static void sample_point_attribute(const Mesh &mesh,
|
|||
const Span<MLoop> loops = mesh.loops();
|
||||
const Span<MLoopTri> looptris = mesh.looptris();
|
||||
|
||||
for (const int i : mask) {
|
||||
mask.foreach_index([&](const int i) {
|
||||
const int looptri_index = looptri_indices[i];
|
||||
const MLoopTri &looptri = looptris[looptri_index];
|
||||
const float3 &bary_coord = bary_coords[i];
|
||||
|
@ -39,7 +39,7 @@ BLI_NOINLINE static void sample_point_attribute(const Mesh &mesh,
|
|||
|
||||
const T interpolated_value = attribute_math::mix3(bary_coord, v0, v1, v2);
|
||||
dst[i] = interpolated_value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sample_point_attribute(const Mesh &mesh,
|
||||
|
@ -70,7 +70,7 @@ BLI_NOINLINE static void sample_corner_attribute(const Mesh &mesh,
|
|||
{
|
||||
const Span<MLoopTri> looptris = mesh.looptris();
|
||||
|
||||
for (const int i : mask) {
|
||||
mask.foreach_index([&](const int i) {
|
||||
const int looptri_index = looptri_indices[i];
|
||||
const MLoopTri &looptri = looptris[looptri_index];
|
||||
const float3 &bary_coord = bary_coords[i];
|
||||
|
@ -85,7 +85,7 @@ BLI_NOINLINE static void sample_corner_attribute(const Mesh &mesh,
|
|||
|
||||
const T interpolated_value = attribute_math::mix3(bary_coord, v0, v1, v2);
|
||||
dst[i] = interpolated_value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sample_corner_attribute(const Mesh &mesh,
|
||||
|
@ -115,12 +115,12 @@ void sample_face_attribute(const Mesh &mesh,
|
|||
{
|
||||
const Span<MLoopTri> looptris = mesh.looptris();
|
||||
|
||||
for (const int i : mask) {
|
||||
mask.foreach_index([&](const int i) {
|
||||
const int looptri_index = looptri_indices[i];
|
||||
const MLoopTri &looptri = looptris[looptri_index];
|
||||
const int poly_index = looptri.poly;
|
||||
dst[i] = src[poly_index];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void sample_face_attribute(const Mesh &mesh,
|
||||
|
@ -160,7 +160,7 @@ Span<float3> MeshAttributeInterpolator::ensure_barycentric_coords()
|
|||
const Span<MLoop> loops = mesh_->loops();
|
||||
const Span<MLoopTri> looptris = mesh_->looptris();
|
||||
|
||||
for (const int i : mask_) {
|
||||
mask_.foreach_index([&](const int i) {
|
||||
const int looptri_index = looptri_indices_[i];
|
||||
const MLoopTri &looptri = looptris[looptri_index];
|
||||
|
||||
|
@ -173,7 +173,7 @@ Span<float3> MeshAttributeInterpolator::ensure_barycentric_coords()
|
|||
positions[v1_index],
|
||||
positions[v2_index],
|
||||
positions_[i]);
|
||||
}
|
||||
});
|
||||
return bary_coords_;
|
||||
}
|
||||
|
||||
|
@ -189,7 +189,7 @@ Span<float3> MeshAttributeInterpolator::ensure_nearest_weights()
|
|||
const Span<MLoop> loops = mesh_->loops();
|
||||
const Span<MLoopTri> looptris = mesh_->looptris();
|
||||
|
||||
for (const int i : mask_) {
|
||||
mask_.foreach_index([&](const int i) {
|
||||
const int looptri_index = looptri_indices_[i];
|
||||
const MLoopTri &looptri = looptris[looptri_index];
|
||||
|
||||
|
@ -202,7 +202,7 @@ Span<float3> MeshAttributeInterpolator::ensure_nearest_weights()
|
|||
const float d2 = len_squared_v3v3(positions_[i], positions[v2_index]);
|
||||
|
||||
nearest_weights_[i] = MIN3_PAIR(d0, d1, d2, float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
|
||||
}
|
||||
});
|
||||
return nearest_weights_;
|
||||
}
|
||||
|
||||
|
|
|
@ -406,7 +406,6 @@ IndexMask IndexMask::slice_and_offset(const int64_t start,
|
|||
|
||||
IndexMask IndexMask::complement(const IndexRange universe, IndexMaskMemory &memory) const
|
||||
{
|
||||
IndexMaskMemory memory;
|
||||
const AtomicExpr atomic_expr{*this};
|
||||
const ComplementExpr complement_expr{atomic_expr};
|
||||
return IndexMask::from_expr(complement_expr, universe, memory);
|
||||
|
|
|
@ -112,7 +112,7 @@ bool has_anything_selected(const VArray<bool> &varray, IndexRange range_to_check
|
|||
* Find curves that have any point selected (a selection factor greater than zero),
|
||||
* or curves that have their own selection factor greater than zero.
|
||||
*/
|
||||
IndexMask retrieve_selected_curves(const Curves &curves_id, Vector<int64_t> &r_indices);
|
||||
IndexMask retrieve_selected_curves(const Curves &curves_id, IndexMaskMemory &memory);
|
||||
|
||||
/**
|
||||
* Find points that are selected (a selection factor greater than zero),
|
||||
|
|
|
@ -97,7 +97,7 @@ struct CombOperationExecutor {
|
|||
CurvesGeometry *curves_orig_ = nullptr;
|
||||
|
||||
VArray<float> point_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
float2 brush_pos_prev_re_;
|
||||
|
@ -135,7 +135,7 @@ struct CombOperationExecutor {
|
|||
|
||||
point_factors_ = curves_orig_->attributes().lookup_or_default<float>(
|
||||
".selection", ATTR_DOMAIN_POINT, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_orig_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_orig_, selected_curve_memory_);
|
||||
|
||||
brush_pos_prev_re_ = self_->brush_pos_last_re_;
|
||||
brush_pos_re_ = stroke_extension.mouse_position;
|
||||
|
|
|
@ -71,7 +71,7 @@ struct DeleteOperationExecutor {
|
|||
Curves *curves_id_ = nullptr;
|
||||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
const CurvesSculpt *curves_sculpt_ = nullptr;
|
||||
|
@ -95,8 +95,7 @@ struct DeleteOperationExecutor {
|
|||
curves_id_ = static_cast<Curves *>(object_->data);
|
||||
curves_ = &curves_id_->geometry.wrap();
|
||||
|
||||
selected_curve_indices_.clear();
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
|
||||
brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
|
||||
|
|
|
@ -507,7 +507,7 @@ struct DensitySubtractOperationExecutor {
|
|||
Curves *curves_id_ = nullptr;
|
||||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
Object *surface_ob_orig_ = nullptr;
|
||||
|
@ -572,7 +572,7 @@ struct DensitySubtractOperationExecutor {
|
|||
|
||||
minimum_distance_ = brush_->curves_sculpt_settings->minimum_distance;
|
||||
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
transforms_ = CurvesSurfaceTransforms(*object_, curves_id_->surface);
|
||||
const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
|
||||
|
|
|
@ -245,7 +245,7 @@ struct CurvesEffectOperationExecutor {
|
|||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
VArray<float> curve_selection_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
const Brush *brush_ = nullptr;
|
||||
|
@ -286,7 +286,7 @@ struct CurvesEffectOperationExecutor {
|
|||
|
||||
curve_selection_factors_ = curves_->attributes().lookup_or_default(
|
||||
".selection", ATTR_DOMAIN_CURVE, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
const CurvesSculpt &curves_sculpt = *ctx_.scene->toolsettings->curves_sculpt;
|
||||
brush_ = BKE_paint_brush_for_read(&curves_sculpt.paint);
|
||||
|
|
|
@ -68,7 +68,7 @@ struct PinchOperationExecutor {
|
|||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
VArray<float> point_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
CurvesSurfaceTransforms transforms_;
|
||||
|
@ -110,7 +110,7 @@ struct PinchOperationExecutor {
|
|||
|
||||
point_factors_ = curves_->attributes().lookup_or_default<float>(
|
||||
".selection", ATTR_DOMAIN_POINT, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
brush_pos_re_ = stroke_extension.mouse_position;
|
||||
const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
|
||||
|
|
|
@ -56,7 +56,7 @@ struct PuffOperationExecutor {
|
|||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
VArray<float> point_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
const CurvesSculpt *curves_sculpt_ = nullptr;
|
||||
|
@ -107,7 +107,7 @@ struct PuffOperationExecutor {
|
|||
|
||||
point_factors_ = curves_->attributes().lookup_or_default<float>(
|
||||
".selection", ATTR_DOMAIN_POINT, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
falloff_shape_ = static_cast<eBrushFalloffShape>(brush_->falloff_shape);
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ struct SlideOperationExecutor {
|
|||
BVHTreeFromMesh surface_bvh_eval_;
|
||||
|
||||
VArray<float> curve_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
float2 brush_pos_re_;
|
||||
|
@ -159,7 +159,7 @@ struct SlideOperationExecutor {
|
|||
|
||||
curve_factors_ = curves_orig_->attributes().lookup_or_default(
|
||||
".selection", ATTR_DOMAIN_CURVE, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_orig_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_orig_, selected_curve_memory_);
|
||||
|
||||
brush_pos_re_ = stroke_extension.mouse_position;
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ struct SmoothOperationExecutor {
|
|||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
VArray<float> point_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
const CurvesSculpt *curves_sculpt_ = nullptr;
|
||||
|
@ -81,7 +81,7 @@ struct SmoothOperationExecutor {
|
|||
|
||||
point_factors_ = curves_->attributes().lookup_or_default<float>(
|
||||
".selection", ATTR_DOMAIN_POINT, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
transforms_ = CurvesSurfaceTransforms(*object_, curves_id_->surface);
|
||||
|
||||
const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
|
||||
|
|
|
@ -85,7 +85,7 @@ struct SnakeHookOperatorExecutor {
|
|||
CurvesGeometry *curves_ = nullptr;
|
||||
|
||||
VArray<float> curve_factors_;
|
||||
Vector<int64_t> selected_curve_indices_;
|
||||
IndexMaskMemory selected_curve_memory_;
|
||||
IndexMask curve_selection_;
|
||||
|
||||
CurvesSurfaceTransforms transforms_;
|
||||
|
@ -126,7 +126,7 @@ struct SnakeHookOperatorExecutor {
|
|||
|
||||
curve_factors_ = curves_->attributes().lookup_or_default(
|
||||
".selection", ATTR_DOMAIN_CURVE, 1.0f);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_indices_);
|
||||
curve_selection_ = curves::retrieve_selected_curves(*curves_id_, selected_curve_memory_);
|
||||
|
||||
brush_pos_prev_re_ = self.last_mouse_position_re_;
|
||||
brush_pos_re_ = stroke_extension.mouse_position;
|
||||
|
|
|
@ -403,9 +403,9 @@ IndexMask GeometryDataSource::apply_selection_filter(IndexMaskMemory &memory) co
|
|||
const Curves &curves_id = *component.get_for_read();
|
||||
switch (domain_) {
|
||||
case ATTR_DOMAIN_POINT:
|
||||
return curves::retrieve_selected_points(curves_id, indices);
|
||||
return curves::retrieve_selected_points(curves_id, memory);
|
||||
case ATTR_DOMAIN_CURVE:
|
||||
return curves::retrieve_selected_curves(curves_id, indices);
|
||||
return curves::retrieve_selected_curves(curves_id, memory);
|
||||
default:
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
|
|
|
@ -304,6 +304,7 @@ static IndexMask apply_row_filter(const SpreadsheetRowFilter &row_filter,
|
|||
prev_mask,
|
||||
memory);
|
||||
}
|
||||
return prev_mask;
|
||||
}
|
||||
|
||||
static bool use_row_filters(const SpaceSpreadsheet &sspreadsheet)
|
||||
|
|
Loading…
Reference in New Issue