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