chrono_batch_render/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
414 / 450
92.0%
Branches:
333 / 462
72.1%
Functions:
33 / 34, 1 excl
97.1%

scene/tables/tangents.cpp
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 #include "chrono_batch_render/scene/tables/tangents.h"
3
4 #include <algorithm>
5 #include <array>
6 #include <bit>
7 #include <cmath>
8 #include <map>
9 #include <set>
10 #include <vector>
11
12 #include <mikktspace.h>
13
14 #include "chrono_batch_render/scene/kernels.h"
15 #include "chrono_batch_render/scene/records/render_tables.h"
16 #include "chrono_batch_render/scene/schema/columns.h"
17 #include "chrono_batch_render/scene/tables/kernels/tangents.h"
18 #include "chrono_batch_render/stage/publish/change_set.h"
19 #include "chrono_batch_render/stage/structure/packing.h"
20 #include "chrono_batch_render/util/profiler.h"
21 #include "chrono_batch_render/util/span.h"
22
23 namespace chrono::batch_render {
24 namespace {
25
26 39 struct Faces {
27 const TangentMesh* mesh = nullptr;
28 std::vector<std::array<uint32_t, 4>> corners;
29 std::vector<uint32_t> sizes;
30 std::vector<Float4>* tangents = nullptr;
31 };
32
33 15766 Faces& ContextFaces(const SMikkTSpaceContext* context) {
34 15766 return *static_cast<Faces*>(context->m_pUserData);
35 }
36
37 7705 uint32_t Corner(const SMikkTSpaceContext* context, int face, int vertex) {
38 7705 return ContextFaces(context).corners[face][vertex];
39 }
40
41 146 int FaceCount(const SMikkTSpaceContext* context) {
42 146 return int(ContextFaces(context).corners.size());
43 }
44
45 210 int FaceSize(const SMikkTSpaceContext* context, int face) {
46 210 return int(ContextFaces(context).sizes[face]);
47 }
48
49 4619 void Position(const SMikkTSpaceContext* context, float out[], int face, int vertex) {
50 4619 const Faces& faces = ContextFaces(context);
51 4619 const Float3& p = faces.mesh->points[faces.mesh->indices[Corner(context, face, vertex)]];
52 4619 out[0] = p.x;
53 4619 out[1] = p.y;
54 4619 out[2] = p.z;
55 4619 }
56
57 1476 void Normal(const SMikkTSpaceContext* context, float out[], int face, int vertex) {
58 1476 const Float3& n = ContextFaces(context).mesh->normals[Corner(context, face, vertex)];
59 1476 out[0] = n.x;
60 1476 out[1] = n.y;
61 1476 out[2] = n.z;
62 1476 }
63
64 1333 void TexCoord(const SMikkTSpaceContext* context, float out[], int face, int vertex) {
65 1333 const Float2& st = ContextFaces(context).mesh->st[Corner(context, face, vertex)];
66 1333 out[0] = st.x;
67 1333 out[1] = st.y;
68 1333 }
69
70 277 void StoreTangent(const SMikkTSpaceContext* context, const float tangent[], float sign, int face,
71 int vertex) {
72 277 (*ContextFaces(context).tangents)[Corner(context, face, vertex)] = {tangent[0], tangent[1],
73 277 tangent[2], sign};
74 277 }
75
76 512 Float3 Sub(Float3 a, Float3 b) {
77 512 return {a.x - b.x, a.y - b.y, a.z - b.z};
78 }
79
80 256 Float3 Cross(Float3 a, Float3 b) {
81 256 return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
82 }
83
84 }
85
86 ✗ std::vector<Float3> SmoothCornerNormals(std::span<const Float3> points,
87 std::span<const int32_t> indices,
88 std::span<const int32_t> counts) {
89 ✗ std::vector<Float3> sums(points.size());
90 ✗ uint32_t first = 0;
91
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const int32_t count : counts) {
92
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ for (int32_t k = 1; k + 1 < count && first + k + 1 < indices.size(); ++k) {
93
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const uint32_t a = uint32_t(indices[first]);
94 ✗ const uint32_t b = uint32_t(indices[first + k]);
95 ✗ const uint32_t c = uint32_t(indices[first + k + 1]);
96
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ if (a >= points.size() || b >= points.size() || c >= points.size()) {
97 ✗ continue;
98 }
99 ✗ const Float3 n = Cross(Sub(points[b], points[a]), Sub(points[c], points[a]));
100
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const uint32_t point : {a, b, c}) {
101 ✗ sums[point] = {sums[point].x + n.x, sums[point].y + n.y, sums[point].z + n.z};
102 }
103 }
104
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ first += uint32_t(std::max(count, 0));
105 }
106
0/1
✗ Branch 1 not taken.
✗ std::vector<Float3> corners(indices.size());
107
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (uint32_t corner = 0; corner < indices.size(); ++corner) {
108
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const uint32_t point = uint32_t(indices[corner]);
109
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const Float3 n = point < sums.size() ? sums[point] : Float3{0, 0, 1};
110
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const float length = std::sqrt(n.x * n.x + n.y * n.y + n.z * n.z);
111 ✗ corners[corner] =
112
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ length > 0 ? Float3{n.x / length, n.y / length, n.z / length} : Float3{0, 0, 1};
113 }
114 ✗ return corners;
115 ✗ }
116
117 34 std::vector<Float3> FacetedCornerNormals(std::span<const Float3> points,
118 std::span<const int32_t> indices,
119 std::span<const int32_t> counts) {
120 34 std::vector<Float3> corners(indices.size(), Float3{0, 0, 1});
121 34 uint32_t first = 0;
122
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 34 times.
98 for (const int32_t count : counts) {
123
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 const uint32_t size = uint32_t(std::max(count, 0));
124
1/2
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
64 if (first + size > indices.size()) {
125 break;
126 }
127 64 Float3 n{};
128
2/4
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 64 times.
64 bool valid = size >= 3 && uint32_t(indices[first]) < points.size();
129
2/2
✓ Branch 0 taken 256 times.
✓ Branch 1 taken 64 times.
320 for (uint32_t k = 0; valid && k < size; ++k) {
130
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 const uint32_t a = uint32_t(indices[first + k]);
131 256 const uint32_t b = uint32_t(indices[first + (k + 1) % size]);
132
2/4
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 256 times.
256 if (a >= points.size() || b >= points.size()) {
133 ✗ valid = false;
134 ✗ break;
135 }
136 256 const Float3 origin = points[uint32_t(indices[first])];
137 256 const Float3 c = Cross(Sub(points[a], origin), Sub(points[b], origin));
138 256 n = {n.x + c.x, n.y + c.y, n.z + c.z};
139 }
140
1/2
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
64 const float length = std::sqrt(n.x * n.x + n.y * n.y + n.z * n.z);
141
2/4
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 64 times.
✗ Branch 3 not taken.
64 if (valid && length > 0) {
142 128 std::fill_n(corners.begin() + first, size,
143 64 Float3{n.x / length, n.y / length, n.z / length});
144 }
145 64 first += size;
146 }
147 34 return corners;
148 }
149
150 39 std::vector<Float4> MikkTangents(const TangentMesh& mesh) {
151 39 std::vector<Float4> tangents(mesh.indices.size(), Float4{1, 0, 0, 1});
152 39 Faces faces{.mesh = &mesh, .tangents = &tangents};
153 39 uint32_t first = 0;
154
2/2
✓ Branch 0 taken 69 times.
✓ Branch 1 taken 39 times.
108 for (const int32_t count : mesh.counts) {
155
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 68 times.
69 if (count == 3 || count == 4) {
156
1/1
✓ Branch 1 taken 68 times.
68 faces.corners.push_back({first, first + 1, first + 2, first + 3});
157
1/1
✓ Branch 1 taken 68 times.
68 faces.sizes.push_back(uint32_t(count));
158 } else {
159
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 for (int32_t k = 1; k + 1 < count; ++k) {
160
1/1
✓ Branch 1 taken 3 times.
3 faces.corners.push_back({first, first + k, first + k + 1, 0});
161
1/1
✓ Branch 1 taken 3 times.
3 faces.sizes.push_back(3);
162 }
163 }
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 69 times.
69 first += uint32_t(std::max(count, 0));
165 }
166
4/6
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 39 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 38 times.
39 if (first > mesh.indices.size() || mesh.normals.size() != mesh.indices.size() ||
167
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 38 times.
39 mesh.st.size() != mesh.indices.size()) {
168 1 return {};
169 }
170 38 SMikkTSpaceInterface callbacks{};
171 38 callbacks.m_getNumFaces = FaceCount;
172 38 callbacks.m_getNumVerticesOfFace = FaceSize;
173 38 callbacks.m_getPosition = Position;
174 38 callbacks.m_getNormal = Normal;
175 38 callbacks.m_getTexCoord = TexCoord;
176 38 callbacks.m_setTSpaceBasic = StoreTangent;
177 38 SMikkTSpaceContext context{&callbacks, &faces};
178
1/1
✓ Branch 1 taken 38 times.
38 genTangSpaceDefault(&context);
179 38 return tangents;
180 39 }
181
182 namespace {
183
184 constexpr ColumnRef kInputs[] = {
185 kPointBasedPoints, kMeshFaceVertexIndices, kMeshFaceVertexCounts, kGprimPrimvars,
186 kPointBasedNormals, kPointBasedNormalsInterpolation, kMeshSubdivisionScheme};
187
188 }
189
190 2437 std::span<const ColumnRef> TangentTable::Inputs() const {
191 2437 return kInputs;
192 }
193
194 559 void TangentTable::Describe(RenderTables& tables) const {
195
1/2
✓ Branch 0 taken 559 times.
✗ Branch 1 not taken.
559 tables.tangents.address = m_records.Buffer() ? m_records.Buffer()->Address() : 0;
196 559 }
197
198 940 void TangentTable::Hold(std::vector<Ref<const RefCounted>>& held) const {
199
4/8
✓ Branch 0 taken 940 times.
✓ Branch 1 taken 940 times.
✓ Branch 2 taken 939 times.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1880 HoldBuffers(held, {m_records.Buffer()});
200 1880 }
201
202 85 void TangentTable::CacheCells(Mesh& mesh, const Cells& cells) const {
203 85 mesh.interpolation = cells.interpolation[mesh.pointsRow];
204 85 mesh.scheme = cells.scheme[mesh.meshRow];
205 85 }
206
207 97 TangentTable::Key TangentTable::KeyFor(const Mesh& mesh) const {
208 97 ColumnStore& store = m_structure.GetColumnStore();
209 97 Key key;
210 97 key.points = store.Array(kPointBasedPoints, mesh.pointsRow).address;
211 97 key.indices = store.Array(kMeshFaceVertexIndices, mesh.meshRow).address;
212 97 key.counts = store.Array(kMeshFaceVertexCounts, mesh.meshRow).address;
213 97 key.primvars = store.Array(kGprimPrimvars, mesh.gprimRow).address;
214 97 key.normals = store.Array(kPointBasedNormals, mesh.pointsRow).address;
215 97 key.interpolation = mesh.interpolation;
216 97 key.scheme = mesh.scheme;
217 97 return key;
218 }
219
220 77 void TangentTable::Compute(const Mesh& mesh, const Key& key, Set& set) {
221
4/5
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 75 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 2 times.
77 CHRONO_BATCH_RENDER_ZONE("Tangents/Compute");
222 77 ++m_stats.computed;
223
1/1
✓ Branch 1 taken 77 times.
77 ColumnStore& store = m_structure.GetColumnStore();
224
2/2
✓ Branch 1 taken 77 times.
✓ Branch 4 taken 77 times.
77 store.Arrays(ColumnType::IntArray).Free(set.tangents);
225 77 set.tangents = {};
226 77 set.words = 0;
227 77 set.deform.reset();
228
1/1
✓ Branch 1 taken 77 times.
77 const ArrayRef points = store.Array(kPointBasedPoints, mesh.pointsRow);
229
1/1
✓ Branch 1 taken 77 times.
77 const ArrayRef indices = store.Array(kMeshFaceVertexIndices, mesh.meshRow);
230
1/1
✓ Branch 1 taken 77 times.
77 const ArrayRef counts = store.Array(kMeshFaceVertexCounts, mesh.meshRow);
231
1/1
✓ Branch 1 taken 77 times.
77 const ArrayRef table = store.Array(kGprimPrimvars, mesh.gprimRow);
232
6/8
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 76 times.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 34 times.
✓ Branch 7 taken 42 times.
77 if (points.count == 0 || indices.count == 0 || counts.count == 0 || table.count == 0) {
233 return;
234 }
235
1/1
✓ Branch 1 taken 34 times.
34 ArrayPool& ints = store.Arrays(ColumnType::IntArray);
236
1/1
✓ Branch 1 taken 34 times.
34 ArrayPool& float3s = store.Arrays(ColumnType::Float3Array);
237 34 const std::vector<Primvar> primvars =
238
3/3
✓ Branch 1 taken 34 times.
✓ Branch 4 taken 34 times.
✓ Branch 7 taken 34 times.
68 UnpackPrimvars(m_structure.GetTokens(), Items<int32_t>(ints.Read(table)));
239 34 const auto st = std::ranges::find_if(primvars, [](const Primvar& primvar) {
240
3/4
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
34 return primvar.name == "st" && primvar.type == PrimvarType::Float2;
241 });
242
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 time.
34 if (st == primvars.end()) {
243 return;
244 }
245 33 TangentMesh input;
246
2/2
✓ Branch 1 taken 33 times.
✓ Branch 4 taken 33 times.
99 input.points = Items<Float3>(float3s.Read(points));
247
2/2
✓ Branch 1 taken 33 times.
✓ Branch 4 taken 33 times.
99 input.indices = Items<int32_t>(ints.Read(indices));
248
2/2
✓ Branch 1 taken 33 times.
✓ Branch 4 taken 33 times.
99 input.counts = Items<int32_t>(ints.Read(counts));
249
1/1
✓ Branch 1 taken 33 times.
33 const std::optional<std::vector<Float2>> corners = CornerValues<Float2>(
250
2/2
✓ Branch 1 taken 33 times.
✓ Branch 4 taken 33 times.
33 st->Values<Float2>(), st->indices, st->interpolation, input.indices, input.counts);
251
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 if (!corners) {
252 return;
253 }
254
1/1
✓ Branch 1 taken 33 times.
33 input.st = *corners;
255
1/1
✓ Branch 1 taken 33 times.
33 const ArrayRef normals = store.Array(kPointBasedNormals, mesh.pointsRow);
256 33 const SubdivisionScheme scheme = SubdivisionScheme(key.scheme);
257 33 const bool smooth =
258 scheme == SubdivisionScheme::CatmullClark || scheme == SubdivisionScheme::Loop;
259 33 const auto primvarNormals = std::ranges::find_if(primvars, [](const Primvar& primvar) {
260
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
33 return primvar.name == "normals" && primvar.type == PrimvarType::Float3 &&
261
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ primvar.elementSize == 1;
262 });
263 33 std::optional<std::vector<Float3>> authored;
264
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
33 if (scheme == SubdivisionScheme::None && primvarNormals != primvars.end()) {
265
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ authored = CornerValues<Float3>(primvarNormals->Values<Float3>(), primvarNormals->indices,
266
0/1
✗ Branch 1 not taken.
✗ primvarNormals->interpolation, input.indices, input.counts);
267
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
33 } else if (scheme == SubdivisionScheme::None && normals.count > 0) {
268
0/1
✗ Branch 1 not taken.
✗ authored =
269
0/2
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ CornerValues<Float3>(Items<Float3>(float3s.Read(normals)), {},
270
0/1
✗ Branch 1 not taken.
✗ Interpolation(key.interpolation), input.indices, input.counts);
271 }
272
2/5
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✗ Branch 6 not taken.
✓ Branch 9 taken 33 times.
66 input.normals = authored ? *authored
273
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 : smooth ? SmoothCornerNormals(input.points, input.indices, input.counts)
274 33 : FacetedCornerNormals(input.points, input.indices, input.counts);
275
1/1
✓ Branch 1 taken 33 times.
33 const std::vector<Float4> tangents = MikkTangents(input);
276
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 if (tangents.empty()) {
277 return;
278 }
279
1/1
✓ Branch 1 taken 33 times.
33 device::Device& device = store.GetDevice();
280
1/1
✓ Branch 1 taken 33 times.
33 ArrayPool& words = store.Arrays(ColumnType::IntArray);
281
1/1
✓ Branch 1 taken 33 times.
33 set.words = uint32_t(tangents.size() * 4);
282
1/1
✓ Branch 1 taken 33 times.
33 set.tangents = words.Allocate(set.words);
283
1/1
✓ Branch 1 taken 33 times.
33 words.Write(set.tangents, std::as_bytes(std::span(tangents)));
284
285
2/4
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 33 times.
33 const bool faceted = !smooth && !authored;
286 33 std::vector<uint32_t> triangles;
287
1/1
✓ Branch 1 taken 33 times.
33 std::vector<uint32_t> cornerFaces(input.indices.size());
288 33 uint32_t first = 0;
289
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 33 times.
81 for (uint32_t face = 0; face < input.counts.size(); ++face) {
290 48 const int32_t count = input.counts[face];
291
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 48 times.
144 for (int32_t k = 1; k + 1 < count; ++k) {
292 96 triangles.insert(triangles.end(),
293
1/1
✓ Branch 1 taken 96 times.
96 {first, first + uint32_t(k), first + uint32_t(k) + 1});
294 }
295
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 const uint32_t size = uint32_t(std::max(count, 0));
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
48 std::fill_n(cornerFaces.begin() + first, std::min<size_t>(size, cornerFaces.size() - first),
297 face);
298 48 first += size;
299 }
300
1/1
✓ Branch 1 taken 33 times.
33 std::map<std::array<uint32_t, 6>, uint32_t> keys;
301
1/1
✓ Branch 1 taken 33 times.
33 std::vector<uint32_t> groups(input.indices.size(), kNoRow);
302
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 33 times.
321 for (const uint32_t corner : triangles) {
303
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 288 times.
288 const Float2 uv = input.st[corner];
304
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 288 times.
288 const Float3 normal = authored ? input.normals[corner] : Float3{};
305
1/2
✓ Branch 0 taken 288 times.
✗ Branch 1 not taken.
288 const std::array<uint32_t, 6> weld = {uint32_t(input.indices[corner]),
306 288 std::bit_cast<uint32_t>(uv.x),
307 288 std::bit_cast<uint32_t>(uv.y),
308
1/2
✓ Branch 0 taken 288 times.
✗ Branch 1 not taken.
288 faceted ? cornerFaces[corner]
309 ✗ : std::bit_cast<uint32_t>(normal.x),
310 288 std::bit_cast<uint32_t>(normal.y),
311
2/3
✓ Branch 0 taken 288 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 288 times.
576 std::bit_cast<uint32_t>(normal.z)};
312
1/1
✓ Branch 1 taken 288 times.
288 groups[corner] = keys.try_emplace(weld, uint32_t(keys.size())).first->second;
313 }
314
1/1
✓ Branch 1 taken 33 times.
33 std::vector<uint32_t> starts(keys.size() + 1, 0);
315
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 33 times.
321 for (const uint32_t corner : triangles) {
316 288 ++starts[groups[corner] + 1];
317 }
318
2/2
✓ Branch 0 taken 192 times.
✓ Branch 1 taken 33 times.
225 for (uint32_t group = 0; group < keys.size(); ++group) {
319 192 starts[group + 1] += starts[group];
320 }
321
1/1
✓ Branch 1 taken 33 times.
33 std::vector<uint32_t> entries(triangles.size());
322
1/1
✓ Branch 1 taken 33 times.
33 std::vector<uint32_t> filled(starts.begin(), starts.end() - 1);
323
2/2
✓ Branch 0 taken 288 times.
✓ Branch 1 taken 33 times.
321 for (uint32_t entry = 0; entry < triangles.size(); ++entry) {
324 288 entries[filled[groups[triangles[entry]]]++] = entry;
325 }
326
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
33 if (keys.empty()) {
327 ✗ return;
328 }
329 33 Deform deform;
330
2/3
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
33 deform.st = device.CreateBuffer({.label = "tangent st"}, input.st);
331 33 if (authored) {
332
0/3
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ deform.normals = device.CreateBuffer({.label = "tangent normals"}, input.normals);
333 }
334 33 deform.faceted = faceted;
335
2/3
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
33 deform.triangles = device.CreateBuffer({.label = "tangent triangles"}, triangles);
336
2/3
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
33 deform.starts = device.CreateBuffer({.label = "tangent group starts"}, starts);
337
2/3
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
33 deform.corners = device.CreateBuffer({.label = "tangent group corners"}, entries);
338
2/3
✓ Branch 1 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
33 deform.groups = device.CreateBuffer({.label = "tangent corner groups"}, groups);
339 33 deform.results =
340
3/4
✓ Branch 1 taken 33 times.
✓ Branch 4 taken 33 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 33 times.
33 device.CreateBuffer({.bytes = keys.size() * sizeof(Float4), .label = "tangent groups"});
341 33 deform.groupCount = uint32_t(keys.size());
342 33 deform.cornerCount = uint32_t(input.indices.size());
343 33 set.deform = std::move(deform);
344 275 }
345
346 3 void TangentTable::Recompute(const Mesh& mesh, const Key& key, Set& set,
347 device::CommandEncoder& encoder) {
348 3 ++m_stats.deformed;
349
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 const Deform& deform = *set.deform;
350 3 TangentParams params{};
351 3 params.points.address = key.points;
352
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 params.smoothNormals.address = m_geometry.Record(mesh.meshRow).normals.address;
353
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 params.cornerNormals.address = deform.normals ? deform.normals->Address() : 0;
354
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 params.cornerSt.address = deform.st->Address();
355 3 params.indices.address = key.indices;
356 3 params.triangles.address = deform.triangles->Address();
357 3 params.groupStarts.address = deform.starts->Address();
358 3 params.groupCorners.address = deform.corners->Address();
359 3 params.cornerGroups.address = deform.groups->Address();
360 3 params.groupTangents.address = deform.results->Address();
361 3 params.tangents.address = set.tangents.address;
362 3 params.groupCount = deform.groupCount;
363 3 params.cornerCount = deform.cornerCount;
364
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 params.faceted = deform.faceted ? 1 : 0;
365 3 encoder.Dispatch(Kernels().Entry("GroupTangents"), params,
366 3 {.x = device::GroupCount(params.groupCount, kTangentsGroup)});
367 3 encoder.Dispatch(Kernels().Entry("CornerTangents"), params,
368 3 {.x = device::GroupCount(params.cornerCount, kTangentsGroup)});
369 3 }
370
371 87 void TangentTable::Link(uint32_t prim, const Mesh& mesh) {
372 87 TangentRecord record{};
373
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 if (mesh.key) {
374 87 const Set& set = m_sets.at(*mesh.key);
375
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 46 times.
87 if (set.words > 0) {
376 41 record.tangents = set.tangents;
377 41 record.primvar = m_structure.GetTokens().Find("st").id;
378 }
379 }
380 87 const Prims& prims = m_structure.GetPrims();
381
3/4
✓ Branch 1 taken 36 times.
✓ Branch 2 taken 51 times.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
87 if (m_records.Set(mesh.meshRow, record) && prims.Alive(prim)) {
382 36 m_relinked.push_back(prims.Record(prim).world);
383 }
384 87 }
385
386 81 void TangentTable::Place(uint32_t prim, const Cells& cells) {
387 81 const Prims& prims = m_structure.GetPrims();
388 81 Mesh mesh;
389 81 mesh.generation = prims.Generation(prim);
390 81 mesh.pointsRow = prims.Row(prim, SchemaId::PointBased);
391 81 mesh.meshRow = prims.Row(prim, SchemaId::Mesh);
392 81 mesh.gprimRow = prims.Row(prim, SchemaId::Gprim);
393 81 m_byPointsRow[mesh.pointsRow] = prim;
394 81 m_byMeshRow[mesh.meshRow] = prim;
395 81 m_byGprimRow[mesh.gprimRow] = prim;
396 81 CacheCells(mesh, cells);
397 81 const Key key = KeyFor(mesh);
398 81 const auto [entry, fresh] = m_sets.try_emplace(key);
399
2/2
✓ Branch 0 taken 75 times.
✓ Branch 1 taken 6 times.
81 if (fresh) {
400 75 Compute(mesh, key, entry->second);
401 }
402
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
81 ++entry->second.users;
403
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 81 times.
81 mesh.key = key;
404 81 mesh.set = &entry->second;
405 81 Link(prim, mesh);
406 81 m_meshes[prim] = mesh;
407 81 }
408
409 1118 void TangentTable::Prune() {
410
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1107 times.
1118 if (!m_stale) {
411 return;
412 }
413 11 m_stale = false;
414 11 ArrayPool& words = m_structure.GetColumnStore().Arrays(ColumnType::IntArray);
415 11 std::erase_if(m_sets, [&](const auto& entry) {
416
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 11 times.
14 if (entry.second.users > 0) {
417 return false;
418 }
419 11 words.Free(entry.second.tangents);
420 return true;
421 });
422 }
423
424 13 void TangentTable::Release(Mesh& mesh) {
425
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 if (mesh.key) {
426 13 --m_sets.at(*mesh.key).users;
427
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 mesh.key.reset();
428 13 mesh.set = nullptr;
429 13 m_stale = true;
430 }
431 13 }
432
433 13 void TangentTable::Forget(uint32_t prim) {
434 13 Mesh& mesh = m_meshes.at(prim);
435 13 Release(mesh);
436 13 m_byPointsRow.erase(mesh.pointsRow);
437 13 m_byMeshRow.erase(mesh.meshRow);
438 13 m_byGprimRow.erase(mesh.gprimRow);
439 13 m_meshes.erase(prim);
440 13 }
441
442 559 void TangentTable::Run(const ChangeSet& changes, TableVersions& versions) {
443 559 Process(changes);
444 559 m_records.Reserve(m_structure.GetColumnStore().Schema(SchemaId::Mesh).Capacity());
445
2/2
✓ Branch 1 taken 281 times.
✓ Branch 2 taken 278 times.
559 if (m_records.Upload(m_structure.GetColumnStore().GetDevice())) {
446 281 m_relinked = {kGlobalWorld};
447 }
448
2/2
✓ Branch 0 taken 283 times.
✓ Branch 1 taken 276 times.
559 if (!m_relinked.empty()) {
449 283 versions.Record(TableId::Tangents, m_relinked);
450 }
451 559 m_relinked.clear();
452 559 }
453
454 559 void TangentTable::Process(const ChangeSet& changes) {
455
4/5
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 554 times.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 5 times.
559 CHRONO_BATCH_RENDER_ZONE("Tangents/Run");
456 559 m_stats.computed = 0;
457
2/2
✓ Branch 0 taken 278 times.
✓ Branch 1 taken 281 times.
559 ColumnStore& store = m_structure.GetColumnStore();
458
2/2
✓ Branch 0 taken 278 times.
✓ Branch 1 taken 281 times.
559 const Prims& prims = m_structure.GetPrims();
459 837 const bool rekey = changes.resync ||
460
5/5
✓ Branch 0 taken 278 times.
✓ Branch 1 taken 281 times.
✓ Branch 3 taken 278 times.
✓ Branch 5 taken 277 times.
✓ Branch 6 taken 1 time.
559 store.LastWrite(kPointBasedNormalsInterpolation) > m_seen ||
461
3/3
✓ Branch 1 taken 277 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 274 times.
277 store.LastWrite(kMeshSubdivisionScheme) > m_seen;
462
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 278 times.
559 m_seen = store.WriteSequence();
463 559 m_stats.deformed = 0;
464 559 bool read = false;
465 644 const auto cells = [&]() -> const Cells& {
466 85 ColumnTable& points = store.Schema(SchemaId::PointBased);
467 85 ColumnTable& meshes = store.Schema(SchemaId::Mesh);
468
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 const bool stale = rekey || changes.resync || !changes.added.empty() ||
469
4/6
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 77 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
93 m_cells.interpolation.size() < points.Rows() ||
470
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 m_cells.scheme.size() < meshes.Rows();
471
4/4
✓ Branch 0 taken 69 times.
✓ Branch 1 taken 16 times.
✓ Branch 2 taken 61 times.
✓ Branch 3 taken 8 times.
85 if (!read && stale) {
472
1/1
✓ Branch 2 taken 61 times.
183 m_cells.interpolation = Items<uint32_t>(
473 122 points.Read(kPointBasedNormalsInterpolation.index, 0, points.Rows()));
474 61 m_cells.scheme =
475
1/1
✓ Branch 2 taken 61 times.
122 Items<uint32_t>(meshes.Read(kMeshSubdivisionScheme.index, 0, meshes.Rows()));
476 61 read = true;
477 }
478 85 return m_cells;
479 559 };
480 7677 const auto isMesh = [&](uint32_t prim) {
481
4/4
✓ Branch 1 taken 7102 times.
✓ Branch 2 taken 16 times.
✓ Branch 5 taken 7032 times.
✓ Branch 6 taken 70 times.
7118 return prims.Alive(prim) && IsA(prims.Record(prim).schema, SchemaId::Mesh);
482 559 };
483
484
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 278 times.
559 std::set<uint32_t> placed;
485
2/2
✓ Branch 0 taken 281 times.
✓ Branch 1 taken 278 times.
559 if (changes.resync) {
486
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 281 times.
281 while (!m_meshes.empty()) {
487
0/1
✗ Branch 2 not taken.
✗ Forget(m_meshes.begin()->first);
488 }
489
2/2
✓ Branch 0 taken 1871 times.
✓ Branch 1 taken 281 times.
2152 for (uint32_t prim = 0; prim < prims.Size(); ++prim) {
490
3/3
✓ Branch 1 taken 1871 times.
✓ Branch 3 taken 69 times.
✓ Branch 4 taken 1802 times.
1871 if (isMesh(prim)) {
491
1/1
✓ Branch 1 taken 69 times.
69 placed.insert(prim);
492 }
493 }
494 } else {
495
2/2
✓ Branch 0 taken 142 times.
✓ Branch 1 taken 278 times.
420 for (const StructureEntry& entry : changes.removed) {
496 142 const auto found = m_meshes.find(entry.prim);
497
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 140 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
144 if (found != m_meshes.end() && found->second.generation == entry.generation) {
498
1/1
✓ Branch 1 taken 2 times.
2 Forget(entry.prim);
499 }
500 }
501
2/2
✓ Branch 0 taken 5247 times.
✓ Branch 1 taken 278 times.
5525 for (const StructureEntry& entry : changes.added) {
502
5/6
✓ Branch 1 taken 5247 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 5246 times.
✓ Branch 6 taken 1 time.
✓ Branch 8 taken 1 time.
✗ Branch 9 not taken.
5247 if (isMesh(entry.prim) && prims.Generation(entry.prim) == entry.generation) {
503
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if (m_meshes.contains(entry.prim)) {
504
0/1
✗ Branch 1 not taken.
✗ Forget(entry.prim);
505 }
506
1/1
✓ Branch 1 taken 1 time.
1 placed.insert(entry.prim);
507 }
508 }
509 }
510
511
2/2
✓ Branch 0 taken 285 times.
✓ Branch 1 taken 274 times.
559 std::set<uint32_t> affected;
512 559 std::set<uint32_t> reshaped;
513
2/2
✓ Branch 0 taken 285 times.
✓ Branch 1 taken 274 times.
559 if (rekey) {
514
3/3
✓ Branch 1 taken 4 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 285 times.
293 for (auto& [prim, mesh] : m_meshes) {
515
1/1
✓ Branch 1 taken 4 times.
4 CacheCells(mesh, cells());
516
1/1
✓ Branch 1 taken 4 times.
4 affected.insert(prim);
517
1/1
✓ Branch 1 taken 4 times.
4 reshaped.insert(prim);
518 }
519 }
520 3354 const auto collect = [&](ColumnRef column, const auto& owners, bool points) {
521
2/2
✓ Branch 2 taken 477 times.
✓ Branch 3 taken 2795 times.
3272 for (const uint32_t row : changes.ArrayRows(column)) {
522
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 457 times.
477 if (const auto owner = owners.find(row); owner != owners.end()) {
523
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
20 if (points) {
524 15 const Mesh& mesh = m_meshes.at(owner->second);
525
5/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 4 times.
25 if (mesh.set && mesh.set->words == 0 &&
526 10 store.Array(kPointBasedPoints, mesh.pointsRow).address ==
527
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 4 times.
10 mesh.key->points) {
528 6 continue;
529 }
530 }
531 14 affected.insert(owner->second);
532
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 9 times.
14 if (!points) {
533 5 reshaped.insert(owner->second);
534 }
535 }
536 }
537 3354 };
538
1/1
✓ Branch 1 taken 559 times.
559 collect(kPointBasedPoints, m_byPointsRow, true);
539
1/1
✓ Branch 1 taken 559 times.
559 collect(kPointBasedNormals, m_byPointsRow, false);
540
1/1
✓ Branch 1 taken 559 times.
559 collect(kMeshFaceVertexIndices, m_byMeshRow, false);
541
1/1
✓ Branch 1 taken 559 times.
559 collect(kMeshFaceVertexCounts, m_byMeshRow, false);
542
1/1
✓ Branch 1 taken 559 times.
559 collect(kGprimPrimvars, m_byGprimRow, false);
543
544 559 std::set<Key> stale;
545 559 std::set<Key> exact;
546 559 std::vector<uint32_t> rekeyed;
547
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 559 times.
575 for (const uint32_t prim : affected) {
548
2/4
✓ Branch 1 taken 16 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
16 if (placed.contains(prim) || !m_meshes.contains(prim)) {
549 5 continue;
550 }
551
1/1
✓ Branch 1 taken 16 times.
16 Mesh& mesh = m_meshes.at(prim);
552
1/1
✓ Branch 1 taken 16 times.
16 const Key key = KeyFor(mesh);
553
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 5 times.
16 if (mesh.key && *mesh.key == key) {
554
1/1
✓ Branch 1 taken 5 times.
5 stale.insert(key);
555
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
5 if (reshaped.contains(prim)) {
556
1/1
✓ Branch 1 taken 2 times.
2 exact.insert(key);
557 }
558 5 continue;
559 }
560
1/1
✓ Branch 1 taken 11 times.
11 Forget(prim);
561
1/1
✓ Branch 1 taken 11 times.
11 rekeyed.push_back(prim);
562 }
563
1/1
✓ Branch 1 taken 559 times.
559 Prune();
564
2/2
✓ Branch 0 taken 70 times.
✓ Branch 1 taken 559 times.
629 for (const uint32_t prim : placed) {
565
2/2
✓ Branch 1 taken 70 times.
✓ Branch 4 taken 70 times.
70 Place(prim, cells());
566 }
567
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 559 times.
570 for (const uint32_t prim : rekeyed) {
568
2/2
✓ Branch 1 taken 11 times.
✓ Branch 4 taken 11 times.
11 Place(prim, cells());
569 }
570
4/5
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 554 times.
✓ Branch 3 taken 5 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 5 times.
559 static const profiler::ZoneId zone = profiler::ZoneId::Named("Tangents/Recompute");
571
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 554 times.
559 device::Device& device = store.GetDevice();
572 559 std::optional<device::CommandEncoder> encoder;
573 559 std::optional<device::ZoneScope> scope;
574
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 554 times.
559 std::map<Key, std::vector<uint32_t>> users;
575
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 554 times.
559 if (!stale.empty()) {
576
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 5 times.
19 for (const auto& [prim, mesh] : m_meshes) {
577
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 1 time.
7 if (mesh.key && stale.contains(*mesh.key)) {
578
2/2
✓ Branch 1 taken 6 times.
✓ Branch 4 taken 6 times.
6 users[*mesh.key].push_back(prim);
579 }
580 }
581 }
582
3/3
✓ Branch 1 taken 5 times.
✓ Branch 3 taken 5 times.
✓ Branch 4 taken 559 times.
564 for (const auto& [key, owners] : users) {
583
1/1
✓ Branch 1 taken 5 times.
5 Set& set = m_sets.at(key);
584 5 bool computed = false;
585
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11 for (const uint32_t prim : owners) {
586
1/1
✓ Branch 1 taken 6 times.
6 const Mesh& mesh = m_meshes.at(prim);
587
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
6 if (!computed) {
588 5 const bool onDevice =
589
5/6
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
5 set.deform && !exact.contains(key) &&
590
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
6 (set.deform->normals || m_geometry.Record(mesh.meshRow).normals.address != 0);
591 5 if (onDevice) {
592
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!encoder) {
593
1/1
✓ Branch 1 taken 3 times.
3 encoder = device.GetQueue().CreateEncoder();
594
1/1
✓ Branch 1 taken 3 times.
3 scope.emplace(*encoder, zone);
595 }
596
1/1
✓ Branch 1 taken 3 times.
3 Recompute(mesh, key, set, *encoder);
597 } else {
598
1/1
✓ Branch 1 taken 2 times.
2 Compute(mesh, key, set);
599 }
600 computed = true;
601 }
602
1/1
✓ Branch 1 taken 6 times.
6 Link(prim, mesh);
603 }
604 }
605
1/1
✓ Branch 1 taken 559 times.
559 Prune();
606
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 556 times.
559 m_stats.meshes = uint32_t(m_meshes.size());
607
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 556 times.
559 m_stats.sets = uint32_t(m_sets.size());
608
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 556 times.
559 if (encoder) {
609 3 scope.reset();
610
1/1
✓ Branch 1 taken 3 times.
3 store.Flush();
611
2/2
✓ Branch 1 taken 3 times.
✓ Branch 4 taken 3 times.
3 device.GetQueue().Submit(encoder->Finish());
612 }
613 1118 }
614
615 353 void TangentTable::Prepare() {
616 353 Kernels();
617 353 }
618
619 359 device::Program& TangentTable::Kernels() {
620
2/2
✓ Branch 0 taken 354 times.
✓ Branch 1 taken 5 times.
359 if (!m_program) {
621
2/2
✓ Branch 2 taken 354 times.
✓ Branch 5 taken 354 times.
1062 m_program = m_structure.GetColumnStore().GetDevice().CreateProgram(
622 {.modules = {"chrono_batch_render.scene.tables.kernels.tangents"},
623 .library = &kSceneKernels,
624 .entries = {"GroupTangents", "CornerTangents"},
625
2/3
✓ Branch 1 taken 354 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 354 times.
708 .label = "tangents"});
626 }
627 359 return *m_program;
628 708 }
629
630 }
631