aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/texture_cache
diff options
context:
space:
mode:
authorPastaLaPate <pastalapate@proton.me>2026-11-02 20:34:15 +0100
committerGitHub <noreply@github.com>2026-11-02 20:34:15 +0100
commit6f002badce45fa2fdb414043544c829e834a7e27 (patch)
tree51c21a851fbc8218ebe7febb69fb710a891cc4fc /src/video_core/texture_cache
parent396f011ea875dfe4fdb1b0f489651ab74a505b74 (diff)
parent046646e3514ca4950622d75609e68498f889989e (diff)
Merge branch 'nx2' into ci/qt-6.12.0
Diffstat (limited to 'src/video_core/texture_cache')
-rw-r--r--src/video_core/texture_cache/descriptor_table.h10
-rw-r--r--src/video_core/texture_cache/texture_cache.h199
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h34
3 files changed, 226 insertions, 17 deletions
diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h
index e40c128ab5..75383809c5 100644
--- a/src/video_core/texture_cache/descriptor_table.h
+++ b/src/video_core/texture_cache/descriptor_table.h
@@ -30,11 +30,17 @@ public:
}
void Invalidate() noexcept {
- std::ranges::fill(read_descriptors, 0);
+ // Cleared on the next read: the texture cache invalidates every channel's tables for
+ // each image it deletes, dozens of times a frame while its collector runs.
+ invalidated = true;
}
[[nodiscard]] std::pair<T, bool> Read(Tegra::MemoryManager const& gpu_memory, u32 index) noexcept {
DEBUG_ASSERT(index <= current_limit);
+ if (invalidated) [[unlikely]] {
+ std::ranges::fill(read_descriptors, 0);
+ invalidated = false;
+ }
const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T);
std::pair<T, bool> result;
gpu_memory.ReadBlockUnsafe(gpu_addr, std::addressof(result.first), sizeof(T));
@@ -60,6 +66,7 @@ public:
read_descriptors.resize(Common::DivCeil(num_descriptors, 64U));
old_size = (std::min)(old_size, read_descriptors.size());
std::fill(read_descriptors.begin(), read_descriptors.begin() + old_size, 0);
+ invalidated = false;
//
descriptors.resize(num_descriptors);
}
@@ -68,6 +75,7 @@ public:
std::vector<T> descriptors;
GPUVAddr current_gpu_addr{};
u32 current_limit{};
+ bool invalidated{}; ///< read_descriptors is to be cleared before the next read
};
} // namespace VideoCommon
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 55ec4c64df..3f3ae9cafd 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -178,6 +178,57 @@ void TextureCache<P>::RunGarbageCollector() {
}
return false;
};
+ // Deleting by age alone left every device memory block partly used: the collector deletes
+ // thousands of images a minute and new ones land in the holes, so blocks seldom empty (title
+ // 1: ~0.8 GB of holes in ~190 32 MiB blocks after ten minutes, which the driver counts as
+ // used). Among the oldest candidates (8 per image this pass may delete), those in the
+ // emptiest blocks go first, so blocks drain and go back to the driver; placement already
+ // prefers the fullest blocks (best fit). Images with memory of their own sit in the middle.
+ const auto Collect = [&] {
+ if constexpr (requires(const Image& image) { image.MemoryBlock(); }) {
+ std::vector<ImageId> candidates;
+ const size_t limit = num_iterations * 8;
+ lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, [&](ImageId image_id) {
+ candidates.push_back(image_id);
+ return candidates.size() >= limit;
+ });
+ if (candidates.empty()) {
+ return;
+ }
+ if (block_image_bytes_frame == ~0ULL || frame_tick - block_image_bytes_frame >= 60) {
+ block_image_bytes_frame = frame_tick;
+ block_image_bytes.clear();
+ for (const auto [image_id, image] : slot_images) {
+ const auto block = image->MemoryBlock();
+ if (block.block != 0) {
+ block_image_bytes[block.block] += block.size;
+ }
+ }
+ }
+ std::vector<std::pair<double, ImageId>> ordered;
+ ordered.reserve(candidates.size());
+ for (const ImageId image_id : candidates) {
+ const auto block = slot_images[image_id].MemoryBlock();
+ double fill = 0.5;
+ if (block.block != 0 && block.block_size != 0) {
+ const auto it = block_image_bytes.find(block.block);
+ fill = it != block_image_bytes.end()
+ ? static_cast<double>(it->second) / static_cast<double>(block.block_size)
+ : 0.0;
+ }
+ ordered.emplace_back(fill, image_id);
+ }
+ std::stable_sort(ordered.begin(), ordered.end(),
+ [](const auto& a, const auto& b) { return a.first < b.first; });
+ for (const auto& [fill, image_id] : ordered) {
+ if (Cleanup(image_id)) {
+ break;
+ }
+ }
+ } else {
+ lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ }
+ };
Configure(false);
static const u64 old_age = [] {
const char* value = std::getenv("EDEN_NX2_TC_OLD_AGE");
@@ -188,15 +239,23 @@ void TextureCache<P>::RunGarbageCollector() {
lru_cache.ForEachItemBelow(frame_tick - old_age, Cleanup);
evict_old_costly = false;
}
- lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ Collect();
if (total_used_memory >= critical_memory) {
Configure(true);
- lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ Collect();
}
}
template <class P>
void TextureCache<P>::ApplyThresholdOverrides(bool log) {
+ // On every GPU: past what a frame needs, more device memory only keeps the textures of earlier
+ // scenes (title 1 filled 6.7 GB of an 8 GB card and draws its races in ~5 GB), so collect from
+ // MaxCriticalMemory on rather than near the card's budget.
+ critical_memory = (std::min)(critical_memory, MaxCriticalMemory);
+ expected_memory =
+ (std::min)(expected_memory, critical_memory - (std::min)(critical_memory, u64{768_MiB}));
+ minimum_memory =
+ (std::min)(minimum_memory, expected_memory - (std::min)(expected_memory, u64{1_GiB}));
// EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB override the collector's thresholds.
for (const auto& [name, target] : {std::pair{"EDEN_NX2_TC_EXPECTED_MIB", &expected_memory},
std::pair{"EDEN_NX2_TC_CRITICAL_MIB", &critical_memory}}) {
@@ -242,26 +301,78 @@ void TextureCache<P>::TickFrame() {
// budget as the card fills (16 GB card: 13.5 GB left mid-race), hence the re-read every 30
// frames. EDEN_NX2_TC_*_MIB overrides still win.
if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) {
+ if constexpr (requires { runtime.MemoryPressureEvents(); }) {
+ // Images that did not fit (made in host memory or with nothing bound) mean the
+ // thresholds sit too high for this card and driver: leave more room until the
+ // pressure has been gone for a while.
+ if (native_thresholds_applied) {
+ const u32 events = runtime.MemoryPressureEvents();
+ if (events != seen_pressure_events) {
+ seen_pressure_events = events;
+ pressure_margin = (std::min)(pressure_margin + 512_MiB, 3_GiB);
+ pressure_quiet_frames = 0;
+ budget_check_frames = 0; // thresholds are recomputed below
+ LOG_WARNING(HW_GPU, "Out of device memory: keeping {} MiB more free",
+ pressure_margin >> 20);
+ } else if (pressure_margin != 0 && ++pressure_quiet_frames % 600 == 0) {
+ pressure_margin -= (std::min)(pressure_margin, 128_MiB);
+ budget_check_frames = 0;
+ }
+ }
+ }
if (native_thresholds_applied && budget_check_frames++ % 30 == 0) {
const u64 budget = runtime.GetDeviceMemoryBudget();
if (budget > 2_GiB) {
const u64 old_critical = critical_memory;
const u64 margin = (std::min)(budget / 8, 1_GiB);
- critical_memory = budget - margin;
- expected_memory = budget - 2 * margin;
+ if constexpr (requires { runtime.GetDeviceMemorySlack(); }) {
+ // The usage counted here leaves out the unused part of the allocator's
+ // blocks, which the driver counts and a full heap cannot hand to a new
+ // image (RX 7800 XT: 1.06 GiB of it, the whole margin, with the heap at
+ // its budget for a minute before images stopped fitting). Reserve it
+ // too: at once when it grows, slowly when it shrinks, so evicting images
+ // (which leaves partly used blocks) does not chase the threshold down.
+ const u64 slack = (std::min)(runtime.GetDeviceMemorySlack(), 1536_MiB);
+ slack_margin = slack >= slack_margin
+ ? slack
+ : slack_margin - (std::min)(slack_margin - slack, 16_MiB);
+ }
+ critical_memory =
+ budget - (std::min)(margin + pressure_margin + slack_margin, budget / 2);
+ expected_memory = critical_memory - (std::min)(margin, critical_memory / 2);
// Log the first values and real changes (the budget moves by a few MiB all
// the time).
const u64 change = critical_memory > old_critical
? critical_memory - old_critical
: old_critical - critical_memory;
- ApplyThresholdOverrides(budget_check_frames == 1 || change >= 256_MiB);
+ const bool log = budget_check_frames == 1 || change >= 256_MiB;
+ ApplyThresholdOverrides(log);
+ if (log) {
+ LOG_INFO(HW_GPU,
+ "Memory margins: base {} MiB, out of memory {} MiB, block slack {} MiB",
+ margin >> 20, pressure_margin >> 20, slack_margin >> 20);
+ }
}
}
}
}
+ if constexpr (requires { runtime.CurrentTick(); }) {
+ while (!gpu_sentenced_images.empty() &&
+ runtime.IsFree(gpu_sentenced_images.front().tick)) {
+ gpu_sentenced_bytes -= gpu_sentenced_images.front().bytes;
+ gpu_sentenced_images.pop_front();
+ }
+ }
// If we can obtain the memory info, use it instead of the estimate.
if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage();
+ // Images the collector deleted keep their memory for TICKS_TO_DESTROY frames: counting
+ // it kept the collector in aggressive mode, deleting 40+ images a frame for frames after
+ // it had freed enough (title 1: frames over 100 ms for seconds at a time).
+ for (const u64 bytes : sentenced_image_bytes) {
+ total_used_memory -= (std::min)(total_used_memory, bytes);
+ }
+ total_used_memory -= (std::min)(total_used_memory, gpu_sentenced_bytes);
}
// Every 600 frames: device memory in use against the collector's thresholds, and what the
// images themselves account for (VRAM pressure makes the collector thrash).
@@ -328,10 +439,18 @@ void TextureCache<P>::TickFrame() {
RunGarbageCollector();
}
sentenced_images.Tick();
+ sentenced_image_index = (sentenced_image_index + 1) % TICKS_TO_DESTROY;
+ sentenced_image_bytes[sentenced_image_index] = 0;
sentenced_framebuffers.Tick();
sentenced_image_view.Tick();
TickAsyncDecode();
TickAsyncUnswizzle();
+ if (frame_tick % 600 == 0 && !uploaded_ranges.empty()) {
+ for (const auto& [address, size] : uploaded_ranges) {
+ device_memory.HintCold(address, size);
+ }
+ uploaded_ranges.clear();
+ }
runtime.TickFrame();
++frame_tick;
@@ -690,6 +809,7 @@ typename P::Framebuffer* TextureCache<P>::GetFramebuffer() {
template <class P>
ImageViewId TextureCache<P>::VisitImageView(u32 index, bool compute) {
auto& table = compute ? channel_state->compute_image_table : channel_state->graphics_image_table;
+ index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit);
if (index > table.current_limit) {
LOG_DEBUG(HW_GPU, "Invalid image view index={}", index);
return NULL_IMAGE_VIEW_ID;
@@ -701,8 +821,13 @@ ImageViewId TextureCache<P>::VisitImageView(u32 index, bool compute) {
if (IsValidEntry(*gpu_memory, descriptor)) {
// Is new (registered view) on the texture cache side?
const auto [pair, is_new_tc] = channel_state->image_views.try_emplace(descriptor);
- if (is_new_tc)
+ if (is_new_tc) {
pair->second = CreateImageView(descriptor);
+ if (pair->second != NULL_IMAGE_VIEW_ID) {
+ image_view_descriptors[pair->second].emplace_back(current_channel_id,
+ descriptor);
+ }
+ }
PrepareImageView(pair->second, false, false);
channel_state->image_view_ids.insert_or_assign(map_index, pair->second);
return pair->second;
@@ -1444,6 +1569,21 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image));
UploadImageContents(image, staging);
runtime.InsertUploadMemoryBarrier();
+ // The host GPU has its own copy now and the guest rarely reads this memory again: TickFrame
+ // marks it cold, so the OS pages it out before anything else when it needs RAM (title 1
+ // streams several GB of textures through it).
+ constexpr u64 ColdHintMinBytes = 64_KiB;
+ if (image.guest_size_bytes >= ColdHintMinBytes &&
+ False(image.flags & ImageFlagBits::GpuModified)) {
+ if (True(image.flags & ImageFlagBits::Sparse)) {
+ // Non-contiguous: each mapped piece (title 1 maps its own memory into these).
+ ForEachSparseSegment(image, [&](GPUVAddr, DAddr address, size_t size) {
+ uploaded_ranges.emplace_back(address, size);
+ });
+ } else {
+ uploaded_ranges.emplace_back(image.cpu_addr, image.guest_size_bytes);
+ }
+ }
}
template <class P>
@@ -1870,9 +2010,14 @@ void TextureCache<P>::InvalidateScale(Image& image) {
for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c];
+#ifndef NDEBUG
+ // A walk over every entry (title 1: ~5,400 in 5 channels) for each deleted image: debug
+ // builds only. The tables are invalidated below, so each entry is written again before
+ // it is read.
if constexpr (ENABLE_VALIDATION)
for (auto& e : channel_info.image_view_ids)
e.second = CORRUPT_ID;
+#endif
channel_info.graphics_image_table.Invalidate();
channel_info.compute_image_table.Invalidate();
}
@@ -2990,6 +3135,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
}
// TickFrame sets the total from the driver, which can be under the sum of these estimates.
total_used_memory -= (std::min)(total_used_memory, ImageMemoryBytes(slot_images[image_id]));
+ const u64 freed_bytes = (image.HasScaled() ? GetScaledImageSizeBytes(image) : 0) +
+ ImageMemoryBytes(slot_images[image_id]);
const GPUVAddr gpu_addr = image.gpu_addr;
const auto alloc_it = image_allocs_table.find(gpu_addr);
if (alloc_it == image_allocs_table.end()) {
@@ -3054,19 +3201,32 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
if (retained) {
LOG_INFO(Render_Vulkan, "Retained image for queued download: id={}", image_id.Value());
} else if (!immediate_delete) {
- sentenced_images.Push(std::move(slot_images[image_id]));
+ if constexpr (requires { runtime.CurrentTick(); }) {
+ gpu_sentenced_images.push_back(
+ {runtime.CurrentTick(), freed_bytes, std::move(slot_images[image_id])});
+ gpu_sentenced_bytes += freed_bytes;
+ } else {
+ sentenced_images.Push(std::move(slot_images[image_id]));
+ sentenced_image_bytes[sentenced_image_index] += freed_bytes;
+ }
}
slot_images.erase(image_id);
alloc_images.erase(alloc_image_it);
if (alloc_images.empty()) {
image_allocs_table.erase(alloc_it);
+ slot_image_allocs.erase(alloc_id);
}
for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c];
+#ifndef NDEBUG
+ // A walk over every entry (title 1: ~5,400 in 5 channels) for each deleted image: debug
+ // builds only. The tables are invalidated below, so each entry is written again before
+ // it is read.
if constexpr (ENABLE_VALIDATION)
for (auto& e : channel_info.image_view_ids)
e.second = CORRUPT_ID;
+#endif
channel_info.graphics_image_table.Invalidate();
channel_info.compute_image_table.Invalidate();
}
@@ -3075,17 +3235,23 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
template <class P>
void TextureCache<P>::RemoveImageViewReferences(std::span<const ImageViewId> removed_views) {
- for (size_t c : active_channel_ids) {
- auto& channel_info = channel_storage[c];
- auto it = channel_info.image_views.begin();
- while (it != channel_info.image_views.end()) {
- const auto found = std::ranges::find(removed_views, it->second);
- if (found != removed_views.end()) {
- it = channel_info.image_views.erase(it);
- } else {
- ++it;
+ for (const ImageViewId removed : removed_views) {
+ const auto refs = image_view_descriptors.find(removed);
+ if (refs == image_view_descriptors.end()) {
+ continue;
+ }
+ for (const auto& [channel, descriptor] : refs->second) {
+ // The channel may be gone, or its entry point to another view since.
+ if (std::ranges::find(active_channel_ids, channel) == active_channel_ids.end()) {
+ continue;
+ }
+ auto& image_views = channel_storage[channel].image_views;
+ if (const auto it = image_views.find(descriptor);
+ it != image_views.end() && it->second == removed) {
+ image_views.erase(it);
}
}
+ image_view_descriptors.erase(refs);
}
}
@@ -3102,6 +3268,7 @@ void TextureCache<P>::RemoveFramebuffers(std::span<const ImageViewId> removed_vi
last_framebuffer_serial = 0;
}
sentenced_framebuffers.Push(std::move(slot_framebuffers[framebuffer_id]));
+ slot_framebuffers.erase(framebuffer_id);
it = framebuffers.erase(it);
} else {
++it;
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index d4b0f597ae..09deb8da02 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -136,6 +136,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
u64 native_critical_memory{};
u64 native_expected_memory{};
u64 budget_check_frames{};
+ u32 seen_pressure_events{}; ///< Allocator pressure events already answered
+ u64 pressure_margin{}; ///< Extra room under the budget after images did not fit
+ u64 pressure_quiet_frames{}; ///< Frames since the last such image
+ u64 slack_margin{}; ///< Extra room for allocator block space the driver counts as used
u64 astc_opaque_images{};
u64 astc_alpha_images{};
std::vector<u8> astc_scan_buffer;
@@ -144,6 +148,9 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
+ /// The most device memory the collector lets the cache fill before it collects in earnest,
+ /// on every GPU (ApplyThresholdOverrides).
+ static constexpr u64 MaxCriticalMemory = 5_GiB;
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
using Runtime = typename P::Runtime;
@@ -452,6 +459,12 @@ private:
/// Remove image views references from the cache
void RemoveImageViewReferences(std::span<const ImageViewId> removed_views);
+ /// Per image view, the (channel, descriptor) entries of the channels' image_views maps that
+ /// point to it. Deleting an image erased them by scanning every map (title 1: ~5,000 entries,
+ /// 50-130 us for each deleted image, dozens a frame while the collector runs).
+ std::unordered_map<ImageViewId, boost::container::small_vector<std::pair<size_t, TICEntry>, 2>>
+ image_view_descriptors;
+
/// Remove framebuffers using the given image views from the cache
void RemoveFramebuffers(std::span<const ImageViewId> removed_views);
@@ -571,6 +584,25 @@ private:
static constexpr size_t TICKS_TO_DESTROY = 8;
#endif
DelayedDestructionRing<Image, TICKS_TO_DESTROY> sentenced_images;
+ /// Device memory of the images in each slot of sentenced_images (same index): deleted, but
+ /// counted in the driver's usage until the ring destroys them.
+ std::array<u64, TICKS_TO_DESTROY> sentenced_image_bytes{};
+ size_t sentenced_image_index = 0;
+ /// Deleted images on runtimes that report GPU progress (CurrentTick, IsFree): destroyed, with
+ /// their device memory, as soon as the GPU is past them rather than TICKS_TO_DESTROY frames
+ /// later. While the collector deletes dozens a frame, eight frames of them held 1-1.5 GB of
+ /// VRAM above its threshold (title 1 on an 8 GB card: 6.6-7.0 GB in use against 5 GiB).
+ struct GpuSentencedImage {
+ u64 tick;
+ u64 bytes;
+ Image image;
+ };
+ std::deque<GpuSentencedImage> gpu_sentenced_images;
+ u64 gpu_sentenced_bytes{};
+
+ /// Bytes of images per device memory block, for the collector (refreshed every 60 frames).
+ std::unordered_map<u64, u64> block_image_bytes;
+ u64 block_image_bytes_frame{~0ULL};
DelayedDestructionRing<ImageView, TICKS_TO_DESTROY> sentenced_image_view;
DelayedDestructionRing<Framebuffer, TICKS_TO_DESTROY> sentenced_framebuffers;
@@ -585,6 +617,8 @@ private:
Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {},
Common::ThreadPlacement::Efficiency};
std::vector<std::unique_ptr<AsyncDecodeContext>> async_decodes;
+ /// Guest memory images were uploaded from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_ranges;
std::deque<PendingUnswizzle> unswizzle_queue;
u8 current_unswizzle_frame;