Page MenuHome
Paste P2196

Experimenting with avoiding allocation in curve evaluation functions
ActivePublic

Authored by Hans Goudey (HooglyBoogly) on Jun 22 2021, 6:11 AM.
diff --git a/source/blender/blenkernel/BKE_spline.hh b/source/blender/blenkernel/BKE_spline.hh
index 38a6d41a4d3..1af2b06efc6 100644
--- a/source/blender/blenkernel/BKE_spline.hh
+++ b/source/blender/blenkernel/BKE_spline.hh
@@ -174,6 +174,9 @@ class Spline {
void sample_with_index_factors(const blender::fn::GVArray &src,
blender::Span<float> index_factors,
blender::fn::GMutableSpan dst) const;
+ void sample_with_index_factors(blender::fn::GSpan src,
+ blender::Span<float> index_factors,
+ blender::fn::GMutableSpan dst) const;
template<typename T>
void sample_with_index_factors(const blender::VArray<T> &src,
blender::Span<float> index_factors,
@@ -194,14 +197,32 @@ class Spline {
* Interpolate a virtual array of data with the size of the number of control points to the
* evaluated points. For poly splines, the lifetime of the returned virtual array must not
* exceed the lifetime of the input data.
+ *
+ * \param buffer: A container for temporary storage of result values, when allocating them is
+ * necessary (not a poly spline). The vector may be resized to fit the result data.
+ */
+ virtual blender::fn::GSpan interpolate_to_evaluated(const blender::fn::GVArray &src,
+ blender::Vector<int8_t> &buffer) const = 0;
+ blender::fn::GSpan interpolate_to_evaluated(blender::fn::GSpan src,
+ blender::Vector<int8_t> &buffer) const;
+ template<typename T>
+ blender::Span<T> interpolate_to_evaluated(blender::Span<T> src,
+ blender::Vector<int8_t> &buffer) const
+ {
+ return this->interpolate_to_evaluated(blender::fn::GSpan(src), buffer).typed<T>();
+ }
+
+ /**
+ * Like #interpolate_to_evaluated, but for situations where the result data is more permanent
+ * and must always be copied to a result span, even for poly splines when that is unnecessary.
*/
- virtual blender::fn::GVArrayPtr interpolate_to_evaluated(
- const blender::fn::GVArray &src) const = 0;
- blender::fn::GVArrayPtr interpolate_to_evaluated(blender::fn::GSpan data) const;
+ virtual void interpolate_to_evaluated_copy(const blender::fn::GVArray &src,
+ blender::fn::GMutableSpan dst) const = 0;
+ void interpolate_to_evaluated_copy(blender::fn::GSpan src, blender::fn::GMutableSpan dst) const;
template<typename T>
- blender::fn::GVArray_Typed<T> interpolate_to_evaluated(blender::Span<T> data) const
+ void interpolate_to_evaluated_copy(blender::Span<T> src, blender::Span<T> dst) const
{
- return blender::fn::GVArray_Typed<T>(this->interpolate_to_evaluated(blender::fn::GSpan(data)));
+ this->interpolate_to_evaluated_copy(blender::fn::GSpan(src), blender::fn::GMutableSpan(dst));
}
protected:
@@ -332,7 +353,10 @@ class BezierSpline final : public Spline {
};
InterpolationData interpolation_data_from_index_factor(const float index_factor) const;
- virtual blender::fn::GVArrayPtr interpolate_to_evaluated(const blender::fn::GVArray &src) const;
+ blender::fn::GSpan interpolate_to_evaluated(const blender::fn::GVArray &src,
+ blender::Vector<int8_t> &buffer) const final;
+ void interpolate_to_evaluated_copy(const blender::fn::GVArray &src,
+ blender::fn::GMutableSpan dst) const final;
void evaluate_segment(const int index,
const int next_index,
@@ -454,7 +478,10 @@ class NURBSpline final : public Spline {
blender::Span<blender::float3> evaluated_positions() const final;
- blender::fn::GVArrayPtr interpolate_to_evaluated(const blender::fn::GVArray &src) const final;
+ blender::fn::GSpan interpolate_to_evaluated(const blender::fn::GVArray &src,
+ blender::Vector<int8_t> &buffer) const final;
+ void interpolate_to_evaluated_copy(const blender::fn::GVArray &src,
+ blender::fn::GMutableSpan dst) const final;
protected:
void correct_end_tangents() const final;
@@ -503,7 +530,10 @@ class PolySpline final : public Spline {
blender::Span<blender::float3> evaluated_positions() const final;
- blender::fn::GVArrayPtr interpolate_to_evaluated(const blender::fn::GVArray &src) const final;
+ blender::fn::GSpan interpolate_to_evaluated(const blender::fn::GVArray &src,
+ blender::Vector<int8_t> &buffer) const final;
+ void interpolate_to_evaluated_copy(const blender::fn::GVArray &src,
+ blender::fn::GMutableSpan dst) const final;
protected:
void correct_end_tangents() const final;
diff --git a/source/blender/blenkernel/intern/spline_base.cc b/source/blender/blenkernel/intern/spline_base.cc
index c18f44e07b2..41366d347f0 100644
--- a/source/blender/blenkernel/intern/spline_base.cc
+++ b/source/blender/blenkernel/intern/spline_base.cc
@@ -28,6 +28,7 @@ using blender::float3;
using blender::IndexRange;
using blender::MutableSpan;
using blender::Span;
+using blender::Vector;
using blender::fn::GMutableSpan;
using blender::fn::GSpan;
using blender::fn::GVArray;
@@ -331,9 +332,10 @@ Span<float3> Spline::evaluated_normals() const
}
/* Rotate the generated normals with the interpolated tilt data. */
- GVArray_Typed<float> tilts = this->interpolate_to_evaluated(this->tilts());
+ Vector<int8_t> buffer;
+ Span<float> interpolated_tilts = this->interpolate_to_evaluated(this->tilts(), buffer);
for (const int i : normals.index_range()) {
- normals[i] = rotate_direction_around_axis(normals[i], tangents[i], tilts[i]);
+ normals[i] = rotate_direction_around_axis(normals[i], tangents[i], interpolated_tilts[i]);
}
normal_cache_dirty_ = false;
@@ -438,11 +440,6 @@ void Spline::bounds_min_max(float3 &min, float3 &max, const bool use_evaluated)
}
}
-GVArrayPtr Spline::interpolate_to_evaluated(GSpan data) const
-{
- return this->interpolate_to_evaluated(GVArray_For_GSpan(data));
-}
-
/**
* Sample any input data with a value for each evaluated point (already interpolated to evaluated
* points) to arbitrary parameters in between the evaluated points. The interpolation is quite
@@ -468,3 +465,20 @@ void Spline::sample_with_index_factors(const GVArray &src,
});
});
}
+
+void Spline::sample_with_index_factors(GSpan src,
+ Span<float> index_factors,
+ GMutableSpan dst) const
+{
+ this->sample_with_index_factors(GVArray_For_GSpan(src), index_factors, dst);
+}
+
+GSpan Spline::interpolate_to_evaluated(GSpan src, Vector<int8_t> &buffer) const
+{
+ return this->interpolate_to_evaluated(GVArray_For_GSpan(src), buffer);
+}
+
+void Spline::interpolate_to_evaluated_copy(GSpan src, GMutableSpan dst) const
+{
+ this->interpolate_to_evaluated_copy(GVArray_For_GSpan(src), dst);
+}
diff --git a/source/blender/blenkernel/intern/spline_bezier.cc b/source/blender/blenkernel/intern/spline_bezier.cc
index daae03167ef..633d2a05cba 100644
--- a/source/blender/blenkernel/intern/spline_bezier.cc
+++ b/source/blender/blenkernel/intern/spline_bezier.cc
@@ -25,9 +25,10 @@ using blender::float3;
using blender::IndexRange;
using blender::MutableSpan;
using blender::Span;
+using blender::Vector;
+using blender::fn::GMutableSpan;
+using blender::fn::GSpan;
using blender::fn::GVArray;
-using blender::fn::GVArray_For_ArrayContainer;
-using blender::fn::GVArrayPtr;
SplinePtr BezierSpline::copy() const
{
@@ -567,28 +568,22 @@ static void interpolate_to_evaluated_impl(const BezierSpline &spline,
}
}
-GVArrayPtr BezierSpline::interpolate_to_evaluated(const GVArray &src) const
+void BezierSpline::interpolate_to_evaluated_copy(const GVArray &src, GMutableSpan dst) const
{
- BLI_assert(src.size() == this->size());
-
- if (src.is_single()) {
- return src.shallow_copy();
- }
-
- const int eval_size = this->evaluated_points_size();
- if (eval_size == 1) {
- return src.shallow_copy();
- }
-
- GVArrayPtr new_varray;
blender::attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
using T = decltype(dummy);
if constexpr (!std::is_void_v<blender::attribute_math::DefaultMixer<T>>) {
- Array<T> values(eval_size);
- interpolate_to_evaluated_impl<T>(*this, src.typed<T>(), values);
- new_varray = std::make_unique<GVArray_For_ArrayContainer<Array<T>>>(std::move(values));
+ interpolate_to_evaluated_impl<T>(*this, src.typed<T>(), dst.typed<T>());
}
});
+}
+
+GSpan BezierSpline::interpolate_to_evaluated(const GVArray &src, Vector<int8_t> &buffer) const
+{
+ const int eval_size = this->evaluated_points_size();
+ buffer.resize(src.type().size() * eval_size);
+ GMutableSpan buffer_span{src.type(), buffer.data(), eval_size};
- return new_varray;
+ this->interpolate_to_evaluated_copy(src, buffer_span);
+ return buffer_span;
}
diff --git a/source/blender/blenkernel/intern/spline_nurbs.cc b/source/blender/blenkernel/intern/spline_nurbs.cc
index 31ac23589be..bc2ee6601dd 100644
--- a/source/blender/blenkernel/intern/spline_nurbs.cc
+++ b/source/blender/blenkernel/intern/spline_nurbs.cc
@@ -26,10 +26,10 @@ using blender::float3;
using blender::IndexRange;
using blender::MutableSpan;
using blender::Span;
+using blender::Vector;
+using blender::fn::GMutableSpan;
+using blender::fn::GSpan;
using blender::fn::GVArray;
-using blender::fn::GVArray_For_ArrayContainer;
-using blender::fn::GVArray_Typed;
-using blender::fn::GVArrayPtr;
SplinePtr NURBSpline::copy() const
{
@@ -377,6 +377,27 @@ Span<NURBSpline::BasisCache> NURBSpline::calculate_basis_cache() const
return basis_cache_;
}
+Span<float3> NURBSpline::evaluated_positions() const
+{
+ if (!position_cache_dirty_) {
+ return evaluated_position_cache_;
+ }
+
+ std::lock_guard lock{position_cache_mutex_};
+ if (!position_cache_dirty_) {
+ return evaluated_position_cache_;
+ }
+
+ const int eval_size = this->evaluated_points_size();
+ evaluated_position_cache_.resize(eval_size);
+
+ Spline::interpolate_to_evaluated_copy(positions_.as_span(),
+ evaluated_position_cache_.as_mutable_span());
+
+ position_cache_dirty_ = false;
+ return evaluated_position_cache_;
+}
+
template<typename T>
void interpolate_to_evaluated_impl(Span<NURBSpline::BasisCache> weights,
const blender::VArray<T> &src,
@@ -398,47 +419,26 @@ void interpolate_to_evaluated_impl(Span<NURBSpline::BasisCache> weights,
mixer.finalize();
}
-GVArrayPtr NURBSpline::interpolate_to_evaluated(const GVArray &src) const
+void NURBSpline::interpolate_to_evaluated_copy(const GVArray &src, GMutableSpan dst) const
{
BLI_assert(src.size() == this->size());
- if (src.is_single()) {
- return src.shallow_copy();
- }
-
Span<BasisCache> basis_cache = this->calculate_basis_cache();
- GVArrayPtr new_varray;
blender::attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
using T = decltype(dummy);
if constexpr (!std::is_void_v<blender::attribute_math::DefaultMixer<T>>) {
- Array<T> values(this->evaluated_points_size());
- interpolate_to_evaluated_impl<T>(basis_cache, src.typed<T>(), values);
- new_varray = std::make_unique<GVArray_For_ArrayContainer<Array<T>>>(std::move(values));
+ interpolate_to_evaluated_impl<T>(basis_cache, src.typed<T>(), dst.typed<T>());
}
});
-
- return new_varray;
}
-Span<float3> NURBSpline::evaluated_positions() const
+GSpan NURBSpline::interpolate_to_evaluated(const GVArray &src, Vector<int8_t> &buffer) const
{
- if (!position_cache_dirty_) {
- return evaluated_position_cache_;
- }
-
- std::lock_guard lock{position_cache_mutex_};
- if (!position_cache_dirty_) {
- return evaluated_position_cache_;
- }
-
const int eval_size = this->evaluated_points_size();
- evaluated_position_cache_.resize(eval_size);
-
- /* TODO: Avoid copying the evaluated data from the temporary array. */
- GVArray_Typed<float3> evaluated = Spline::interpolate_to_evaluated(positions_.as_span());
- evaluated->materialize(evaluated_position_cache_);
+ buffer.resize(src.type().size() * eval_size);
+ GMutableSpan buffer_span{src.type(), buffer.data(), eval_size};
- position_cache_dirty_ = false;
- return evaluated_position_cache_;
+ this->interpolate_to_evaluated_copy(src, buffer_span);
+ return buffer_span;
}
diff --git a/source/blender/blenkernel/intern/spline_poly.cc b/source/blender/blenkernel/intern/spline_poly.cc
index e344b8d4910..8b2fee3920f 100644
--- a/source/blender/blenkernel/intern/spline_poly.cc
+++ b/source/blender/blenkernel/intern/spline_poly.cc
@@ -22,8 +22,10 @@
using blender::float3;
using blender::MutableSpan;
using blender::Span;
+using blender::Vector;
+using blender::fn::GMutableSpan;
+using blender::fn::GSpan;
using blender::fn::GVArray;
-using blender::fn::GVArrayPtr;
SplinePtr PolySpline::copy() const
{
@@ -111,15 +113,31 @@ Span<float3> PolySpline::evaluated_positions() const
return this->positions();
}
+void PolySpline::interpolate_to_evaluated_copy(const GVArray &src, GMutableSpan dst) const
+{
+ BLI_assert(src.size() == this->size());
+ BLI_assert(dst.size() == this->evaluated_points_size());
+ src.materialize(dst.data());
+}
+
/**
* Poly spline interpolation from control points to evaluated points is a special case, since
- * the result data is the same as the input data. This function returns a GVArray that points to
+ * the result data is the same as the input data. This function returns a GSpan that points to
* the original data. Therefore the lifetime of the returned virtual array must not be longer than
* the source data.
*/
-GVArrayPtr PolySpline::interpolate_to_evaluated(const GVArray &src) const
+GSpan PolySpline::interpolate_to_evaluated(const GVArray &src, Vector<int8_t> &buffer) const
{
BLI_assert(src.size() == this->size());
+ const int eval_size = this->evaluated_points_size();
+
+ /* If the source data is a span already, simply return that. */
+ if (src.is_span()) {
+ return src.get_internal_span();
+ }
- return src.shallow_copy();
+ /* Otherwise materialize the source data into the result buffer and return a reference to it. */
+ buffer.resize(src.type().size() * eval_size);
+ src.materialize(buffer.data());
+ return {src.type(), static_cast<void *>(buffer.data()), eval_size};
}
diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc
index fc65d1754e9..6360b963745 100644
--- a/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc
+++ b/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc
@@ -15,6 +15,7 @@
*/
#include "BLI_array.hh"
+#include "BLI_enumerable_thread_specific.hh"
#include "BLI_task.hh"
#include "BLI_timeit.hh"
@@ -76,7 +77,9 @@ struct SampleModeParam {
std::optional<int> count;
};
-static SplinePtr resample_spline(const Spline &input_spline, const int count)
+static SplinePtr resample_spline(const Spline &input_spline,
+ const int count,
+ Vector<int8_t> &buffer)
{
std::unique_ptr<PolySpline> output_spline = std::make_unique<PolySpline>();
output_spline->set_cyclic(input_spline.is_cyclic());
@@ -98,12 +101,12 @@ static SplinePtr resample_spline(const Spline &input_spline, const int count)
input_spline.evaluated_positions(), uniform_samples, output_spline->positions());
input_spline.sample_with_index_factors<float>(
- input_spline.interpolate_to_evaluated(input_spline.radii()),
+ input_spline.interpolate_to_evaluated(input_spline.radii(), buffer),
uniform_samples,
output_spline->radii());
input_spline.sample_with_index_factors<float>(
- input_spline.interpolate_to_evaluated(input_spline.tilts()),
+ input_spline.interpolate_to_evaluated(input_spline.tilts(), buffer),
uniform_samples,
output_spline->tilts());
@@ -124,7 +127,7 @@ static SplinePtr resample_spline(const Spline &input_spline, const int count)
}
input_spline.sample_with_index_factors(
- *input_spline.interpolate_to_evaluated(*input_attribute),
+ input_spline.interpolate_to_evaluated(*input_attribute, buffer),
uniform_samples,
*output_attribute);
@@ -144,11 +147,13 @@ static std::unique_ptr<CurveEval> resample_curve(const CurveEval &input_curve,
output_curve->resize(input_splines.size());
MutableSpan<SplinePtr> output_splines = output_curve->splines();
+ threading::EnumerableThreadSpecific<Vector<int8_t>> buffers;
+
if (mode_param.mode == GEO_NODE_CURVE_SAMPLE_COUNT) {
threading::parallel_for(input_splines.index_range(), 128, [&](IndexRange range) {
for (const int i : range) {
BLI_assert(mode_param.count);
- output_splines[i] = resample_spline(*input_splines[i], *mode_param.count);
+ output_splines[i] = resample_spline(*input_splines[i], *mode_param.count, buffers.local());
}
});
}
@@ -157,7 +162,7 @@ static std::unique_ptr<CurveEval> resample_curve(const CurveEval &input_curve,
for (const int i : range) {
const float length = input_splines[i]->length();
const int count = std::max(int(length / *mode_param.length), 1);
- output_splines[i] = resample_spline(*input_splines[i], count);
+ output_splines[i] = resample_spline(*input_splines[i], count, buffers.local());
}
});
}
diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_to_mesh.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_to_mesh.cc
index c0d817385e2..8587f362561 100644
--- a/source/blender/nodes/geometry/nodes/node_geo_curve_to_mesh.cc
+++ b/source/blender/nodes/geometry/nodes/node_geo_curve_to_mesh.cc
@@ -181,7 +181,8 @@ static void spline_extrude_to_mesh_data(const Spline &spline,
Span<float3> normals = spline.evaluated_normals();
Span<float3> profile_positions = profile_spline.evaluated_positions();
- GVArray_Typed<float> radii = spline.interpolate_to_evaluated(spline.radii());
+ Vector<int8_t> buffer;
+ Span<float> radii = spline.interpolate_to_evaluated(spline.radii(), buffer);
for (const int i_ring : IndexRange(spline_vert_len)) {
float4x4 point_matrix = float4x4::from_normalized_axis_data(
positions[i_ring], normals[i_ring], tangents[i_ring]);
diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_to_points.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_to_points.cc
index 2725c625913..0dee9adb9d5 100644
--- a/source/blender/nodes/geometry/nodes/node_geo_curve_to_points.cc
+++ b/source/blender/nodes/geometry/nodes/node_geo_curve_to_points.cc
@@ -15,6 +15,7 @@
*/
#include "BLI_array.hh"
+#include "BLI_enumerable_thread_specific.hh"
#include "BLI_task.hh"
#include "BLI_timeit.hh"
@@ -71,6 +72,7 @@ namespace blender::nodes {
*/
static void evaluate_splines(Span<SplinePtr> splines)
{
+
threading::parallel_for_each(splines, [](const SplinePtr &spline) {
/* These functions fill the corresponding caches on each spline. */
spline->evaluated_positions();
@@ -85,6 +87,7 @@ static Array<int> calculate_spline_point_offsets(GeoNodeExecParams &params,
const CurveEval &curve,
const Span<SplinePtr> splines)
{
+
const int size = curve.splines().size();
switch (mode) {
case GEO_NODE_CURVE_SAMPLE_COUNT: {
@@ -199,8 +202,8 @@ static void copy_evaluated_point_attributes(Span<SplinePtr> splines,
const int size = offsets[i + 1] - offsets[i];
data.positions.slice(offset, size).copy_from(spline.evaluated_positions());
- spline.interpolate_to_evaluated(spline.radii())->materialize(data.radii.slice(offset, size));
- spline.interpolate_to_evaluated(spline.tilts())->materialize(data.tilts.slice(offset, size));
+ spline.interpolate_to_evaluated_copy(spline.radii(), data.radii.slice(offset, size));
+ spline.interpolate_to_evaluated_copy(spline.tilts(), data.tilts.slice(offset, size));
for (const Map<std::string, GMutableSpan>::Item &item : data.point_attributes.items()) {
const StringRef name = item.key;
@@ -209,8 +212,7 @@ static void copy_evaluated_point_attributes(Span<SplinePtr> splines,
BLI_assert(spline.attributes.get_for_read(name));
GSpan spline_span = *spline.attributes.get_for_read(name);
- spline.interpolate_to_evaluated(spline_span)
- ->materialize(point_span.slice(offset, size).data());
+ spline.interpolate_to_evaluated_copy(spline_span, point_span.slice(offset, size));
}
data.tangents.slice(offset, size).copy_from(spline.evaluated_tangents());
@@ -223,7 +225,10 @@ static void copy_uniform_sample_point_attributes(Span<SplinePtr> splines,
Span<int> offsets,
ResultAttributes &data)
{
+ threading::EnumerableThreadSpecific<Vector<int8_t>> buffers;
+
threading::parallel_for(splines.index_range(), 64, [&](IndexRange range) {
+ Vector<int8_t> buffer = buffers.local();
for (const int i : range) {
const Spline &spline = *splines[i];
const int offset = offsets[i];
@@ -234,16 +239,16 @@ static void copy_uniform_sample_point_attributes(Span<SplinePtr> splines,
const Array<float> uniform_samples = spline.sample_uniform_index_factors(size);
- spline.sample_with_index_factors<float3>(
+ spline.sample_with_index_factors(
spline.evaluated_positions(), uniform_samples, data.positions.slice(offset, size));
- spline.sample_with_index_factors<float>(spline.interpolate_to_evaluated(spline.radii()),
- uniform_samples,
- data.radii.slice(offset, size));
+ spline.sample_with_index_factors(spline.interpolate_to_evaluated(spline.radii(), buffer),
+ uniform_samples,
+ data.radii.slice(offset, size));
- spline.sample_with_index_factors<float>(spline.interpolate_to_evaluated(spline.tilts()),
- uniform_samples,
- data.tilts.slice(offset, size));
+ spline.sample_with_index_factors(spline.interpolate_to_evaluated(spline.tilts(), buffer),
+ uniform_samples,
+ data.tilts.slice(offset, size));
for (const Map<std::string, GMutableSpan>::Item &item : data.point_attributes.items()) {
const StringRef name = item.key;
@@ -252,18 +257,18 @@ static void copy_uniform_sample_point_attributes(Span<SplinePtr> splines,
BLI_assert(spline.attributes.get_for_read(name));
GSpan spline_span = *spline.attributes.get_for_read(name);
- spline.sample_with_index_factors(*spline.interpolate_to_evaluated(spline_span),
+ spline.sample_with_index_factors(spline.interpolate_to_evaluated(spline_span, buffer),
uniform_samples,
point_span.slice(offset, size));
}
- spline.sample_with_index_factors<float3>(
+ spline.sample_with_index_factors(
spline.evaluated_tangents(), uniform_samples, data.tangents.slice(offset, size));
for (float3 &tangent : data.tangents) {
tangent.normalize();
}
- spline.sample_with_index_factors<float3>(
+ spline.sample_with_index_factors(
spline.evaluated_normals(), uniform_samples, data.normals.slice(offset, size));
for (float3 &normals : data.normals) {
normals.normalize();

Event Timeline

Hans Goudey (HooglyBoogly) changed the title of this paste from Command-Line Input to Experimenting with avoiding allocation in curve evaluation functions.Jun 22 2021, 6:11 AM
Hans Goudey (HooglyBoogly) updated the paste's language from autodetect to diff.