Texture cache: stop the churn, the per-frame image leak and garbage textures
- Unmapped GPU addresses get a stand-in CPU address fixed by the GPU address. A fresh one per lookup meant FindImage never found an image there again: title 1's bloom chain at 0xf0000-0x15b600 was re-created on every use (~5,400 images per 600 frames), filling VRAM. - ASTC whose blocks carry no alpha (exact endpoint-mode scan reusing the decoder's block parsing) is re-compressed to BC1 instead of BC3, half the size; about 70% of title 1's ASTC qualifies. Copies between BC1 and BC3 hosts are skipped (their guest data is uploaded instead). EDEN_NX2_ASTC_BC1=0 turns it off. - Deleting an image cancels every decode still queued for it. An image re-decoded mid-decode has two; the first to finish cleared IsDecoding, the second was not cancelled and was uploaded into the next image to take the slot: garbage textures (the title logo, road patches) and a staging overrun when that image was smaller. - Diagnostics: images created and deleted per 600 frames by shape, collector deletions, async decodes, BC1/BC3 split; collector thresholds overridable (EDEN_NX2_TC_EXPECTED_MIB/CRITICAL_MIB); GPU ASTC decode wait timing (EDEN_NX2_ASTC_FLUSH=1 submits without it). Title 1 attract loop: device memory 4.8-5.1 GB instead of 5.9-6.1 GB, no decoded textures evicted, ~17 -> 22-23 fps.
This commit is contained in:
parent
eb9f32a073
commit
69e7033d82
11 changed files with 276 additions and 34 deletions
|
|
@ -151,16 +151,21 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in
|
|||
| 93 | title 1 colour cast (magenta/cyan, red/green, changing per course) | the sky/atmosphere 32^3 volume shader starts with an unguarded R2UR; R2UR/MATCH/HFMA2.RELU saved P6 with the uniform-guard mask `1 << (6 - 7)` (undefined), which shifted into the opcode bits | always save/restore P6 (2ed0c48). Found with `EDEN_NX2_DUMP` (flush GPU ranges at a present to files) and `nx2work/vol3d.py` / `volslices.py` (untile Switch 2 3D textures) |
|
||||
| 94 | near terrain black | 8192x8192x68 ASTC 8x8 virtual textures: 1.5 GB in guest memory, 23 GB each as Eden's RGBA8 decode | huge GPU-decoded ASTC arrays keep levels from the first that fits 512 MiB (here 3) on the host (60c4791) |
|
||||
| 95 | wind/foliage shards | SM70+ MUFU.SIN/COS take turns (sin(2pi x)); Eden's Maxwell MUFU takes radians | scale by 2pi (245e6ad); jungle canopy not re-checked yet |
|
||||
| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player reads "video-frame-count" (MediaProperty, main+0x19e5860) and requires < 9; the game itself sets it from a count of registered entries (main+0x7107c0 adds, main+0x70f620 removes) that only grows: in-game movies never finish (no NVDEC/H.264 decode activity in the log) | open: Switch 2 movie/H.264 decode path. `nx2work/nso_plt.py` names PLT imports |
|
||||
| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player (main+0x19e5860) lays out its decoder frames with nn::codec::CalculateDecoderImagePoolBuffer, which sends nx2:gsv 991; auto-stubbed, the reply kept the request words, so the frame size read as 0x167027f'00000001 and the budget check at main+0x19e5b58 failed. (Not the "video-frame-count" < 9 check: the game sets that from a two-slot registry, at most 2.) | gsv 991 answers a block-linear NV12 size (640x360 -> 0x64000) |
|
||||
| 97 | pop-in, objects flicker in and out, low-quality meshes | high-detail LODs drawn through task/mesh programs (bound in stage slot 1, header type VS, ISBERD/ISBEWR), which are rejected and skipped | open: task/mesh support |
|
||||
| 98 | ~12 fps at the title | host GPU ~20% busy, emulated cores ~0.9 host cores, GPU thread 45-70% busy, much of it waiting on the host GPU: semaphore acquires wait for host fences under GPU accuracy High; ASTC uploads end in Finish() (a Flush() there crashed at boot); a 1 ms poll sleep while channels are suspended | gpu_accuracy=Low: ~16 fps; SDL main loop capped at 240 Hz (88f04ad). Open: ASTC upload without a full stall (and per-layer re-upload of the trimmed virtual textures), native MME interpreter cost, draw path |
|
||||
| 99 | VRAM over the card (8.4-9.8 GB used on an 8 GB RTX 4070 Laptop), texture cache always collecting | ~700 ASTC 8x8 textures decoded to RGBA8 (16x their size, ~4.5 GB) | astc_recompression=Bc3 + accelerate_astc=CpuAsynchronous in the config; huge ASTC arrays stay on the GPU decoder as trimmed RGBA8 (ed0a3c9): 4.6-5.6 GB, ~10 -> 16 fps |
|
||||
| 100 | 8 ImageTranscode threads at ~44% each (3.5 cores) | Eden's default pool is half the hardware threads | an eighth, at least 2 (`EDEN_NX2_TRANSCODE_THREADS` overrides): ~19 fps on the attract loop |
|
||||
| 101 | Medal's capture hook inside Eden (`medal_hook64!OBS_Negotiate` in GPU-thread stacks) | Medal registers a per-user Vulkan implicit layer that loads into every Vulkan app | launches set `DISABLE_VULKAN_MEDAL_OBS_CAPTURE=1` (the layer's own off switch) |
|
||||
| 102 | host crash (log stops, segfault) at 113 s in one run; under cdb "Free Heap block modified after it was freed" at 243 s, twice | Eden's TickAsyncDecode erased a finished decode while still holding its mutex (unlocked freed memory); only reached with accelerate_astc=CpuAsynchronous (the BC3 setup) | release the lock first, cancel decodes of deleted images (8766a72); clean past 330 s. Also made nx2 service callbacks (vsync timer, syncpt actions, capture/dump) independent of their sessions' lifetime (190747a) |
|
||||
| 103 | (title 1) texture churn: the collector evicted ~1,250 decoded ASTC textures per 600 frames and they were decoded again (lag and pop-in turning the camera) | device memory sat above the collector's critical line (5.4 GB of a 7.0 GB budget): ASTC 8x8 is 4x larger as BC3, and a bloom chain at unmapped GPU 0xf0000-0x15b600 was re-created on every use (each lookup got a fresh stand-in CPU address), ~5,400 images per 600 frames on the map | unmapped GPU addresses get a stand-in address fixed by the GPU address, so images are found again; ASTC whose blocks carry no alpha (exact endpoint-mode scan, ~70% of them) goes to BC1 (`EDEN_NX2_ASTC_BC1=0` turns it off). Attract loop: 4.8-5.1 GB, no costly evictions, ~17 -> 22-23 fps |
|
||||
| 104 | garbage textures (title logo, road patches) and a host crash 90-120 s in that skipped the crash handler (fail-fast) | an image re-decoded while a decode was in flight has two; the first to finish cleared IsDecoding, so deleting the image did not cancel the second, which was then uploaded into the next image to take the slot (another texture's data; a staging overrun when that image was smaller) | every pending decode of a deleted image is cancelled; a decode larger than its image is dropped and logged. Frames clean; crash: verifying with long runs |
|
||||
| 105 | (title 1) movie abort moved on: `nn::codec::InitializeVp9Decoder` fails with 2001-0114 (InvalidHandle) | gsv 901/902/1001/1002 are auto-stubbed and hand back handle 0 | open: the Switch 2 video decoder service (VP9) |
|
||||
|
||||
Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4`
|
||||
(12 GB). Debug knobs: `EDEN_NX2_AUTO_STUB`, `EDEN_NX2_IPC_TRACE`, `EDEN_NX2_SVC_TRACE`,
|
||||
(12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds),
|
||||
`EDEN_NX2_ASTC_FLUSH=1` (GPU ASTC decode without the full wait), `EDEN_NX2_CRASH_DUMP=<dir>` (prints the
|
||||
faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_NX2_IPC_TRACE`, `EDEN_NX2_SVC_TRACE`,
|
||||
`EDEN_NX2_PULLER_TRACE`, `EDEN_NX2_METHOD_TRACE`, `EDEN_NX2_HIDE_WINDOW`,
|
||||
`EDEN_NX2_FRAME_CAPTURE=<dir>` (every 120th presented frame as TGA), `EDEN_NX2_PB_CHECK`
|
||||
(pushbuffer entries changed between submit and execution), `EDEN_NX2_QUERY_TRACE` (every GPU
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
|
|
@ -14,6 +16,7 @@
|
|||
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/div_ceil.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "video_core/host_shaders/astc_decoder_comp_spv.h"
|
||||
|
|
@ -627,7 +630,33 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
|
|||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, 0, image_barrier);
|
||||
});
|
||||
scheduler.Finish();
|
||||
// Each decode ends with the GPU thread waiting for the host GPU to go idle. Measured here
|
||||
// (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=1 submits without waiting instead.
|
||||
static const bool flush_only = [] {
|
||||
const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH");
|
||||
return value != nullptr && value[0] == '1';
|
||||
}();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
if (flush_only) {
|
||||
scheduler.Flush();
|
||||
} else {
|
||||
scheduler.Finish();
|
||||
}
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
static u32 decodes{};
|
||||
static std::chrono::nanoseconds waited{};
|
||||
static auto window_start = end;
|
||||
++decodes;
|
||||
waited += end - start;
|
||||
if (end - window_start >= std::chrono::seconds{5}) {
|
||||
LOG_INFO(Render_Vulkan, "ASTC GPU decodes: {} in {} ms, {} ms waiting ({})", decodes,
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(end - window_start).count(),
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(waited).count(),
|
||||
flush_only ? "flush" : "finish");
|
||||
decodes = 0;
|
||||
waited = {};
|
||||
window_start = end;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr u32 BL3D_BINDING_INPUT_BUFFER = 0;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <memory>
|
||||
|
|
@ -176,6 +177,22 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
|
|||
constexpr std::array<VkFormat, 2> HugeAstcViewFormats{VK_FORMAT_A8B8G8R8_UNORM_PACK32,
|
||||
VK_FORMAT_A8B8G8R8_SRGB_PACK32};
|
||||
|
||||
// Opaque ASTC (ImageInfo::astc_opaque) re-compressed to BC1 instead of BC3.
|
||||
[[nodiscard]] bool IsOpaqueBc1Astc(const Device& device, const ImageInfo& info) {
|
||||
return info.astc_opaque && IsPixelFormatASTC(info.format) && !IsHugeAstcArray(device, info);
|
||||
}
|
||||
|
||||
constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM_BLOCK,
|
||||
VK_FORMAT_BC1_RGBA_SRGB_BLOCK};
|
||||
|
||||
[[nodiscard]] std::optional<VkFormat> AstcHostFormat(const Device& device, const ImageInfo& info) {
|
||||
if (IsOpaqueBc1Astc(device, info)) {
|
||||
return VideoCore::Surface::IsPixelFormatSRGB(info.format) ? VK_FORMAT_BC1_RGBA_SRGB_BLOCK
|
||||
: VK_FORMAT_BC1_RGBA_UNORM_BLOCK;
|
||||
}
|
||||
return HugeAstcHostFormat(device, info);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) {
|
||||
if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) {
|
||||
return false;
|
||||
|
|
@ -1899,6 +1916,16 @@ void TextureCacheRuntime::LogMemoryStatistics() const {
|
|||
Vulkan::LogMemoryStatistics(device);
|
||||
}
|
||||
|
||||
bool TextureCacheRuntime::UseOpaqueAstcFormat() const {
|
||||
// EDEN_NX2_ASTC_BC1=0 keeps every re-compressed ASTC image in BC3.
|
||||
static const bool enabled = [] {
|
||||
const char* value = std::getenv("EDEN_NX2_ASTC_BC1");
|
||||
return value == nullptr || value[0] != '0';
|
||||
}();
|
||||
return enabled && !device.IsOptimalAstcSupported() &&
|
||||
Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3;
|
||||
}
|
||||
|
||||
void TextureCacheRuntime::FlushDeferredClear() {
|
||||
scheduler.FlushDeferredClear();
|
||||
}
|
||||
|
|
@ -1965,8 +1992,10 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
|
|||
TrimmedHostInfo(runtime_.device, info, host_level_offset),
|
||||
IsHugeAstcArray(runtime_.device, info)
|
||||
? std::span<const VkFormat>{HugeAstcViewFormats}
|
||||
: IsOpaqueBc1Astc(runtime_.device, info)
|
||||
? std::span<const VkFormat>{OpaqueAstcViewFormats}
|
||||
: runtime->ViewFormats(info.format),
|
||||
HugeAstcHostFormat(runtime_.device, info))),
|
||||
AstcHostFormat(runtime_.device, info))),
|
||||
aspect_mask(ImageAspectMask(info.format)) {
|
||||
if (original_image.IsUnbacked()) {
|
||||
flags |= VideoCommon::ImageFlagBits::Unbacked;
|
||||
|
|
@ -2628,6 +2657,10 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
|
|||
format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format)
|
||||
? VK_FORMAT_A8B8G8R8_SRGB_PACK32
|
||||
: VK_FORMAT_A8B8G8R8_UNORM_PACK32;
|
||||
} else if (IsOpaqueBc1Astc(*device, image.info) && IsPixelFormatASTC(format)) {
|
||||
format_info.format = VideoCore::Surface::IsPixelFormatSRGB(format)
|
||||
? VK_FORMAT_BC1_RGBA_SRGB_BLOCK
|
||||
: VK_FORMAT_BC1_RGBA_UNORM_BLOCK;
|
||||
}
|
||||
if (device->ApiVersion() >= VK_API_VERSION_1_3) {
|
||||
const VkFormatProperties3 properties3 =
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public:
|
|||
|
||||
void LogMemoryStatistics() const;
|
||||
|
||||
/// True when ASTC is re-compressed to BC3 and opaque images may use BC1 instead.
|
||||
bool UseOpaqueAstcFormat() const;
|
||||
|
||||
bool CanDownloadMsaa(const VideoCommon::ImageInfo& info) const;
|
||||
|
||||
[[nodiscard]] VkImage AcquireMsaaScratchImage(const VkImageCreateInfo& image_ci);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ struct ImageInfo {
|
|||
bool forced_flushed = false;
|
||||
bool dma_downloaded = false;
|
||||
bool is_sparse = false;
|
||||
// ASTC data whose blocks carry no alpha: re-compressed to BC1 instead of BC3 (half the size).
|
||||
bool astc_opaque = false;
|
||||
};
|
||||
|
||||
} // namespace VideoCommon
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
|
@ -79,6 +80,15 @@ TextureCache<P>::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag
|
|||
critical_memory = DEFAULT_CRITICAL_MEMORY + 1_GiB;
|
||||
minimum_memory = 0;
|
||||
}
|
||||
// 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}}) {
|
||||
if (const char* value = std::getenv(name)) {
|
||||
*target = std::strtoull(value, nullptr, 10) << 20;
|
||||
}
|
||||
}
|
||||
LOG_INFO(HW_GPU, "Texture cache thresholds: expected {} MiB, critical {} MiB, minimum {} MiB",
|
||||
expected_memory >> 20, critical_memory >> 20, minimum_memory >> 20);
|
||||
|
||||
const bool gpu_unswizzle_enabled = Settings::values.gpu_unswizzle_enabled.GetValue();
|
||||
|
||||
|
|
@ -151,6 +161,7 @@ void TextureCache<P>::RunGarbageCollector() {
|
|||
if (True(image.flags & ImageFlagBits::Tracked)) {
|
||||
UntrackImage(image, image_id);
|
||||
}
|
||||
++(True(image.flags & ImageFlagBits::CostlyLoad) ? gc_costly_deletes : gc_deletes);
|
||||
UnregisterImage(image_id);
|
||||
DeleteImage(image_id, image.scale_tick > frame_tick + 5);
|
||||
if (aggressive_mode && total_used_memory < critical_memory) {
|
||||
|
|
@ -215,6 +226,23 @@ void TextureCache<P>::TickFrame() {
|
|||
if constexpr (requires { runtime.LogMemoryStatistics(); }) {
|
||||
runtime.LogMemoryStatistics();
|
||||
}
|
||||
LOG_INFO(HW_GPU, "Texture cache last 600 frames: created {} images, deleted {} (collector {}, {} costly), {} async decodes, ASTC {} opaque / {} alpha",
|
||||
images_created, images_deleted, gc_deletes + gc_costly_deletes, gc_costly_deletes,
|
||||
async_decodes_queued, astc_opaque_images, astc_alpha_images);
|
||||
astc_opaque_images = astc_alpha_images = 0;
|
||||
std::vector<std::pair<u32, u64>> shapes;
|
||||
for (const auto& [key, value] : created_shapes) shapes.emplace_back(value.first, key);
|
||||
std::sort(shapes.rbegin(), shapes.rend());
|
||||
for (size_t i = 0; i < std::min<size_t>(10, shapes.size()); ++i) {
|
||||
const u64 key = shapes[i].second;
|
||||
LOG_INFO(HW_GPU, " created {}x: format {} type {} levels {} layers {} {}x{} (last at {:#x})",
|
||||
shapes[i].first, key >> 56, (key >> 52) & 0xF, (key >> 48) & 0xF,
|
||||
(key >> 36) & 0xFFF, (key >> 18) & 0x3FFFF, key & 0x3FFFF,
|
||||
created_shapes[key].second);
|
||||
}
|
||||
created_shapes.clear();
|
||||
gc_deletes = gc_costly_deletes = async_decodes_queued = 0;
|
||||
images_created = images_deleted = 0;
|
||||
}
|
||||
if (total_used_memory > minimum_memory) {
|
||||
RunGarbageCollector();
|
||||
|
|
@ -1323,12 +1351,9 @@ ImageId TextureCache<P>::FindOrInsertImage(const ImageInfo& info, GPUVAddr gpu_a
|
|||
template <class P>
|
||||
ImageId TextureCache<P>::FindImage(const ImageInfo& info, GPUVAddr gpu_addr,
|
||||
RelaxedOptions options) {
|
||||
std::optional<DAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
|
||||
const std::optional<DAddr> cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info));
|
||||
if (!cpu_addr) {
|
||||
cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info));
|
||||
if (!cpu_addr) {
|
||||
return ImageId{};
|
||||
}
|
||||
return ImageId{};
|
||||
}
|
||||
const bool broken_views =
|
||||
runtime.HasBrokenTextureViewFormats() || True(options & RelaxedOptions::ForceBrokenViews);
|
||||
|
|
@ -1457,6 +1482,7 @@ void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) {
|
|||
LOG_DEBUG(HW_GPU, "Queuing async texture decode {:#x}", image.gpu_addr);
|
||||
|
||||
image.flags |= ImageFlagBits::IsDecoding;
|
||||
++async_decodes_queued;
|
||||
auto decode = std::make_unique<AsyncDecodeContext>();
|
||||
auto* decode_ptr = decode.get();
|
||||
decode->image_id = image_id;
|
||||
|
|
@ -1516,6 +1542,12 @@ void TextureCache<P>::TickAsyncDecode() {
|
|||
continue;
|
||||
}
|
||||
Image& image = slot_images[async_decode->image_id];
|
||||
if (async_decode->decoded_data.size() > MapSizeBytes(image)) {
|
||||
LOG_ERROR(HW_GPU, "Async decode of {} bytes for a {}-byte image at {:#x}, dropped",
|
||||
async_decode->decoded_data.size(), MapSizeBytes(image), image.gpu_addr);
|
||||
i = async_decodes.erase(i);
|
||||
continue;
|
||||
}
|
||||
auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image));
|
||||
std::memcpy(staging.mapped_span.data(), async_decode->decoded_data.data(),
|
||||
async_decode->decoded_data.size());
|
||||
|
|
@ -1630,18 +1662,32 @@ bool TextureCache<P>::ScaleDown(Image& image) {
|
|||
return true;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
std::optional<DAddr> TextureCache<P>::ImageCpuAddress(GPUVAddr gpu_addr, size_t size) const {
|
||||
if (const std::optional<DAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr)) {
|
||||
return cpu_addr;
|
||||
}
|
||||
if (const std::optional<DAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, size)) {
|
||||
return cpu_addr;
|
||||
}
|
||||
// No CPU backing (title 1 renders a bloom chain at GPU 0xf0000-0x15b600): a stand-in address
|
||||
// fixed by the GPU address, at or above ~(1 << 40) so TrackImage skips it. A fresh address per
|
||||
// lookup made every use create another image (thousands a minute, never found again).
|
||||
constexpr GPUVAddr StandInRange = 1ULL << 40;
|
||||
if (gpu_addr >= StandInRange || size > StandInRange - gpu_addr - 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return ~(1ULL << 40ULL) + gpu_addr;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
ImageId TextureCache<P>::InsertImage(const ImageInfo& info, GPUVAddr gpu_addr,
|
||||
RelaxedOptions options) {
|
||||
std::optional<DAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
|
||||
std::optional<DAddr> cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info));
|
||||
if (!cpu_addr) {
|
||||
const auto size = CalculateGuestSizeInBytes(info);
|
||||
cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, size);
|
||||
if (!cpu_addr) {
|
||||
const DAddr fake_addr = ~(1ULL << 40ULL) + virtual_invalid_space;
|
||||
virtual_invalid_space += Common::AlignUp(size, 32);
|
||||
cpu_addr = std::optional<DAddr>(fake_addr);
|
||||
}
|
||||
const DAddr fake_addr = ~(1ULL << 40ULL) + virtual_invalid_space;
|
||||
virtual_invalid_space += Common::AlignUp(CalculateGuestSizeInBytes(info), 32);
|
||||
cpu_addr = std::optional<DAddr>(fake_addr);
|
||||
}
|
||||
ASSERT_MSG(cpu_addr, "Tried to insert an image to an invalid gpu_addr={:#x}", gpu_addr);
|
||||
ImageInfo storage_info = info;
|
||||
|
|
@ -1760,10 +1806,36 @@ ImageId TextureCache<P>::JoinImages(const ImageInfo& info, GPUVAddr gpu_addr, DA
|
|||
}
|
||||
}
|
||||
|
||||
if constexpr (requires { runtime.UseOpaqueAstcFormat(); }) {
|
||||
// Opaque ASTC goes to BC1 rather than BC3 when it is re-compressed (the Vulkan runtime
|
||||
// decides). The guest data is already there when an image is first used.
|
||||
constexpr size_t MaxScanBytes = 64_MiB;
|
||||
if (IsPixelFormatASTC(new_info.format) && runtime.UseOpaqueAstcFormat()) {
|
||||
const size_t scan_bytes = CalculateGuestSizeInBytes(new_info);
|
||||
if (scan_bytes <= MaxScanBytes) {
|
||||
astc_scan_buffer.resize(scan_bytes);
|
||||
gpu_memory->ReadBlockUnsafe(gpu_addr, astc_scan_buffer.data(), scan_bytes);
|
||||
new_info.astc_opaque = AstcDataLooksOpaque(astc_scan_buffer);
|
||||
++(new_info.astc_opaque ? astc_opaque_images : astc_alpha_images);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ImageId new_image_id = slot_images.insert(runtime, new_info, gpu_addr, cpu_addr);
|
||||
Image& new_image = slot_images[new_image_id];
|
||||
|
||||
new_image.allocation_tick = frame_tick;
|
||||
{
|
||||
// Which shapes are being created (periodic log): format, type, levels, layers, size.
|
||||
const u64 key = (u64{static_cast<u32>(new_info.format)} << 56) |
|
||||
(u64{static_cast<u32>(new_info.type)} << 52) |
|
||||
(u64{static_cast<u32>(new_info.resources.levels) & 0xF} << 48) |
|
||||
(u64{static_cast<u32>(new_info.resources.layers) & 0xFFF} << 36) |
|
||||
(u64{new_info.size.width & 0x3FFFF} << 18) | (new_info.size.height & 0x3FFFF);
|
||||
auto& shape = created_shapes[key];
|
||||
++shape.first;
|
||||
shape.second = gpu_addr;
|
||||
++images_created;
|
||||
}
|
||||
|
||||
if (!gpu_memory->IsContinuousRange(new_image.gpu_addr, new_image.guest_size_bytes) &&
|
||||
new_info.is_sparse) {
|
||||
|
|
@ -1959,12 +2031,9 @@ std::optional<typename TextureCache<P>::BlitImages> TextureCache<P>::GetBlitImag
|
|||
|
||||
template <class P>
|
||||
ImageId TextureCache<P>::FindDMAImage(const ImageInfo& info, GPUVAddr gpu_addr) {
|
||||
std::optional<DAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
|
||||
const std::optional<DAddr> cpu_addr = ImageCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info));
|
||||
if (!cpu_addr) {
|
||||
cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr, CalculateGuestSizeInBytes(info));
|
||||
if (!cpu_addr) {
|
||||
return ImageId{};
|
||||
}
|
||||
return ImageId{};
|
||||
}
|
||||
ImageId image_id{};
|
||||
boost::container::small_vector<ImageId, 8> image_ids;
|
||||
|
|
@ -2463,12 +2532,14 @@ void TextureCache<P>::UntrackImage(ImageBase& image, ImageId image_id) {
|
|||
template <class P>
|
||||
void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
|
||||
ImageBase& image = slot_images[image_id];
|
||||
if (True(image.flags & ImageFlagBits::IsDecoding)) {
|
||||
// A worker is still decoding it: keep its context alive but drop the result.
|
||||
for (auto& async_decode : async_decodes) {
|
||||
if (async_decode->image_id == image_id) {
|
||||
async_decode->cancelled = true;
|
||||
}
|
||||
++images_deleted;
|
||||
// Drop every decode still queued for it, whatever IsDecoding says: an image re-decoded while
|
||||
// a decode was in flight has two, and the first to finish clears the flag. A decode left
|
||||
// behind was uploaded into the next image to take this slot (another texture's data: garbage
|
||||
// textures, and a staging overrun when that image was smaller).
|
||||
for (auto& async_decode : async_decodes) {
|
||||
if (async_decode->image_id == image_id) {
|
||||
async_decode->cancelled = true;
|
||||
}
|
||||
}
|
||||
if (image.HasScaled()) {
|
||||
|
|
@ -2696,6 +2767,11 @@ template <class P>
|
|||
void TextureCache<P>::CopyImage(ImageId dst_id, ImageId src_id, std::vector<ImageCopy> copies) {
|
||||
Image& dst = slot_images[dst_id];
|
||||
Image& src = slot_images[src_id];
|
||||
if (dst.info.astc_opaque != src.info.astc_opaque) {
|
||||
// BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the
|
||||
// same guest data, which is uploaded from guest memory instead.
|
||||
return;
|
||||
}
|
||||
const bool is_rescaled = True(src.flags & ImageFlagBits::Rescaled);
|
||||
if (is_rescaled) {
|
||||
ASSERT(True(dst.flags & ImageFlagBits::Rescaled));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
|
|
@ -120,6 +121,17 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
|
|||
static constexpr s64 TARGET_THRESHOLD = 4_GiB;
|
||||
#endif
|
||||
|
||||
// Periodic statistics (TickFrame).
|
||||
u64 gc_deletes{};
|
||||
u64 gc_costly_deletes{};
|
||||
u64 async_decodes_queued{};
|
||||
u64 images_created{};
|
||||
u64 astc_opaque_images{};
|
||||
u64 astc_alpha_images{};
|
||||
std::vector<u8> astc_scan_buffer;
|
||||
u64 images_deleted{};
|
||||
std::map<u64, std::pair<u32, GPUVAddr>> created_shapes;
|
||||
|
||||
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
|
||||
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
|
||||
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
|
||||
|
|
@ -450,6 +462,9 @@ private:
|
|||
|
||||
DAddr virtual_invalid_space{};
|
||||
|
||||
/// The CPU address an image at gpu_addr is keyed by: its backing, or a fixed stand-in.
|
||||
[[nodiscard]] std::optional<DAddr> ImageCpuAddress(GPUVAddr gpu_addr, size_t size) const;
|
||||
|
||||
bool has_deleted_images = false;
|
||||
bool is_rescaling = false;
|
||||
u64 total_used_memory = 0;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
|
@ -603,6 +604,10 @@ u32 CalculateUnswizzledSizeBytes(const ImageInfo& info) noexcept {
|
|||
return NumBlocksPerLayer(info, tile_size) * info.resources.layers * BytesPerBlock(info.format);
|
||||
}
|
||||
|
||||
bool AstcDataLooksOpaque(std::span<const u8> data) noexcept {
|
||||
return Tegra::Texture::ASTC::IsOpaque(data);
|
||||
}
|
||||
|
||||
u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept {
|
||||
if (info.type == ImageType::Buffer) {
|
||||
return info.size.width * BytesPerBlock(info.format);
|
||||
|
|
@ -611,8 +616,10 @@ u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept {
|
|||
if (IsPixelFormatASTC(info.format) && Settings::values.astc_recompression.GetValue() !=
|
||||
Settings::AstcRecompression::Uncompressed) {
|
||||
const u32 bpp_div =
|
||||
Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 ? 2
|
||||
: 1;
|
||||
Settings::values.astc_recompression.GetValue() == Settings::AstcRecompression::Bc1 ||
|
||||
info.astc_opaque
|
||||
? 2
|
||||
: 1;
|
||||
// NumBlocksPerLayer doesn't account for this correctly, so we have to do it manually.
|
||||
u32 output_size = 0;
|
||||
for (s32 i = 0; i < info.resources.levels; i++) {
|
||||
|
|
@ -955,10 +962,11 @@ void ConvertImage(std::span<const u8> input, const ImageInfo& info, std::span<u8
|
|||
} else if (astc) {
|
||||
// BC1 uses 0.5 bytes per texel
|
||||
// BC3 uses 1 byte per texel
|
||||
const auto compress = recompression_setting == Settings::AstcRecompression::Bc1
|
||||
? Tegra::Texture::BCN::CompressBC1
|
||||
: Tegra::Texture::BCN::CompressBC3;
|
||||
const auto bpp_div = recompression_setting == Settings::AstcRecompression::Bc1 ? 2 : 1;
|
||||
const bool bc1 =
|
||||
recompression_setting == Settings::AstcRecompression::Bc1 || info.astc_opaque;
|
||||
const auto compress =
|
||||
bc1 ? Tegra::Texture::BCN::CompressBC1 : Tegra::Texture::BCN::CompressBC3;
|
||||
const auto bpp_div = bc1 ? 2 : 1;
|
||||
|
||||
const u32 plane_dim = copy.image_extent.width * copy.image_extent.height;
|
||||
const u32 level_size = plane_dim * copy.image_extent.depth *
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ struct OverlapResult {
|
|||
|
||||
[[nodiscard]] u32 CalculateConvertedSizeBytes(const ImageInfo& info) noexcept;
|
||||
|
||||
/// True if no 16-byte ASTC block in `data` decodes to alpha below 1.
|
||||
[[nodiscard]] bool AstcDataLooksOpaque(std::span<const u8> data) noexcept;
|
||||
|
||||
[[nodiscard]] u32 CalculateLayerStride(const ImageInfo& info) noexcept;
|
||||
|
||||
[[nodiscard]] u32 CalculateLayerSize(const ImageInfo& info) noexcept;
|
||||
|
|
|
|||
|
|
@ -2038,4 +2038,69 @@ void Decompress(std::span<const uint8_t> data, uint32_t width, uint32_t height,
|
|||
}
|
||||
}
|
||||
|
||||
bool IsOpaque(std::span<const uint8_t> data) {
|
||||
// Endpoint modes with an alpha channel: LDR luminance+alpha (4, 5), RGB scale + two alpha
|
||||
// (10), LDR RGBA (12, 13) and HDR RGBA (14, 15).
|
||||
constexpr u32 AlphaModes = (1U << 4) | (1U << 5) | (1U << 10) | (1U << 12) | (1U << 13) |
|
||||
(1U << 14) | (1U << 15);
|
||||
for (size_t offset = 0; offset + 16 <= data.size(); offset += 16) {
|
||||
const std::span<const u8> block = data.subspan(offset, 16);
|
||||
InputBitStream strm(block);
|
||||
const TexelWeightParams params = DecodeBlockInfo(strm);
|
||||
if (params.m_bError) {
|
||||
continue; // decodes to the opaque error colour
|
||||
}
|
||||
if (params.m_bVoidExtentLDR || params.m_bVoidExtentHDR) {
|
||||
// One RGBA16 colour in bits 64-127: UNORM16, or FP16 for HDR.
|
||||
const u32 alpha = block[14] | (u32{block[15]} << 8);
|
||||
if (alpha != (params.m_bVoidExtentHDR ? 0x3C00U : 0xFFFFU)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const u32 partitions = strm.ReadBits<2>() + 1;
|
||||
if (partitions == 4 && params.m_bDualPlane) {
|
||||
continue;
|
||||
}
|
||||
if (partitions == 1) {
|
||||
if ((AlphaModes >> strm.ReadBits<4>()) & 1) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
strm.ReadBits<10>(); // partition index
|
||||
const u32 base_cem = strm.ReadBits<6>();
|
||||
const u32 base_mode = base_cem & 3;
|
||||
if (base_mode == 0) {
|
||||
if ((AlphaModes >> (base_cem >> 2)) & 1) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// The rest of the per-partition modes sit just below the weights (DecompressBlock).
|
||||
const u32 weight_bits = params.GetPackedBitSize();
|
||||
if (weight_bits < 24 || weight_bits > 96) {
|
||||
continue;
|
||||
}
|
||||
const u32 extra_bits = partitions == 2 ? 2 : partitions == 3 ? 5 : 8;
|
||||
const u32 extra_offset = 128 - weight_bits - extra_bits;
|
||||
u32 extra = 0;
|
||||
for (u32 bit = 0; bit < extra_bits; ++bit) {
|
||||
const u32 position = extra_offset + bit;
|
||||
extra |= ((block[position / 8] >> (position % 8)) & 1U) << bit;
|
||||
}
|
||||
u32 cem = ((extra << 6) | base_cem) >> 2;
|
||||
const u32 classes = cem;
|
||||
cem >>= partitions;
|
||||
for (u32 i = 0; i < partitions; ++i) {
|
||||
const u32 mode = ((base_mode - (((classes >> i) & 1) != 0 ? 0 : 1)) << 2) |
|
||||
((cem >> (2 * i)) & 3);
|
||||
if ((AlphaModes >> mode) & 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Tegra::Texture::ASTC
|
||||
|
|
|
|||
|
|
@ -8,4 +8,7 @@ namespace Tegra::Texture::ASTC {
|
|||
void Decompress(std::span<const uint8_t> data, uint32_t width, uint32_t height, uint32_t depth,
|
||||
uint32_t block_width, uint32_t block_height, std::span<uint8_t> output);
|
||||
|
||||
/// True if no 16-byte block in `data` decodes to alpha below 1.
|
||||
bool IsOpaque(std::span<const uint8_t> data);
|
||||
|
||||
} // namespace Tegra::Texture::ASTC
|
||||
|
|
|
|||
Loading…
Reference in a new issue