1 // Copyright (C) 2019 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 #include "VkReconstruction.h"
15 
16 #include <string.h>
17 
18 #include <unordered_map>
19 
20 #include "FrameBuffer.h"
21 #include "render-utils/IOStream.h"
22 #include "VkDecoder.h"
23 #include "aemu/base/containers/EntityManager.h"
24 
25 namespace gfxstream {
26 namespace vk {
27 
28 #define DEBUG_RECONSTRUCTION 0
29 
30 #if DEBUG_RECONSTRUCTION
31 
32 #define DEBUG_RECON(fmt, ...) INFO(fmt, ##__VA_ARGS__);
33 
34 #else
35 
36 #define DEBUG_RECON(fmt, ...)
37 
38 #endif
39 
40 VkReconstruction::VkReconstruction() = default;
41 
typeTagSortedHandles(const std::vector<VkReconstruction::HandleWithState> & handles)42 std::vector<VkReconstruction::HandleWithState> typeTagSortedHandles(
43     const std::vector<VkReconstruction::HandleWithState>& handles) {
44     using EntityManagerTypeForHandles = android::base::EntityManager<32, 16, 16, int>;
45 
46     std::vector<VkReconstruction::HandleWithState> res = handles;
47 
48     std::sort(res.begin(), res.end(),
49               [](const VkReconstruction::HandleWithState& lhs,
50                  const VkReconstruction::HandleWithState& rhs) {
51                   if (lhs.second != rhs.second) {
52                       return lhs.second < rhs.second;
53                   }
54                   return EntityManagerTypeForHandles::getHandleType(lhs.first) <
55                          EntityManagerTypeForHandles::getHandleType(rhs.first);
56               });
57 
58     return res;
59 }
60 
save(android::base::Stream * stream)61 void VkReconstruction::save(android::base::Stream* stream) {
62     DEBUG_RECON("start")
63 
64 #if DEBUG_RECONSTRUCTION
65     dump();
66 #endif
67 
68     std::unordered_set<uint64_t> savedApis;
69 
70     std::unordered_map<HandleWithState, int, HandleWithStateHash> totalParents;
71     std::vector<HandleWithState> next;
72 
73     mHandleReconstructions.forEachLiveComponent_const(
74         [&totalParents, &next](bool live, uint64_t componentHandle, uint64_t entityHandle,
75                                const HandleWithStateReconstruction& item) {
76             for (int state = BEGIN; state < HANDLE_STATE_COUNT; state++) {
77                 const auto& parents = item.states[state].parentHandles;
78                 HandleWithState handleWithState = {entityHandle, static_cast<HandleState>(state)};
79                 totalParents[handleWithState] = parents.size();
80                 if (parents.empty()) {
81                     next.push_back(handleWithState);
82                 }
83             }
84         });
85 
86     std::vector<std::vector<HandleWithState>> handlesByTopoOrder;
87 
88     while (!next.empty()) {
89         next = typeTagSortedHandles(next);
90         handlesByTopoOrder.push_back(std::move(next));
91         const std::vector<HandleWithState>& current = handlesByTopoOrder.back();
92         for (const auto& handle : current) {
93             const auto& item = mHandleReconstructions.get(handle.first)->states[handle.second];
94             for (const auto& childHandle : item.childHandles) {
95                 if (--totalParents[childHandle] == 0) {
96                     next.push_back(childHandle);
97                 }
98             }
99         }
100     }
101 
102     std::vector<std::vector<uint64_t>> uniqApiRefsByTopoOrder;
103     uniqApiRefsByTopoOrder.reserve(handlesByTopoOrder.size() + 1);
104     for (const auto& handles : handlesByTopoOrder) {
105         std::vector<uint64_t> nextApis;
106         for (const auto& handle : handles) {
107             auto item = mHandleReconstructions.get(handle.first)->states[handle.second];
108             for (uint64_t apiRef : item.apiRefs) {
109                 auto apiItem = mApiTrace.get(apiRef);
110                 if (!apiItem) continue;
111                 if (savedApis.find(apiRef) != savedApis.end()) continue;
112                 savedApis.insert(apiRef);
113 #if DEBUG_RECONSTRUCTION
114                 DEBUG_RECON("adding handle 0x%lx API 0x%lx op code %d", handle.first, apiRef,
115                             apiItem->opCode);
116 #endif
117                 nextApis.push_back(apiRef);
118             }
119         }
120         uniqApiRefsByTopoOrder.push_back(std::move(nextApis));
121     }
122 
123     uniqApiRefsByTopoOrder.push_back(getOrderedUniqueModifyApis());
124 
125     size_t totalApiTraceSize = 0;  // 4 bytes to store size of created handles
126 
127     for (size_t i = 0; i < uniqApiRefsByTopoOrder.size(); ++i) {
128         for (auto apiHandle : uniqApiRefsByTopoOrder[i]) {
129             auto item = mApiTrace.get(apiHandle);
130             totalApiTraceSize += 4;                 // opcode
131             totalApiTraceSize += 4;                 // buffer size of trace
132             totalApiTraceSize += item->traceBytes;  // the actual trace
133         }
134     }
135 
136     DEBUG_RECON("total api trace size: %zu", totalApiTraceSize);
137 
138     std::vector<uint64_t> createdHandleBuffer;
139 
140     for (size_t i = 0; i < uniqApiRefsByTopoOrder.size(); ++i) {
141         for (auto apiHandle : uniqApiRefsByTopoOrder[i]) {
142             auto item = mApiTrace.get(apiHandle);
143             for (auto createdHandle : item->createdHandles) {
144                 DEBUG_RECON("save handle: 0x%lx", createdHandle);
145                 createdHandleBuffer.push_back(createdHandle);
146             }
147         }
148     }
149 
150     std::vector<uint8_t> apiTraceBuffer;
151     apiTraceBuffer.resize(totalApiTraceSize);
152 
153     uint8_t* apiTracePtr = apiTraceBuffer.data();
154 
155     for (size_t i = 0; i < uniqApiRefsByTopoOrder.size(); ++i) {
156         for (auto apiHandle : uniqApiRefsByTopoOrder[i]) {
157             auto item = mApiTrace.get(apiHandle);
158             // 4 bytes for opcode, and 4 bytes for saveBufferRaw's size field
159             DEBUG_RECON("saving api handle 0x%lx op code %d", apiHandle, item->opCode);
160             memcpy(apiTracePtr, &item->opCode, sizeof(uint32_t));
161             apiTracePtr += 4;
162             uint32_t traceBytesForSnapshot = item->traceBytes + 8;
163             memcpy(apiTracePtr, &traceBytesForSnapshot,
164                    sizeof(uint32_t));  // and 8 bytes for 'self' struct of { opcode, packetlen } as
165                                        // that is what decoder expects
166             apiTracePtr += 4;
167             memcpy(apiTracePtr, item->trace.data(), item->traceBytes);
168             apiTracePtr += item->traceBytes;
169         }
170     }
171 
172     DEBUG_RECON("created handle buffer size: %zu trace: %zu", createdHandleBuffer.size(),
173                 apiTraceBuffer.size());
174 
175     android::base::saveBufferRaw(stream, (char*)(createdHandleBuffer.data()),
176                                  createdHandleBuffer.size() * sizeof(uint64_t));
177     android::base::saveBufferRaw(stream, (char*)(apiTraceBuffer.data()), apiTraceBuffer.size());
178 }
179 
180 class TrivialStream : public IOStream {
181    public:
TrivialStream()182     TrivialStream() : IOStream(4) {}
183     virtual ~TrivialStream() = default;
184 
allocBuffer(size_t minSize)185     void* allocBuffer(size_t minSize) {
186         size_t allocSize = (m_bufsize < minSize ? minSize : m_bufsize);
187         if (!m_buf) {
188             m_buf = (unsigned char*)malloc(allocSize);
189         } else if (m_bufsize < allocSize) {
190             unsigned char* p = (unsigned char*)realloc(m_buf, allocSize);
191             if (p != NULL) {
192                 m_buf = p;
193                 m_bufsize = allocSize;
194             } else {
195                 ERR("realloc (%zu) failed", allocSize);
196                 free(m_buf);
197                 m_buf = NULL;
198                 m_bufsize = 0;
199             }
200         }
201 
202         return m_buf;
203     }
204 
commitBuffer(size_t size)205     int commitBuffer(size_t size) {
206         if (size == 0) return 0;
207         return writeFully(m_buf, size);
208     }
209 
writeFully(const void * buf,size_t len)210     int writeFully(const void* buf, size_t len) { return 0; }
211 
readFully(void * buf,size_t len)212     const unsigned char* readFully(void* buf, size_t len) { return NULL; }
213 
getDmaForReading(uint64_t guest_paddr)214     virtual void* getDmaForReading(uint64_t guest_paddr) { return nullptr; }
unlockDma(uint64_t guest_paddr)215     virtual void unlockDma(uint64_t guest_paddr) {}
216 
217    protected:
readRaw(void * buf,size_t * inout_len)218     virtual const unsigned char* readRaw(void* buf, size_t* inout_len) { return nullptr; }
onSave(android::base::Stream * stream)219     virtual void onSave(android::base::Stream* stream) {}
onLoad(android::base::Stream * stream)220     virtual unsigned char* onLoad(android::base::Stream* stream) { return nullptr; }
221 };
222 
load(android::base::Stream * stream,emugl::GfxApiLogger & gfxLogger,emugl::HealthMonitor<> * healthMonitor)223 void VkReconstruction::load(android::base::Stream* stream, emugl::GfxApiLogger& gfxLogger,
224                             emugl::HealthMonitor<>* healthMonitor) {
225     DEBUG_RECON("start. assuming VkDecoderGlobalState has been cleared for loading already");
226     mApiTrace.clear();
227     mHandleReconstructions.clear();
228 
229     std::vector<uint8_t> createdHandleBuffer;
230     std::vector<uint8_t> apiTraceBuffer;
231 
232     android::base::loadBuffer(stream, &createdHandleBuffer);
233     android::base::loadBuffer(stream, &apiTraceBuffer);
234 
235     DEBUG_RECON("created handle buffer size: %zu trace: %zu", createdHandleBuffer.size(),
236                 apiTraceBuffer.size());
237 
238     uint32_t createdHandleBufferSize = createdHandleBuffer.size();
239 
240     mLoadedTrace.resize(4 + createdHandleBufferSize + apiTraceBuffer.size());
241 
242     unsigned char* finalTraceData = (unsigned char*)(mLoadedTrace.data());
243 
244     memcpy(finalTraceData, &createdHandleBufferSize, sizeof(uint32_t));
245     memcpy(finalTraceData + 4, createdHandleBuffer.data(), createdHandleBufferSize);
246     memcpy(finalTraceData + 4 + createdHandleBufferSize, apiTraceBuffer.data(),
247            apiTraceBuffer.size());
248 
249     VkDecoder decoderForLoading;
250     // A decoder that is set for snapshot load will load up the created handles first,
251     // if any, allowing us to 'catch' the results as they are decoded.
252     decoderForLoading.setForSnapshotLoad(true);
253     TrivialStream trivialStream;
254 
255     DEBUG_RECON("start decoding trace");
256 
257     // TODO: This needs to be the puid seqno ptr
258     auto resources = ProcessResources::create();
259     VkDecoderContext context = {
260         .processName = nullptr,
261         .gfxApiLogger = &gfxLogger,
262         .healthMonitor = healthMonitor,
263     };
264     decoderForLoading.decode(mLoadedTrace.data(), mLoadedTrace.size(), &trivialStream, resources.get(),
265                              context);
266 
267     DEBUG_RECON("finished decoding trace");
268 }
269 
createApiInfo()270 VkReconstruction::ApiHandle VkReconstruction::createApiInfo() {
271     auto handle = mApiTrace.add(ApiInfo(), 1);
272     return handle;
273 }
274 
removeHandleFromApiInfo(VkReconstruction::ApiHandle h,uint64_t toRemove)275 void VkReconstruction::removeHandleFromApiInfo(VkReconstruction::ApiHandle h, uint64_t toRemove) {
276     auto vk_item = mHandleReconstructions.get(toRemove);
277     if (!vk_item) return;
278     auto apiInfo = mApiTrace.get(h);
279     if (!apiInfo) return;
280 
281     auto& handles = apiInfo->createdHandles;
282     auto it = std::find(handles.begin(), handles.end(), toRemove);
283 
284     if (it != handles.end()) {
285         handles.erase(it);
286     }
287     DEBUG_RECON("removed 1 vk handle  0x%llx from apiInfo  0x%llx, now it has %d left",
288                 (unsigned long long)toRemove, (unsigned long long)h, (int)handles.size());
289 }
290 
destroyApiInfo(VkReconstruction::ApiHandle h)291 void VkReconstruction::destroyApiInfo(VkReconstruction::ApiHandle h) {
292     auto item = mApiTrace.get(h);
293 
294     if (!item) return;
295 
296     if (!item->createdHandles.empty()) return;
297 
298     item->traceBytes = 0;
299     item->createdHandles.clear();
300 
301     mApiTrace.remove(h);
302 }
303 
getApiInfo(VkReconstruction::ApiHandle h)304 VkReconstruction::ApiInfo* VkReconstruction::getApiInfo(VkReconstruction::ApiHandle h) {
305     return mApiTrace.get(h);
306 }
307 
setApiTrace(VkReconstruction::ApiInfo * apiInfo,uint32_t opCode,const uint8_t * traceBegin,size_t traceBytes)308 void VkReconstruction::setApiTrace(VkReconstruction::ApiInfo* apiInfo, uint32_t opCode,
309                                    const uint8_t* traceBegin, size_t traceBytes) {
310     if (apiInfo->trace.size() < traceBytes) apiInfo->trace.resize(traceBytes);
311     apiInfo->opCode = opCode;
312     memcpy(apiInfo->trace.data(), traceBegin, traceBytes);
313     apiInfo->traceBytes = traceBytes;
314 }
315 
dump()316 void VkReconstruction::dump() {
317     INFO("%s: api trace dump", __func__);
318 
319     size_t traceBytesTotal = 0;
320 
321     mApiTrace.forEachLiveEntry_const(
322         [&traceBytesTotal](bool live, uint64_t handle, const ApiInfo& info) {
323             INFO("VkReconstruction::%s: api handle 0x%llx: %s", __func__,
324                     (unsigned long long)handle, api_opcode_to_string(info.opCode));
325             traceBytesTotal += info.traceBytes;
326         });
327 
328     mHandleReconstructions.forEachLiveComponent_const(
329         [this](bool live, uint64_t componentHandle, uint64_t entityHandle,
330                const HandleWithStateReconstruction& reconstruction) {
331             INFO("VkReconstruction::%s: %p handle 0x%llx api refs:", __func__, this,
332                     (unsigned long long)entityHandle);
333             for (const auto& state : reconstruction.states) {
334                 for (auto apiHandle : state.apiRefs) {
335                     auto apiInfo = mApiTrace.get(apiHandle);
336                     const char* apiName =
337                         apiInfo ? api_opcode_to_string(apiInfo->opCode) : "unalloced";
338                     INFO("VkReconstruction::%s:     0x%llx: %s", __func__,
339                             (unsigned long long)apiHandle, apiName);
340                     for (auto createdHandle : apiInfo->createdHandles) {
341                         INFO("VkReconstruction::%s:         created 0x%llx", __func__,
342                                 (unsigned long long)createdHandle);
343                     }
344                 }
345             }
346         });
347 
348     mHandleModifications.forEachLiveComponent_const([this](bool live, uint64_t componentHandle,
349                                                            uint64_t entityHandle,
350                                                            const HandleModification& modification) {
351         INFO("VkReconstruction::%s: mod: %p handle 0x%llx api refs:", __func__, this,
352                 (unsigned long long)entityHandle);
353         for (auto apiHandle : modification.apiRefs) {
354             auto apiInfo = mApiTrace.get(apiHandle);
355             const char* apiName = apiInfo ? api_opcode_to_string(apiInfo->opCode) : "unalloced";
356             INFO("VkReconstruction::%s: mod:     0x%llx: %s", __func__,
357                     (unsigned long long)apiHandle, apiName);
358         }
359     });
360 
361     INFO("%s: total trace bytes: %zu", __func__, traceBytesTotal);
362 }
363 
addHandles(const uint64_t * toAdd,uint32_t count)364 void VkReconstruction::addHandles(const uint64_t* toAdd, uint32_t count) {
365     if (!toAdd) return;
366 
367     for (uint32_t i = 0; i < count; ++i) {
368         DEBUG_RECON("add 0x%llx", (unsigned long long)toAdd[i]);
369         mHandleReconstructions.add(toAdd[i], HandleWithStateReconstruction());
370     }
371 }
372 
removeHandles(const uint64_t * toRemove,uint32_t count,bool recursive)373 void VkReconstruction::removeHandles(const uint64_t* toRemove, uint32_t count, bool recursive) {
374     if (!toRemove) return;
375 
376     for (uint32_t i = 0; i < count; ++i) {
377         DEBUG_RECON("remove 0x%llx", (unsigned long long)toRemove[i]);
378         auto item = mHandleReconstructions.get(toRemove[i]);
379         // Delete can happen in arbitrary order.
380         // It might delete the parents before children, which will automatically remove
381         // the name.
382         if (!item) continue;
383         // Break circuler references
384         if (item->destroying) continue;
385         item->destroying = true;
386         if (!recursive) {
387             bool couldDestroy = true;
388             for (const auto& state : item->states) {
389                 if (!state.childHandles.size()) {
390                     continue;
391                 }
392                 couldDestroy = false;
393                 break;
394             }
395             // TODO(b/330769702): perform delayed destroy when all children are destroyed.
396             if (couldDestroy) {
397                 forEachHandleDeleteApi(toRemove + i, 1);
398                 mHandleReconstructions.remove(toRemove[i]);
399             } else {
400                 DEBUG_RECON("delay destroy of 0x%lx, TODO: actually destroy it", toRemove[i]);
401                 item->delayed_destroy = true;
402                 item->destroying = false;
403             }
404             continue;
405         }
406         for (size_t j = 0; j < item->states.size(); j++) {
407             for (const auto& parentHandle : item->states[j].parentHandles) {
408                 auto parentItem = mHandleReconstructions.get(parentHandle.first);
409                 if (!parentItem) {
410                     continue;
411                 }
412                 parentItem->states[parentHandle.second].childHandles.erase(
413                     {toRemove[i], static_cast<HandleState>(j)});
414             }
415             item->states[j].parentHandles.clear();
416             std::vector<uint64_t> childHandles;
417             for (const auto& childHandle : item->states[j].childHandles) {
418                 if (childHandle.second == CREATED) {
419                     childHandles.push_back(childHandle.first);
420                 }
421             }
422             item->states[j].childHandles.clear();
423             removeHandles(childHandles.data(), childHandles.size());
424         }
425         forEachHandleDeleteApi(toRemove + i, 1);
426         mHandleReconstructions.remove(toRemove[i]);
427     }
428 }
429 
forEachHandleAddApi(const uint64_t * toProcess,uint32_t count,uint64_t apiHandle,HandleState state)430 void VkReconstruction::forEachHandleAddApi(const uint64_t* toProcess, uint32_t count,
431                                            uint64_t apiHandle, HandleState state) {
432     if (!toProcess) return;
433 
434     for (uint32_t i = 0; i < count; ++i) {
435         auto item = mHandleReconstructions.get(toProcess[i]);
436         if (!item) continue;
437 
438         item->states[state].apiRefs.push_back(apiHandle);
439         DEBUG_RECON("handle 0x%lx state %d added api 0x%lx", toProcess[i], state, apiHandle);
440     }
441 }
442 
forEachHandleDeleteApi(const uint64_t * toProcess,uint32_t count)443 void VkReconstruction::forEachHandleDeleteApi(const uint64_t* toProcess, uint32_t count) {
444     if (!toProcess) return;
445 
446     for (uint32_t i = 0; i < count; ++i) {
447         DEBUG_RECON("deleting api for 0x%lx", toProcess[i]);
448         auto item = mHandleReconstructions.get(toProcess[i]);
449 
450         if (!item) continue;
451 
452         for (auto& state : item->states) {
453             for (auto handle : state.apiRefs) {
454                 removeHandleFromApiInfo(handle, toProcess[i]);
455                 destroyApiInfo(handle);
456             }
457             state.apiRefs.clear();
458         }
459 
460         auto modifyItem = mHandleModifications.get(toProcess[i]);
461 
462         if (!modifyItem) continue;
463 
464         modifyItem->apiRefs.clear();
465     }
466 }
467 
addHandleDependency(const uint64_t * handles,uint32_t count,uint64_t parentHandle,HandleState childState,HandleState parentState)468 void VkReconstruction::addHandleDependency(const uint64_t* handles, uint32_t count,
469                                            uint64_t parentHandle, HandleState childState,
470                                            HandleState parentState) {
471     if (!handles) return;
472 
473     if (!parentHandle) return;
474 
475     auto parentItem = mHandleReconstructions.get(parentHandle);
476 
477     if (!parentItem) {
478         DEBUG_RECON("WARN: adding null parent item: 0x%lx", parentHandle);
479         return;
480     }
481     auto& parentItemState = parentItem->states[parentState];
482 
483     for (uint32_t i = 0; i < count; ++i) {
484         auto childItem = mHandleReconstructions.get(handles[i]);
485         if (!childItem) {
486             continue;
487         }
488         parentItemState.childHandles.insert({handles[i], static_cast<HandleState>(childState)});
489         childItem->states[childState].parentHandles.push_back(
490             {parentHandle, static_cast<HandleState>(parentState)});
491         DEBUG_RECON("Child handle 0x%lx state %d depends on parent handle 0x%lx state %d",
492                     handles[i], childState, parentHandle, parentState);
493     }
494 }
495 
setCreatedHandlesForApi(uint64_t apiHandle,const uint64_t * created,uint32_t count)496 void VkReconstruction::setCreatedHandlesForApi(uint64_t apiHandle, const uint64_t* created,
497                                                uint32_t count) {
498     if (!created) return;
499 
500     auto item = mApiTrace.get(apiHandle);
501 
502     if (!item) return;
503 
504     item->createdHandles.insert(item->createdHandles.end(), created, created + count);
505     item->createdHandles.insert(item->createdHandles.end(), mExtraHandlesForNextApi.begin(),
506                                 mExtraHandlesForNextApi.end());
507     mExtraHandlesForNextApi.clear();
508 }
509 
createExtraHandlesForNextApi(const uint64_t * created,uint32_t count)510 void VkReconstruction::createExtraHandlesForNextApi(const uint64_t* created, uint32_t count) {
511     mExtraHandlesForNextApi.assign(created, created + count);
512 }
513 
forEachHandleAddModifyApi(const uint64_t * toProcess,uint32_t count,uint64_t apiHandle)514 void VkReconstruction::forEachHandleAddModifyApi(const uint64_t* toProcess, uint32_t count,
515                                                  uint64_t apiHandle) {
516     if (!toProcess) return;
517 
518     for (uint32_t i = 0; i < count; ++i) {
519         auto item = mHandleModifications.get(toProcess[i]);
520         if (!item) {
521             mHandleModifications.add(toProcess[i], HandleModification());
522             item = mHandleModifications.get(toProcess[i]);
523         }
524 
525         if (!item) continue;
526 
527         item->apiRefs.push_back(apiHandle);
528     }
529 }
530 
forEachHandleClearModifyApi(const uint64_t * toProcess,uint32_t count)531 void VkReconstruction::forEachHandleClearModifyApi(const uint64_t* toProcess, uint32_t count) {
532     if (!toProcess) return;
533 
534     for (uint32_t i = 0; i < count; ++i) {
535         auto item = mHandleModifications.get(toProcess[i]);
536 
537         if (!item) continue;
538 
539         item->apiRefs.clear();
540     }
541 }
542 
getOrderedUniqueModifyApis() const543 std::vector<uint64_t> VkReconstruction::getOrderedUniqueModifyApis() const {
544     std::vector<HandleModification> orderedModifies;
545 
546     // Now add all handle modifications to the trace, ordered by the .order field.
547     mHandleModifications.forEachLiveComponent_const(
548         [&orderedModifies](bool live, uint64_t componentHandle, uint64_t entityHandle,
549                            const HandleModification& mod) { orderedModifies.push_back(mod); });
550 
551     // Sort by the |order| field for each modify API
552     // since it may be important to apply modifies in a particular
553     // order (e.g., when dealing with descriptor set updates
554     // or commands in a command buffer).
555     std::sort(orderedModifies.begin(), orderedModifies.end(),
556               [](const HandleModification& lhs, const HandleModification& rhs) {
557                   return lhs.order < rhs.order;
558               });
559 
560     std::unordered_set<uint64_t> usedModifyApis;
561     std::vector<uint64_t> orderedUniqueModifyApis;
562 
563     for (const auto& mod : orderedModifies) {
564         for (auto apiRef : mod.apiRefs) {
565             if (usedModifyApis.find(apiRef) == usedModifyApis.end()) {
566                 orderedUniqueModifyApis.push_back(apiRef);
567                 usedModifyApis.insert(apiRef);
568             }
569         }
570     }
571 
572     return orderedUniqueModifyApis;
573 }
574 
575 }  // namespace vk
576 }  // namespace gfxstream
577