Loading...
Searching...
No Matches
arrayEditOps.h
Go to the documentation of this file.
1//
2// Copyright 2025 Pixar
3//
4// Licensed under the terms set forth in the LICENSE.txt file available at
5// https://openusd.org/license.
6//
7
8#ifndef PXR_BASE_VT_ARRAY_EDIT_OPS_H
9#define PXR_BASE_VT_ARRAY_EDIT_OPS_H
10
12
13#include "pxr/pxr.h"
14#include "pxr/base/vt/api.h"
15
16#include "pxr/base/tf/hash.h"
17
18#include <cstdint>
19#include <cstdlib>
20#include <cstring>
21#include <limits>
22#include <vector>
23
24PXR_NAMESPACE_OPEN_SCOPE
25
26// A helper class used in the implementation of VtArrayEdit.
27class Vt_ArrayEditOps
28{
29public:
30 static constexpr int64_t EndIndex = std::numeric_limits<int64_t>::min();
31
32 // The supported operations.
33 //
34 // This enum's underlying type is int64_t because MSVC won't pack it
35 // correctly when used as a bitfield in a struct. For example, on MSVC if
36 // we make the underlying type uint8_t, then struct Foo { int64_t x:56; Op:8
37 // }; has size 16. Making the underlying type int64_t fixes this.
38 enum Op : int64_t {
39 OpWriteLiteral, // write <literal> to [index]
40 OpWriteRef, // write [index1] to [index2]
41 OpInsertLiteral, // insert <literal> at [index]
42 OpInsertRef, // insert [index1] at [index2]
43 OpEraseRef, // erase [index]
44 OpMinSize, // minsize <size>
45 OpMinSizeFill, // minsize <size> <literal>
46 OpSetSize, // resize <size>
47 OpSetSizeFill, // resize <size> <literal>
48 OpMaxSize, // maxsize <size>
49 };
50 static constexpr uint8_t NumOps = OpMaxSize + 1;
51
52 static constexpr bool IsValidOp(Op op) {
53 return op >= static_cast<Op>(0) && op < NumOps;
54 }
55
56 static constexpr int GetArity(Op op) {
57 if (op == OpWriteLiteral || op == OpWriteRef ||
58 op == OpInsertLiteral || op == OpInsertRef ||
59 op == OpMinSizeFill || op == OpSetSizeFill) {
60 return 2;
61 }
62 return 1;
63 }
64
65 // Repetitions of a given op.
66 struct OpAndCount {
67 int64_t count:56;
68 Op op:8;
69 };
70 static_assert(sizeof(OpAndCount) == sizeof(int64_t));
71
72 // Invoke fn(Op, arg1, arg2) for each valid instruction. Normalize index
73 // args according to initialSize (if negative or EndIndex). Invalid
74 // instructions with out-of-bounds indexes are skipped.
75 template <class Fn>
76 void ForEachValid(size_t numLiterals, size_t initialSize, Fn &&fn) const {
77 return _ForEachImpl(numLiterals, initialSize, std::forward<Fn>(fn));
78 }
79
80 // Invoke fn(Op, arg1, arg2) for each instruction as-is, with no index
81 // normalization or range checking.
82 template <class Fn>
83 void ForEach(Fn &&fn) const {
84 return _ForEachImpl(-1, -1, std::forward<Fn>(fn));
85 }
86
87 // Add `offset` to every literal index in these ops. This is used when
88 // composing edits, where the weaker edit's literals are placed ahead of the
89 // stronger edit's, shifting the stronger edit's indexes.
90 //
91 // Note that the write and insert ops's first args indexes the literals,
92 // while the size-fill ops' second args do. The switch below deliberately
93 // enumerates every op rather than using a `default` label, so that adding
94 // an op that may reference a literal draws a warning here.
95 void OffsetLiteralIndexes(int64_t offset) {
96 _ModifyImpl([offset](Op op, int64_t &a1, int64_t &a2) {
97 switch (op) {
98 case OpWriteLiteral:
99 case OpInsertLiteral:
100 a1 += offset;
101 break;
102 case OpMinSizeFill:
103 case OpSetSizeFill:
104 a2 += offset;
105 break;
106 case OpWriteRef:
107 case OpInsertRef:
108 case OpEraseRef:
109 case OpMinSize:
110 case OpSetSize:
111 case OpMaxSize:
112 break;
113 };
114 });
115 }
116
117 // Return true if there are no ops, else false.
118 bool IsEmpty() const {
119 return _ins.empty();
120 }
121
122private:
123 template <class ELEM>
124 friend class VtArrayEdit;
125
126 template <class ELEM>
127 friend class VtArrayEditBuilder;
128
129 friend class Vt_ArrayEditOpsBuilder;
130
131 template <class HashState>
132 friend void TfHashAppend(HashState &h, Vt_ArrayEditOps const &self) {
133 h.Append(self._ins);
134 }
135
136 friend bool
137 operator==(Vt_ArrayEditOps const &l, Vt_ArrayEditOps const &r) {
138 return l._ins == r._ins;
139 }
140 friend bool
141 operator!=(Vt_ArrayEditOps const &l, Vt_ArrayEditOps const &r) {
142 return !(l == r);
143 }
144
145 static OpAndCount _ToOpAndCount(int64_t i64) {
146 OpAndCount oc;
147 memcpy(&oc, &i64, sizeof(oc));
148 return oc;
149 }
150
151 static int64_t _ToInt64(OpAndCount oc) {
152 int64_t i64;
153 memcpy(&i64, &oc, sizeof(i64));
154 return i64;
155 }
156
157 static constexpr size_t _DisableBoundsCheck = size_t(-1);
158
159 VT_API
160 void _IssueInvalidInsError(
161 OpAndCount oc, size_t offset, size_t required, size_t actual) const;
162
163 VT_API
164 void _IssueInvalidOpError(OpAndCount oc, size_t offset) const;
165
166 VT_API static void _LiteralOutOfBounds(int64_t idx, size_t size);
167 VT_API static void _ReferenceOutOfBounds(int64_t idx, size_t size);
168 VT_API static void _InsertOutOfBounds(int64_t idx, size_t size);
169 VT_API static void _NegativeSizeArg(Op op, int64_t idx);
170
171 static bool _CheckLiteralIndex(int64_t idx, size_t size) {
172 if (size == _DisableBoundsCheck ||
173 (idx >= 0 && static_cast<size_t>(idx) < size)) {
174 return true;
175 }
176 _LiteralOutOfBounds(idx, size);
177 return false;
178 }
179
180 static bool _NormalizeAndCheckRefIndex(int64_t &idx, size_t size) {
181 if (size == _DisableBoundsCheck) {
182 return true;
183 }
184 if (idx < 0) {
185 idx += size;
186 }
187 if (idx >= 0 && static_cast<size_t>(idx) < size) {
188 return true;
189 }
190 _ReferenceOutOfBounds(idx, size);
191 return false;
192 }
193
194 static bool _NormalizeAndCheckInsertIndex(int64_t &idx, size_t size) {
195 if (size == _DisableBoundsCheck) {
196 return true;
197 }
198 if (idx == EndIndex) {
199 idx = size;
200 }
201 if (idx < 0) {
202 idx += size;
203 }
204 if (idx >= 0 && static_cast<size_t>(idx) <= size) {
205 return true;
206 }
207 _InsertOutOfBounds(idx, size);
208 return false;
209 }
210
211 static bool _CheckSizeArg(Op op, int64_t arg) {
212 if (arg < 0) {
213 _NegativeSizeArg(op, arg);
214 return false;
215 }
216 return true;
217 }
218
219 static void _UpdateWorkingSize(size_t &workingSize, size_t newSize) {
220 if (workingSize == _DisableBoundsCheck) {
221 return;
222 }
223 workingSize = newSize;
224 }
225
226 template <class Fn>
227 void _ForEachImpl(size_t numLiterals, size_t initialSize, Fn &&fn) const {
228
229 // Walk and call fn with each.
230 const auto begin = std::begin(_ins);
231 auto iter = std::begin(_ins);
232 const auto end = std::end(_ins);
233
234 while (iter != end) {
235 OpAndCount oc = _ToOpAndCount(*iter);
236
237 if (!IsValidOp(oc.op)) {
238 _IssueInvalidOpError(oc, std::distance(begin, iter));
239 return;
240 }
241
242 const int arity = GetArity(oc.op);
243
244 // Check sufficient args.
245 if (std::distance(++iter, end) < oc.count * arity) {
246 _IssueInvalidInsError(
247 oc, std::distance(begin, iter),
248 oc.count * arity, std::distance(iter, end));
249 }
250
251 // Do each set of args.
252 for (; oc.count--; iter += arity) {
253 int64_t a1 = iter[0];
254 int64_t a2 = arity > 1 ? iter[1] : -1;
255
256 // Normalize and check indexes if requested.
257 switch (oc.op) {
258 default:
259 _IssueInvalidOpError(oc, std::distance(begin, iter));
260 return;
261 case OpWriteLiteral:
262 if (!_CheckLiteralIndex(a1, numLiterals) ||
263 !_NormalizeAndCheckRefIndex(a2, initialSize)) {
264 continue;
265 }
266 break;
267 case OpWriteRef:
268 if (!_NormalizeAndCheckRefIndex(a1, initialSize) ||
269 !_NormalizeAndCheckRefIndex(a2, initialSize)) {
270 continue;
271 }
272 break;
273 case OpInsertLiteral:
274 if (!_CheckLiteralIndex(a1, numLiterals) ||
275 !_NormalizeAndCheckInsertIndex(a2, initialSize)) {
276 continue;
277 }
278 _UpdateWorkingSize(initialSize, initialSize + 1);
279 break;
280 case OpInsertRef:
281 if (!_NormalizeAndCheckRefIndex(a1, initialSize) ||
282 !_NormalizeAndCheckInsertIndex(a2, initialSize)) {
283 continue;
284 }
285 _UpdateWorkingSize(initialSize, initialSize + 1);
286 break;
287 case OpEraseRef:
288 if (!_NormalizeAndCheckRefIndex(a1, initialSize)) {
289 continue;
290 }
291 _UpdateWorkingSize(initialSize, initialSize - 1);
292 break;
293
294 case OpMinSizeFill:
295 if (!_CheckLiteralIndex(a2, numLiterals)) {
296 continue;
297 } // intentional fall-thru
298 case OpMinSize:
299 if (!_CheckSizeArg(oc.op, a1)) {
300 continue;
301 }
302 _UpdateWorkingSize(
303 initialSize,
304 std::max(initialSize, static_cast<size_t>(a1)));
305 break;
306
307 case OpSetSizeFill:
308 if (!_CheckLiteralIndex(a2, numLiterals)) {
309 continue;
310 } // intentional fall-thru
311 case OpSetSize:
312 if (!_CheckSizeArg(oc.op, a1)) {
313 continue;
314 }
315 _UpdateWorkingSize(initialSize, a1);
316 break;
317
318 case OpMaxSize:
319 if (!_CheckSizeArg(oc.op, a1)) {
320 continue;
321 }
322 _UpdateWorkingSize(
323 initialSize,
324 std::min(initialSize, static_cast<size_t>(a1)));
325 break;
326
327 };
328
329 // Invoke caller.
330 std::forward<Fn>(fn)(oc.op, a1, a2);
331 }
332 }
333 }
334
335 // Invoke fn(Op, arg1, arg2) for each instruction as-is, with no index
336 // normalization or range checking, passing mutable references for arg1 and
337 // arg2 so fn() can modify indexes. Note that single-argument ops have no
338 // arg2 in the instruction stream, so arg2 refers to a scratch value for
339 // those and modifications to it are discarded.
340 template <class Fn>
341 void _ModifyImpl(Fn &&fn) {
342
343 // Walk and call fn with each.
344 const auto begin = std::begin(_ins);
345 auto iter = std::begin(_ins);
346 const auto end = std::end(_ins);
347
348 while (iter != end) {
349 OpAndCount oc = _ToOpAndCount(*iter);
350
351 if (!IsValidOp(oc.op)) {
352 _IssueInvalidOpError(oc, std::distance(begin, iter));
353 return;
354 }
355
356 const int arity = GetArity(oc.op);
357
358 // Check sufficient args.
359 if (std::distance(++iter, end) < oc.count * arity) {
360 _IssueInvalidInsError(
361 oc, std::distance(begin, iter),
362 oc.count * arity, std::distance(iter, end));
363 }
364
365 // Do each set of args.
366 for (; oc.count--; iter += arity) {
367 int64_t invalid = -1;
368 int64_t &a1 = iter[0];
369 int64_t &a2 = arity > 1 ? iter[1] : invalid;
370
371 // Invoke caller.
372 std::forward<Fn>(fn)(oc.op, a1, a2);
373 }
374 }
375 }
376
377 // Instructions and arguments are stored together in order. For example the
378 // following operation sequence:
379 //
380 // resize 1024
381 // write <literal 0> to [2]
382 // write <literal 1> to [4]
383 // write [5] to [6]
384 // erase [9]
385 // erase [9]
386 //
387 // Would be encoded as the following 64-bit quantities, each denoted by [].
388 //
389 // [1 OpSetSize] [1024] [2 OpWriteLiteral] [0] [2] [1] [4] [1 OpWriteRef]
390 // [5] [6] [2 OpErase] [9] [9].
391 //
392 // The meaning of the 64-bit quantities that follow an op are determined by
393 // the op itself. For example, in the case of [2 OpWriteLiteral], there are
394 // four quantities that follow, two for each instruction: a literal index
395 // and a destination index. The GetArity() member function returns the
396 // arity for a given op. Currently either 1 or 2.
397 std::vector<int64_t> _ins;
398
399};
400
401PXR_NAMESPACE_CLOSE_SCOPE
402
403#endif // PXR_BASE_VT_ARRAY_EDIT_OPS_H
A builder type that produces instances of VtArrayEdit representing sequences of array edit operations...
An array edit represents a sequence of per-element modifications to a VtArray.
Definition arrayEdit.h:53