Repository navigation
Expand file tree
/
Copy pathShaderResources.cpp
More file actions
3218 lines (3016 loc) · 192 KB
/
Copy pathShaderResources.cpp
File metadata and controls
3218 lines (3016 loc) · 192 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "ThreadOwned.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include <algorithm>
#include <atomic>
#include <memory>
#include <mutex>
#include <array>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "prx/libc/include/General.hpp"
#include "Optimization/include/Optimization/ShaderStageInputInfo.hpp"
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <cstring>
#include <limits>
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include "prx/libc/include/GuestAllocations.hpp"
namespace AgcDriver::Graphics {
namespace {
bool overlap(std::uint64_t first, std::size_t firstSize, std::uint64_t second, std::size_t secondSize) {
return first < second + secondSize && second < first + firstSize;
}
VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) {
switch (dstSel) {
case 0: return VK_COMPONENT_SWIZZLE_ZERO;
case 1: return VK_COMPONENT_SWIZZLE_ONE;
case 4: return VK_COMPONENT_SWIZZLE_R;
case 5: return VK_COMPONENT_SWIZZLE_G;
case 6: return VK_COMPONENT_SWIZZLE_B;
case 7: return VK_COMPONENT_SWIZZLE_A;
default: throw std::runtime_error("AGC graphics: guest texture descriptor has an invalid destination channel selector " + std::to_string(dstSel));
}
}
VkComponentMapping ViewComponents(const GuestTextureResource& resource) {
if (IsConvertedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A};
return {ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)};
}
// Sampled textures are reused across draws and dispatches while their guest bytes are unchanged; a
// byte copy of the guest surface validates each reuse, so CPU or GPU writes to it force a re-upload.
// The key is everything a lookup matches: the device, the eight descriptor words and the swizzle.
struct TextureKey {
VkDevice device;
std::array<std::uint32_t, 8> words;
std::array<std::uint32_t, 4> components;
bool depthCompare = false;
bool operator==(const TextureKey&) const = default;
};
std::uint64_t hashWords(std::uint64_t hash, const std::uint32_t* words, std::size_t count) {
for (std::size_t i = 0; i < count; ++i) hash = (hash ^ words[i]) * 1099511628211ull;
return hash;
}
struct TextureKeyHash {
std::size_t operator()(const TextureKey& key) const noexcept {
auto hash = 14695981039346656037ull ^ reinterpret_cast<std::uintptr_t>(key.device);
hash = hashWords(hash, key.words.data(), key.words.size()) ^ static_cast<std::uint64_t>(key.depthCompare);
return static_cast<std::size_t>(hashWords(hash, key.components.data(), key.components.size()));
}
};
TextureKey MakeTextureKey(VkDevice device, std::span<const std::uint32_t> words, VkComponentMapping components, bool depthCompare = false) {
TextureKey key{device, {}, {static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a)}};
key.depthCompare = depthCompare;
std::copy(words.begin(), words.end(), key.words.begin());
return key;
}
struct CachedTexture {
TextureKey key;
std::uint64_t address;
std::vector<std::byte> bytes;
std::shared_ptr<Texture> texture;
// A fast-cleared surface is cached as its clear texels; it stays valid while the keys are unchanged.
DccKeys keys = DccKeys::Uncompressed;
// Write generation the snapshot is known current at (see GuestMemory::CollectWrites).
std::uint64_t generation = 0;
// A texture viewing a storage image on the GPU has no snapshot; it stays valid while that image
// is still its surface's cached image and nothing wrote the guest memory since it matched.
std::shared_ptr<StorageTexture> source;
std::uint64_t sourceVersion = 0;
std::uint64_t accounted = 0;
};
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
// eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is
// still kept), the linear lookup of before.
struct TextureCache {
std::mutex mutex;
std::list<CachedTexture> entries;
std::unordered_map<TextureKey, std::list<CachedTexture>::iterator, TextureKeyHash> index;
std::unordered_multimap<const StorageTexture*, std::list<CachedTexture>::iterator> views;
std::uint64_t bytes = 0;
VkPhysicalDevice budgetDevice = VK_NULL_HANDLE;
std::chrono::steady_clock::time_point budgetRead{};
std::uint64_t budget = 0;
std::uint64_t reportedBudget = 0;
std::chrono::steady_clock::time_point reportedAt{};
};
TextureCache& Textures() {
static TextureCache cache;
return cache;
}
struct UncachedImages {
std::mutex mutex;
std::vector<std::weak_ptr<StorageTexture>> images;
std::atomic<bool> any{false};
};
UncachedImages& Uncached() {
static UncachedImages uncached;
return uncached;
}
void noteUncached(const std::shared_ptr<StorageTexture>& image) {
auto& uncached = Uncached();
std::lock_guard lock(uncached.mutex);
uncached.images.push_back(image);
uncached.any.store(true, std::memory_order_release);
}
bool TextureHashEnabled() {
static const bool disabled = std::getenv("APS5_NO_TEXTURE_HASH") != nullptr;
return !disabled;
}
std::list<CachedTexture>::iterator findTexture(TextureCache& cache, const TextureKey& key) {
if (TextureHashEnabled()) {
const auto found = cache.index.find(key);
return found == cache.index.end() ? cache.entries.end() : found->second;
}
for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) {
if (it->key == key) return it;
}
return cache.entries.end();
}
void eraseTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
cache.bytes -= it->accounted;
if (it->source != nullptr) {
for (auto [view, last] = cache.views.equal_range(it->source.get()); view != last; ++view) {
if (view->second == it) {
cache.views.erase(view);
break;
}
}
}
cache.index.erase(it->key);
cache.entries.erase(it);
}
void dropUncachedViews(TextureCache& cache) {
auto& uncached = Uncached();
if (!uncached.any.load(std::memory_order_acquire)) return;
std::vector<std::weak_ptr<StorageTexture>> images;
{
std::lock_guard lock(uncached.mutex);
images.swap(uncached.images);
uncached.any.store(false, std::memory_order_release);
}
for (const auto& weak : images) {
const auto image = weak.lock();
if (image == nullptr) continue;
for (auto view = cache.views.find(image.get()); view != cache.views.end(); view = cache.views.find(image.get())) eraseTexture(cache, view->second);
}
}
// Moves an entry to the front (most recently used).
void touchTexture(TextureCache& cache, std::list<CachedTexture>::iterator it) {
cache.entries.splice(cache.entries.begin(), cache.entries, it);
}
// APS5_PROFILE_DRAW: what the sampled-texture and storage-image lookups did, printed as [textures]
// every 10 s (cumulative). Storage-sourced sampled textures need no CPU read; snapshots do, and a
// pending read is a snapshot compare or read made while recorded work still wrote the surface (the
// flush hook may find that work already signaled, so this bounds the hook syncs from above; the
// [hooksync] line attributes the actual waits).
struct TextureCounters {
std::atomic<std::uint64_t> fromStorage{0};
std::atomic<std::uint64_t> snapshots{0};
std::atomic<std::uint64_t> pendingReads{0};
std::atomic<std::uint64_t> storageFallbacks{0};
std::atomic<std::uint64_t> records{0};
std::atomic<std::uint64_t> fastHits{0};
std::atomic<std::uint64_t> fastMisses{0};
std::atomic<std::uint64_t> storageHits{0};
std::atomic<std::uint64_t> storageCreated{0};
// Storage images a Revalidate refreshed directly instead of through the lookups (T1, see
// ShaderResources::refreshOwnObjects).
std::atomic<std::uint64_t> ownRefreshes{0};
std::atomic<std::int64_t> lastReport{0};
};
TextureCounters& TextureCounts() {
static TextureCounters counters;
return counters;
}
void reportTextureCounters() {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
if (!profile) return;
auto& counters = TextureCounts();
const auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
auto last = counters.lastReport.load();
if (nowMs - last < 10000 || !counters.lastReport.compare_exchange_strong(last, nowMs)) return;
const auto count = [](const std::atomic<std::uint64_t>& value) { return static_cast<unsigned long long>(value.load(std::memory_order_relaxed)); };
AgcDriver::ProfilePrint_nid_no_patch("[textures] sampled created: %llu from storage images, %llu snapshots (%llu snapshot reads over recorded writes inside cachedTexture, %llu storage-path fallbacks); stage-A records %llu: %llu fast hits, %llu full lookups; storage images %llu hits, %llu created, %llu own-object refreshes\n", count(counters.fromStorage), count(counters.snapshots), count(counters.pendingReads), count(counters.storageFallbacks), count(counters.records), count(counters.fastHits), count(counters.fastMisses), count(counters.storageHits), count(counters.storageCreated), count(counters.ownRefreshes));
}
// What the sampled-texture lookups on this thread proved their returned objects current against,
// taken by object into the ShaderResources being built or revalidated (captureValidation), which
// then repeats the proof from write stamps on its next Revalidate. The lookups run inside one build
// on one thread; records nobody takes (a build that threw, a resident target looked up outside a
// build, a thread that never captures) are dropped by the next capture or by the bound below.
struct LookupRecord {
const void* object;
GuestTextureResource resource;
std::uint64_t bytes;
DccKeys keys;
std::uint64_t generation;
const StorageTexture* source;
};
thread_local std::vector<LookupRecord>* lookupLogSlot = nullptr;
std::vector<LookupRecord>& lookupLog() {
return ShaderRecompiler::ThreadOwned(lookupLogSlot);
}
void logLookup(const LookupRecord& record) {
// The bound drops the oldest half, never everything: one build's lookups (a few dozen at most)
// are the newest records, and a capture must find them even when the bound trips mid-build.
constexpr std::size_t bound = 1024;
auto& log = lookupLog();
if (log.size() >= bound) log.erase(log.begin(), log.begin() + bound / 2);
log.push_back(record);
}
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0);
std::uint64_t heldBytes(const StorageTexture& image) {
return std::max<std::uint64_t>(image.AllocationBytes(), image.GuestBytes());
}
bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) {
return resource.dccAddress != 0 && image.Descriptor().dccAddress != resource.dccAddress && !image.ServesKeysAt(resource.dccAddress);
}
// Whether a sampled texture over `resource` can be a view of the surface's cached storage image
// instead of a CPU snapshot (see cachedTexture): the format has a storage form and is not block
// compressed, and the surface lives in host-imported memory, where the image uploads and refreshes
// GPU-direct (a snapshot of such a surface reads and compares its bytes on the CPU, waiting for
// the recorded work that wrote them). APS5_NO_SAMPLED_FROM_STORAGE=1 keeps every sampled texture a
// snapshot as before.
bool SampledFromStorageEligible(const Context& context, std::uint32_t format, std::uint64_t address, std::uint64_t guestBytes) {
static const bool disabled = std::getenv("APS5_NO_SAMPLED_FROM_STORAGE") != nullptr;
if (disabled || IsBlockCompressed(format) || !StorageFormatAvailable(context, format)) return false;
return HostImportCovers(context, address, static_cast<std::size_t>(guestBytes));
}
bool SampledFromStorageEligible(const Context& context, const GuestTextureResource& resource, std::uint64_t guestBytes) {
return SampledFromStorageEligible(context, resource.format, resource.baseAddress, guestBytes);
}
// Whether a sampled texture over a fast-cleared, storage-eligible surface views the cleared storage
// image (see cachedTexture). APS5_NO_CLEARED_VIEW=1 keeps such surfaces snapshots, as before.
bool ClearedViewEnabled() {
static const bool disabled = std::getenv("APS5_NO_CLEARED_VIEW") != nullptr;
return !disabled;
}
// Surfaces whose storage image could not be made (no writable committed pages, say): remembered
// so the sampled lookup does not throw and fall back on every use. A failure is keyed by the surface
// (address and size: the heap reuses addresses) and holds only while the guest mappings are what
// they were when it failed (the allocation generation): pages committed later, or another surface
// at the address, get a fresh attempt.
struct StorageFailures {
std::mutex mutex;
std::map<std::pair<std::uint64_t, std::uint64_t>, std::uint64_t> generations;
};
StorageFailures& StorageFailed() {
static StorageFailures failures;
return failures;
}
// The cached storage image for a sampled descriptor, or null when it cannot be made (then the
// snapshot path serves the descriptor, as before). Every null return counts as a fallback.
std::shared_ptr<StorageTexture> sampledStorageSource(const Context& context, const GuestTextureResource& resource, std::uint64_t guestBytes) {
auto& counters = TextureCounts();
auto& failures = StorageFailed();
const auto surface = std::make_pair(resource.baseAddress, guestBytes);
const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
{
std::lock_guard lock(failures.mutex);
if (const auto it = failures.generations.find(surface); it != failures.generations.end()) {
if (it->second == generation) {
counters.storageFallbacks.fetch_add(1, std::memory_order_relaxed);
return nullptr;
}
failures.generations.erase(it);
}
}
try {
return cachedStorageTexture(context, {}, resource, 0, guestBytes);
} catch (const std::exception& error) {
counters.storageFallbacks.fetch_add(1, std::memory_order_relaxed);
std::lock_guard lock(failures.mutex);
if (failures.generations.size() < 4096 && failures.generations.emplace(surface, generation).second) std::fprintf(stderr, "[textures] sampled texture 0x%llx (%ux%u format %u) keeps the snapshot path: %s\n", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, error.what());
return nullptr;
}
}
std::uint64_t storageCacheBytes();
std::optional<std::uint32_t> largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) {
std::optional<std::uint32_t> heap;
for (std::uint32_t i = 0; i < std::min<std::uint32_t>(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) {
if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i;
}
return heap;
}
std::uint64_t forcedCacheBudget() {
static const std::uint64_t forced = [] {
const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB");
return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u;
}();
return forced;
}
std::uint64_t storageBudget(const Context& context) {
const auto forced = forcedCacheBudget();
return forced != 0 ? forced : TextureCacheBudget(context.memory);
}
std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
if (const auto forced = forcedCacheBudget(); forced != 0) return forced;
if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory);
const auto now = std::chrono::steady_clock::now();
if (cache.budgetDevice == context.physical && now - cache.budgetRead < std::chrono::seconds(1)) return cache.budget;
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
properties.pNext = &reported;
context.memoryProperties2(context.physical, &properties);
const auto sampled = SampledTextureMemory();
const auto storage = storageCacheBytes();
const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage);
const bool fresh = cache.budgetDevice != context.physical;
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) {
char was[48] = "";
if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast<unsigned long long>(cache.reportedBudget >> 20u));
std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast<unsigned long long>(budget >> 20u), was, *heap, static_cast<unsigned long long>(reported.heapBudget[*heap] >> 20u), static_cast<unsigned long long>(reported.heapUsage[*heap] >> 20u), static_cast<unsigned long long>(sampled >> 20u), static_cast<unsigned long long>(cache.bytes >> 20u), static_cast<unsigned long long>(storage >> 20u), static_cast<unsigned long long>(TextureCacheBudget(context.memory) >> 20u));
cache.reportedBudget = budget;
cache.reportedAt = now;
}
cache.budgetDevice = context.physical;
cache.budgetRead = now;
cache.budget = budget;
return budget;
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) {
CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(resource.dccAddress));
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth;
const auto depthBitsWidth = words.size() >= 4 ? ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) : 0u;
if (depthBitsWidth == 32u) {
char text[160];
std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast<unsigned long long>(resource.baseAddress));
throw std::runtime_error(text);
}
constexpr auto unorm16 = static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format16UNorm);
if (depthBitsWidth == 16u && resource.format != unorm16) {
auto normalized = resource;
normalized.format = unorm16;
return cachedTexture(context, words, normalized, components, guestBytes, depthCompare);
}
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
const bool profile = LookupOutcomes::Profiled();
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const auto scanKeys = [&] {
const auto scanStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const auto keys = TextureClearKeys(resource, guestBytes);
if (profile && resource.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart);
return keys;
};
if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes;
auto& counters = TextureCounts();
const auto address = resource.baseAddress;
const auto bytes = static_cast<std::size_t>(guestBytes);
// A storage image whose results for this surface (or for a mip chain containing it) are still on
// the GPU supplies the texture by a view of it; anything else needs those results in guest
// memory first.
auto source = StorageTexture::FindPending(address, guestBytes);
if (source != nullptr && (depthCompare || !Texture::CanCopyFrom(*source, resource) || MetadataMoved(*source, resource))) source.reset();
std::optional<DccKeys> keys;
bool clearThroughKeys = false;
if (source != nullptr && resource.dccAddress != 0 && IsDccClear(source->FilledKeys())) {
keys = scanKeys();
if (*keys == source->FilledKeys()) {
std::array<std::byte, 16> probe{};
if (!FillDccClear(ResolveTextureFormat(resource.format), *keys, resource.dccAlphaOnMsb, probe)) {
char text[256];
std::snprintf(text, sizeof(text), "AGC graphics: sampled texture 0x%llx (%ux%u format %u, dcc 0x%llx) reads %s DCC keys filled over its pending image, a clear value the format has no encoding for", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, static_cast<unsigned long long>(resource.dccAddress), DccKeysName(*keys));
throw std::runtime_error(text);
}
static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (traceKeys) std::fprintf(stderr, "[dcc-keys] sampled 0x%llx through keys 0x%llx reads the %s fill, not the pending image\n", static_cast<unsigned long long>(address), static_cast<unsigned long long>(resource.dccAddress), DccKeysName(*keys));
source.reset();
clearThroughKeys = true;
}
}
// Otherwise a surface in host-imported memory is viewed through its cached storage image (made
// here when there is none): its refresh after a CPU or GPU write is a GPU-direct detile from the
// import, recorded behind the producer, so no bytes are read or compared on the CPU and nothing
// waits for the producer. A fast-cleared surface (keys) is viewed only through an image whose own
// descriptor carries the DCC address (below); it stays a snapshot otherwise, its texels not read.
if (!depthCompare && source == nullptr && !clearThroughKeys && SampledFromStorageEligible(context, resource, guestBytes)) {
keys = scanKeys();
if (*keys == DccKeys::Uncompressed) {
source = sampledStorageSource(context, resource, guestBytes);
} else if (ClearedViewEnabled() && StorageClearAvailable(context, resource.format, *keys)) {
// A fast-cleared surface is viewed as well, through its image cleared on the GPU (the
// lookup's Refresh, see StorageTexture::upload), when the image's own descriptor names
// the same DCC metadata so the refresh sees the keys. A snapshot of the clear texels
// (filled and uploaded on the CPU, then replaced by a view once results are pending: two
// 4K uploads per clear at the movie stage) serves the surface otherwise, as before.
auto candidate = sampledStorageSource(context, resource, guestBytes);
if (candidate != nullptr && candidate->Descriptor().dccAddress == resource.dccAddress) source = std::move(candidate);
}
// The lookup's Refresh may have flushed another image's results over the memory (which
// marks the keys uncompressed): a surface that stays a snapshot re-reads them behind that
// flush, as it does behind its own. APS5_NO_KEYS_RESCAN=1 keeps the keys scanned above.
static const bool rescan = std::getenv("APS5_NO_KEYS_RESCAN") == nullptr;
if (rescan && source == nullptr) keys.reset();
}
// Pages of the surface written since they were last collected are stamped now, before any
// UnchangedSince (here and on a cache hit) looks at them: the checks only compare stamped blocks,
// so a write the game made since the last walk is invisible until a collect stamps its page. The
// walk is memoized per packet, so a texture viewed from a storage image repeats it for free.
auto generation = GuestMemory::CollectWrites(address, bytes);
if (source != nullptr && !GuestMemory::UnchangedSince(address, bytes, source->Generation())) {
// The CPU wrote the memory while results were pending: Refresh merges them the usual way.
source->Refresh();
}
// Results stored to guest memory just now are stamped newer than `generation`; a snapshot read
// after them is current at the generation of a second (memoized) collect. The store marks the
// surface's DCC keys uncompressed, so the keys are read after it.
const auto flushStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
if (source == nullptr && !clearThroughKeys && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) {
if (profile) LookupOutcomes::Add(LookupOutcomes::PendingFlush, flushStart);
generation = GuestMemory::CollectWrites(address, bytes);
keys.reset();
}
if (!keys.has_value()) keys = scanKeys();
if (disabled) {
if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components);
std::vector<std::byte> snapshot(bytes);
ReadTextureSurface(resource, *keys, snapshot);
return std::make_shared<Texture>(context, *context.detiler, resource, components, snapshot, depthCompare);
}
auto& cache = Textures();
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
std::lock_guard lock(cache.mutex);
dropUncachedViews(cache);
if (auto it = findTexture(cache, key); it != cache.entries.end()) {
if (it->source != nullptr) {
// The view follows the storage image, whatever the GPU wrote to it since; guest memory
// written meanwhile is taken in by refreshing the image. A fast clear the image cannot
// see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear.
if ((source == nullptr || source == it->source) && (source != nullptr || *keys == DccKeys::Uncompressed) && StorageImageServesKeys(*it->source, resource.dccAddress)) {
if (!GuestMemory::UnchangedSince(address, bytes, it->source->Generation())) it->source->Refresh();
it->keys = *keys;
touchTexture(cache, it);
logLookup({it->texture.get(), resource, guestBytes, *keys, 0, it->source.get()});
reportTextureCounters();
if (profile) LookupOutcomes::Add(*keys != DccKeys::Uncompressed ? LookupOutcomes::SampledHitClearedView : LookupOutcomes::SampledHitView, start);
return it->texture;
}
} else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) {
// Unwritten pages need no comparison; partially resident textures compare only committed
// pages. The compare goes through the flush hook: it waits for recorded work over the
// surface (counted, and named for the [hooksync] line).
const auto equalsCommitted = [&] {
if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed);
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
if (*keys != DccKeys::Uncompressed || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) {
it->generation = generation;
touchTexture(cache, it);
logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr});
reportTextureCounters();
if (profile) LookupOutcomes::Add(LookupOutcomes::SampledHitSnapshot, start);
return it->texture;
}
}
eraseTexture(cache, it);
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, generation};
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
entry.texture = std::make_shared<Texture>(context, source, resource, components);
entry.accounted = source->Cached() ? 0u : heldBytes(*source);
counters.fromStorage.fetch_add(1, std::memory_order_relaxed);
} else {
// Snapshot before the upload so a write racing with it is caught by the next comparison.
if (*keys == DccKeys::Uncompressed && Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed);
ReadTextureSurface(resource, *keys, entry.bytes);
static const bool traceTextures = std::getenv("APS5_TRACE_TEXTURES") != nullptr;
if (traceTextures) {
std::size_t nonzero = 0;
for (std::size_t i = 0; i < entry.bytes.size(); i += 64) nonzero += entry.bytes[i] != std::byte{0};
std::fprintf(stderr, "[texture] 0x%llx %ux%u format %u tile %d: %zu of %zu sampled bytes nonzero\n", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), nonzero, entry.bytes.size() / 64);
}
entry.texture = std::make_shared<Texture>(context, *context.detiler, resource, components, entry.bytes, depthCompare);
entry.accounted = entry.texture->AllocationBytes();
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
const auto budget = sampledBudget(context, cache);
while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) eraseTexture(cache, std::prev(cache.entries.end()));
cache.bytes += entry.accounted;
auto texture = entry.texture;
logLookup({texture.get(), resource, guestBytes, *keys, generation, source.get()});
cache.entries.push_front(std::move(entry));
cache.index[key] = cache.entries.begin();
if (source != nullptr) cache.views.emplace(source.get(), cache.entries.begin());
reportTextureCounters();
if (profile) LookupOutcomes::Add(source != nullptr ? LookupOutcomes::SampledMadeView : LookupOutcomes::SampledMadeSnapshot, start);
return texture;
}
// Storage images stay on the GPU between dispatches: while guest memory still holds what an image was
// last uploaded from or written back as, its next use skips the upload and detile.
struct StorageKey {
VkDevice device;
std::array<std::uint32_t, 8> words;
bool operator==(const StorageKey&) const = default;
};
struct StorageKeyHash {
std::size_t operator()(const StorageKey& key) const noexcept {
return static_cast<std::size_t>(hashWords(14695981039346656037ull ^ reinterpret_cast<std::uintptr_t>(key.device), key.words.data(), key.words.size()));
}
};
struct CachedStorageTexture {
StorageKey key;
std::uint32_t mip;
std::shared_ptr<StorageTexture> texture;
std::uint64_t accounted = 0;
};
// As TextureCache: use order with a hash index by key, plus one by image for StorageImageCached.
struct StorageTextureCache {
std::mutex mutex;
std::list<CachedStorageTexture> entries;
std::unordered_map<StorageKey, std::list<CachedStorageTexture>::iterator, StorageKeyHash> index;
std::unordered_map<const StorageTexture*, std::list<CachedStorageTexture>::iterator> byImage;
std::atomic<std::uint64_t> bytes{0};
};
StorageTextureCache& StorageTextures() {
static StorageTextureCache cache;
return cache;
}
std::uint64_t storageCacheBytes() {
return StorageTextures().bytes.load(std::memory_order_relaxed);
}
std::list<CachedStorageTexture>::iterator findStorage(StorageTextureCache& cache, const StorageKey& key) {
if (TextureHashEnabled()) {
const auto found = cache.index.find(key);
return found == cache.index.end() ? cache.entries.end() : found->second;
}
for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) {
if (it->key == key) return it;
}
return cache.entries.end();
}
std::list<CachedStorageTexture>::iterator findStorageByImage(StorageTextureCache& cache, VkDevice device, const StorageTexture* image) {
if (TextureHashEnabled()) {
const auto found = cache.byImage.find(image);
return found == cache.byImage.end() || found->second->key.device != device ? cache.entries.end() : found->second;
}
for (auto it = cache.entries.begin(); it != cache.entries.end(); ++it) {
if (it->key.device == device && it->texture.get() == image) return it;
}
return cache.entries.end();
}
// Evicts an entry: its pending results go to guest memory first (the image may die with the entry).
void evictStorage(StorageTextureCache& cache, std::list<CachedStorageTexture>::iterator it) {
it->texture->SetCached(false);
noteUncached(it->texture);
it->texture->Flush();
cache.bytes -= it->accounted;
cache.index.erase(it->key);
cache.byImage.erase(it->texture.get());
cache.entries.erase(it);
}
// Storage images are shared by every descriptor of one surface (address, extent, layers, format, tile
// mode): the image holds the whole mip chain, and render targets in the same memory attach to it.
std::array<std::uint32_t, 8> SurfaceKey(const Context& context, const GuestTextureResource& resource) {
// Guest formats that store in the same Vulkan format share the image (views carry the difference).
return {static_cast<std::uint32_t>(resource.baseAddress), static_cast<std::uint32_t>(resource.baseAddress >> 32u), resource.width, resource.height, (resource.depthOrLastArray << 16u) | (resource.mipCount & 0xffffu), (static_cast<std::uint32_t>(resource.tileMode) << 12u) | (static_cast<std::uint32_t>(resource.dimension) << 20u), resource.baseArray, static_cast<std::uint32_t>(StorageFormatForGuest(context, resource.format))};
}
struct ExtendedSurfaces {
std::mutex mutex;
std::map<std::array<std::uint32_t, 8>, std::uint32_t> levels;
std::atomic<bool> any{false};
};
ExtendedSurfaces& ExtendedChains() {
static ExtendedSurfaces surfaces;
return surfaces;
}
std::uint32_t AllocatedLevels(const GuestTextureResource& resource) {
return resource.allocatedMipCount != 0 ? resource.allocatedMipCount : resource.mipCount;
}
GuestTextureResource StorageSurface(const Context& context, const GuestTextureResource& viewed) {
auto& surfaces = ExtendedChains();
const bool extended = viewed.mipCount > AllocatedLevels(viewed);
if (!extended && !surfaces.any.load(std::memory_order_acquire)) return viewed;
auto surface = viewed;
surface.mipCount = AllocatedLevels(viewed);
const auto identity = SurfaceKey(context, surface);
std::lock_guard lock(surfaces.mutex);
if (extended) {
auto& levels = surfaces.levels[identity];
levels = std::max(levels, viewed.mipCount);
surfaces.any.store(true, std::memory_order_release);
surface.mipCount = levels;
} else if (const auto it = surfaces.levels.find(identity); it != surfaces.levels.end()) {
surface.mipCount = it->second;
} else {
surface.mipCount = viewed.mipCount;
}
return surface;
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) {
if (DepthSurfaceAt(viewed.baseAddress)) {
char text[112];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast<unsigned long long>(viewed.baseAddress));
throw std::runtime_error(text);
}
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if (disabled) return std::make_shared<StorageTexture>(context, *context.detiler, viewed, mip);
static_cast<void>(words);
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
const bool profile = LookupOutcomes::Profiled();
auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
auto& counters = TextureCounts();
const auto resource = StorageSurface(context, viewed);
const StorageKey key{context.device, SurfaceKey(context, resource)};
auto& cache = StorageTextures();
std::lock_guard lock(cache.mutex);
if (resource.mipCount > AllocatedLevels(resource)) {
auto allocated = resource;
allocated.mipCount = AllocatedLevels(resource);
if (const auto found = findStorage(cache, {context.device, SurfaceKey(context, allocated)}); found != cache.entries.end()) evictStorage(cache, found);
}
auto it = findStorage(cache, key);
if (it != cache.entries.end() && MetadataMoved(*it->texture, resource)) {
evictStorage(cache, it);
it = cache.entries.end();
}
if (it != cache.entries.end()) {
it->texture->Refresh();
cache.entries.splice(cache.entries.begin(), cache.entries, it);
counters.storageHits.fetch_add(1, std::memory_order_relaxed);
if (profile) LookupOutcomes::Add(LookupOutcomes::StorageHit, start);
return it->texture;
}
// Results other images hold over this memory reach it before the new image reads it: a hit's
// Refresh flushes them, the constructor's upload does not, and a GPU-direct upload reads the
// import buffer without the flush hook. The flush is recorded ahead of the upload in the batch.
if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes;
if (StorageTexture::FlushPending(resource.baseAddress, static_cast<std::size_t>(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start);
CachedStorageTexture entry{key, mip, std::make_shared<StorageTexture>(context, *context.detiler, resource, mip)};
// The constructor's upload may have recorded into the open batch (a GPU clear, a direct
// detile) before the image could keep itself (no weak_from_this yet): the batch keeps it here,
// so an eviction or a failed view before it ran cannot destroy a referenced image.
// APS5_NO_KEEP_NEW_STORAGE=1 leaves the image to its cache entry alone, as before.
static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr;
if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture);
entry.accounted = heldBytes(*entry.texture);
const auto budget = storageBudget(context);
while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evictStorage(cache, std::prev(cache.entries.end()));
cache.bytes += entry.accounted;
auto texture = entry.texture;
cache.entries.push_front(std::move(entry));
cache.index[key] = cache.entries.begin();
cache.byImage[texture.get()] = cache.entries.begin();
texture->SetCached(true);
counters.storageCreated.fetch_add(1, std::memory_order_relaxed);
if (profile) LookupOutcomes::Add(LookupOutcomes::StorageMade, start);
return texture;
}
}
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) {
const auto heap = largestDeviceLocalHeap(memory);
return std::max<std::uint64_t>(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u);
}
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) {
const auto heap = largestDeviceLocalHeap(memory);
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
const auto budget = reported->heapBudget[*heap];
const auto usage = reported->heapUsage[*heap];
const auto reserved = usage - std::min<std::uint64_t>(usage, textureBytes) + TextureCacheBudget(memory) + budget / 8u;
return std::max<std::uint64_t>(2048ull << 20u, budget > reserved ? budget - reserved : 0u);
}
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport) {
const auto change = budget > reported ? budget - reported : reported - budget;
return change > reported / 10u && sinceReport >= std::chrono::seconds(10);
}
TextureCacheUse TextureCacheUsage() {
TextureCacheUse use;
{
auto& sampled = Textures();
std::lock_guard lock(sampled.mutex);
dropUncachedViews(sampled);
use.sampledEntries = sampled.entries.size();
use.sampledBytes = sampled.bytes;
}
use.storageBytes = storageCacheBytes();
return use;
}
std::uint64_t SampledTextureCacheBudget(const Context& context) {
auto& cache = Textures();
std::lock_guard lock(cache.mutex);
return sampledBudget(context, cache);
}
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words) {
const auto resource = DecodeTextureResource(words);
return cachedTexture(context, words, resource, ViewComponents(resource));
}
void FlushCachedTextures(VkDevice device) {
Require(device != VK_NULL_HANDLE, "cannot flush textures without a Vulkan device");
GuestMemory::AssertGpuLockHeld("FlushCachedTextures");
auto& cache = StorageTextures();
std::lock_guard lock(cache.mutex);
for (const auto& entry : cache.entries) {
if (entry.key.device == device) entry.texture->Flush();
}
}
void ClearCachedTextures(VkDevice device) {
Require(device != VK_NULL_HANDLE, "cannot clear textures without a Vulkan device");
auto& sampled = Textures();
{
std::lock_guard lock(sampled.mutex);
for (auto it = sampled.entries.begin(); it != sampled.entries.end();) {
if (it->key.device == device) eraseTexture(sampled, it++);
else ++it;
}
}
auto& storage = StorageTextures();
std::lock_guard lock(storage.mutex);
for (auto it = storage.entries.begin(); it != storage.entries.end();) {
if (it->key.device != device) {
++it;
continue;
}
it->texture->SetCached(false);
noteUncached(it->texture);
storage.bytes -= it->accounted;
storage.index.erase(it->key);
storage.byImage.erase(it->texture.get());
it = storage.entries.erase(it);
}
}
bool StorageImageCached(const Context& context, const StorageTexture* image) {
auto& cache = StorageTextures();
std::lock_guard lock(cache.mutex);
const auto it = findStorageByImage(cache, context.device, image);
if (it == cache.entries.end()) return false;
cache.entries.splice(cache.entries.begin(), cache.entries, it);
return true;
}
namespace {
// StorageImageCached for several images under one acquisition of the cache mutex.
bool StorageImagesCached(const Context& context, std::span<const StorageTexture* const> images) {
if (images.empty()) return true;
auto& cache = StorageTextures();
std::lock_guard lock(cache.mutex);
for (const auto* image : images) {
const auto it = findStorageByImage(cache, context.device, image);
if (it == cache.entries.end()) return false;
cache.entries.splice(cache.entries.begin(), cache.entries, it);
}
return true;
}
}
std::shared_ptr<StorageTexture> CachedStorageSurface(const Context& context, const GuestTextureResource& resource) {
return cachedStorageTexture(context, {}, resource, 0);
}
bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress) {
GuestTextureResource resource{};
resource.dccAddress = dccAddress;
return !MetadataMoved(image, resource);
}
namespace {
const char* roleName(ShaderRecompiler::DescriptorRole role) {
switch (role) {
case ShaderRecompiler::DescriptorRole::GuestBuffers: return "GuestBuffers";
case ShaderRecompiler::DescriptorRole::GuestImages: return "GuestImages";
case ShaderRecompiler::DescriptorRole::GuestSamplers: return "GuestSamplers";
case ShaderRecompiler::DescriptorRole::Gds: return "Gds";
case ShaderRecompiler::DescriptorRole::BdaPagetable: return "BdaPagetable";
case ShaderRecompiler::DescriptorRole::FaultBuffer: return "FaultBuffer";
case ShaderRecompiler::DescriptorRole::FlattenedSrt: return "FlattenedSrt";
case ShaderRecompiler::DescriptorRole::ShaderData: return "ShaderData";
}
throw std::runtime_error("AGC graphics: unknown descriptor role");
}
const char* kindName(ShaderRecompiler::DescriptorKind kind) {
switch (kind) {
case ShaderRecompiler::DescriptorKind::UniformBuffer: return "UniformBuffer";
case ShaderRecompiler::DescriptorKind::StorageBuffer: return "StorageBuffer";
case ShaderRecompiler::DescriptorKind::UniformTexelBuffer: return "UniformTexelBuffer";
case ShaderRecompiler::DescriptorKind::StorageTexelBuffer: return "StorageTexelBuffer";
case ShaderRecompiler::DescriptorKind::SampledImage: return "SampledImage";
case ShaderRecompiler::DescriptorKind::StorageImage: return "StorageImage";
case ShaderRecompiler::DescriptorKind::Sampler: return "Sampler";
}
throw std::runtime_error("AGC graphics: unknown descriptor kind");
}
// Consecutive identical storage descriptors address successive mips of one texture (dynamic-mip
// storage writes), so the second and later ones share the first one's image lookup.
// APS5_NO_STORAGE_DEDUPE=1 looks each one up (and refreshes the image) separately as before.
bool StorageDedupeEnabled() {
static const bool disabled = std::getenv("APS5_NO_STORAGE_DEDUPE") != nullptr;
return !disabled;
}
// Whether storage element `element` of `binding` repeats the previous element's eight words.
bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t element) {
if (element == 0) return false;
const auto words = binding.guestDescriptor.begin() + static_cast<std::size_t>(element) * 8u;
return std::equal(words, words + 8, words - 8);
}
}
ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array<CompiledShader, 2>{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast<std::uint32_t>(vertex.pushConstants.size())}}}, target, indexAddress, indexBytes) {}
ShaderResources::ShaderResources(const Context& context, std::span<const CompiledShader> shaders, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes, std::span<const GuestMemorySnapshot> snapshots) : context(context), guestMemory(context) {
prepareAddressBindings(shaders, snapshots);
build(shaders, &target, indexAddress, indexBytes);
}
ShaderResources::ShaderResources(const Context& context, const CompiledShader& compute, std::span<const GuestMemorySnapshot> snapshots) : ShaderResources(context, compute, snapshots, false) {}
namespace {
// Whether a compute stage maps registered guest memory through BDA tables (see prepareAddressBindings).
bool UsesAddressTables(const CompiledShader& compute) {
Require(compute.program != nullptr, "missing compiled shader");
return std::any_of(compute.program->bindings.begin(), compute.program->bindings.end(), [](const auto& binding) { return binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable; });
}
}
ShaderResources::ShaderResources(const Context& context, const CompiledShader& compute, std::span<const GuestMemorySnapshot> snapshots, bool deferred) : context(context), guestMemory(context), deferredCompute(compute), deferredSnapshots(snapshots) {
Require(compute.stage == ShaderRecompiler::ShaderStage::Compute, "compute resources require a compute shader");
// Every use of a compute build is a recorded dispatch that calls MarkGpuWrites, which staged
// buffers need (a synchronous draw's use would not).
guestMemory.AllowDeviceStaging();
const std::span<const CompiledShader> shaders(&deferredCompute, 1);
if (!deferred) {
prepareAddressBindings(shaders, snapshots);
build(shaders, nullptr, 0, 0);
forgetDeferredInputs();
return;
}
// Address-based shaders pin and mirror registered memory in prepareAddressBindings (reconciling
// imports retires buffers to the recorder, refreshing mirrors waits for recorded work): that is
// device-lock work, so their whole build waits for Complete(). Without tables the call only
// checks the bindings.
lockedBuild = UsesAddressTables(compute);
if (lockedBuild) return;
unlockedPrepare = true;
prepareAddressBindings(shaders, snapshots);
buildPrepare(shaders, nullptr, 0, 0);
}
void ShaderResources::Complete() {
Require(!completed, "shader resources were already completed");
const std::span<const CompiledShader> shaders(&deferredCompute, 1);
if (lockedBuild) {
prepareAddressBindings(shaders, deferredSnapshots);
buildPrepare(shaders, nullptr, 0, 0);
}
buildComplete();
forgetDeferredInputs();
}
void ShaderResources::forgetDeferredInputs() {
// The compiled shader and the captured regions belong to the dispatch, which returns while this
// object lives on in the resource cache and the recorder: nothing may reach for them after the
// build, so they are dropped rather than left dangling.
deferredCompute = {};
deferredSnapshots = {};
}
void ShaderResources::build(std::span<const CompiledShader> shaders, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes) {
buildPrepare(shaders, target, indexAddress, indexBytes);
buildComplete();
}
namespace {
// APS5_PROFILE_DRAW: the [resources] phase totals. Each thread accumulates its builds' phases in
// arrays of its own and merges them into the shared totals every 1000 of its builds (and when it
// ends), so no build takes the shared mutex per phase; the totals lag by up to 999 builds per
// worker.
constexpr std::size_t BuildPhaseCount = static_cast<std::size_t>(ShaderResources::BuildPhase::Count);
constexpr std::array<const char*, BuildPhaseCount> BuildPhaseNames{"bindings", "precollect", "guest memory upload", "descriptors", "stage A", "images", "bda", "stage B"};
struct BuildProfile {
std::mutex mutex;
std::array<double, BuildPhaseCount> ms{};
std::uint64_t builds = 0;
};
BuildProfile& Builds() {
static BuildProfile profile;
return profile;
}
bool BuildProfiled() {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
return profile;
}
struct ThreadBuildProfile {
std::array<double, BuildPhaseCount> ms{};
std::uint64_t builds = 0;
~ThreadBuildProfile() { merge(); }
void merge() {
auto& profile = Builds();
std::lock_guard lock(profile.mutex);
for (std::size_t i = 0; i < BuildPhaseCount; ++i) profile.ms[i] += ms[i];
profile.builds += builds;
ms = {};
if (builds == 0) return;