aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-11-02 23:55:44 +0100
committercirakg <cirakg@cirakg.xyz>2026-11-02 23:55:44 +0100
commita931609f31672f782d2817f5764d65c00dea4dad (patch)
tree3cb5bd166d1a54a76969afd61c1db835c9cb293d /src
parent3e10d48548a285172ea7cb1f94c58783e1d97754 (diff)
parent8362c44ff835d7be73d2adad9ce3e2d4467a6aeb (diff)
Merge branch 'nx2' into ldn/room-handshake
Diffstat (limited to 'src')
-rw-r--r--src/common/game_presets.h37
-rw-r--r--src/common/settings.h8
-rw-r--r--src/common/slot_vector.h16
-rw-r--r--src/core/CMakeLists.txt5
-rw-r--r--src/core/file_sys/nup_container.cpp210
-rw-r--r--src/core/file_sys/nup_container.h54
-rw-r--r--src/core/file_sys/nup_writer.h7
-rw-r--r--src/core/hle/kernel/k_priority_queue.h20
-rw-r--r--src/core/hle/kernel/kernel.cpp4
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp10
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp5
-rw-r--r--src/core/hle/service/nx2_gsv.h77
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp22
-rw-r--r--src/core/memory.cpp15
-rw-r--r--src/input_common/drivers/udp_client.cpp4
-rw-r--r--src/input_common/drivers/udp_client.h1
-rw-r--r--src/shader_recompiler/ir_opt/texture_pass.cpp69
-rw-r--r--src/shader_recompiler/shader_info.h9
-rw-r--r--src/tests/CMakeLists.txt7
-rw-r--r--src/tests/common/game_presets.cpp111
-rw-r--r--src/tests/common/slot_vector.cpp54
-rw-r--r--src/tests/core/core_timing.cpp3
-rw-r--r--src/tests/core/nup_writer.cpp119
-rw-r--r--src/tests/core/priority_queue.cpp111
-rw-r--r--src/tests/input_common/calibration_configuration_job.cpp20
-rw-r--r--src/tests/video_core/descriptor_table.cpp170
-rw-r--r--src/tests/video_core/memory_tracker.cpp38
-rw-r--r--src/tests/video_core/native_macro_fifo.cpp74
-rw-r--r--src/tests/video_core/shader_invalidation.cpp120
-rw-r--r--src/tests/video_core/uniform_stream_reuse.cpp116
-rw-r--r--src/tests/video_core/vertex_repack.cpp33
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h73
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h11
-rw-r--r--src/video_core/buffer_cache/uniform_stream_reuse.h121
-rw-r--r--src/video_core/control/scheduler.cpp2
-rw-r--r--src/video_core/control/scheduler.h6
-rw-r--r--src/video_core/dma_pusher.cpp3
-rw-r--r--src/video_core/dma_pusher.h9
-rw-r--r--src/video_core/engines/draw_manager.cpp19
-rw-r--r--src/video_core/engines/fermi_2d.h3
-rw-r--r--src/video_core/engines/kepler_compute.cpp26
-rw-r--r--src/video_core/engines/maxwell_3d.cpp17
-rw-r--r--src/video_core/engines/maxwell_3d.h16
-rw-r--r--src/video_core/engines/puller.cpp2
-rw-r--r--src/video_core/gpu.cpp16
-rw-r--r--src/video_core/gpu.h7
-rw-r--r--src/video_core/gpu_thread.cpp11
-rw-r--r--src/video_core/gpu_thread.h5
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt1
-rw-r--r--src/video_core/host_shaders/blit_color_srgb_to_linear.frag18
-rw-r--r--src/video_core/renderer_opengl/gl_compute_pipeline.cpp25
-rw-r--r--src/video_core/renderer_opengl/gl_graphics_pipeline.cpp22
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp7
-rw-r--r--src/video_core/renderer_vulkan/blit_image.h1
-rw-r--r--src/video_core/renderer_vulkan/fixed_pipeline_state.cpp2
-rw-r--r--src/video_core/renderer_vulkan/fixed_pipeline_state.h1
-rw-r--r--src/video_core/renderer_vulkan/vertex_repack.h35
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp17
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h33
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.cpp5
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.h7
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pipeline.cpp25
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp22
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp26
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp29
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp45
-rw-r--r--src/video_core/shader_cache.cpp27
-rw-r--r--src/video_core/shader_cache.h4
-rw-r--r--src/video_core/shader_environment.cpp13
-rw-r--r--src/video_core/texture_cache/descriptor_table.h18
-rw-r--r--src/video_core/texture_cache/texture_cache.h3
-rw-r--r--src/video_core/textures/texture.h8
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h6
-rw-r--r--src/yuzu/main_window.cpp61
-rw-r--r--src/yuzu/main_window.h4
75 files changed, 2090 insertions, 271 deletions
diff --git a/src/common/game_presets.h b/src/common/game_presets.h
new file mode 100644
index 0000000000..83dd4d3c3f
--- /dev/null
+++ b/src/common/game_presets.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "common/settings.h"
+
+namespace Settings {
+
+inline constexpr u64 MarioKartWorldProgramId = 0x0400C3F00006E000ULL;
+
+inline void SetMarioKartWorldRecommendedSettings(Values& settings, bool global) {
+ const auto session = [global](auto& setting, auto value) {
+ setting.SetGlobal(global);
+ setting.SetValue(value);
+ };
+ session(settings.renderer_backend, RendererBackend::Vulkan);
+ session(settings.resolution_setup, ResolutionSetup::Res1X);
+ session(settings.gpu_accuracy, GpuAccuracy::Low);
+ session(settings.use_asynchronous_shaders, true);
+ session(settings.astc_recompression, AstcRecompression::Bc3);
+ session(settings.vram_usage_mode, VramUsageMode::Conservative);
+ session(settings.use_reactive_flushing, false);
+ session(settings.vsync_mode, VSyncMode::FifoRelaxed);
+ session(settings.use_docked_mode, ConsoleMode::Handheld);
+ session(settings.scaling_filter, ScalingFilter::ScaleForce);
+ session(settings.anti_aliasing, AntiAliasing::Fxaa);
+}
+
+inline bool ApplyMarioKartWorldPreset(Values& settings, u64 program_id, bool accepted) {
+ if (!accepted || program_id != MarioKartWorldProgramId) {
+ return false;
+ }
+ SetMarioKartWorldRecommendedSettings(settings, false);
+ return true;
+}
+
+} // namespace Settings
diff --git a/src/common/settings.h b/src/common/settings.h
index 74e2006d15..6669f98f4c 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -358,7 +358,7 @@ struct Values {
true};
SwitchableSetting<ScalingFilter> scaling_filter{linkage,
- ScalingFilter::NearestNeighbor,
+ ScalingFilter::ScaleForce,
"scaling_filter",
Category::Renderer,
Specialization::Default,
@@ -387,7 +387,7 @@ struct Values {
true};
SwitchableSetting<AntiAliasing> anti_aliasing{linkage,
- AntiAliasing::None,
+ AntiAliasing::Fxaa,
"anti_aliasing",
Category::Renderer,
Specialization::Default,
@@ -770,11 +770,7 @@ struct Values {
Setting<s32> current_user{linkage, 0, "current_user", Category::System};
SwitchableSetting<ConsoleMode> use_docked_mode{linkage,
-#ifdef __ANDROID__
ConsoleMode::Handheld,
-#else
- ConsoleMode::Docked,
-#endif
"use_docked_mode",
Category::System,
Specialization::Radio,
diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h
index f6da7a59d1..8e1c06e35a 100644
--- a/src/common/slot_vector.h
+++ b/src/common/slot_vector.h
@@ -52,12 +52,16 @@ public:
const u64* const bitset = slot_vector->stored_bitset.data();
const u32 size = static_cast<u32>(slot_vector->stored_bitset.size()) * 64;
if (id.Value() < size) {
- do {
- ++id.index;
- } while (id.Value() < size && !IsValid(bitset));
- if (id.Value() == size) {
- id = SlotId{};
+ u32 index = id.Value() + 1;
+ while (index < size) {
+ const u64 word = bitset[index / 64] >> (index % 64);
+ if (word != 0) {
+ index += static_cast<u32>(std::countr_zero(word));
+ break;
+ }
+ index = (index / 64 + 1) * 64;
}
+ id = index < size ? SlotId{index} : SlotId{};
}
return *this;
}
@@ -139,7 +143,7 @@ public:
if (it == stored_bitset.end()) {
return end();
}
- const u32 word_index = static_cast<u32>(std::distance(it, stored_bitset.begin()));
+ const u32 word_index = static_cast<u32>(std::distance(stored_bitset.begin(), it));
const SlotId first_id{word_index * 64 + static_cast<u32>(std::countr_zero(*it))};
return Iterator(this, first_id);
}
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index eeb3202d12..1ff9f271d2 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1290,4 +1290,9 @@ else()
target_sources(core PRIVATE hle/service/ssl/ssl_backend_none.cpp)
endif()
+if (ENABLE_LTO AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC)
+ # Fibers can migrate between host threads; keep kernel TLS lookups outside LTO callers.
+ set_source_files_properties(hle/kernel/kernel.cpp PROPERTIES COMPILE_OPTIONS -fno-lto)
+endif()
+
create_target_directory_groups(core)
diff --git a/src/core/file_sys/nup_container.cpp b/src/core/file_sys/nup_container.cpp
index 0e485b44ec..27ac4486ca 100644
--- a/src/core/file_sys/nup_container.cpp
+++ b/src/core/file_sys/nup_container.cpp
@@ -8,11 +8,15 @@
#include <cstring>
#include <list>
#include <map>
+#include <random>
+#include <span>
#include <unordered_map>
+#include <fmt/format.h>
#include <openssl/evp.h>
#include <zstd.h>
+#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/swap.h"
#include "core/file_sys/vfs/vfs_offset.h"
@@ -31,6 +35,7 @@ constexpr u32 TocMagic = 0x01434F54; // "TOC\x01" little-endian
constexpr u8 CodecRaw = 0;
constexpr u8 CodecZstd = 1;
constexpr u8 PackageFlagStandaloneUpdate = 0x01;
+constexpr u32 MaxDirDepth = 128;
#pragma pack(push, 1)
struct Superblock {
@@ -142,8 +147,8 @@ bool Sha256(const u8* data, std::size_t size, std::array<u8, 0x20>& out) {
return length == out.size();
}
-/// A member file whose extent is zstd-compressed. Decompresses on first read
-/// and caches the result (LRU-evicted) unless the file exceeds the cache cap.
+/// A member file whose extent is zstd-compressed. Decompresses on first read and caches the
+/// result (LRU-evicted); a file over half the cache cap is decompressed once to a temporary file.
class NupVfsFile final : public VfsFile {
public:
NupVfsFile(VirtualFile backing_, u64 extent_offset_, u64 extent_size_, u64 uncompressed_size_,
@@ -151,7 +156,7 @@ public:
std::size_t cache_index_)
: backing(std::move(backing_)), extent_offset(extent_offset_), extent_size(extent_size_),
uncompressed_size(uncompressed_size_), name(std::move(name_)), cache(std::move(cache_)),
- cache_index(cache_index_), cache_enabled(uncompressed_size <= NupDecompressCache::DecompressCacheCap) {}
+ cache_index(cache_index_), cache_enabled(uncompressed_size <= cache->cap / 2) {}
std::string GetName() const override {
return name;
@@ -183,6 +188,10 @@ public:
}
length = std::min(length, static_cast<std::size_t>(uncompressed_size) - offset);
+ if (!cache_enabled) {
+ return ReadSpilled(data, length, offset);
+ }
+
{
std::scoped_lock lock{cache->mutex};
if (const auto* cached = cache->Find(cache_index)) {
@@ -198,9 +207,7 @@ public:
}
{
std::scoped_lock lock{cache->mutex};
- if (cache_enabled) {
- cache->Insert(cache_index, scratch);
- }
+ cache->Insert(cache_index, scratch);
if (const auto* cached = cache->Find(cache_index)) {
std::memcpy(data, cached->data() + offset, length);
return length;
@@ -220,6 +227,88 @@ public:
}
private:
+ std::size_t ReadSpilled(u8* data, std::size_t length, std::size_t offset) const {
+ std::shared_ptr<NupSpillFile> spill;
+ {
+ std::scoped_lock lock{cache->mutex};
+ spill = cache->GetSpill(cache_index);
+ }
+ std::scoped_lock lock{spill->mutex};
+ if (!spill->attempted) {
+ spill->attempted = true;
+ DecompressToFile(*spill);
+ }
+ if (!spill->file.IsOpen() || !spill->file.Seek(static_cast<s64>(offset))) {
+ return 0;
+ }
+ return spill->file.ReadSpan(std::span{data, length});
+ }
+
+ void DecompressToFile(NupSpillFile& spill) const {
+ const auto fail = [&](const char* reason) {
+ LOG_ERROR(Loader, "NUP zstd decompression of {} to a temporary file failed: {}", name,
+ reason);
+ spill.file.Close();
+ if (!spill.path.empty()) {
+ std::error_code ec;
+ std::filesystem::remove(spill.path, ec);
+ }
+ };
+ if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) {
+ return fail("extent out of range");
+ }
+
+ std::random_device random;
+ std::error_code ec;
+ std::filesystem::create_directories(cache->spill_dir, ec);
+ spill.path =
+ cache->spill_dir / fmt::format("{:08x}{:08x}-{}.tmp", random(), random(), cache_index);
+ Common::FS::IOFile out{spill.path, Common::FS::FileAccessMode::Write};
+ if (!out.IsOpen()) {
+ return fail("cannot create the temporary file");
+ }
+
+ std::unique_ptr<ZSTD_DCtx, decltype(&ZSTD_freeDCtx)> dctx{ZSTD_createDCtx(),
+ &ZSTD_freeDCtx};
+ std::vector<u8> in_buffer(ZSTD_DStreamInSize());
+ std::vector<u8> out_buffer(ZSTD_DStreamOutSize());
+ u64 read = 0;
+ u64 written = 0;
+ bool frame_done = false;
+ while (!frame_done && read < extent_size) {
+ const auto chunk =
+ static_cast<std::size_t>(std::min<u64>(in_buffer.size(), extent_size - read));
+ if (backing->Read(in_buffer.data(), chunk,
+ static_cast<std::size_t>(extent_offset + read)) != chunk) {
+ return fail("short read of the compressed data");
+ }
+ read += chunk;
+ ZSTD_inBuffer input{in_buffer.data(), chunk, 0};
+ ZSTD_outBuffer output{};
+ do {
+ output = {out_buffer.data(), out_buffer.size(), 0};
+ const std::size_t result = ZSTD_decompressStream(dctx.get(), &output, &input);
+ if (ZSTD_isError(result) || output.pos > uncompressed_size - written) {
+ return fail("corrupt or oversized data");
+ }
+ if (out.WriteSpan(std::span<const u8>{out_buffer.data(), output.pos}) !=
+ output.pos) {
+ return fail("cannot write the temporary file");
+ }
+ written += output.pos;
+ frame_done = result == 0;
+ } while (!frame_done && (input.pos < input.size || output.pos == output.size));
+ }
+ if (!frame_done || written != uncompressed_size) {
+ return fail("size mismatch");
+ }
+ out.Close();
+ spill.file.Open(spill.path, Common::FS::FileAccessMode::Read);
+ if (!spill.file.IsOpen()) {
+ fail("cannot reopen the temporary file");
+ }
+ }
+
std::vector<u8> Decompress() const {
if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) {
return {};
@@ -251,6 +340,13 @@ private:
} // namespace
+NupSpillFile::~NupSpillFile() {
+ file.Close();
+ if (!path.empty()) {
+ std::error_code ec;
+ std::filesystem::remove(path, ec);
+ }
+}
bool NupContainer::IsNupFile(const VirtualFile& file) {
if (file == nullptr || file->GetSize() < SuperblockSize) {
@@ -261,8 +357,13 @@ bool NupContainer::IsNupFile(const VirtualFile& file) {
return magic == HeaderMagic;
}
-NupContainer::NupContainer(VirtualFile file_)
+NupContainer::NupContainer(VirtualFile file_, std::size_t memory_cache_cap,
+ std::filesystem::path spill_dir_)
: file(std::move(file_)), cache(std::make_shared<NupDecompressCache>()) {
+ cache->cap = memory_cache_cap;
+ cache->spill_dir = spill_dir_.empty()
+ ? Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "nup"
+ : std::move(spill_dir_);
if (file == nullptr) {
return;
}
@@ -293,9 +394,9 @@ bool NupContainer::Parse() {
toc_size = super.toc_size;
blob_offset = super.blob_offset;
file_size = super.file_size;
- if (file_size != file->GetSize() || toc_offset < SuperblockSize ||
- blob_offset < toc_offset + toc_size || blob_offset % 0x10 != 0 ||
- blob_offset > file_size) {
+ if (file_size != file->GetSize() || toc_offset < SuperblockSize || blob_offset > file_size ||
+ toc_offset > blob_offset || toc_size > blob_offset - toc_offset ||
+ blob_offset % 0x10 != 0) {
status = Loader::ResultStatus::ErrorNUPBadFormat;
return false;
}
@@ -325,15 +426,22 @@ bool NupContainer::Parse() {
return false;
}
+ // The counts come from the file, so bound them by the TOC size before multiplying.
+ if (header.dir_count > toc.size() / DirRecordSize ||
+ header.file_count > toc.size() / FileRecordSize) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
const u64 needed = TocHeaderSize + static_cast<u64>(header.package_count) * PackageRecordSize +
header.dir_count * DirRecordSize + header.file_count * FileRecordSize;
- const u64 children_offset = needed;
- const u64 children_bytes = header.pool_offset - needed;
- if (toc.size() < needed || header.pool_offset < needed ||
- header.pool_offset + header.pool_size > toc.size() || children_bytes % sizeof(u32) != 0) {
+ if (needed > toc.size() || header.pool_offset < needed || header.pool_offset > toc.size() ||
+ header.pool_size > toc.size() - header.pool_offset ||
+ (header.pool_offset - needed) % sizeof(u32) != 0) {
status = Loader::ResultStatus::ErrorNUPBadFormat;
return false;
}
+ const u64 children_offset = needed;
+ const u64 children_bytes = header.pool_offset - needed;
const u8* base = toc.data();
std::size_t pos = TocHeaderSize;
@@ -358,8 +466,8 @@ bool NupContainer::Parse() {
package.root_dir = record.root_dir;
package.exefs_dir = record.exefs_dir;
package.romfs_dir = record.romfs_dir;
- if (record.meta_size > 0 && record.meta_offset != 0 &&
- record.meta_offset + record.meta_size <= file_size) {
+ if (record.meta_size > 0 && record.meta_offset != 0 && record.meta_offset <= file_size &&
+ record.meta_size <= file_size - record.meta_offset) {
package.meta_offset = record.meta_offset;
package.meta_size = record.meta_size;
}
@@ -371,8 +479,9 @@ bool NupContainer::Parse() {
DirRecord record{};
std::memcpy(&record, base + pos, sizeof(record));
pos += DirRecordSize;
- if (record.first_child + record.child_count > children_bytes / sizeof(u32) ||
- record.name_offset + record.name_length > header.pool_size) {
+ if (static_cast<u64>(record.first_child) + record.child_count >
+ children_bytes / sizeof(u32) ||
+ static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) {
status = Loader::ResultStatus::ErrorNUPBadFormat;
return false;
}
@@ -392,9 +501,9 @@ bool NupContainer::Parse() {
(record.size == 0 && (record.extent_offset != 0 || record.extent_size != 0)) ||
(record.size != 0 &&
(record.extent_size == 0 || record.extent_offset < blob_offset ||
- record.extent_offset % 0x10 != 0 ||
- record.extent_offset + record.extent_size > file_size)) ||
- record.name_offset + record.name_length > header.pool_size) {
+ record.extent_offset % 0x10 != 0 || record.extent_offset > file_size ||
+ record.extent_size > file_size - record.extent_offset)) ||
+ static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) {
status = Loader::ResultStatus::ErrorNUPBadFormat;
return false;
}
@@ -417,6 +526,11 @@ bool NupContainer::Parse() {
}
}
+ if (!ValidateDirTree()) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
for (auto& package : packages) {
if (!ParseMeta(package)) {
status = Loader::ResultStatus::ErrorNUPBadFormat;
@@ -426,6 +540,57 @@ bool NupContainer::Parse() {
return true;
}
+bool NupContainer::ValidateDirTree() const {
+ enum : u8 { Unvisited, InProgress, Done };
+ struct Frame {
+ u32 dir;
+ u32 next;
+ };
+ std::vector<u8> state(dirs.size(), Unvisited);
+ std::vector<u32> height(dirs.size(), 0);
+ std::vector<Frame> stack;
+ for (u32 start = 0; start < dirs.size(); ++start) {
+ if (state[start] != Unvisited) {
+ continue;
+ }
+ state[start] = InProgress;
+ stack.push_back({start, 0});
+ while (!stack.empty()) {
+ const Frame frame = stack.back();
+ const DirNode& node = dirs[frame.dir];
+ if (frame.next == node.child_count) {
+ state[frame.dir] = Done;
+ stack.pop_back();
+ if (!stack.empty()) {
+ u32& parent_height = height[stack.back().dir];
+ parent_height = std::max(parent_height, height[frame.dir] + 1);
+ }
+ continue;
+ }
+ ++stack.back().next;
+ const u32 child = children[static_cast<std::size_t>(node.first_child) + frame.next];
+ if (child >= dirs.size()) {
+ continue;
+ }
+ if (state[child] == InProgress) {
+ LOG_ERROR(Loader, "NUP directory tree contains a cycle");
+ return false;
+ }
+ if (state[child] == Done) {
+ height[frame.dir] = std::max(height[frame.dir], height[child] + 1);
+ } else {
+ state[child] = InProgress;
+ stack.push_back({child, 0});
+ }
+ if (stack.size() + height[child] > MaxDirDepth) {
+ LOG_ERROR(Loader, "NUP directory tree is nested too deeply");
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
bool NupContainer::ParseMeta(PackageInfo& package) {
if (package.meta_offset == 0) {
return true;
@@ -547,7 +712,8 @@ VirtualFile NupContainer::OpenFile(std::size_t file_index) {
void NupContainer::BuildDirTree(u32 dir_index, VectorVfsDirectory& out) {
const DirNode& node = dirs[dir_index];
- for (u32 slot = node.first_child; slot < node.first_child + node.child_count; ++slot) {
+ const u64 end = static_cast<u64>(node.first_child) + node.child_count;
+ for (u64 slot = node.first_child; slot < end; ++slot) {
const u32 child = children[slot];
if (child >= dirs.size()) {
out.AddFile(OpenFile(child - static_cast<u32>(dirs.size())));
diff --git a/src/core/file_sys/nup_container.h b/src/core/file_sys/nup_container.h
index 118b137a7b..8787717d47 100644
--- a/src/core/file_sys/nup_container.h
+++ b/src/core/file_sys/nup_container.h
@@ -4,6 +4,7 @@
#pragma once
#include <array>
+#include <filesystem>
#include <functional>
#include <list>
#include <memory>
@@ -14,19 +15,36 @@
#include <vector>
#include "common/common_types.h"
+#include "common/fs/file.h"
#include "core/file_sys/vfs/vfs_types.h"
#include "core/file_sys/vfs/vfs_vector.h"
#include "core/loader/loader.h"
namespace FileSys {
+/// A zstd member too large for the in-memory cache, decompressed once into a temporary file.
+struct NupSpillFile {
+ ~NupSpillFile();
+
+ std::mutex mutex;
+ std::filesystem::path path;
+ Common::FS::IOFile file;
+ bool attempted{};
+};
+
/// LRU over decompressed zstd extents, shared by the files of one container.
struct NupDecompressCache {
+ static constexpr std::size_t DefaultCap = 512 * 1024 * 1024;
+
+ using Entry = std::pair<std::size_t, std::vector<u8>>;
+
std::mutex mutex;
- std::list<std::pair<std::size_t, std::vector<u8>>> lru;
- std::unordered_map<std::size_t, std::list<std::pair<std::size_t, std::vector<u8>>>::iterator>
- index;
+ std::list<Entry> lru;
+ std::unordered_map<std::size_t, std::list<Entry>::iterator> index;
std::size_t bytes{};
+ std::size_t cap{DefaultCap};
+ std::filesystem::path spill_dir;
+ std::unordered_map<std::size_t, std::shared_ptr<NupSpillFile>> spills;
std::vector<u8>* Find(std::size_t file_index) {
const auto it = index.find(file_index);
@@ -37,21 +55,31 @@ struct NupDecompressCache {
return &it->second->second;
}
+ /// Takes ownership of `data` only when it is inserted.
void Insert(std::size_t file_index, std::vector<u8>& data) {
- while (bytes + data.size() > DecompressCacheCap && !lru.empty()) {
+ if (index.contains(file_index)) {
+ return;
+ }
+ while (bytes + data.size() > cap && !lru.empty()) {
bytes -= lru.back().second.size();
index.erase(lru.back().first);
lru.pop_back();
}
- if (bytes + data.size() > DecompressCacheCap) {
+ if (bytes + data.size() > cap) {
return;
}
- lru.emplace_front(file_index, data);
- index.emplace(file_index, lru.begin());
bytes += data.size();
+ lru.emplace_front(file_index, std::move(data));
+ index.emplace(file_index, lru.begin());
}
- static constexpr std::size_t DecompressCacheCap = 512 * 1024 * 1024;
+ std::shared_ptr<NupSpillFile> GetSpill(std::size_t file_index) {
+ auto& spill = spills[file_index];
+ if (!spill) {
+ spill = std::make_shared<NupSpillFile>();
+ }
+ return spill;
+ }
};
/**
@@ -101,7 +129,11 @@ public:
/// Recursively merges overlay into a copy of base; overlay paths win.
static VirtualDir MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay);
- explicit NupContainer(VirtualFile file_);
+ /// Zstd members larger than half of `memory_cache_cap` are decompressed to a temporary file
+ /// in `spill_dir` (the cache directory by default) instead of being held in memory.
+ explicit NupContainer(VirtualFile file_,
+ std::size_t memory_cache_cap = NupDecompressCache::DefaultCap,
+ std::filesystem::path spill_dir_ = {});
~NupContainer();
Loader::ResultStatus GetStatus() const {
@@ -119,7 +151,8 @@ public:
std::optional<std::size_t> FindBestUpdatePackage() const;
/// Opens a member file by node index; raw extents are zero-copy views into
- /// the container, zstd extents are lazily decompressed and cached.
+ /// the container, zstd extents are lazily decompressed, cached in memory
+ /// or, when large, in a temporary file.
VirtualFile OpenFile(std::size_t file_index);
VirtualDir GetExeFSDir(std::size_t package_index);
@@ -151,6 +184,7 @@ private:
bool Parse();
bool ParseMeta(PackageInfo& package);
+ bool ValidateDirTree() const;
void BuildDirTree(u32 dir_index, VectorVfsDirectory& out);
VirtualDir BuildPackageDir(u32 dir_index);
std::vector<u8> ReadExtentRaw(const FileNode& node) const;
diff --git a/src/core/file_sys/nup_writer.h b/src/core/file_sys/nup_writer.h
index d2423dd1f4..dd7b7e0883 100644
--- a/src/core/file_sys/nup_writer.h
+++ b/src/core/file_sys/nup_writer.h
@@ -25,10 +25,9 @@ struct PackProgress {
enum class PackResult { Success, Cancelled, InvalidSource, IoError };
-// NupContainer keeps a zstd file in memory only while it fits its 512 MiB decompression cache;
-// a larger one is decompressed again in full on every read, which makes big compressed RomFS
-// files unusable. Files above this size are therefore stored raw, with headroom below the cap so
-// that one file does not evict everything else from the cache.
+// NupContainer keeps a zstd file in memory only while it fits half of its 512 MiB decompression
+// cache; a larger one is decompressed to a temporary file on first read, which costs time and
+// disk space. Files above this size are therefore stored raw.
constexpr u64 DefaultRawThreshold = 256ULL * 1024 * 1024;
// Application metadata written to the base package (or the first package when there is no base).
diff --git a/src/core/hle/kernel/k_priority_queue.h b/src/core/hle/kernel/k_priority_queue.h
index 89a0c5bbea..5d93455d33 100644
--- a/src/core/hle/kernel/k_priority_queue.h
+++ b/src/core/hle/kernel/k_priority_queue.h
@@ -11,6 +11,7 @@
#include <bitset>
#include <concepts>
#include <cstddef>
+#include <type_traits>
#include "common/alignment.h"
#include "common/assert.h"
@@ -196,12 +197,7 @@ public:
constexpr Member* GetFront(s32 core) const {
ASSERT(IsValidCore(core));
- const s32 priority = s32([](auto const& e) {
- for (size_t i = 0; i < e.size(); ++i)
- if (e[i])
- return i;
- return e.size();
- }(m_available_priorities[core]));
+ const s32 priority = s32(GetNextSet(m_available_priorities[core], size_t(-1)));
if (priority <= LowestPriority) {
return m_queues[priority].GetFront(core);
} else {
@@ -222,7 +218,17 @@ public:
template<size_t N>
constexpr size_t GetNextSet(std::bitset<N> const& bit, size_t n) const {
- for (size_t i = n + 1; i < bit.size(); i++)
+ const size_t first = n + 1;
+ if (first >= bit.size()) {
+ return bit.size();
+ }
+ if constexpr (N <= 64) {
+ if (!std::is_constant_evaluated()) {
+ const u64 remaining = bit.to_ullong() >> first;
+ return remaining != 0 ? first + size_t(std::countr_zero(remaining)) : bit.size();
+ }
+ }
+ for (size_t i = first; i < bit.size(); i++)
if (bit[i])
return i;
return bit.size();
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index f006d129a7..9e6ccc2d89 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -79,7 +79,7 @@ struct KernelCore::Impl {
// And we have the guarantee that the data won't move out of the way so we can safely
// take a reference to it. This isn't always universally true but this is "global" data
// so it will be statically given a TLS slot anyways.
- static inline thread_local ThreadLocalData tls_data = {};
+ static thread_local ThreadLocalData tls_data;
explicit Impl(Core::System& system_, KernelCore& kernel_) : system{system_} {
tls_data.lock = true;
@@ -861,6 +861,8 @@ struct KernelCore::Impl {
Core::System& system;
};
+thread_local ThreadLocalData KernelCore::Impl::tls_data{};
+
KernelCore::KernelCore(Core::System& system) : impl{std::make_unique<Impl>(system, *this)} {}
KernelCore::~KernelCore() = default;
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index 40313a10fb..c5b23bef3e 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -176,7 +176,7 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address);
// On the GPU thread, like unmaps: a GMMU map takes the texture cache lock, and the callers hold
// nx2:gpu locks that the GPU thread takes under that lock (sparse blocks backed on write).
- system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
});
LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
@@ -253,7 +253,7 @@ void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) {
}
nx2_sparse_ranges[gpu_address] = {size, device_address};
// On the GPU thread, as in Nx2Map.
- system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
});
LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address,
@@ -270,7 +270,7 @@ void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) {
--it;
const auto& [start, range] = *it;
const auto& [region_size, device_address] = range;
- if (gpu_address + size > start + region_size) {
+ if (size > region_size || gpu_address - start > region_size - size) {
return;
}
system.Host1x().MemoryManager().Map(device_address + (gpu_address - start), cpu_address, size,
@@ -287,7 +287,7 @@ void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) {
--it;
const auto& [start, range] = *it;
const auto& [region_size, device_address] = range;
- if (gpu_address + size > start + region_size) {
+ if (size > region_size || gpu_address - start > region_size - size) {
return;
}
// In GPU-thread order: command lists submitted before the request still see the old tile.
@@ -295,7 +295,7 @@ void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) {
// whose contents are now what the game's memory holds there.
const DAddr device = device_address + (gpu_address - start);
auto& smmu = system.Host1x().MemoryManager();
- system.GPU().RunInOrder([&smmu, device, cpu_address, size, asid = session->asid] {
+ system.GPU().MapInOrder([&smmu, device, cpu_address, size, asid = session->asid] {
smmu.Unmap(device, size);
if (cpu_address != 0) {
smmu.Map(device, cpu_address, size, asid, true);
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 848c15d6f8..79586446d5 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -445,7 +445,10 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
const char* value = std::getenv("EDEN_NX2_PB_SNAPSHOT");
return value == nullptr || value[0] != '0';
}();
- if (snapshot && channel_state->memory_manager) {
+ if (snapshot && channel_state->memory_manager && channel_state->payload) {
+ // Before the copy: a suspension during it must count as one after it.
+ entries.snapshot_suspensions =
+ channel_state->payload->dma_pusher.suspensions.load(std::memory_order_acquire);
size_t words = 0;
for (const auto& entry : entries.command_lists) {
words += entry.size;
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 37c797fa3f..259681a2a2 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -15,6 +15,7 @@
#include <cstdlib>
#include <cstdio>
#include <initializer_list>
+#include <limits>
#include <atomic>
#include <deque>
#include <map>
@@ -1076,13 +1077,18 @@ private:
}
const u64 gpu_address = request.gpu_address;
const u64 size = request.size;
+ if (size > std::numeric_limits<u64>::max() - gpu_address) {
+ LOG_WARNING(Service, "nx2:gpu 216 [{:#x}, +{:#x}] wraps the address space", gpu_address,
+ size);
+ return;
+ }
auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
auto it = m_sparse_regions.upper_bound(gpu_address);
SparseRegion* region = nullptr;
u64 start = 0;
if (it != m_sparse_regions.begin()) {
--it;
- if (gpu_address + size <= it->first + it->second.size) {
+ if (size <= it->second.size && gpu_address - it->first <= it->second.size - size) {
start = it->first;
region = &it->second;
}
@@ -1145,7 +1151,7 @@ private:
return std::nullopt;
}
--it;
- if (gpu_address + size > it->first + it->second) {
+ if (size > it->second || gpu_address - it->first > it->second - size) {
return std::nullopt;
}
return it->first;
@@ -1240,7 +1246,7 @@ private:
return std::nullopt;
}
--it;
- if (gpu_address + size > it->first + it->second.size) {
+ if (size > it->second.size || gpu_address - it->first > it->second.size - size) {
return std::nullopt;
}
return it->second.cpu_address + (gpu_address - it->first);
@@ -1705,37 +1711,41 @@ private:
LOG_INFO(Service, "nx2:vi2 229 present #{} token={:#x} slot={} image={}", m_presents,
u64{w[2]} | (u64{w[3]} << 32), w[4], w[5]);
}
- // Wait for the previous frame's fence instead of this one, so the game prepares frame N+1
- // while the GPU thread runs frame N (the console keeps several frames in flight). Waiting
- // for this frame serialized the two: 8-56 ms of every 34-105 ms frame on title 1.
- // EDEN_NX2_FRAMES_AHEAD=0 restores the wait for the current frame.
- static const bool frames_ahead = [] {
+ // Wait for this frame's fence, so the game doesn't write the next frame's data (constant
+ // buffers holding texture handles, vertex data, Pokopia's glyph atlas) while the GPU
+ // thread still reads this frame's. But title 1's GPU waits on semaphores its CPU releases
+ // only after this present returns: while a channel stays suspended on one, wait for the
+ // previous frame's fence instead and let the game prepare frame N+1 meanwhile (waiting
+ // for this frame there cost 8-56 ms of every 34-105 ms frame, or never finished).
+ // EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame, =0 always for this one.
+ static const int frames_ahead = [] {
const char* value = std::getenv("EDEN_NX2_FRAMES_AHEAD");
- return value == nullptr || value[0] != '0';
+ return value == nullptr ? -1 : value[0] == '1' ? 1 : 0;
}();
const auto entered = std::chrono::steady_clock::now();
- if (frames_ahead) {
+ if (frames_ahead == 1 || (frames_ahead == -1 && !WaitForFrame(~w[22], w[24], true))) {
LOG_DEBUG(Service, "nx2:vi2 present #{} fence ({}, {:#x}), waiting for ({}, {:#x}); now {:#x}",
m_presents, ~w[22], w[24], m_previous_fence_id, m_previous_fence_value,
~w[22] < 192 ? system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(~w[22]) : 0U);
- // Only run a frame ahead while the GPU keeps up. With the previous frame still on the
- // GPU here, the GPU is the limit and running ahead gains nothing; and Splatoon Raiders,
- // which reuses its command memory as if the GPU were at most a frame behind, rewrote
- // command lists Eden had not read yet (misread commands, then crashes, with a slow
- // GPU). Wait for this frame too then.
- const bool gpu_behind =
- m_previous_fence_id < 192 &&
- !system.Host1x().GetSyncpointManager().IsReadyGuest(m_previous_fence_id,
- m_previous_fence_value);
+ // With EDEN_NX2_FRAMES_AHEAD=1, only run a frame ahead while the GPU keeps up. With
+ // the previous frame still on the GPU here, the GPU is the limit and running ahead
+ // gains nothing; and Splatoon Raiders, which reuses its command memory as if the GPU
+ // were at most a frame behind, rewrote command lists Eden had not read yet (misread
+ // commands, then crashes, with a slow GPU). Wait for this frame too then. Not when a
+ // suspended channel brought us here: this frame can't finish before the present
+ // returns.
+ const bool gpu_behind = frames_ahead == 1 && m_previous_fence_id < 192 &&
+ !system.Host1x().GetSyncpointManager().IsReadyGuest(
+ m_previous_fence_id, m_previous_fence_value);
WaitForFrame(m_previous_fence_id, m_previous_fence_value);
if (gpu_behind) {
WaitForFrame(~w[22], w[24]);
}
- m_previous_fence_id = ~w[22];
- m_previous_fence_value = w[24];
- } else {
+ } else if (frames_ahead == 0) {
WaitForFrame(~w[22], w[24]);
}
+ m_previous_fence_id = ~w[22];
+ m_previous_fence_value = w[24];
const auto waited = std::chrono::steady_clock::now();
Composite(w[5], ~w[22], w[24], w[7]);
const auto composited = std::chrono::steady_clock::now();
@@ -1832,9 +1842,12 @@ private:
m_next_vblank += interval;
}
- void WaitForFrame(u32 fence_id, u32 fence_value) {
+ /// Waits until the fence is reached. With `unless_gpu_waits`, gives up after a few ms once a
+ /// GPU channel was seen suspended on a semaphore (it may be one this thread releases later).
+ /// Returns whether the fence was reached.
+ bool WaitForFrame(u32 fence_id, u32 fence_value, bool unless_gpu_waits = false) {
if (fence_id >= 192) {
- return;
+ return true;
}
auto& syncpoints = system.Host1x().GetSyncpointManager();
const auto start = std::chrono::steady_clock::now();
@@ -1859,11 +1872,24 @@ private:
wake->cv.notify_all();
});
std::unique_lock lk{wake->mutex};
+ // Title 1's channels resume and suspend again while they wait on this thread, so any
+ // suspension counts, not only an unbroken one.
+ bool gpu_waited = false;
while (!wake->done && system.IsPoweredOn() &&
std::chrono::steady_clock::now() < deadline) {
- wake->cv.wait_for(lk, std::chrono::milliseconds{50});
+ if (unless_gpu_waits) {
+ gpu_waited |= system.GPU().HasSuspendedChannels();
+ if (gpu_waited &&
+ std::chrono::steady_clock::now() - start > std::chrono::milliseconds{3}) {
+ return false;
+ }
+ }
+ wake->cv.wait_for(lk, std::chrono::milliseconds{unless_gpu_waits ? 1 : 50});
}
}
+ if (unless_gpu_waits && !syncpoints.IsReadyGuest(fence_id, fence_value)) {
+ return false;
+ }
if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) {
LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})",
m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id));
@@ -1875,6 +1901,7 @@ private:
std::chrono::steady_clock::now() - start)
.count());
}
+ return syncpoints.IsReadyGuest(fence_id, fence_value);
}
// Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 4b1168a59b..49352a5b49 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -81,6 +81,24 @@ struct PatchCollection {
namespace {
+FileSys::VirtualDir FindLooseRomFSDirectory(const FileSys::VirtualDir& parent) {
+ if (parent == nullptr) {
+ return nullptr;
+ }
+ if (const auto dir = parent->GetSubdirectory("romfs")) {
+ return dir;
+ }
+ for (const auto& dir : parent->GetSubdirectories()) {
+ const auto name = dir->GetName();
+ if (name.size() == 5 && std::equal(name.begin(), name.end(), "romfs", [](char a, char b) {
+ return std::tolower(static_cast<unsigned char>(a)) == b;
+ })) {
+ return dir;
+ }
+ }
+ return nullptr;
+}
+
// RomFS for a loose ExeFS: the first .romfs file next to main, else a romfs/ directory next to
// the exefs/ directory (the usual layout of an extracted title).
FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
@@ -93,7 +111,7 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
}
}
if (const auto parent = exefs_dir->GetParentDirectory()) {
- if (const auto romfs_dir = parent->GetSubdirectory("romfs")) {
+ if (const auto romfs_dir = FindLooseRomFSDirectory(parent)) {
// Building it walks every file and keeps an object for each (title 1: 206k files, ~150
// MiB), so loaders of the same directory share one build while any of them holds it.
static std::mutex cache_mutex;
@@ -268,7 +286,7 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
romfs_exefs_dir = file_dir;
if (const auto title_dir = file_dir->GetParentDirectory()) {
- romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs"));
+ romfs_version = RomFSProductVersion(FindLooseRomFSDirectory(title_dir));
}
}
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index cfc39916db..1f7315f3c1 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -549,6 +549,8 @@ struct Memory::Impl {
}
void RasterizerArmReadTraps(u64 vaddr, u64 size) {
+ // Clean cached read areas also need invalidation when reactive flushing is enabled.
+ read_trap_generation.fetch_add(1, std::memory_order_release);
if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) ||
!ReadTrapsEnabled()) {
return;
@@ -781,6 +783,19 @@ struct Memory::Impl {
gpu_device_memory = &system.Host1x().MemoryManager();
}
const size_t core = system.GetCurrentHostThreadID();
+ if (core >= Core::Hardware::NUM_CPU_CORES) {
+ // A service thread has no slot in the arrays below, and it does not use a slot of a
+ // core. It keeps no area, so it asks on each read.
+ if (static std::atomic_flag logged; !logged.test_and_set(std::memory_order_relaxed)) {
+ LOG_INFO(HW_Memory, "Read of cached guest memory from a thread that is not a core");
+ }
+ Common::ScratchBuffer<u32> scratch;
+ gpu_device_memory->ApplyOpOnPointer(p, scratch, [&](DAddr address) {
+ DisarmReadTraps(v_address, size);
+ (void)system.GPU().OnCPURead(address, size);
+ });
+ return;
+ }
auto& current_area = rasterizer_read_areas[core];
if (const u64 generation = read_trap_generation.load(std::memory_order_acquire);
read_area_generations[core] != generation) {
diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp
index 620e0cf630..a7840db32f 100644
--- a/src/input_common/drivers/udp_client.cpp
+++ b/src/input_common/drivers/udp_client.cpp
@@ -603,7 +603,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob(
const std::string& host, u16 port, std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback) {
- std::thread([=, this] {
+ worker = std::jthread([=, this] {
u16 min_x{UINT16_MAX};
u16 min_y{UINT16_MAX};
u16 max_x{};
@@ -647,7 +647,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob(
complete_event.Wait();
socket.Stop();
worker_thread.join();
- }).detach();
+ });
}
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h
index cea9f579ac..eac3df07aa 100644
--- a/src/input_common/drivers/udp_client.h
+++ b/src/input_common/drivers/udp_client.h
@@ -183,6 +183,7 @@ public:
private:
Common::Event complete_event;
+ std::jthread worker;
};
void TestCommunication(const std::string& host, u16 port,
diff --git a/src/shader_recompiler/ir_opt/texture_pass.cpp b/src/shader_recompiler/ir_opt/texture_pass.cpp
index 8a72f565dc..b6aca0a118 100644
--- a/src/shader_recompiler/ir_opt/texture_pass.cpp
+++ b/src/shader_recompiler/ir_opt/texture_pass.cpp
@@ -16,6 +16,7 @@
#include <limits>
#include <boost/container/small_vector.hpp>
+#include "common/scope_exit.h"
#include "common/settings.h"
#include "shader_recompiler/environment.h"
#include "shader_recompiler/frontend/ir/basic_block.h"
@@ -345,7 +346,8 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
// Cached variants of existing helpers
// A handle computed per draw has no descriptor to read at translation time: assume an ordinary
-// floating-point 2D texture, the instruction's own type deciding the rest.
+// floating-point 2D texture, the instruction's own type deciding the rest (see the TexturePass
+// ImageQueryDimensions case, which has none).
static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) {
if (cbuf.handle_program != ~0U) return TextureType::Color2D;
return env.ReadTextureType(GetTextureHandleCached(env, cbuf));
@@ -436,9 +438,10 @@ bool IsUndefined(IR::Opcode opcode) {
opcode == IR::Opcode::UndefU64;
}
-// Values already emitted by the program being built, by the slot that keeps them, and how deep
-// in nested BuildHandleProgram calls the build is.
+// Values already emitted by the program being built, by the slot that keeps them, how many slots
+// are in use, and how deep in nested BuildHandleProgram calls the build is.
thread_local std::unordered_map<const IR::Inst*, u32> g_handle_slots;
+thread_local u32 g_handle_slot_count{};
thread_local int g_handle_depth{};
bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget,
@@ -453,7 +456,15 @@ bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& bud
const bool outermost = g_handle_depth++ == 0;
if (outermost) {
g_handle_slots.clear();
+ g_handle_slot_count = 0;
}
+ SCOPE_EXIT {
+ --g_handle_depth;
+ if (outermost) {
+ g_handle_slots.clear();
+ g_handle_slot_count = 0;
+ }
+ };
const IR::Inst* const inst{value.InstRecursive()};
bool result;
if (const auto it = g_handle_slots.find(inst); it != g_handle_slots.end()) {
@@ -470,23 +481,25 @@ bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& bud
} else {
g_handle_slots.emplace(inst, ~0U);
const size_t start = code.size();
+ const u32 first_slot = g_handle_slot_count;
result = BuildHandleNode(value, code, budget, block);
g_handle_slots.erase(inst);
if (!result) {
+ // The Tee of every value the dropped code kept goes with it: a later use of one of
+ // them (another Phi input) must build it again, not load a slot nothing wrote.
code.resize(start);
+ std::erase_if(g_handle_slots, [first_slot](const auto& entry) {
+ return entry.second != ~0U && entry.second >= first_slot;
+ });
+ g_handle_slot_count = first_slot;
}
if (result && inst->UseCount() > 1 && g_handle_slots.size() < 256) {
- const u32 slot = static_cast<u32>(
- std::ranges::count_if(g_handle_slots, [](const auto& entry) { return entry.second != ~0U; }));
+ const u32 slot = g_handle_slot_count++;
g_handle_slots.emplace(inst, slot);
code.push_back(static_cast<u32>(Op::Tee));
code.push_back(slot);
}
}
- --g_handle_depth;
- if (outermost) {
- g_handle_slots.clear();
- }
return result;
}
@@ -1010,7 +1023,8 @@ public:
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
desc.count == existing.count && desc.size_shift == existing.size_shift &&
- desc.has_secondary == existing.has_secondary;
+ desc.has_secondary == existing.has_secondary &&
+ desc.handle_program == existing.handle_program;
});
}
@@ -1018,7 +1032,8 @@ public:
const u32 index{Add(image_buffer_descriptors, desc, [&desc](const auto& existing) {
return desc.format == existing.format && desc.cbuf_index == existing.cbuf_index &&
desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count &&
- desc.size_shift == existing.size_shift;
+ desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
image_buffer_descriptors[index].is_written |= desc.is_written;
image_buffer_descriptors[index].is_read |= desc.is_read;
@@ -1036,7 +1051,8 @@ public:
desc.secondary_cbuf_index == existing.secondary_cbuf_index &&
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
- desc.count == existing.count && desc.size_shift == existing.size_shift;
+ desc.count == existing.count && desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
// TODO: Read this from TIC
texture_descriptors[index].is_multisample |= desc.is_multisample;
@@ -1049,7 +1065,8 @@ public:
return desc.type == existing.type && desc.format == existing.format &&
desc.cbuf_index == existing.cbuf_index &&
desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count &&
- desc.size_shift == existing.size_shift;
+ desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
image_descriptors[index].is_written |= desc.is_written;
image_descriptors[index].is_read |= desc.is_read;
@@ -1171,6 +1188,10 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
u32 last_cbuf_offset{};
bool last_is_integer{false};
const auto is_texture_pixel_format_integer{[&](const ConstBufferAddr& cbuf_addr) {
+ if (cbuf_addr.handle_program != ~0U) {
+ // Every handle program is at cbuf 0 offset 0: the cache below can't tell them apart.
+ return IsTexturePixelFormatIntegerCached(env, cbuf_addr);
+ }
if (has_last_is_integer && last_cbuf_index == cbuf_addr.index &&
last_cbuf_offset == cbuf_addr.offset) {
return last_is_integer;
@@ -1181,6 +1202,18 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
has_last_is_integer = true;
return last_is_integer;
}};
+ // A handle program has no descriptor to read a type from, and ImageQueryDimensions carries
+ // none: it takes the type of another instruction using the same handle.
+ std::unordered_map<u32, TextureType> handle_program_types;
+ for (const TextureInst& texture_inst : to_replace) {
+ const IR::Opcode opcode{IndexedInstruction(*texture_inst.inst)};
+ if (texture_inst.cbuf.handle_program != ~0U && opcode != IR::Opcode::ImageQueryDimensions &&
+ !IsStorageImageOpcode(opcode)) {
+ handle_program_types.try_emplace(
+ texture_inst.cbuf.handle_program,
+ texture_inst.inst->Flags<IR::TextureInstInfo>().type.Value());
+ }
+ }
for (TextureInst& texture_inst : to_replace) {
// TODO: Handle arrays
IR::Inst* const inst{texture_inst.inst};
@@ -1191,7 +1224,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
bool is_multisample{false};
switch (inst->GetOpcode()) {
case IR::Opcode::ImageQueryDimensions:
- flags.type.Assign(ReadTextureTypeCached(env, cbuf));
+ if (const auto it{handle_program_types.find(cbuf.handle_program)};
+ it != handle_program_types.end()) {
+ flags.type.Assign(it->second);
+ } else {
+ flags.type.Assign(ReadTextureTypeCached(env, cbuf));
+ }
inst->SetFlags(flags);
break;
case IR::Opcode::ImageSampleImplicitLod:
@@ -1256,6 +1294,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_offset = cbuf.offset,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
} else {
index = descriptors.Add(ImageDescriptor{
@@ -1268,6 +1307,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_offset = cbuf.offset,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
}
break;
@@ -1284,6 +1324,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_shift_left = cbuf.secondary_shift_left,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
} else {
count = std::min(count, sampled_dynamic_cap);
diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h
index eeb8d768d7..e2b341b0c3 100644
--- a/src/shader_recompiler/shader_info.h
+++ b/src/shader_recompiler/shader_info.h
@@ -189,6 +189,9 @@ struct TextureBufferDescriptor {
u32 secondary_shift_left;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const TextureBufferDescriptor&) const = default;
};
@@ -203,6 +206,9 @@ struct ImageBufferDescriptor {
u32 cbuf_offset;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const ImageBufferDescriptor&) const = default;
};
@@ -240,6 +246,9 @@ struct ImageDescriptor {
u32 cbuf_offset;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const ImageDescriptor&) const = default;
};
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index a9be2ec7a8..1eb1040e34 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -9,19 +9,26 @@ add_executable(tests
common/cityhash.cpp
common/container_hash.cpp
common/fibers.cpp
+ common/game_presets.cpp
common/host_memory.cpp
common/param_package.cpp
common/range_map.cpp
common/ring_buffer.cpp
common/scratch_buffer.cpp
+ common/slot_vector.cpp
common/undefined_fix.cpp
common/unique_function.cpp
core/core_timing.cpp
core/nup_writer.cpp
+ core/priority_queue.cpp
core/internal_network/network.cpp
video_core/memory_tracker.cpp
video_core/vertex_repack.cpp
video_core/ampere_returns.cpp
+ video_core/native_macro_fifo.cpp
+ video_core/uniform_stream_reuse.cpp
+ video_core/shader_invalidation.cpp
+ video_core/descriptor_table.cpp
input_common/calibration_configuration_job.cpp
)
diff --git a/src/tests/common/game_presets.cpp b/src/tests/common/game_presets.cpp
new file mode 100644
index 0000000000..481f5c5e1d
--- /dev/null
+++ b/src/tests/common/game_presets.cpp
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <memory>
+#include <catch2/catch_test_macros.hpp>
+#include "common/game_presets.h"
+
+namespace {
+void SetConflictingPreferences(Settings::Values& settings, bool per_game) {
+ const auto set = [per_game](auto& setting, auto value) {
+ setting.SetGlobal(!per_game);
+ setting.SetValue(value);
+ };
+ set(settings.renderer_backend, Settings::RendererBackend::Null);
+ set(settings.resolution_setup, Settings::ResolutionSetup::Res2X);
+ set(settings.gpu_accuracy, Settings::GpuAccuracy::High);
+ set(settings.use_asynchronous_shaders, false);
+ set(settings.astc_recompression, Settings::AstcRecompression::Uncompressed);
+ set(settings.vram_usage_mode, Settings::VramUsageMode::Aggressive);
+ set(settings.use_reactive_flushing, true);
+ set(settings.vsync_mode, Settings::VSyncMode::Immediate);
+ set(settings.scaling_filter, Settings::ScalingFilter::NearestNeighbor);
+ set(settings.anti_aliasing, Settings::AntiAliasing::Smaa);
+}
+
+void CheckPreset(const Settings::Values& settings) {
+ CHECK(settings.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan);
+ CHECK(settings.resolution_setup.GetValue() == Settings::ResolutionSetup::Res1X);
+ CHECK(settings.gpu_accuracy.GetValue() == Settings::GpuAccuracy::Low);
+ CHECK(settings.use_asynchronous_shaders.GetValue());
+ CHECK(settings.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3);
+ CHECK(settings.vram_usage_mode.GetValue() == Settings::VramUsageMode::Conservative);
+ CHECK_FALSE(settings.use_reactive_flushing.GetValue());
+ CHECK(settings.vsync_mode.GetValue() == Settings::VSyncMode::FifoRelaxed);
+ CHECK(settings.use_docked_mode.GetValue() == Settings::ConsoleMode::Handheld);
+ CHECK(settings.scaling_filter.GetValue() == Settings::ScalingFilter::ScaleForce);
+ CHECK(settings.anti_aliasing.GetValue() == Settings::AntiAliasing::Fxaa);
+}
+
+void CheckPreferences(const Settings::Values& settings, bool global = false) {
+ CHECK(settings.renderer_backend.GetValue(global) == Settings::RendererBackend::Null);
+ CHECK(settings.resolution_setup.GetValue(global) == Settings::ResolutionSetup::Res2X);
+ CHECK(settings.gpu_accuracy.GetValue(global) == Settings::GpuAccuracy::High);
+ CHECK_FALSE(settings.use_asynchronous_shaders.GetValue(global));
+ CHECK(settings.astc_recompression.GetValue(global) ==
+ Settings::AstcRecompression::Uncompressed);
+ CHECK(settings.vram_usage_mode.GetValue(global) == Settings::VramUsageMode::Aggressive);
+ CHECK(settings.use_reactive_flushing.GetValue(global));
+ CHECK(settings.vsync_mode.GetValue(global) == Settings::VSyncMode::Immediate);
+ CHECK(settings.scaling_filter.GetValue(global) == Settings::ScalingFilter::NearestNeighbor);
+ CHECK(settings.anti_aliasing.GetValue(global) == Settings::AntiAliasing::Smaa);
+}
+} // namespace
+
+TEST_CASE("Display filter defaults use ScaleForce and FXAA", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ CHECK(settings->scaling_filter.GetDefault() == Settings::ScalingFilter::ScaleForce);
+ CHECK(settings->anti_aliasing.GetDefault() == Settings::AntiAliasing::Fxaa);
+}
+
+TEST_CASE("Accepted MKW settings replace visible global preferences", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ Settings::SetMarioKartWorldRecommendedSettings(*settings, true);
+ CheckPreset(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->gpu_accuracy.UsingGlobal());
+ CHECK(settings->renderer_backend.GetValue(true) == Settings::RendererBackend::Vulkan);
+ CHECK(settings->gpu_accuracy.GetValue(true) == Settings::GpuAccuracy::Low);
+}
+
+TEST_CASE("MKW preset overrides saved settings for the session", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ SetConflictingPreferences(*settings, true);
+ REQUIRE(
+ Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true));
+ CheckPreset(*settings);
+ CheckPreferences(*settings, true);
+ for (const auto& restore : settings->linkage.restore_functions) {
+ restore();
+ }
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->resolution_setup.UsingGlobal());
+ REQUIRE(
+ Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true));
+ CheckPreset(*settings);
+}
+
+TEST_CASE("MKW preset does not change unrelated games", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, 0, true));
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings,
+ Settings::MarioKartWorldProgramId + 1, true));
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->resolution_setup.UsingGlobal());
+}
+
+TEST_CASE("Declining the MKW preset preserves global and per-game preferences", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ for (bool per_game : {false, true}) {
+ SetConflictingPreferences(*settings, per_game);
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings,
+ Settings::MarioKartWorldProgramId, false));
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal() == !per_game);
+ CHECK(settings->resolution_setup.UsingGlobal() == !per_game);
+ }
+}
diff --git a/src/tests/common/slot_vector.cpp b/src/tests/common/slot_vector.cpp
new file mode 100644
index 0000000000..d8f242085c
--- /dev/null
+++ b/src/tests/common/slot_vector.cpp
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <map>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "common/slot_vector.h"
+
+TEST_CASE("SlotVector visits live entries across empty bitmap words", "[common]") {
+ Common::SlotVector<u32> slots;
+ std::vector<Common::SlotId> ids;
+ for (u32 i = 0; i < 513; ++i) {
+ ids.push_back(slots.insert(i));
+ }
+ for (u32 i = 0; i < 512; ++i) {
+ slots.erase(ids[i]);
+ }
+ auto it = slots.begin();
+ REQUIRE(it != slots.end());
+ REQUIRE((*it).first == ids[512]);
+ REQUIRE(*(*it).second == 512);
+ REQUIRE(++it == slots.end());
+ slots.erase(ids[512]);
+ REQUIRE(slots.begin() == slots.end());
+}
+
+TEST_CASE("SlotVector sparse iteration matches a live-entry model", "[common]") {
+ Common::SlotVector<u32> slots;
+ std::map<u32, u32> live;
+ std::mt19937 random{9813};
+ for (u32 batch = 0; batch < 200; ++batch) {
+ for (u32 i = 0; i < 100; ++i) {
+ if (!live.empty() && (random() % 3) == 0) {
+ auto selected = live.begin();
+ std::advance(selected, random() % live.size());
+ slots.erase(Common::SlotId{selected->first});
+ live.erase(selected);
+ } else {
+ const u32 value = random();
+ const auto id = slots.insert(value);
+ live.emplace(id.Value(), value);
+ }
+ }
+ auto expected = live.begin();
+ for (const auto [id, value] : slots) {
+ REQUIRE(expected != live.end());
+ REQUIRE(id.Value() == expected->first);
+ REQUIRE(*value == expected->second);
+ ++expected;
+ }
+ REQUIRE(expected == live.end());
+ }
+}
diff --git a/src/tests/core/core_timing.cpp b/src/tests/core/core_timing.cpp
index 81898a1d38..6b424cc52c 100644
--- a/src/tests/core/core_timing.cpp
+++ b/src/tests/core/core_timing.cpp
@@ -33,6 +33,9 @@ std::optional<std::chrono::nanoseconds> HostCallbackTemplate(s64 time,
struct ScopeInit final {
ScopeInit() {
+ callbacks_ran_flags.reset();
+ delays.fill(0);
+ expected_callback = 0;
core_timing.SetMulticore(true);
core_timing.Initialize([]() {});
}
diff --git a/src/tests/core/nup_writer.cpp b/src/tests/core/nup_writer.cpp
index c0230d8694..3167a28baf 100644
--- a/src/tests/core/nup_writer.cpp
+++ b/src/tests/core/nup_writer.cpp
@@ -19,6 +19,7 @@
#include "core/file_sys/nup_container.h"
#include "core/file_sys/nup_writer.h"
#include "core/file_sys/vfs/vfs_real.h"
+#include "core/file_sys/vfs/vfs_vector.h"
namespace {
@@ -173,6 +174,23 @@ void MakeDump(const fs::path& root) {
Put(root / "dlc/0400ABCD00011001/0/romfs/addon.bin", "addon data");
}
+void SetLe(std::string& buf, size_t at, u64 value, size_t bytes) {
+ for (size_t i = 0; i < bytes; ++i) {
+ buf[at + i] = static_cast<char>(value >> (8 * i));
+ }
+}
+
+// Applies `patch` to a valid archive, fixes up the TOC hash and opens the result.
+template <typename Patch>
+Loader::ResultStatus StatusAfterPatch(std::string nup, Patch&& patch) {
+ const u64 toc = Le(nup, 0x08, 8);
+ patch(nup, toc);
+ const auto hash = Sha256(std::string_view{nup}.substr(toc, Le(nup, 0x10, 8)));
+ std::copy(hash.begin(), hash.end(), nup.begin() + 0x18);
+ const auto data = std::vector<u8>(nup.begin(), nup.end());
+ return NupContainer{std::make_shared<FileSys::VectorVfsFile>(data, "bad.nup")}.GetStatus();
+}
+
} // namespace
TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContainer", "[nup]") {
@@ -197,9 +215,11 @@ TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContain
REQUIRE(packages[0].kind == NupContainer::PackageKind::Base);
REQUIRE(packages[0].title_id == TitleId);
REQUIRE(packages[0].version == 0);
+ REQUIRE(packages[0].display_version == "1.0.0");
REQUIRE(packages[0].npdm.size() == 0x100);
REQUIRE(packages[1].kind == NupContainer::PackageKind::Update);
REQUIRE(packages[1].version == 65536);
+ REQUIRE(packages[1].display_version == "1.1.0");
REQUIRE(packages[1].title_id == TitleId);
REQUIRE_FALSE(packages[1].standalone_update);
REQUIRE(packages[2].kind == NupContainer::PackageKind::Update);
@@ -253,6 +273,102 @@ TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContain
REQUIRE_FALSE(by_name.contains(".listing"));
}
+TEST_CASE("NUP reads zstd members above the memory cache through a temporary file", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ std::string large(300 * 1024, '\0');
+ u32 x = 7;
+ for (auto& c : large) {
+ x = x * 1664525 + 1013904223;
+ c = static_cast<char>('a' + (x >> 28));
+ }
+ Put(root / "Base/RomFS/large.bin", large);
+ const auto archive = t.path / "Game.nup";
+ FileSys::Nup::PackOptions options{.raw_threshold = 1024 * 1024};
+ REQUIRE(FileSys::Nup::PackDump(root, archive, options) == FileSys::Nup::PackResult::Success);
+ for (const auto& file : RawFiles(ReadAll(archive))) {
+ if (file.name == "large.bin") {
+ REQUIRE(file.codec == 1);
+ }
+ }
+
+ const auto spill_dir = t.path / "spill";
+ FileSys::RealVfsFilesystem real;
+ {
+ // Members above 32 KiB do not fit the 64 KiB cache budget and are spilled to disk.
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read), 64 * 1024,
+ spill_dir};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ const auto file = nup.GetRomFSDir(0)->GetFile("large.bin");
+ REQUIRE(file != nullptr);
+ REQUIRE(file->GetSize() == large.size());
+
+ std::string chunk(4096, '\0');
+ for (size_t offset = 0; offset < large.size(); offset += chunk.size()) {
+ const size_t count =
+ file->Read(reinterpret_cast<u8*>(chunk.data()), chunk.size(), offset);
+ REQUIRE(count == std::min(chunk.size(), large.size() - offset));
+ REQUIRE(chunk.compare(0, count, large, offset, count) == 0);
+ }
+ for (const size_t offset : {size_t{0}, size_t{123456}, size_t{77}, large.size() - 5}) {
+ const size_t count = file->Read(reinterpret_cast<u8*>(chunk.data()), 100, offset);
+ REQUIRE(count == std::min<size_t>(100, large.size() - offset));
+ REQUIRE(chunk.compare(0, count, large, offset, count) == 0);
+ }
+ REQUIRE(file->Read(reinterpret_cast<u8*>(chunk.data()), 10, large.size()) == 0);
+
+ REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) > 0);
+ REQUIRE(PackedTree(nup.GetRomFSDir(0)) == SourceTree(root / "Base/RomFS"));
+ REQUIRE(nup.VerifyIntegrity({}) == Loader::ResultStatus::Success);
+ }
+ REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) == 0);
+}
+
+TEST_CASE("NUP rejects a TOC whose counts or offsets wrap", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+ const auto nup = ReadAll(archive);
+ const auto no_patch = [](std::string&, u64) {};
+ REQUIRE(StatusAfterPatch(nup, no_patch) == Loader::ResultStatus::Success);
+
+ // 2^61 directories wrap dir_count * 0x18 back to the real record size.
+ REQUIRE(StatusAfterPatch(nup, [](std::string& buf, u64 toc) {
+ SetLe(buf, toc + 0x08, Le(buf, toc + 0x08, 8) + (1ULL << 61), 8);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+
+ const auto first_dir = [](const std::string& buf, u64 toc) {
+ return toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40;
+ };
+ // first_child + child_count wraps to a small value.
+ REQUIRE(StatusAfterPatch(nup, [&](std::string& buf, u64 toc) {
+ SetLe(buf, first_dir(buf, toc) + 0x08, 0xFFFFFFFF, 4);
+ SetLe(buf, first_dir(buf, toc) + 0x0C, 2, 4);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+}
+
+TEST_CASE("NUP rejects a directory that contains its own ancestor", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+
+ // Give the base RomFS directory the same children as the package root, which lists it.
+ REQUIRE(StatusAfterPatch(ReadAll(archive), [](std::string& buf, u64 toc) {
+ const u64 dirs = toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40;
+ const u64 root_dir = Le(buf, toc + 0x30 + 0x08, 4);
+ const u64 romfs_dir = Le(buf, toc + 0x30 + 0x10, 4);
+ const u64 root_record = dirs + root_dir * 0x18;
+ const u64 romfs_record = dirs + romfs_dir * 0x18;
+ SetLe(buf, romfs_record + 0x08, Le(buf, root_record + 0x08, 4), 4);
+ SetLe(buf, romfs_record + 0x0C, Le(buf, root_record + 0x0C, 4), 4);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+}
+
TEST_CASE("NUP marks updates standalone when the dump has no base", "[nup]") {
TempDir t;
const auto root = t.path / "Game";
@@ -388,7 +504,8 @@ TEST_CASE("NUP output is reproducible", "[nup]") {
for (const u8 b : Sha256(ReadAll(archive))) {
hex += fmt::format("{:02x}", b);
}
- REQUIRE(hex == "df8446903ab2c39a51af0304935eb4aa5d8ead105440976ceae7ab97133f6143");
+ // The mixed-case Base/RomFS directory supplies base version 1.0.0 on every platform.
+ REQUIRE(hex == "718231e0974ca04c7be7bd6ed4334015b777c6e5f1ed51de23f899f81ee004ab");
}
TEST_CASE("NUP writes metadata overrides to the base and the newest update", "[nup]") {
diff --git a/src/tests/core/priority_queue.cpp b/src/tests/core/priority_queue.cpp
new file mode 100644
index 0000000000..0d7174f793
--- /dev/null
+++ b/src/tests/core/priority_queue.cpp
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "core/hle/kernel/k_priority_queue.h"
+
+namespace {
+struct Affinity {
+ u64 GetAffinityMask() const {
+ return 3;
+ }
+ void SetAffinityMask(u64) {}
+ bool GetAffinity(s32) const {
+ return true;
+ }
+ void SetAffinity(s32, bool) {}
+ void SetAll() {}
+};
+
+struct Member {
+ struct QueueEntry {
+ Member* prev{};
+ Member* next{};
+ void Initialize() {
+ prev = next = nullptr;
+ }
+ void SetPrev(Member* value) {
+ prev = value;
+ }
+ void SetNext(Member* value) {
+ next = value;
+ }
+ Member* GetNext() const {
+ return next;
+ }
+ Member* GetPrev() const {
+ return prev;
+ }
+ };
+ std::array<QueueEntry, 2> entries{};
+ s32 priority{};
+ QueueEntry& GetPriorityQueueEntry(s32 core) {
+ return entries[core];
+ }
+ const QueueEntry& GetPriorityQueueEntry(s32 core) const {
+ return entries[core];
+ }
+ Affinity GetAffinityMask() const {
+ return {};
+ }
+ s32 GetActiveCore() const {
+ return 0;
+ }
+ s32 GetPriority() const {
+ return priority;
+ }
+ bool IsDummyThread() const {
+ return false;
+ }
+};
+
+template <size_t Priorities>
+void CheckQueue() {
+ using Queue = Kernel::KPriorityQueue<Member, 2, Priorities - 1, 0>;
+ typename Queue::KPriorityQueueImpl queue;
+ std::array<Member, 160> members{};
+ std::array<std::array<bool, 2>, 160> present{};
+ std::array<std::array<std::vector<Member*>, Priorities>, 2> model{};
+ std::mt19937 random{6152};
+ for (size_t i = 0; i < members.size(); ++i) {
+ members[i].priority = static_cast<s32>(i % Priorities);
+ }
+ REQUIRE(queue.GetFront(0) == nullptr);
+ for (u32 operation = 0; operation < 3000; ++operation) {
+ const size_t index = random() % members.size();
+ const s32 core = random() % 2;
+ auto& member = members[index];
+ auto& expected = model[core][member.priority];
+ if (present[index][core]) {
+ queue.Remove(member.priority, core, &member);
+ std::erase(expected, &member);
+ } else if ((random() & 1) != 0) {
+ queue.PushFront(member.priority, core, &member);
+ expected.insert(expected.begin(), &member);
+ } else {
+ queue.PushBack(member.priority, core, &member);
+ expected.push_back(&member);
+ }
+ present[index][core] = !present[index][core];
+ for (s32 checked_core = 0; checked_core < 2; ++checked_core) {
+ Member* actual = queue.GetFront(checked_core);
+ for (const auto& priority : model[checked_core]) {
+ for (Member* wanted : priority) {
+ REQUIRE(actual == wanted);
+ actual = queue.GetNext(checked_core, actual);
+ }
+ }
+ REQUIRE(actual == nullptr);
+ }
+ }
+}
+} // namespace
+
+TEST_CASE("Priority queue bitmap traversal preserves thread order", "[core]") {
+ CheckQueue<64>();
+ CheckQueue<128>();
+}
diff --git a/src/tests/input_common/calibration_configuration_job.cpp b/src/tests/input_common/calibration_configuration_job.cpp
index 6ded0dfc86..c45ef157a5 100644
--- a/src/tests/input_common/calibration_configuration_job.cpp
+++ b/src/tests/input_common/calibration_configuration_job.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
+#include <memory>
#include <string>
#include <thread>
#include <utility>
@@ -19,7 +20,7 @@
class FakeCemuhookServer {
public:
FakeCemuhookServer()
- : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
+ : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::loopback(), 0)) {}
~FakeCemuhookServer() {
boost::system::error_code error_code;
@@ -132,3 +133,20 @@ TEST_CASE("CalibrationConfigurationJob completed", "[input_common]") {
REQUIRE(max_x == 200);
REQUIRE(max_y == 200);
}
+
+TEST_CASE("CalibrationConfigurationJob cancellation joins its worker", "[input_common]") {
+ FakeCemuhookServer server;
+ for (unsigned i = 0; i < 32; ++i) {
+ auto lifetime = std::make_shared<int>(0);
+ std::weak_ptr<int> observer = lifetime;
+ {
+ InputCommon::CemuhookUDP::CalibrationConfigurationJob job(
+ server.GetHost(), server.GetPort(),
+ [lifetime](InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status) {},
+ [](u16, u16, u16, u16) {});
+ lifetime.reset();
+ job.Stop();
+ }
+ REQUIRE(observer.expired());
+ }
+}
diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp
new file mode 100644
index 0000000000..8b20564949
--- /dev/null
+++ b/src/tests/video_core/descriptor_table.cpp
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <chrono>
+#include <cstring>
+#include <iostream>
+#include <limits>
+#include <random>
+#include <catch2/catch_test_macros.hpp>
+#include "video_core/texture_cache/descriptor_table.h"
+
+namespace {
+struct DescriptorMemory {
+ std::array<std::array<u64, 128>, 2> banks{};
+
+ void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const {
+ const auto bank = (address - 0x1000) / 0x1000;
+ const auto offset = (address - 0x1000) % 0x1000;
+ std::memcpy(output, reinterpret_cast<const u8*>(banks[bank].data()) + offset, size);
+ }
+};
+} // namespace
+
+TEST_CASE("Descriptor invalidation matches eager refresh across mutations", "[descriptor_table]") {
+ DescriptorMemory memory;
+ VideoCommon::DescriptorTable<u64> table;
+ REQUIRE(table.Synchronize(0x1000, 127));
+ std::array<bool, 128> seen{};
+ std::array<u64, 128> previous{};
+ std::mt19937 random{0x5080};
+ unsigned bank{};
+ u32 limit = 127;
+ for (unsigned i = 0; i < 40000; ++i) {
+ switch (random() % 5) {
+ case 0:
+ table.Invalidate();
+ seen.fill(false);
+ break;
+ case 1:
+ memory.banks[random() % 2][random() % 128] = random();
+ break;
+ case 2: {
+ const auto next_bank = random() % 2;
+ const u32 next_limit = random() % 2 ? 63 : 127;
+ const bool changed = next_bank != bank || next_limit != limit;
+ REQUIRE(table.Synchronize(0x1000 + next_bank * 0x1000, next_limit) == changed);
+ if (changed) {
+ seen.fill(false);
+ }
+ bank = next_bank;
+ limit = next_limit;
+ break;
+ }
+ default: {
+ const u32 index = random() % (limit + 1);
+ const u64 expected = memory.banks[bank][index];
+ const bool changed = !seen[index] || previous[index] != expected;
+ const auto actual = table.Read(memory, index);
+ REQUIRE(actual.first == expected);
+ REQUIRE(actual.second == changed);
+ seen[index] = true;
+ previous[index] = expected;
+ }
+ }
+ }
+}
+
+TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[descriptor_table]") {
+ DescriptorMemory memory;
+ std::array<VideoCommon::DescriptorTable<u64>, 5> channels;
+ std::array<std::array<unsigned, 128>, 5> view_ids{};
+ unsigned live_generation = 1;
+ const auto visit = [&](size_t channel, u32 index) {
+ if (channels[channel].Read(memory, index).second) {
+ view_ids[channel][index] = live_generation;
+ }
+ return view_ids[channel][index];
+ };
+ for (size_t channel = 0; channel < channels.size(); ++channel) {
+ REQUIRE(channels[channel].Synchronize(0x1000, 127));
+ for (u32 index = 0; index < 128; ++index) {
+ REQUIRE(visit(channel, index) == live_generation);
+ }
+ }
+ for (unsigned generation = 0; generation < 32; ++generation) {
+ ++live_generation;
+ for (unsigned deletion = 0; deletion < 50; ++deletion) {
+ for (auto& table : channels) {
+ table.Invalidate();
+ }
+ }
+ for (size_t channel = 0; channel < channels.size(); ++channel) {
+ for (u32 index = 0; index < 128; ++index) {
+ REQUIRE(visit(channel, index) == live_generation);
+ REQUIRE_FALSE(channels[channel].Read(memory, index).second);
+ }
+ }
+ }
+}
+
+TEST_CASE("Descriptor limits clear exposed slots after shrink and growth", "[descriptor_table]") {
+ struct AddressMemory {
+ void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const {
+ std::memcpy(output, &address, size);
+ }
+ } memory;
+ constexpr std::array<u32, 9> limits{0, 63, 64, 127, 128, 0x7ffff, 0x80000, 0x80001, 0x100000};
+ for (const u32 limit : limits) {
+ VideoCommon::DescriptorTable<u64> table;
+ REQUIRE(table.Synchronize(0x1000, limit));
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ table.Refresh(0x1000, 0);
+ table.Invalidate();
+ CHECK(table.Read(memory, 0).second);
+ CHECK_FALSE(table.Read(memory, 0).second);
+ table.Refresh(0x1000, limit);
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ table.Invalidate();
+ table.Invalidate();
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ }
+ for (const u32 limit :
+ {std::numeric_limits<u32>::max(), std::numeric_limits<u32>::max() - 0x1000}) {
+ VideoCommon::DescriptorTable<u64> table;
+ table.Refresh(0x1000, limit);
+ CHECK(table.Read(memory, 0).second);
+ table.Invalidate();
+ CHECK(table.Read(memory, 0).second);
+ CHECK_FALSE(table.Read(memory, 0).second);
+ }
+}
+
+TEST_CASE("Descriptor invalidation CPU benchmark", "[.][cache_bench]") {
+ DescriptorMemory memory;
+ const auto measure = [&](bool deferred) {
+ std::array<VideoCommon::DescriptorTable<u64>, 5> tables;
+ for (auto& table : tables) {
+ REQUIRE(table.Synchronize(0x1000, 127));
+ }
+ void (*volatile invalidate)(VideoCommon::DescriptorTable<u64>&) =
+ deferred ? +[](VideoCommon::DescriptorTable<u64>& table) { table.Invalidate(); }
+ : +[](VideoCommon::DescriptorTable<u64>& table) {
+ std::ranges::fill(table.read_descriptors, 0);
+ };
+ size_t changed{};
+ const auto start = std::chrono::steady_clock::now();
+ for (unsigned batch = 0; batch < 2000; ++batch) {
+ for (unsigned deletion = 0; deletion < 50; ++deletion) {
+ for (auto& table : tables) {
+ invalidate(table);
+ }
+ }
+ for (auto& table : tables) {
+ changed += table.Read(memory, 0).second;
+ }
+ }
+ const auto ms =
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ REQUIRE(changed == 10000);
+ return ms;
+ };
+ const double eager = measure(false);
+ const double deferred = measure(true);
+ std::cout << "Descriptor bitmap invalidation: eager=" << eager << " ms deferred=" << deferred
+ << " ms (CPU only, not gameplay FPS)\n";
+}
diff --git a/src/tests/video_core/memory_tracker.cpp b/src/tests/video_core/memory_tracker.cpp
index c2236848b5..3ac4cee840 100644
--- a/src/tests/video_core/memory_tracker.cpp
+++ b/src/tests/video_core/memory_tracker.cpp
@@ -30,6 +30,10 @@ class RasterizerInterface {
public:
void UpdatePagesCachedCount(DAddr addr, size_t size, s32 delta) {
++update_calls;
+ ApplyPagesCachedCount(addr, size, delta);
+ }
+
+ void ApplyPagesCachedCount(DAddr addr, size_t size, s32 delta) {
calls.emplace_back(addr, size, delta);
const u64 page_start{addr >> Core::DEVICE_PAGEBITS};
const u64 page_end{(addr + size + Core::DEVICE_PAGESIZE - 1) >> Core::DEVICE_PAGEBITS};
@@ -45,7 +49,13 @@ public:
}
void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta) {
- // TODO: for now assume fine?
+ if (ranges.empty()) {
+ return;
+ }
+ ++update_calls;
+ for (const auto& [addr, size] : ranges) {
+ ApplyPagesCachedCount(addr, size, delta);
+ }
}
[[nodiscard]] size_t UpdateCalls() const noexcept { return update_calls; }
@@ -557,20 +567,32 @@ TEST_CASE("MemoryTracker: Cached write downloads") {
REQUIRE(rasterizer.Count() == 0);
}
-TEST_CASE("MemoryTracker: FlushCachedWrites batching") {
+TEST_CASE("MemoryTracker: cached writes preserve tracking counts") {
RasterizerInterface rasterizer;
std::optional<MemoryTracker> memory_track(rasterizer);
memory_track->UnmarkRegionAsCpuModified(c, WORD * 2);
+ REQUIRE(rasterizer.UpdateCalls() == 1);
+ REQUIRE(rasterizer.UpdateCallsList().size() == 1);
+ REQUIRE(rasterizer.UpdateCallsList().front() == std::tuple{c, WORD * 2, 1});
+
memory_track->CachedCpuWrite(c + PAGE, PAGE);
memory_track->CachedCpuWrite(c + PAGE * 2, PAGE);
memory_track->CachedCpuWrite(c + PAGE * 4, PAGE);
- REQUIRE(rasterizer.UpdateCalls() == 0);
+ REQUIRE(rasterizer.Count() == 125);
+ REQUIRE(rasterizer.Count(c + PAGE) == 0);
+ REQUIRE(rasterizer.Count(c + PAGE * 2) == 0);
+ REQUIRE(rasterizer.Count(c + PAGE * 3) == 1);
+ REQUIRE(rasterizer.Count(c + PAGE * 4) == 0);
+ const auto update_calls = rasterizer.UpdateCalls();
memory_track->FlushCachedWrites();
- // Now we expect a single batch call (coalesced ranges) to the device memory manager
- REQUIRE(rasterizer.UpdateCalls() == 1);
- const auto& calls = rasterizer.UpdateCallsList();
- REQUIRE(std::get<0>(calls[0]) == c + PAGE);
- REQUIRE(std::get<1>(calls[0]) == PAGE * 3);
+ REQUIRE(rasterizer.UpdateCalls() == update_calls);
+ REQUIRE(rasterizer.Count() == 125);
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE, PAGE));
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 2, PAGE));
+ REQUIRE_FALSE(memory_track->IsRegionCpuModified(c + PAGE * 3, PAGE));
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 4, PAGE));
+ memory_track->MarkRegionAsCpuModified(c, WORD * 2);
+ REQUIRE(rasterizer.Count() == 0);
}
TEST_CASE("DeviceMemoryManager: UpdatePagesCachedBatch basic") {
diff --git a/src/tests/video_core/native_macro_fifo.cpp b/src/tests/video_core/native_macro_fifo.cpp
new file mode 100644
index 0000000000..e19b146192
--- /dev/null
+++ b/src/tests/video_core/native_macro_fifo.cpp
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+
+#ifdef ARCHITECTURE_x86_64
+#include "video_core/macro_turing_jit.h"
+
+namespace {
+std::array<u32, 3> FifoInstruction(u32 method, bool end) {
+ std::array<u32, 3> bits{};
+ const auto field = [&](u32 offset, u32 count, u32 value) {
+ for (u32 bit = 0; bit < count; ++bit) {
+ bits[(offset + bit) / 32] |= ((value >> bit) & 1) << ((offset + bit) % 32);
+ }
+ };
+ field(0, 1, end);
+ for (u32 lane = 0; lane < 2; ++lane) {
+ field(15 + lane * 36, 5, 24);
+ field(20 + lane * 36, 5, 24);
+ field(25 + lane * 36, 5, 24);
+ }
+ field(30, 16, method);
+ field(82, 3, 5);
+ field(85, 4, 3);
+ return {bits[2], bits[1], bits[0]};
+}
+} // namespace
+
+TEST_CASE("Owned native macro FIFOs preserve loads across DMA appends and repeated calls",
+ "[video_core][native_macro]") {
+ std::vector<u32> code;
+ for (const auto instruction : {FifoInstruction(0x158, false), FifoInstruction(0x200, true),
+ FifoInstruction(0x201, false)}) {
+ code.insert(code.end(), instruction.begin(), instruction.end());
+ }
+ Tegra::TuringMacroJit jit;
+ jit.Prepare(code, 1);
+ std::array<u32, 0xc00> ram{};
+ std::vector<u32> parameters;
+ std::vector<u32> reference;
+ for (u32 invocation = 0; invocation < 1000; ++invocation) {
+ const u32 count = 2 + invocation % 127;
+ parameters.push_back(count);
+ reference.assign(parameters.begin(), parameters.end());
+ const auto run = [&](std::vector<u32>& input) {
+ std::vector<std::pair<u32, u32>> writes;
+ const auto result = jit.Execute(
+ 0, input, ram, [](u32) { return 0U; },
+ [&](u32 method, u32 value) {
+ writes.emplace_back(method, value);
+ if (method == 0x158) {
+ for (u32 word = 0; word < value; ++word) {
+ input.push_back(invocation * 256 + word);
+ }
+ }
+ return true;
+ });
+ CHECK(result.success);
+ CHECK(result.consumed == 3);
+ return writes;
+ };
+ const auto expected = run(reference);
+ const auto actual = run(parameters);
+ REQUIRE(actual.size() == 3);
+ CHECK(actual == expected);
+ CHECK(parameters == reference);
+ CHECK(actual[1] == std::pair<u32, u32>{0x200, invocation * 256});
+ CHECK(actual[2] == std::pair<u32, u32>{0x201, invocation * 256 + 1});
+ parameters.clear();
+ }
+}
+#endif
diff --git a/src/tests/video_core/shader_invalidation.cpp b/src/tests/video_core/shader_invalidation.cpp
new file mode 100644
index 0000000000..806fb5c58d
--- /dev/null
+++ b/src/tests/video_core/shader_invalidation.cpp
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "core/device_memory.h"
+#include "video_core/shader_cache.h"
+
+namespace VideoCommon {
+struct ShaderCacheTestAccess {
+ static std::unique_ptr<ShaderCache> Create(Tegra::MaxwellDeviceMemoryManager& memory) {
+ return std::unique_ptr<ShaderCache>{new ShaderCache{memory}};
+ }
+ static void Register(ShaderCache& cache, VAddr address, size_t size, u64 hash) {
+ auto data = std::make_unique<ShaderInfo>();
+ data->unique_hash = hash;
+ cache.Register(std::move(data), address, size);
+ }
+ static const ShaderInfo* Find(const ShaderCache& cache, VAddr address) {
+ return cache.TryGet(address);
+ }
+};
+} // namespace VideoCommon
+
+using VideoCommon::ShaderCacheTestAccess;
+
+TEST_CASE("Shader batch invalidation matches sequential cache contents", "[shader_invalidation]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto batched = ShaderCacheTestAccess::Create(device_memory);
+ auto sequential = ShaderCacheTestAccess::Create(device_memory);
+ std::mt19937 random{0x16'67};
+ constexpr VAddr base = 0x100000;
+ for (unsigned iteration = 0; iteration < 600; ++iteration) {
+ for (unsigned slot = 0; slot < 128; ++slot) {
+ const auto address = base + slot * 4096;
+ if (!ShaderCacheTestAccess::Find(*batched, address)) {
+ const auto size = 1 + random() % 65536;
+ ShaderCacheTestAccess::Register(*batched, address, size, iteration + slot);
+ ShaderCacheTestAccess::Register(*sequential, address, size, iteration + slot);
+ }
+ }
+ std::vector<std::pair<DAddr, size_t>> writes;
+ for (unsigned i = 0, count = random() % 24; i < count; ++i) {
+ writes.emplace_back(base + random() % (128 * 4096), random() % 32768);
+ if (i % 4 == 0) {
+ writes.push_back(writes.back());
+ }
+ }
+ for (const auto& [address, size] : writes) {
+ sequential->InvalidateRegion(address, size);
+ }
+ batched->InvalidateRegions(writes);
+ for (unsigned slot = 0; slot < 128; ++slot) {
+ const auto address = base + slot * 4096;
+ const auto actual = ShaderCacheTestAccess::Find(*batched, address);
+ const auto expected = ShaderCacheTestAccess::Find(*sequential, address);
+ REQUIRE(bool(actual) == bool(expected));
+ if (actual) {
+ REQUIRE(actual->unique_hash == expected->unique_hash);
+ }
+ }
+ }
+}
+
+TEST_CASE("Shader empty batches preserve pending invalidations", "[shader_invalidation]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto cache = ShaderCacheTestAccess::Create(device_memory);
+ constexpr VAddr address = 0x100000;
+ ShaderCacheTestAccess::Register(*cache, address, 32768, 123);
+ cache->OnCacheInvalidation(address, 1);
+ cache->InvalidateRegions({});
+ REQUIRE(ShaderCacheTestAccess::Find(*cache, address));
+ const std::array unrelated{std::pair<DAddr, size_t>{address + 0x100000, 4096}};
+ cache->InvalidateRegions(unrelated);
+ REQUIRE_FALSE(ShaderCacheTestAccess::Find(*cache, address));
+}
+
+TEST_CASE("Shader invalidation CPU benchmark", "[.][cache_bench]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto batched = ShaderCacheTestAccess::Create(device_memory);
+ auto sequential = ShaderCacheTestAccess::Create(device_memory);
+ std::vector<std::pair<DAddr, size_t>> writes;
+ constexpr VAddr base = 0x100000;
+ for (unsigned slot = 0; slot < 768; ++slot) {
+ writes.emplace_back(base + slot * 65536, 1);
+ }
+ double sequential_ms{}, batched_ms{};
+ for (unsigned repeat = 0; repeat < 20; ++repeat) {
+ for (unsigned slot = 0; slot < 1024; ++slot) {
+ const auto address = base + slot * 65536;
+ if (!ShaderCacheTestAccess::Find(*batched, address)) {
+ ShaderCacheTestAccess::Register(*batched, address, 64, slot);
+ ShaderCacheTestAccess::Register(*sequential, address, 64, slot);
+ }
+ }
+ auto start = std::chrono::steady_clock::now();
+ for (const auto& [address, size] : writes) {
+ sequential->InvalidateRegion(address, size);
+ }
+ sequential_ms +=
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ start = std::chrono::steady_clock::now();
+ batched->InvalidateRegions(writes);
+ batched_ms +=
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ REQUIRE_FALSE(ShaderCacheTestAccess::Find(*batched, base));
+ REQUIRE(ShaderCacheTestAccess::Find(*batched, base + 1023 * 65536));
+ }
+ std::cout << "Shader invalidation: sequential=" << sequential_ms << " ms batched=" << batched_ms
+ << " ms (CPU only, not gameplay FPS)\n";
+}
diff --git a/src/tests/video_core/uniform_stream_reuse.cpp b/src/tests/video_core/uniform_stream_reuse.cpp
new file mode 100644
index 0000000000..6059b70eeb
--- /dev/null
+++ b/src/tests/video_core/uniform_stream_reuse.cpp
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <span>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+
+#include "video_core/buffer_cache/uniform_stream_reuse.h"
+
+namespace {
+void Check(bool condition, const char* message) {
+ INFO(message);
+ REQUIRE(condition);
+}
+
+struct Memory {
+ std::array<u8, 8192> bytes{};
+ void ReadBlockUnsafe(u64 address, void* destination, u32 size) {
+ Check(address + size <= bytes.size(), "read outside guest memory");
+ std::memcpy(destination, bytes.data() + address, size);
+ }
+};
+
+struct Runtime {
+ struct StreamUniformRef {
+ size_t allocation;
+ };
+ std::vector<std::vector<u8>> allocations;
+ StreamUniformRef last{}, bound{};
+ u64 tick{1};
+ u64 CurrentTick() const {
+ return tick;
+ }
+ StreamUniformRef LastStreamUniformRef() const {
+ return last;
+ }
+ std::span<u8> BindMappedUniformBuffer(size_t, u32, u32 size) {
+ allocations.emplace_back(size);
+ last = bound = StreamUniformRef{allocations.size() - 1};
+ return allocations.back();
+ }
+ void RebindStreamUniform(StreamUniformRef ref, u32 size) {
+ Check(ref.allocation < allocations.size(), "reused a recycled allocation");
+ Check(allocations[ref.allocation].size() == size, "reused wrong allocation size");
+ bound = ref;
+ }
+ void NextTick() {
+ ++tick;
+ allocations.clear();
+ }
+};
+
+void CheckBound(const Runtime& runtime, const Memory& memory, u64 address, u32 size) {
+ const auto& bytes = runtime.allocations.at(runtime.bound.allocation);
+ Check(bytes.size() == size &&
+ std::memcmp(bytes.data(), memory.bytes.data() + address, size) == 0,
+ "bound uniform data differs from guest memory");
+}
+} // namespace
+
+TEST_CASE("Uniform stream reuse preserves data through mutation and session resets",
+ "[video_core]") {
+ using VideoCommon::BindSmallUniformReusing;
+ Memory memory;
+ Runtime runtime;
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ auto bind = [&](size_t stage, u32 slot, u64 address, u32 size) {
+ Check(BindSmallUniformReusing(runtime, memory, stage, slot, slot, address, size),
+ "valid small uniform rejected");
+ CheckBound(runtime, memory, address, size);
+ };
+ bind(0, 0, 0, 64);
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 1, "identical bytes were copied twice");
+ memory.bytes[7] = 23;
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 2, "changed bytes reused stale data");
+ bind(0, 1, 0, 64);
+ bind(1, 0, 0, 64);
+ Check(runtime.allocations.size() == 4, "stage or slot shared the wrong allocation");
+ runtime.NextTick();
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 1, "a new command buffer reused an old allocation");
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 2, "session reset retained an old allocation");
+ bind(4, 17, 0, 4096);
+ const auto before = runtime.allocations.size();
+ for (auto [stage, slot, size] :
+ {std::array<u32, 3>{5, 0, 64}, {0, 18, 64}, {0, 0, 0}, {0, 0, 4097}}) {
+ Check(!BindSmallUniformReusing(runtime, memory, stage, slot, slot, 0, size),
+ "invalid uniform range did not use the fallback");
+ }
+ Check(runtime.allocations.size() == before, "fallback allocated stream memory");
+
+ u32 random = 17;
+ for (u32 index = 0; index < 10000; ++index) {
+ random = random * 1664525U + 1013904223U;
+ if (index % 37 == 0)
+ runtime.NextTick();
+ if (index % 503 == 0)
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ const u32 size = 1 + random % 4096;
+ const u64 address = (random >> 12) % (memory.bytes.size() - 4096);
+ if (index % 3 == 0)
+ memory.bytes[address + random % size] ^= u8{0x5a};
+ const size_t stage = (random >> 20) % 5;
+ const u32 slot = (random >> 24) % 18;
+ bind(stage, slot, address, size);
+ bind(stage, slot, address, size);
+ }
+}
diff --git a/src/tests/video_core/vertex_repack.cpp b/src/tests/video_core/vertex_repack.cpp
index 6409be3ca4..5e21e3714c 100644
--- a/src/tests/video_core/vertex_repack.cpp
+++ b/src/tests/video_core/vertex_repack.cpp
@@ -76,16 +76,22 @@ TEST_CASE("Vertex repacking requires the captured tail and stride", "[vertex_rep
REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
}
-TEST_CASE("Static vertex repacking requires explicit experimental opt-in", "[vertex_repack]") {
+TEST_CASE("Static vertex repacking follows the device stride alignment", "[vertex_repack]") {
REQUIRE(Vulkan::VertexRepackInputSupported(true, false));
REQUIRE_FALSE(Vulkan::VertexRepackInputSupported(false, false));
REQUIRE(Vulkan::VertexRepackInputSupported(false, true));
+ REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected(nullptr, 1));
+ REQUIRE(Vulkan::StaticVertexRepackSelected(nullptr, 4));
+ REQUIRE(Vulkan::StaticVertexRepackSelected("1", 1));
+ REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected("0", 4));
+ REQUIRE(Vulkan::StaticVertexRepackSelected("other", 4));
}
TEST_CASE("Vertex repack planning keeps descriptor offsets aligned", "[vertex_repack]") {
for (const u32 alignment : {4U, 16U, 64U, 256U, 4096U}) {
for (const u32 offset : {0U, 1U, 3U, 255U, 4099U}) {
- const auto plan = Vulkan::MakeVertexRepackParameters(offset, 70, alignment, 1U << 20, 65535);
+ const auto plan = Vulkan::MakeVertexRepackParameters(offset, 70, alignment, 1U << 20,
+ 65535, 1U << 20);
REQUIRE(plan.has_value());
REQUIRE(plan->source_base % alignment == 0);
REQUIRE(plan->source_base + plan->source_bias == offset);
@@ -99,13 +105,22 @@ TEST_CASE("Vertex repack planning keeps descriptor offsets aligned", "[vertex_re
}
TEST_CASE("Vertex repack planning rejects overflow and device-limit violations", "[vertex_repack]") {
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0, 256, 1U << 20, 65535));
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 0, 1U << 20, 65535));
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 71, 65535));
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(255, 69, 256, 323, 65535));
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 0));
- REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0xffffffffU, 256, ~u64{0}, 0xffffffffU));
- const auto one = Vulkan::MakeVertexRepackParameters(0, 69, 256, 72, 1);
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0, 256, 1U << 20, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 0, 1U << 20, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 71, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(255, 69, 256, 323, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 0, 1U << 20));
+ REQUIRE_FALSE(
+ Vulkan::MakeVertexRepackParameters(0, 0xffffffffU, 256, ~u64{0}, 0xffffffffU, ~u64{0}));
+ const auto one = Vulkan::MakeVertexRepackParameters(0, 69, 256, 72, 1, 72);
REQUIRE(one.has_value());
REQUIRE(one->source_range == 72);
}
+
+TEST_CASE("Vertex repack planning keeps the source range inside the buffer", "[vertex_repack]") {
+ const auto clamped = Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 69);
+ REQUIRE(clamped.has_value());
+ REQUIRE(clamped->source_range == 69);
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 68));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(300, 69, 256, 1U << 20, 65535, 256));
+}
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index cd176dec27..40369b16ef 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -10,11 +10,13 @@
#include <memory>
#include <numeric>
#include <stdexcept>
+#include <utility>
#include <vector>
#include <fmt/ranges.h>
#include "common/range_sets.inc"
#include "video_core/buffer_cache/buffer_cache_base.h"
+#include "video_core/buffer_cache/uniform_stream_reuse.h"
#include "video_core/guest_memory.h"
#include "video_core/host1x/gpu_device_memory_manager.h"
#include "video_core/nx2.h"
@@ -28,6 +30,9 @@ using Core::DEVICE_PAGESIZE;
template <class P>
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} {
+ // A new runtime has a new scheduler (its ticks restart) and a new stream buffer: whatever the
+ // stream uniform reuse remembers refers to the old ones. Before the early return below.
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
// Ensure the first slot is used for the null buffer
void(slot_buffers.insert(runtime, NullBufferParams{}));
gpu_modified_ranges.Clear();
@@ -180,6 +185,7 @@ void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) {
gpu_modified_ranges.Subtract(device_addr, size);
}
memory_tracker.MarkRegionAsCpuModified(device_addr, size);
+ MarkVertexBuffersStale(device_addr, size);
}
template <class P>
@@ -230,8 +236,17 @@ std::optional<VideoCore::RasterizerDownloadArea> BufferCache<P>::GetFlushArea(DA
};
area->preemtive = !IsRegionGpuModified(device_addr_start_aligned,
device_addr_end_aligned - device_addr_start_aligned);
- memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned,
- device_addr_end_aligned - device_addr_start_aligned);
+ // Mark only data that the GPU wrote. A mark stays for the session and sends later GPU writes
+ // in the range to a download at each fence.
+ if (!area->preemtive) {
+ memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned,
+ device_addr_end_aligned - device_addr_start_aligned);
+ } else {
+ // The core keeps this area and has no mark. The next GPU write here must arm the range
+ // again, which ends the area and sets the trap.
+ read_trapped_ranges.Subtract(device_addr_start_aligned,
+ device_addr_end_aligned - device_addr_start_aligned);
+ }
return area;
}
@@ -265,6 +280,7 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am
}
ClearDownload(*cpu_dest_address, amount);
+ MarkVertexBuffersStale(*cpu_dest_address, amount);
BufferId buffer_a;
BufferId buffer_b;
@@ -408,7 +424,8 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr
template <class P>
void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) {
auto& current = channel_state->uniform_buffers[stage][index];
- if (current.device_addr == 0 && current.size == 0 && !current.buffer_id) [[likely]] {
+ if (current.device_addr == 0 && current.size == 0 && current.buffer_id == NULL_BUFFER_ID)
+ [[likely]] {
return;
}
current = NULL_BINDING;
@@ -893,7 +910,39 @@ void BufferCache<P>::UpdateVertexBufferSlot(u32 index, const Binding& binding) {
}
template <class P>
+void BufferCache<P>::MarkVertexBuffersStale(DAddr device_addr, u64 size) {
+ if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) {
+ u32 mask = enabled_vertex_buffers_mask;
+ while (mask != 0) {
+ const u32 index = std::countr_zero(mask);
+ mask &= mask - 1;
+ const Binding& binding = v_buffer[index];
+ if (device_addr < binding.device_addr + binding.size &&
+ binding.device_addr < device_addr + size) {
+ stale_vertex_buffers_mask |= 1U << index;
+ }
+ }
+ }
+}
+
+template <class P>
+void BufferCache<P>::RefreshRepackedVertexBuffers() {
+ if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) {
+ u32 mask = std::exchange(stale_vertex_buffers_mask, 0) & enabled_vertex_buffers_mask;
+ auto& flags = maxwell3d->dirty.flags;
+ while (mask != 0) {
+ const u32 index = std::countr_zero(mask);
+ mask &= mask - 1;
+ if (runtime.IsVertexRepacked(index)) {
+ flags[Dirty::VertexBuffer0 + index] = true;
+ }
+ }
+ }
+}
+
+template <class P>
void BufferCache<P>::BindHostVertexBuffers() {
+ RefreshRepackedVertexBuffers();
#ifdef __ANDROID__
const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue();
@@ -1078,8 +1127,20 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
// Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan
- const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
- device_memory.ReadBlockUnsafe(device_addr, span.data(), size);
+ bool streamed = false;
+ if constexpr (!IS_OPENGL && VideoCommon::StreamUniformReusable<Runtime>) {
+ // The same bytes as this slot's previous bind, in the same command buffer: bind that
+ // stream allocation again instead of allocating and copying (uniform_stream_reuse.h).
+ // EDEN_NX2_UBOREUSE=0 turns it off.
+ if (VideoCommon::UniformStreamReuseEnabled()) {
+ streamed = VideoCommon::BindSmallUniformReusing(
+ runtime, device_memory, stage, index, binding_index, device_addr, size);
+ }
+ }
+ if (!streamed) {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ device_memory.ReadBlockUnsafe(device_addr, span.data(), size);
+ }
return;
}
// Classic cached path
@@ -1655,6 +1716,7 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3
}
}
memory_tracker.MarkRegionAsGpuModified(device_addr, size);
+ MarkVertexBuffersStale(device_addr, size);
gpu_modified_ranges.Add(device_addr, size);
if (DownloadsAtFence(device_addr, size)) {
uncommitted_gpu_modified_ranges.Add(device_addr, size);
@@ -2205,6 +2267,7 @@ void BufferCache<P>::InlineMemoryImplementation(DAddr dest_address, size_t copy_
std::span<const u8> inlined_buffer) {
ClearDownload(dest_address, copy_size);
gpu_modified_ranges.Subtract(dest_address, copy_size);
+ MarkVertexBuffersStale(dest_address, copy_size);
BufferId buffer_id = FindBuffer(dest_address, static_cast<u32>(copy_size), false);
auto& buffer = slot_buffers[buffer_id];
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index 1f0c6cf10f..86c05c8f22 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -59,7 +59,8 @@ constexpr u32 NUM_VERTEX_BUFFERS = 32;
constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4;
constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18;
constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8;
-constexpr u32 NUM_STORAGE_BUFFERS = 16;
+// Native compute shaders bind up to 18 global memory ranges; the enable masks are 32 bits wide.
+constexpr u32 NUM_STORAGE_BUFFERS = 32;
constexpr u32 NUM_TEXTURE_BUFFERS = 32;
constexpr u32 NUM_STAGES = 5;
@@ -525,6 +526,13 @@ private:
void UpdateVertexBufferSlot(u32 index, const Binding& binding);
+ /// Flags the vertex buffers overlapping a written range so host copies derived from them are
+ /// rebuilt.
+ void MarkVertexBuffersStale(DAddr device_addr, u64 size);
+
+ /// Re-dirties repacked vertex buffers whose source data was written since they were bound.
+ void RefreshRepackedVertexBuffers();
+
void ClearDownload(DAddr base_addr, u64 size);
void InlineMemoryImplementation(DAddr dest_address, size_t copy_size,
@@ -545,6 +553,7 @@ private:
u32 last_index_count = 0;
u32 enabled_vertex_buffers_mask = 0;
+ u32 stale_vertex_buffers_mask = 0;
u64 vertex_buffers_serial = 0;
std::array<Binding, 32> v_buffer{};
diff --git a/src/video_core/buffer_cache/uniform_stream_reuse.h b/src/video_core/buffer_cache/uniform_stream_reuse.h
new file mode 100644
index 0000000000..7ba31c4717
--- /dev/null
+++ b/src/video_core/buffer_cache/uniform_stream_reuse.h
@@ -0,0 +1,121 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <concepts>
+#include <cstddef>
+#include <cstdlib>
+#include <cstring>
+#include <span>
+#include <type_traits>
+
+#include "common/common_types.h"
+
+namespace VideoCommon {
+
+/// What a buffer cache runtime offers for stream uniform reuse.
+/// - StreamUniformRef: a small trivially copyable description of one stream allocation (the
+/// host buffer, its offset and whatever else the runtime needs to add it to a descriptor
+/// payload again, e.g. the device address of the descriptor buffer path).
+/// - LastStreamUniformRef(): the allocation made by the latest BindMappedUniformBuffer call.
+/// - RebindStreamUniform(ref, size): adds `ref` to the descriptor payload exactly as
+/// BindMappedUniformBuffer added the allocation the first time.
+template <class R>
+concept StreamUniformReusable = requires(R& runtime, u32 size) {
+ typename R::StreamUniformRef;
+ requires std::is_trivially_copyable_v<typename R::StreamUniformRef>;
+ { runtime.LastStreamUniformRef() } -> std::same_as<typename R::StreamUniformRef>;
+ runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size);
+ { runtime.CurrentTick() } -> std::convertible_to<u64>;
+};
+
+/// EDEN_NX2_UBOREUSE=0 turns the reuse off (every small uniform buffer is copied, as before).
+[[nodiscard]] inline bool UniformStreamReuseEnabled() noexcept {
+ static const bool enabled = [] {
+ const char* const value = std::getenv("EDEN_NX2_UBOREUSE");
+ return value == nullptr || value[0] != '0';
+ }();
+ return enabled;
+}
+
+template <class Ref>
+class UniformStreamReuse {
+public:
+ static constexpr u32 MaxBytes = 4096; // BufferCache's DEFAULT_SKIP_CACHE_SIZE
+ static constexpr size_t NumStages = 5;
+ static constexpr size_t NumSlots = 18; // Maxwell constant buffers per stage
+
+ struct Slot {
+ Ref ref{};
+ u64 tick{};
+ u32 size{}; // 0: nothing streamed yet
+ std::array<u8, MaxBytes> bytes{};
+ };
+
+ void Reset() noexcept {
+ for (auto& stage : slots) {
+ for (Slot& slot : stage) {
+ slot.size = 0;
+ slot.tick = 0;
+ }
+ }
+ }
+
+ std::array<std::array<Slot, NumSlots>, NumStages> slots{};
+ std::array<u8, MaxBytes> scratch{};
+};
+
+template <class Ref>
+[[nodiscard]] inline UniformStreamReuse<Ref>& UniformStreamReuseState() noexcept {
+ static UniformStreamReuse<Ref> state;
+ return state;
+}
+
+/// Forgets every slot. Call it when a buffer cache (and so its runtime, scheduler and stream
+/// buffer) is created: ticks restart with a new scheduler and the old references are dead.
+template <class Runtime>
+inline void ResetUniformStreamReuse() noexcept {
+ if constexpr (StreamUniformReusable<Runtime>) {
+ UniformStreamReuseState<typename Runtime::StreamUniformRef>().Reset();
+ }
+}
+
+/// Binds the small uniform buffer at `device_addr` for (stage, cbuf `index`) through the stream
+/// buffer, reusing the slot's previous allocation when the bytes and the command buffer are the
+/// same. Returns false (nothing bound) when the buffer does not qualify, so the caller binds it
+/// the classic way.
+template <class Runtime, class DeviceMemory>
+[[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory,
+ size_t stage, u32 index, u32 binding_index,
+ u64 device_addr, u32 size) {
+ using Reuse = UniformStreamReuse<typename Runtime::StreamUniformRef>;
+ if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages ||
+ index >= Reuse::NumSlots) {
+ return false;
+ }
+ Reuse& reuse = UniformStreamReuseState<typename Runtime::StreamUniformRef>();
+ typename Reuse::Slot& slot = reuse.slots[stage][index];
+ u8* const scratch = reuse.scratch.data();
+ device_memory.ReadBlockUnsafe(device_addr, scratch, size);
+
+ const bool same_bytes =
+ slot.size == size && std::memcmp(slot.bytes.data(), scratch, size) == 0;
+ if (same_bytes && slot.tick == runtime.CurrentTick()) {
+ runtime.RebindStreamUniform(slot.ref, size);
+ return true;
+ }
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memcpy(span.data(), scratch, size);
+ slot.ref = runtime.LastStreamUniformRef();
+ // After the allocation: the tick the stream region is tagged with is the one current now.
+ slot.tick = runtime.CurrentTick();
+ if (!same_bytes) {
+ slot.size = size;
+ std::memcpy(slot.bytes.data(), scratch, size);
+ }
+ return true;
+}
+
+} // namespace VideoCommon
diff --git a/src/video_core/control/scheduler.cpp b/src/video_core/control/scheduler.cpp
index dc42906752..97d2dcd482 100644
--- a/src/video_core/control/scheduler.cpp
+++ b/src/video_core/control/scheduler.cpp
@@ -48,6 +48,7 @@ void Scheduler::Push(GPU& gpu, s32 channel, CommandList&& entries) {
std::find(pending_channels.begin(), pending_channels.end(), channel_state) ==
pending_channels.end()) {
pending_channels.push_back(channel_state);
+ any_pending.store(true, std::memory_order_release);
}
if (!pending_channels.empty()) {
ResumePending(gpu);
@@ -77,6 +78,7 @@ void Scheduler::ResumePending(GPU& gpu) {
std::erase_if(pending_channels, [](const auto& state) {
return !state->payload->dma_pusher.HasPendingAcquire();
});
+ any_pending.store(!pending_channels.empty(), std::memory_order_release);
}
void Scheduler::DeclareChannel(std::shared_ptr<ChannelState> new_channel) {
diff --git a/src/video_core/control/scheduler.h b/src/video_core/control/scheduler.h
index 9a0f1b919f..b1aaf46b60 100644
--- a/src/video_core/control/scheduler.h
+++ b/src/video_core/control/scheduler.h
@@ -6,6 +6,7 @@
#pragma once
+#include <atomic>
#include <memory>
#include <mutex>
#include <vector>
@@ -30,6 +31,10 @@ public:
[[nodiscard]] bool HasPendingChannels() const {
return !pending_channels.empty();
}
+ /// HasPendingChannels for threads other than the GPU thread.
+ [[nodiscard]] bool HasPendingChannelsAnyThread() const {
+ return any_pending.load(std::memory_order_acquire);
+ }
void DeclareChannel(std::shared_ptr<ChannelState> new_channel);
@@ -39,6 +44,7 @@ private:
::Common::unordered_map<s32, std::shared_ptr<ChannelState>> channels;
std::mutex scheduling_guard;
std::vector<std::shared_ptr<ChannelState>> pending_channels; ///< Suspended on an acquire
+ std::atomic<bool> any_pending{};
};
} // namespace Control
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 6b93bb7f60..6a79e0570e 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -162,6 +162,9 @@ bool DmaPusher::Step() {
// read: their words at submit time are an older frame's.
const bool submitted = command_resume_offset == 0 &&
dma_pushbuffer_subindex < command_list.submitted_offsets.size() &&
+ (command_list.snapshot_suspensions & 1) == 0 &&
+ command_list.snapshot_suspensions ==
+ suspensions.load(std::memory_order_acquire) &&
!IsGpuWrittenCommands(dma_state.dma_get,
size_t{header.size} * sizeof(u32)) &&
!(use_safe && memory_manager.IsMemoryDirty(dma_state.dma_get,
diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h
index 289786bb9e..feab652bb5 100644
--- a/src/video_core/dma_pusher.h
+++ b/src/video_core/dma_pusher.h
@@ -7,6 +7,7 @@
#pragma once
#include <array>
+#include <atomic>
#include <condition_variable>
#include <optional>
#include <span>
@@ -121,6 +122,10 @@ struct CommandList final {
/// executed instead of the memory, which the game may already be refilling for a later frame.
std::vector<CommandHeader> submitted_words;
std::vector<u32> submitted_offsets;
+ /// DmaPusher::suspensions when the words were copied. A list copied during a suspension, or
+ /// that waited behind one, reads memory instead: games write the commands it runs before they
+ /// release the acquire.
+ u64 snapshot_suspensions = 0;
};
/// FNV-1a over pushbuffer words (EDEN_NX2_PB_CHECK).
@@ -139,6 +144,10 @@ inline u64 HashPushbufferWords(std::span<const u32> words) {
/// details on this implementation.
class DmaPusher final {
public:
+ /// Counts suspensions on a semaphore acquire and the resumes after them, so it is odd while
+ /// the channel is suspended (see CommandList::snapshot_suspensions).
+ std::atomic<u64> suspensions{0};
+
explicit DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Control::ChannelState& channel_state_);
~DmaPusher();
diff --git a/src/video_core/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp
index 038e5a2e69..564220b71c 100644
--- a/src/video_core/engines/draw_manager.cpp
+++ b/src/video_core/engines/draw_manager.cpp
@@ -72,11 +72,19 @@ void Maxwell3D::DrawManager::Clear(Maxwell3D& maxwell3d, u32 layer_count) {
}
}
-void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d) {
+void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues) {
if (draw_state.draw_mode != DrawMode::Instance || draw_state.instance_count == 0) {
+ if (!loop_continues) {
+ draw_state.drawn_instances = 0;
+ }
return;
}
- DrawEnd(maxwell3d, draw_state.instance_count + 1, true);
+ // The first draw of a batch also covers the loop's first instance; a batch that continues a
+ // loop split by a register write draws only the instances after the ones already drawn.
+ const bool continued = draw_state.drawn_instances != 0;
+ DrawEnd(maxwell3d, draw_state.instance_count + (continued ? 0 : 1), true);
+ draw_state.drawn_instances =
+ loop_continues ? draw_state.drawn_instances + draw_state.instance_count : 0;
draw_state.instance_count = 0;
}
@@ -121,6 +129,7 @@ void Maxwell3D::DrawManager::DrawArrayInstanced(Maxwell3D& maxwell3d, Maxwell3D:
draw_state.vertex_buffer.count = vertex_count;
if (!subsequent) {
draw_state.instance_count = 1;
+ draw_state.drawn_instances = 0;
}
draw_state.base_instance = draw_state.instance_count - 1;
draw_state.draw_mode = DrawMode::Instance;
@@ -220,6 +229,9 @@ void Maxwell3D::DrawManager::DrawEnd(Maxwell3D& maxwell3d, u32 instance_count, b
[[fallthrough]];
case DrawMode::General:
draw_state.base_instance = maxwell3d.regs.global_base_instance_index;
+ if (draw_state.draw_mode == DrawMode::Instance && draw_state.drawn_instances != 0) {
+ draw_state.base_instance += draw_state.drawn_instances + 1;
+ }
draw_state.base_index = maxwell3d.regs.global_base_vertex_index;
if (draw_state.draw_indexed) {
draw_state.index_buffer = maxwell3d.regs.index_buffer;
@@ -301,6 +313,8 @@ void Maxwell3D::DrawManager::UpdateTopology(Maxwell3D& maxwell3d) {
}
void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed, u32 instance_count) {
+ draw_state.draw_element_count =
+ draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count;
LOG_TRACE(HW_GPU, "called, topology={}, count={}", draw_state.topology, draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count);
UpdateTopology(maxwell3d);
if (maxwell3d.ShouldExecute()) {
@@ -309,6 +323,7 @@ void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed
}
void Maxwell3D::DrawManager::ProcessDrawIndirect(Maxwell3D& maxwell3d) {
+ draw_state.draw_element_count = 0;
LOG_TRACE(HW_GPU, "called, topology={}, is_indexed={}, includes_count={}, buffer_size={}, max_draw_count={}", draw_state.topology, indirect_state.is_indexed, indirect_state.include_count, indirect_state.buffer_size, indirect_state.max_draw_counts);
UpdateTopology(maxwell3d);
if (maxwell3d.ShouldExecute()) {
diff --git a/src/video_core/engines/fermi_2d.h b/src/video_core/engines/fermi_2d.h
index 7fddf9fdc1..04a12fd351 100644
--- a/src/video_core/engines/fermi_2d.h
+++ b/src/video_core/engines/fermi_2d.h
@@ -70,6 +70,9 @@ public:
ROP = 4,
SrcCopyPremult = 5,
BlendPremult = 6,
+ // Not a hardware operation: a copy that decodes sRGB-encoded values to linear, for the
+ // nx2 upscale fallback standing in for a program that decodes its output.
+ SrcCopySrgbToLinear = 7,
};
enum class MemoryLayout : u32 {
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index 4732167369..925b1b2ca1 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -685,8 +685,32 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
result.addr_lower = static_cast<u32>(tic.Address());
return result;
};
+ // Title 4's program ends by decoding its result from sRGB to linear (the 0.04045 threshold
+ // and the 2.4 exponent as half-precision immediates of HSETP2 and HMUL2). The copy of its
+ // sRGB input left the frame gamma-encoded twice, washed out: decode in the copy when the
+ // program does.
+ static std::unordered_map<GPUVAddr, bool> decodes_srgb;
+ const GPUVAddr program = regs.code_loc.Address();
+ const auto [known, inserted] = decodes_srgb.try_emplace(program, false);
+ if (inserted) {
+ bool threshold = false, exponent = false;
+ std::array<u32, 0x400> block{};
+ for (u64 offset = 0; offset < 0x20000 && !(threshold && exponent); offset += sizeof(block)) {
+ if (!memory_manager.IsFullyMappedRange(program + offset, sizeof(block))) break;
+ memory_manager.ReadBlock(program + offset, block.data(), sizeof(block));
+ for (size_t word = 0; word < block.size(); word += 4) {
+ const u32 opcode = block[word] & 0xfff;
+ threshold |= opcode == 0x434 && block[word + 1] == 0x292d292d;
+ exponent |= opcode == 0x832 && block[word + 1] == 0x40cc40cc;
+ }
+ }
+ known->second = threshold && exponent;
+ LOG_INFO(HW_GPU, "Upscale fallback for program {:#x}: {}", program,
+ known->second ? "decodes sRGB like the program" : "plain copy");
+ }
const Fermi2D::Config config{
- .operation = Fermi2D::Operation::SrcCopy,
+ .operation = known->second ? Fermi2D::Operation::SrcCopySrgbToLinear
+ : Fermi2D::Operation::SrcCopy,
.filter = Fermi2D::Filter::Bilinear,
.must_accelerate = true,
.dst_x0 = 0,
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 9731f54584..92313988bd 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -342,7 +342,7 @@ void Maxwell3D::ProcessDirtyRegisters(u32 method, u32 argument) {
// the state they were begun with: NVN's draw macros reset the base instance (and its copy in
// c[0][0x644]) right after the loop, before the macro returns.
if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] {
- draw_manager.DrawDeferred(*this);
+ draw_manager.DrawDeferred(*this, true);
}
regs.reg_array[method] = argument;
for (auto const& table : dirty.tables)
@@ -518,6 +518,10 @@ void Maxwell3D::DumpRecentWrites(u32 method) {
}
LOG_ERROR(HW_GPU, "Write to {:#x} while macro {:#x} collects parameters ({} so far); last writes:",
method, executing_macro, macro_params.size());
+ if (!record_write_history) {
+ LOG_ERROR(HW_GPU, "Register write history disabled; EDEN_NX2_3D_HISTORY=1 enables it");
+ return;
+ }
for (size_t i = 0; i < recent_writes.size(); ++i) {
const auto& w = recent_writes[(recent_write_index + i) % recent_writes.size()];
if (w.amount != 0) {
@@ -537,7 +541,7 @@ void Maxwell3D::CallMultiMethod(Core::System& system, u32 method, const u32* bas
}
// As in ProcessDirtyRegisters: the paths below write without it.
if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] {
- draw_manager.DrawDeferred(*this);
+ draw_manager.DrawDeferred(*this, true);
}
switch (method) {
case MAXWELL3D_REG_INDEX(const_buffer.buffer):
@@ -818,7 +822,6 @@ struct Maxwell3D::FrontState {
Regs::ShadowRamControl control{};
u32 executing_macro{};
std::vector<u32> parameters;
- std::vector<u32> fifo;
u64 calls{};
/// A method the draw manager handles was sent since the back last ran DrawDeferred: only
/// then can it have a deferred instanced draw to issue at the end of a macro.
@@ -1095,9 +1098,9 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split
engine.native_jit = std::make_shared<TuringMacroJit>();
}
engine.native_jit->Prepare(engine.native_code, engine.native_code_generation);
- fs.fifo.assign(fs.parameters.begin(), fs.parameters.end());
+ const size_t parameter_count = fs.parameters.size();
const auto result = engine.native_jit->Execute(
- start, fs.fifo, std::span<u32>{native_data_ram},
+ start, fs.parameters, std::span<u32>{native_data_ram},
[&fs](u32 address) { return address < Regs::NUM_REGS ? fs.regs[address] : 0U; },
[&](u32 address, u32 value) {
if (address >= MacroRegistersStart) {
@@ -1113,7 +1116,7 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split
if (!result.success) {
engine.native_execution_failed = true;
LOG_ERROR(HW_GPU, "Native MME (JIT, split front) failed: start={:#x}, consumed={}/{}, error={}",
- start, result.consumed, fs.parameters.size(), result.error);
+ start, result.consumed, parameter_count, result.error);
}
}
fs.parameters.clear();
@@ -1148,7 +1151,7 @@ void Maxwell3D::FrontDmaRead(VideoCommon::GpuSplit& split, FrontState& fs, u32 m
for (u32 i = 0; i < words; ++i) {
const u32 word = memory_manager.Read<u32>(source + u64{i} * sizeof(u32));
if (method == 0x158) {
- fs.fifo.push_back(word);
+ fs.parameters.push_back(word);
} else if (ram + i < native_data_ram.size()) {
native_data_ram[ram + i] = word;
}
diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h
index af0e37978e..7646ddbbe3 100644
--- a/src/video_core/engines/maxwell_3d.h
+++ b/src/video_core/engines/maxwell_3d.h
@@ -3064,11 +3064,14 @@ public:
Maxwell3D::Regs::PrimitiveTopology topology{};
DrawMode draw_mode{};
bool draw_indexed{};
+ u32 draw_element_count{}; // Zero for indirect draws whose count is not known yet.
u32 base_index{};
Maxwell3D::Regs::VertexBuffer vertex_buffer;
Maxwell3D::Regs::IndexBuffer index_buffer;
u32 base_instance{};
u32 instance_count{};
+ /// Deferred instances already drawn by flushes in the middle of the current loop.
+ u32 drawn_instances{};
std::vector<u8> inline_index_draw_indexes;
};
struct DrawTextureState {
@@ -3095,7 +3098,8 @@ public:
};
void ProcessMethodCall(Maxwell3D& maxwell3d, u32 method, u32 argument);
void Clear(Maxwell3D& maxwell3d, u32 layer_count);
- void DrawDeferred(Maxwell3D& maxwell3d);
+ /// Draws the waiting instances. loop_continues is set when the loop goes on afterwards.
+ void DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues = false);
/// Instances after the first wait to be drawn together (DrawDeferred).
[[nodiscard]] bool HasDeferredDraw() const noexcept {
return draw_state.draw_mode == DrawMode::Instance && draw_state.instance_count != 0;
@@ -3349,7 +3353,17 @@ private:
};
std::array<RecentWrite, 48> recent_writes{};
size_t recent_write_index = 0;
+ const bool record_write_history{
+#ifdef NDEBUG
+ std::getenv("EDEN_NX2_3D_HISTORY") != nullptr
+#else
+ true
+#endif
+ };
void RecordWrite(u32 method, u32 argument, u32 amount, u32 pending) {
+ if (!record_write_history) {
+ return;
+ }
recent_writes[recent_write_index++ % recent_writes.size()] = {method, argument, amount, pending};
}
void DumpRecentWrites(u32 method);
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 0bb1809aa3..37ac3ec851 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -116,6 +116,7 @@ bool Puller::ResumeAcquire(DmaPusher& dma_pusher) {
return false;
}
pending_acquire.reset();
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
Nx2SemaphoreWaiters::Get().Remove(this);
return true;
}
@@ -257,6 +258,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) {
dma_pusher.rasterizer->ReleaseFences();
if (!waiters.Latched(this) && !acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) {
pending_acquire = acquire;
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireWaits, 1);
} else {
waiters.Remove(this);
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 53f8f063f7..6dbabcb43a 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -210,6 +210,10 @@ struct GPU::Impl {
gpu_thread.Run(std::move(func), is_async);
}
+ void MapInOrder(std::function<void()>&& func) {
+ gpu_thread.Run(std::move(func), is_async, true);
+ }
+
/// Notify rasterizer that any caches of the specified region should be flushed to Switch memory
void FlushRegion(DAddr addr, u64 size) {
gpu_thread.FlushRegion(addr, size, is_async);
@@ -228,7 +232,9 @@ struct GPU::Impl {
raster_area.start_address, raster_area.end_address,
reads.load(std::memory_order_relaxed));
}
- const u64 fence = RequestSyncOperation([this, &raster_area]() {
+ const u64 fence = RequestSyncOperation([this, raster_area]() {
+ // Apply collected guest writes before downloading GPU data over guest memory.
+ InvalidateGPUCache();
renderer->ReadRasterizer()->FlushRegion(raster_area.start_address, raster_area.end_address - raster_area.start_address);
});
gpu_thread.TickGPU(is_async);
@@ -505,6 +511,10 @@ bool GPU::IsAsync() const {
return impl->is_async;
}
+bool GPU::HasSuspendedChannels() const {
+ return impl->scheduler.HasPendingChannelsAnyThread();
+}
+
bool GPU::UseNvdec() const {
return impl->use_nvdec;
}
@@ -549,6 +559,10 @@ void GPU::RunInOrder(std::function<void()>&& func) {
impl->RunInOrder(std::move(func));
}
+void GPU::MapInOrder(std::function<void()>&& func) {
+ impl->MapInOrder(std::move(func));
+}
+
void GPU::InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) {
impl->InvalidateRegion(addr, size, preserve_gpu_writes);
}
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 3a6546b41b..d37dfa5990 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -217,6 +217,9 @@ public:
[[nodiscard]] bool IsAsync() const;
+ /// Whether a channel is suspended on a semaphore acquire (callable from any thread).
+ [[nodiscard]] bool HasSuspendedChannels() const;
+
[[nodiscard]] bool UseNvdec() const;
void RendererFrameEndNotify();
@@ -254,6 +257,10 @@ public:
/// Runs func on the GPU thread after the command lists submitted so far.
void RunInOrder(std::function<void()>&& func);
+ /// RunInOrder for GPU memory maps: also done before the GPU thread reads the command lists
+ /// submitted later (with the GPU split, on its front rather than its back).
+ void MapInOrder(std::function<void()>&& func);
+
/// Notify rasterizer that any caches of the specified region should be invalidated
void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes = false);
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 55200b0647..3af8cb65a8 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -99,7 +99,12 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten
} else if (std::holds_alternative<GPUTickCommand>(next.data)) {
split.Defer([gpu = &system.GPU()] { gpu->TickWork(); });
} else if (auto* run = std::get_if<RunCommand>(&next.data)) {
- split.DeferFunction(std::move(run->func));
+ if (run->before_later_reads) {
+ split.Drain();
+ run->func();
+ } else {
+ split.DeferFunction(std::move(run->func));
+ }
} else if (const auto* flush = std::get_if<FlushRegionCommand>(&next.data)) {
split.Defer([rasterizer_ptr, addr = flush->addr, size = flush->size] {
rasterizer_ptr->FlushRegion(addr, size);
@@ -155,8 +160,8 @@ void ThreadManager::TickGPU(bool is_async) {
PushCommand(GPUTickCommand(), false, is_async);
}
-void ThreadManager::Run(std::function<void()>&& func, bool is_async) {
- PushCommand(RunCommand{std::move(func)}, false, is_async);
+void ThreadManager::Run(std::function<void()>&& func, bool is_async, bool before_later_reads) {
+ PushCommand(RunCommand{std::move(func), before_later_reads}, false, is_async);
}
void ThreadManager::InvalidateRegion(DAddr addr, u64 size) {
diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h
index 796c0d89f6..927453dab0 100644
--- a/src/video_core/gpu_thread.h
+++ b/src/video_core/gpu_thread.h
@@ -80,6 +80,9 @@ struct GPUTickCommand final {};
/// Command to run a function on the GPU thread after the commands queued before it
struct RunCommand final {
std::function<void()> func;
+ /// With the GPU split, run on its front (after draining the back) instead of deferring to the
+ /// back, so command lists read after this one see what func did (GMMU maps).
+ bool before_later_reads{};
};
using CommandData =
@@ -132,7 +135,7 @@ public:
void TickGPU(bool is_async);
/// Runs func on the GPU thread once the commands queued before it have been processed.
- void Run(std::function<void()>&& func, bool is_async);
+ void Run(std::function<void()>&& func, bool is_async, bool before_later_reads = false);
void NotifyShutdown();
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 9ff7563ff7..3dc26ec3ba 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -18,6 +18,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_srgb_to_linear.frag
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/blit_depth.frag
diff --git a/src/video_core/host_shaders/blit_color_srgb_to_linear.frag b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
new file mode 100644
index 0000000000..1a38cb52a1
--- /dev/null
+++ b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#version 450
+
+layout(binding = 0) uniform sampler2D tex;
+
+layout(location = 0) in vec2 texcoord;
+layout(location = 0) out vec4 color;
+
+// Copy of an sRGB-encoded source into a linear destination: the piecewise sRGB decode on the
+// color channels, alpha unchanged.
+void main() {
+ const vec4 value = textureLod(tex, texcoord, 0);
+ const vec3 low = value.rgb / 12.92;
+ const vec3 high = pow((value.rgb + 0.055) / 1.055, vec3(2.4));
+ color = vec4(mix(high, low, lessThan(value.rgb, vec3(0.04045))), value.a);
+}
diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
index eb134e86b5..5fc10cfce2 100644
--- a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
+++ b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
@@ -106,19 +106,18 @@ void ComputePipeline::Configure() {
const auto& cbufs{qmd.const_buffer_config};
const bool via_header_index{qmd.linked_tsc != 0};
const auto read_handle{[&](const auto& desc, u32 index) {
- if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
- if (desc.handle_program != ~0U) {
- // Read by the shader from a storage buffer: computed for this dispatch.
- return TexturePair(VideoCommon::EvaluateTextureHandle(
- info, desc.handle_program, *gpu_memory,
- [&](u32 cbuf) -> GPUVAddr {
- return cbuf < cbufs.size() &&
- ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0
- ? cbufs[cbuf].Address()
- : GPUVAddr{};
- }),
- via_header_index);
- }
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this dispatch.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() &&
+ ((qmd.const_buffer_enable_mask >> cbuf) &
+ 1) != 0
+ ? cbufs[cbuf].Address()
+ : GPUVAddr{};
+ }),
+ via_header_index);
}
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
index 81950279d5..4e02f0569b 100644
--- a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
+++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
@@ -309,18 +309,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
- if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
- if (desc.handle_program != ~0U) {
- // Read by the shader from a storage buffer: computed for this draw.
- return TexturePair(VideoCommon::EvaluateTextureHandle(
- info, desc.handle_program, *gpu_memory,
- [&](u32 cbuf) -> GPUVAddr {
- return cbuf < cbufs.size() && cbufs[cbuf].enabled
- ? cbufs[cbuf].address
- : GPUVAddr{};
- }),
- via_header_index);
- }
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this draw.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() && cbufs[cbuf].enabled
+ ? cbufs[cbuf].address
+ : GPUVAddr{};
+ }),
+ via_header_index);
}
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index edd151b563..506daef11e 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -11,6 +11,7 @@
#include "common/div_ceil.h"
#include "common/settings.h"
#include "video_core/host_shaders/blit_color_float_frag_spv.h"
+#include "video_core/host_shaders/blit_color_srgb_to_linear_frag_spv.h"
#include "video_core/host_shaders/blit_color_msaa_frag_spv.h"
#include "video_core/host_shaders/blit_depth_frag_spv.h"
#include "video_core/host_shaders/blit_depth_msaa_frag_spv.h"
@@ -670,6 +671,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)),
blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)),
temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)),
+ blit_color_srgb_to_linear_frag(BuildShader(device, BLIT_COLOR_SRGB_TO_LINEAR_FRAG_SPV)),
blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)),
blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported()
? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV)
@@ -1331,7 +1333,10 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
}
blit_color_keys.push_back(key);
- const std::array stages = MakeStages(*full_screen_vert, *blit_color_to_color_frag);
+ const bool srgb_to_linear =
+ key.operation == Tegra::Engines::Fermi2D::Operation::SrcCopySrgbToLinear;
+ const std::array stages = MakeStages(
+ *full_screen_vert, srgb_to_linear ? *blit_color_srgb_to_linear_frag : *blit_color_to_color_frag);
const VkPipelineColorBlendAttachmentState blend_attachment{
.blendEnable = VK_FALSE,
.srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h
index 303beeea5c..4741f9fe45 100644
--- a/src/video_core/renderer_vulkan/blit_image.h
+++ b/src/video_core/renderer_vulkan/blit_image.h
@@ -229,6 +229,7 @@ private:
vk::ShaderModule full_screen_vert;
vk::ShaderModule blit_color_to_color_frag;
vk::ShaderModule temporal_upscale_frag;
+ vk::ShaderModule blit_color_srgb_to_linear_frag;
vk::ShaderModule blit_color_msaa_frag;
vk::ShaderModule blit_depth_stencil_frag;
vk::ShaderModule blit_depth_frag;
diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
index d1f78c0034..76a564aae9 100644
--- a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
+++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
@@ -269,7 +269,7 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
if (xfb_enabled) {
RefreshXfbState(xfb_state, regs);
}
- if (!dynamic_vertex_input && ExperimentalStaticVertexRepackEnabled()) {
+ if (!dynamic_vertex_input && features.has_static_vertex_repack) {
const u32 repack_mask = VertexRepackMask(regs);
// Refresh from guest values even when only the stride changed: offsets must not accumulate.
for (size_t index = 0; index < attributes.size(); ++index) {
diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.h b/src/video_core/renderer_vulkan/fixed_pipeline_state.h
index 36a4b4b7a2..1fe5cd75f3 100644
--- a/src/video_core/renderer_vulkan/fixed_pipeline_state.h
+++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.h
@@ -33,6 +33,7 @@ struct DynamicFeatures {
bool has_dynamic_state3_logic_op_enable;
bool has_dynamic_state3_line_stipple_enable;
bool has_dynamic_vertex_input;
+ bool has_static_vertex_repack;
bool has_color_write_enable;
bool has_provoking_vertex;
bool has_provoking_vertex_first_mode;
diff --git a/src/video_core/renderer_vulkan/vertex_repack.h b/src/video_core/renderer_vulkan/vertex_repack.h
index b0849c25c7..493eb41bd1 100644
--- a/src/video_core/renderer_vulkan/vertex_repack.h
+++ b/src/video_core/renderer_vulkan/vertex_repack.h
@@ -2,6 +2,7 @@
#pragma once
+#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <limits>
@@ -14,12 +15,21 @@ constexpr bool VertexRepackInputSupported(bool dynamic_input, bool experimental_
return dynamic_input || experimental_static;
}
-inline bool ExperimentalStaticVertexRepackEnabled() {
- static const bool enabled = [] {
- const char* value = std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK");
- return value && std::strcmp(value, "1") == 0;
- }();
- return enabled;
+// Pipelines without dynamic vertex input repack when the device cannot fetch the 69-byte stride.
+// EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK=1 forces the repack on, =0 forces it off.
+inline bool StaticVertexRepackSelected(const char* override_value, u32 stride_alignment) {
+ if (override_value && std::strcmp(override_value, "1") == 0) {
+ return true;
+ }
+ if (override_value && std::strcmp(override_value, "0") == 0) {
+ return false;
+ }
+ return stride_alignment > 1 && 69 % stride_alignment != 0;
+}
+
+inline bool StaticVertexRepackEnabled(u32 stride_alignment) {
+ return StaticVertexRepackSelected(std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK"),
+ stride_alignment);
}
struct VertexRepackParameters {
@@ -31,8 +41,11 @@ struct VertexRepackParameters {
u32 dispatch_groups;
};
-inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(
- u32 offset, u32 size, u64 alignment, u64 max_storage_range, u32 max_dispatch_groups) {
+inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(u32 offset, u32 size,
+ u64 alignment,
+ u64 max_storage_range,
+ u32 max_dispatch_groups,
+ u64 buffer_size) {
if (size == 0 || alignment == 0) {
return std::nullopt;
}
@@ -40,9 +53,11 @@ inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(
const u64 output_size = vertices * 72;
const u64 base = u64{offset} / alignment * alignment;
const u64 bias = offset - base;
- const u64 range = (bias + size + 3) / 4 * 4;
+ // The word-rounded range must not run past the end of the source buffer
+ const u64 available = buffer_size > base ? buffer_size - base : 0;
+ const u64 range = (std::min)((bias + size + 3) / 4 * 4, available);
const u64 groups = (vertices * 18 + 127) / 128;
- if (output_size > std::numeric_limits<u32>::max() ||
+ if (range < bias + size || output_size > std::numeric_limits<u32>::max() ||
range > std::numeric_limits<u32>::max() || output_size > max_storage_range ||
range > max_storage_range || groups > max_dispatch_groups) {
return std::nullopt;
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index 2449265509..43cb7d9414 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -365,6 +365,10 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
LOG_INFO(Render_Vulkan, "69-byte vertex layout repacking {} ({})",
enable_vertex_repack ? "enabled" : "disabled",
"portable Vulkan layout fallback");
+ static_vertex_repack =
+ StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment());
+ LOG_INFO(Render_Vulkan, "Vertex repacking without dynamic vertex input {}",
+ static_vertex_repack ? "enabled" : "disabled");
if (enable_vertex_repack) {
vertex_repack_pass = std::make_unique<VertexRepackPass>(
device, scheduler, descriptor_pool, staging_pool, compute_pass_descriptor_queue);
@@ -387,14 +391,13 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
u32 BufferCacheRuntime::SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs,
bool dynamic_input) {
const u32 old_mask = vertex_repack_mask;
- const bool input_supported = VertexRepackInputSupported(
- dynamic_input, ExperimentalStaticVertexRepackEnabled());
+ const bool input_supported = VertexRepackInputSupported(dynamic_input, static_vertex_repack);
vertex_repack_mask = vertex_repack_pass && input_supported ? VertexRepackMask(regs) : 0;
if (vertex_repack_mask != old_mask) {
LOG_DEBUG(Render_Vulkan, "Vertex repack selection: mask={:#x}, dynamic_input={}",
vertex_repack_mask, dynamic_input);
}
- return old_mask | vertex_repack_mask;
+ return old_mask ^ vertex_repack_mask;
}
StagingBufferRef BufferCacheRuntime::UploadStagingBuffer(size_t size) {
@@ -657,7 +660,8 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset
return;
}
if (buffer != VK_NULL_HANDLE && size != 0 && IsVertexRepacked(index)) {
- const auto repacked = vertex_repack_pass->Assemble(buffer, offset, size);
+ const auto repacked =
+ vertex_repack_pass->Assemble(buffer, offset, size, u64{offset} + size);
if (!repacked) {
return;
}
@@ -702,7 +706,8 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi
if (handle != VK_NULL_HANDLE && IsVertexRepacked(binding) && bindings.strides[i] == 69 &&
bindings.sizes[i] > 0) {
const auto repacked = vertex_repack_pass->Assemble(
- handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i]));
+ handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i]),
+ bindings.buffers[i]->SizeBytes());
if (!repacked) {
continue;
}
@@ -712,7 +717,7 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi
bindings.strides[i] = 72;
static bool reported = false;
if (!reported) {
- LOG_WARNING(Render_Vulkan, "Experimental GPU vertex repacking active: 69 -> 72 bytes");
+ LOG_INFO(Render_Vulkan, "GPU vertex repacking active: 69 -> 72 bytes");
reported = true;
}
}
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index d3295ec6ed..b9a0225094 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -110,6 +110,7 @@ public:
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept;
+ /// Returns the mask of bindings whose repacked state changed.
u32 SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs, bool dynamic_input);
bool IsVertexRepacked(u32 binding) const {
@@ -168,15 +169,40 @@ public:
void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
+ /// This and the two members below are the hooks of VideoCommon::StreamUniformReusable
+ /// (buffer_cache/uniform_stream_reuse.h).
+ struct StreamUniformRef {
+ VkBuffer buffer;
+ VkDeviceAddress device_address; // base of `buffer`, 0 without buffer device address
+ u32 offset;
+ };
+
std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage,
[[maybe_unused]] u32 binding_index,
u32 size) {
const StagingBufferRef ref = staging_pool.Request(size, MemoryUsage::Upload);
- guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address,
- static_cast<u32>(ref.offset), size);
+ last_stream_uniform = StreamUniformRef{
+ .buffer = ref.buffer,
+ .device_address = ref.device_address,
+ .offset = static_cast<u32>(ref.offset),
+ };
+ RebindStreamUniform(last_stream_uniform, size);
return ref.mapped_span;
}
+ /// The allocation of the latest BindMappedUniformBuffer call.
+ [[nodiscard]] StreamUniformRef LastStreamUniformRef() const noexcept {
+ return last_stream_uniform;
+ }
+
+ /// Adds `ref` to the descriptor payload exactly as BindMappedUniformBuffer did when it made
+ /// the allocation (it is the very same call). The caller guarantees the allocation is still
+ /// live: made on the scheduler tick that is current now, so the staging pool has not recycled
+ /// it and the GPU has not even started on it.
+ void RebindStreamUniform(const StreamUniformRef& ref, u32 size) {
+ guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, ref.offset, size);
+ }
+
[[nodiscard]] VkDeviceSize SparseAlignmentFor(bool sparse_compatible) const noexcept {
if (!sparse_compatible || !multi_range_buffers.use_sparse) {
return 0;
@@ -270,12 +296,15 @@ private:
std::unique_ptr<Uint8Pass> uint8_pass;
std::unique_ptr<VertexRepackPass> vertex_repack_pass;
u32 vertex_repack_mask{};
+ bool static_vertex_repack{};
QuadIndexedPass quad_index_pass;
MultiRangeBufferCache multi_range_buffers;
boost::container::small_vector<MultiRangeSource, 16> multi_range_sources;
VkDeviceSize multi_range_total{};
+ StreamUniformRef last_stream_uniform{};
+
bool limit_dynamic_storage_buffers = false;
u32 max_dynamic_storage_buffers = (std::numeric_limits<u32>::max)();
};
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
index cb8dcde42f..5902b7d8f5 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
@@ -335,10 +335,11 @@ VertexRepackPass::VertexRepackPass(const Device& device_, Scheduler& scheduler_,
scheduler{scheduler_}, staging_pool{staging_pool_}, descriptor_queue{descriptor_queue_} {}
std::optional<std::pair<VkBuffer, VkDeviceSize>> VertexRepackPass::Assemble(VkBuffer source,
- u32 offset, u32 size) {
+ u32 offset, u32 size,
+ u64 buffer_size) {
const auto parameters = MakeVertexRepackParameters(
offset, size, device.GetStorageBufferAlignment(), device.GetMaxStorageBufferRange(),
- device.GetMaxComputeWorkGroupCount()[0]);
+ device.GetMaxComputeWorkGroupCount()[0], buffer_size);
if (!parameters) {
LOG_ERROR(Render_Vulkan, "Vertex repack exceeds device limits: offset={}, size={}", offset, size);
return std::nullopt;
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h
index 27fffbbbb7..9687bfff91 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.h
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.h
@@ -60,9 +60,10 @@ class VertexRepackPass final : public ComputePass {
public:
VertexRepackPass(const Device& device, Scheduler& scheduler, DescriptorPool& descriptor_pool,
StagingBufferPool& staging_pool, ComputePassDescriptorQueue& descriptor_queue);
- /// Returns nothing when the range exceeds the device limits; the binding is then left as is.
- std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset,
- u32 size);
+ /// Returns nothing when the range exceeds the device limits or the source buffer; the binding
+ /// is then left as is.
+ std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset, u32 size,
+ u64 buffer_size);
private:
Scheduler& scheduler;
diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
index 1cfe391ba6..3a3861930c 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
@@ -176,19 +176,18 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
const auto& cbufs{qmd.const_buffer_config};
const bool via_header_index{qmd.linked_tsc != 0};
const auto read_handle{[&](const auto& desc, u32 index) {
- if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
- if (desc.handle_program != ~0U) {
- // Read by the shader from a storage buffer: computed for this dispatch.
- return TexturePair(VideoCommon::EvaluateTextureHandle(
- info, desc.handle_program, gpu_memory,
- [&](u32 cbuf) -> GPUVAddr {
- return cbuf < cbufs.size() &&
- ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0
- ? cbufs[cbuf].Address()
- : GPUVAddr{};
- }),
- via_header_index);
- }
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this dispatch.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() &&
+ ((qmd.const_buffer_enable_mask >> cbuf) &
+ 1) != 0
+ ? cbufs[cbuf].Address()
+ : GPUVAddr{};
+ }),
+ via_header_index);
}
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
index fa273f6ef9..4bd2e2735e 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
@@ -400,18 +400,16 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
- if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) {
- if (desc.handle_program != ~0U) {
- // Read by the shader from a storage buffer: computed for this draw.
- return TexturePair(VideoCommon::EvaluateTextureHandle(
- info, desc.handle_program, *gpu_memory,
- [&](u32 cbuf) -> GPUVAddr {
- return cbuf < cbufs.size() && cbufs[cbuf].enabled
- ? cbufs[cbuf].address
- : GPUVAddr{};
- }),
- via_header_index);
- }
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this draw.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() && cbufs[cbuf].enabled
+ ? cbufs[cbuf].address
+ : GPUVAddr{};
+ }),
+ via_header_index);
}
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index ff8e84b019..0223f56ff1 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -34,6 +34,7 @@
#include "video_core/renderer_vulkan/maxwell_to_vk.h"
#include "video_core/renderer_vulkan/pipeline_helper.h"
#include "video_core/renderer_vulkan/pipeline_statistics.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/vk_compute_pipeline.h"
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
@@ -583,6 +584,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
dynamic_features.has_dynamic_vertex_input =
device.IsExtVertexInputDynamicStateSupported() &&
Settings::values.vertex_input_dynamic_state.GetValue();
+ dynamic_features.has_static_vertex_repack =
+ StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment());
dynamic_features.has_provoking_vertex = device.IsExtProvokingVertexSupported();
dynamic_features.has_provoking_vertex_first_mode =
@@ -600,9 +603,17 @@ constexpr u32 UsedPipelinesMagic = 0x31505545; // "EUP1"
// Pipelines the last two sessions used are built at boot. Each holds ~0.45 MB of driver memory:
// title 1 built 4,800 from eight sessions, 1.5 GB more than building them on first use (but that
// stalled frames 4x as often), while one session of its title screen uses ~1,500.
+// MoltenVK keeps the previous eight sessions: a pipeline built while playing goes through
+// SPIR-V -> MSL -> Metal, far more than the 2-3 ms the shorter window assumes, so each one
+// built at boot is a stutter avoided.
constexpr u8 UsedPipelinesMaxAge = 2;
+constexpr u8 UsedPipelinesMaxAgeMoltenVK = 8;
-std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path) {
+u8 MaxUsedPipelineAge(const Device& device) {
+ return device.IsMoltenVK() ? UsedPipelinesMaxAgeMoltenVK : UsedPipelinesMaxAge;
+}
+
+std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path, u8 max_age) {
std::unordered_map<u64, u8> used;
std::ifstream file{path, std::ios::binary};
u32 magic{}, count{};
@@ -618,7 +629,7 @@ std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path)
!file.read(reinterpret_cast<char*>(&age), sizeof(age))) {
break;
}
- if (age < UsedPipelinesMaxAge) { // lists saved with a longer window
+ if (age < max_age) { // lists saved with a longer window
used.emplace(hash, age);
}
}
@@ -669,7 +680,7 @@ void PipelineCache::SaveUsedPipelines() {
next.emplace(hash, u8{0});
}
for (const auto& [hash, age] : used_pipelines) {
- if (age + 1 < UsedPipelinesMaxAge) {
+ if (age + 1 < MaxUsedPipelineAge(device)) {
next.try_emplace(hash, static_cast<u8>(age + 1));
}
}
@@ -744,7 +755,7 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
}
pipeline_cache_filename = base_dir / "vulkan.bin";
used_pipelines_filename = base_dir / "vulkan_used.bin";
- used_pipelines = LoadUsedPipelines(used_pipelines_filename);
+ used_pipelines = LoadUsedPipelines(used_pipelines_filename, MaxUsedPipelineAge(device));
used_pipelines_saved = std::chrono::steady_clock::now();
size_t skipped_pipelines = 0;
size_t queued_graphics = 0;
@@ -1004,6 +1015,11 @@ void PipelineCache::TickFrame() {
}
void PipelineCache::EvictUnusedPipelines() {
+ // Building a pipeline again on MoltenVK means a Metal compile mid-game, far dearer than the
+ // 2-3 ms below, so none are evicted there.
+ if (device.IsMoltenVK()) {
+ return;
+ }
// Each built pipeline holds ~0.45 MB of driver memory and none was ever destroyed, so it grew
// with every place a session went (title 1: 1,500 on its title screen alone). One unused for
// this long is destroyed; it takes 2-3 ms to build again from the driver's pipeline cache.
@@ -1056,7 +1072,7 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
// Usually these shaders are only used once for building textures so we can assume they
// can't be built async
const auto& draw_state = maxwell3d->draw_manager.draw_state;
- if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
+ if (draw_state.draw_element_count <= 6) {
return pipeline;
}
return nullptr;
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index 978f8be547..3898c571f5 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -1127,12 +1127,10 @@ void RasterizerVulkan::InnerInvalidation(std::span<const std::pair<DAddr, std::s
buffer_cache.WriteMemory(addr, size);
}
}
- {
- for (const auto& [addr, size] : sequences) {
- query_cache.InvalidateRegion(addr, size);
- pipeline_cache.InvalidateRegion(addr, size);
- }
+ for (const auto& [addr, size] : sequences) {
+ query_cache.InvalidateRegion(addr, size);
}
+ pipeline_cache.InvalidateRegions(sequences);
}
bool RasterizerVulkan::OnCPUWrite(DAddr addr, u64 size) {
@@ -1424,10 +1422,27 @@ void RasterizerVulkan::FlushWork() {
#endif // __ANDROID__
static_assert(DRAWS_TO_DISPATCH % (CHECK_MASK + 1) == 0);
- if ((++draw_counter & CHECK_MASK) != CHECK_MASK) {
+ if ((++draw_counter & CHECK_MASK) != 0) {
return;
}
- if (draw_counter < DRAWS_TO_DISPATCH) {
+ // Keep the host GPU working while native commands are still being recorded.
+ static const u32 native_flush_draws = [] {
+ constexpr u32 fallback = 128;
+ const char* value = std::getenv("EDEN_NX2_FLUSH_DRAWS");
+ if (value == nullptr || value[0] < '0' || value[0] > '9') {
+ return fallback;
+ }
+ char* end = nullptr;
+ const unsigned long parsed = std::strtoul(value, &end, 10);
+ if (*end != '\0' || parsed == 0) {
+ return fallback;
+ }
+ return static_cast<u32>(std::clamp(parsed, 32UL, 1'048'576UL));
+ }();
+ const u32 draws_to_dispatch = Tegra::g_nx2_ampere.load(std::memory_order_relaxed)
+ ? native_flush_draws
+ : DRAWS_TO_DISPATCH;
+ if (draw_counter < draws_to_dispatch) {
scheduler.DispatchWork();
return;
}
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 30981fe413..928146027d 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -2350,23 +2350,31 @@ void TextureCacheRuntime::TickFrame() {
});
}
+namespace {
+
+// The host image of a texture; unbacked images are created without memory (see MakeImage).
+[[nodiscard]] vk::Image MakeHostImage(TextureCacheRuntime& runtime, const ImageInfo& info,
+ u32 host_level_offset, GPUVAddr gpu_addr, bool backed) {
+ const Device& device = runtime.device;
+ const bool sparse_tiles = !backed && UsesSparseTiles(device, info, gpu_addr);
+ return MakeImage(
+ device, runtime.memory_allocator, TrimmedHostInfo(device, info, host_level_offset),
+ IsHugeAstcArray(device, info) ? std::span<const VkFormat>{HugeAstcViewFormats}
+ : IsOpaqueBc1Astc(device, info) ? std::span<const VkFormat>{OpaqueAstcViewFormats}
+ : runtime.ViewFormats(info.format),
+ AstcHostFormat(device, info),
+ !backed && (UsesSparseLayers(device, info, gpu_addr) || sparse_tiles ||
+ IsUnwrittenSparse(device, info, gpu_addr)),
+ sparse_tiles && SparseTilesModeOf(device) == SparseTilesMode::Aliased);
+}
+
+} // Anonymous namespace
+
Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_,
VAddr cpu_addr_)
: VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler},
runtime{&runtime_},
- original_image(MakeImage(runtime_.device, runtime_.memory_allocator,
- 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),
- AstcHostFormat(runtime_.device, info),
- UsesSparseLayers(runtime_.device, info, gpu_addr_) ||
- UsesSparseTiles(runtime_.device, info, gpu_addr_) ||
- IsUnwrittenSparse(runtime_.device, info, gpu_addr_),
- UsesSparseTiles(runtime_.device, info, gpu_addr_) &&
- SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased)),
+ original_image(MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, false)),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
flags |= VideoCommon::ImageFlagBits::Unbacked;
@@ -2377,6 +2385,17 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
flags |= VideoCommon::ImageFlagBits::SparseLayers;
} else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) {
flags |= VideoCommon::ImageFlagBits::SparseTiles;
+ } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) &&
+ SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased) {
+ // The driver can't sample unbound memory (that is why the blocks were to share one),
+ // so an image whose tiles could not be set up is backed whole like any other.
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: backing it whole", gpu_addr_);
+ original_image = MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, true);
+ if (!original_image.IsUnbacked()) {
+ flags &= ~VideoCommon::ImageFlagBits::Unbacked;
+ } else if (texels <= (u64{1} << 28)) {
+ flags |= VideoCommon::ImageFlagBits::UnwrittenSparse;
+ }
} else if (texels <= (u64{1} << 28)) {
// Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing
// was written there yet, so the first write gets it replaced.
diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp
index 5faed0dba9..5d41dc9697 100644
--- a/src/video_core/shader_cache.cpp
+++ b/src/video_core/shader_cache.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cstring>
+#include <functional>
#include <string>
#include <unordered_set>
#include <fmt/ranges.h>
@@ -36,6 +37,17 @@ void ShaderCache::InvalidateRegion(VAddr addr, size_t size) {
RemovePendingShaders();
}
+void ShaderCache::InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences) {
+ if (sequences.empty()) {
+ return;
+ }
+ std::scoped_lock lock{invalidation_mutex};
+ for (const auto& [addr, size] : sequences) {
+ InvalidatePagesInRegion(addr, size);
+ }
+ RemovePendingShaders();
+}
+
void ShaderCache::OnCacheInvalidation(VAddr addr, size_t size) {
std::scoped_lock lock{invalidation_mutex};
InvalidatePagesInRegion(addr, size);
@@ -304,10 +316,17 @@ void ShaderCache::RemovePendingShaders() {
marked_for_removal.clear();
if (!removed_shaders.empty()) {
- // Remove the given shaders from the cache
- std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
- return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end();
- });
+ if (removed_shaders.size() > 16) {
+ std::ranges::sort(removed_shaders, std::less<ShaderInfo*>{});
+ std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
+ return std::binary_search(removed_shaders.begin(), removed_shaders.end(),
+ shader.get(), std::less<ShaderInfo*>{});
+ });
+ } else {
+ std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
+ return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end();
+ });
+ }
}
}
diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h
index 281a90cb6f..7c370e217c 100644
--- a/src/video_core/shader_cache.h
+++ b/src/video_core/shader_cache.h
@@ -67,6 +67,8 @@ public:
/// @param size Number of bytes of the invalidation
void InvalidateRegion(VAddr addr, size_t size);
+ void InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences);
+
/// @brief Unmarks a memory region as cached and marks it for removal
/// @param addr Start address of the CPU write operation
/// @param size Number of bytes of the CPU write operation
@@ -104,6 +106,8 @@ protected:
bool last_shaders_valid = false;
private:
+ friend struct ShaderCacheTestAccess;
+
/// @brief Tries to obtain a cached shader starting in a given address
/// @note Doesn't check for ranges, the given address has to be the start of the shader
/// @param addr Start address of the shader, this doesn't cache for region
diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp
index 22e661c849..0c7d8a0d42 100644
--- a/src/video_core/shader_environment.cpp
+++ b/src/video_core/shader_environment.cpp
@@ -316,7 +316,8 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
bool via_header_index, u32 raw) {
const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)};
- const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit)};
+ const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit,
+ gpu_memory->UsesNativeTiling())};
ASSERT(index <= tic_limit);
const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)};
Tegra::Texture::TICEntry entry;
@@ -327,9 +328,9 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr,
namespace {
// Reads a native program a 4 KiB page at a time instead of one 16-byte instruction at a time.
-// The reads do not flush the GPU caches first (ReadBlockUnsafe): the previous per-instruction
-// ReadBlock did, once per instruction. An instruction that is not inside a fully mapped page is
-// left to the caller.
+// Each page is read with ReadBlock, so GPU-written code is flushed first, once per page instead
+// of once per instruction. An instruction that is not inside a fully mapped page is left to the
+// caller.
class NativeInstructionReader {
public:
using Instruction = Shader::Ampere::Instruction;
@@ -364,7 +365,7 @@ private:
page_begin = address;
page_end = address;
if (length >= sizeof(Instruction) && gpu_memory.IsFullyMappedRange(address, length)) {
- gpu_memory.ReadBlockUnsafe(address, page.data(), length);
+ gpu_memory.ReadBlock(address, page.data(), length);
page_end = next_page;
}
}
@@ -414,7 +415,7 @@ std::shared_ptr<const LoweredNativeCode> GenericEnvironment::LowerNativeCode(u32
if (!native.empty()) break;
throw Shader::RuntimeError("Unmapped native shader instruction at {:#x}", address);
}
- gpu_memory->ReadBlockUnsafe(address, &instruction, sizeof(instruction));
+ gpu_memory->ReadBlock(address, &instruction, sizeof(instruction));
}
native.push_back(instruction);
const auto status = capture.Observe(instruction, index);
diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h
index 75383809c5..a441e572de 100644
--- a/src/video_core/texture_cache/descriptor_table.h
+++ b/src/video_core/texture_cache/descriptor_table.h
@@ -35,10 +35,11 @@ public:
invalidated = true;
}
- [[nodiscard]] std::pair<T, bool> Read(Tegra::MemoryManager const& gpu_memory, u32 index) noexcept {
+ template <class MemoryManager>
+ [[nodiscard]] std::pair<T, bool> Read(const MemoryManager& gpu_memory, u32 index) noexcept {
DEBUG_ASSERT(index <= current_limit);
- if (invalidated) [[unlikely]] {
- std::ranges::fill(read_descriptors, 0);
+ if (invalidated) {
+ ClearReadDescriptors();
invalidated = false;
}
const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T);
@@ -62,10 +63,8 @@ public:
// Mario Brothership reallocates a lot of times, so use aggressive pre-alloc sizes
// std::vector<T> by default uses quadratic growth, but that isn't even enough to satisfy brothership
const size_t num_descriptors = ((limit + 0x80000) & (~0x7ffff)) + 1;
- size_t old_size = read_descriptors.size();
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);
+ ClearReadDescriptors();
invalidated = false;
//
descriptors.resize(num_descriptors);
@@ -76,6 +75,13 @@ public:
GPUVAddr current_gpu_addr{};
u32 current_limit{};
bool invalidated{}; ///< read_descriptors is to be cleared before the next read
+
+private:
+ void ClearReadDescriptors() noexcept {
+ // Reserved capacity is not addressable. A later limit increase clears its new range.
+ const size_t words = (std::min)(read_descriptors.size(), size_t{current_limit} / 64 + 1);
+ std::fill_n(read_descriptors.begin(), words, u64{0});
+ }
};
} // namespace VideoCommon
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index 3f3ae9cafd..4929c2df44 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -809,7 +809,8 @@ 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);
+ index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit,
+ gpu_memory->UsesNativeTiling());
if (index > table.current_limit) {
LOG_DEBUG(HW_GPU, "Invalid image view index={}", index);
return NULL_IMAGE_VIEW_ID;
diff --git a/src/video_core/textures/texture.h b/src/video_core/textures/texture.h
index 79396d8a68..35a4c8eecf 100644
--- a/src/video_core/textures/texture.h
+++ b/src/video_core/textures/texture.h
@@ -167,10 +167,10 @@ static_assert(sizeof(TextureHandle) == 4, "TextureHandle has wrong size");
/// Some Switch 2 bindless handles (Switch Sports' scenery, read from a uniform register) hold
/// the texture header's byte offset in the pool, index * 32, where the handle normally has the
-/// index. Read it as an offset only when the plain index falls past the pool and the offset
-/// lands inside it, so handles that already resolve are left alone.
-[[nodiscard]] constexpr u32 TextureHeaderIndex(u32 index, u32 limit) {
- if (index > limit && index % 32 == 0 && index / 32 <= limit) {
+/// index. Read it as an offset only on Switch 2 (native tiling), when the plain index falls past
+/// the pool and the offset lands inside it, so handles that already resolve are left alone.
+[[nodiscard]] constexpr u32 TextureHeaderIndex(u32 index, u32 limit, bool switch2) {
+ if (switch2 && index > limit && index % 32 == 0 && index / 32 <= limit) {
return index / 32;
}
return index;
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index 622d792210..5f75bcc18e 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -1161,6 +1161,12 @@ FN_MAX_LIMIT_LIST
return properties.driver.driverID == VK_DRIVER_ID_MOLTENVK;
}
+ /// The multiple vertex input binding strides must be. Metal needs 4 bytes, which MoltenVK
+ /// reports as minVertexInputBindingStrideAlignment through VK_KHR_portability_subset.
+ u32 GetMinVertexInputBindingStrideAlignment() const noexcept {
+ return IsMoltenVK() ? 4 : 1;
+ }
+
NvidiaArchitecture GetNvidiaArch() const noexcept {
return nvidia_arch;
}
diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp
index e5a8f81424..8fa39e07e3 100644
--- a/src/yuzu/main_window.cpp
+++ b/src/yuzu/main_window.cpp
@@ -10,6 +10,7 @@
#include <boost/algorithm/string/split.hpp>
#include "common/fs/path_util.h"
+#include "common/game_presets.h"
#include "common/settings.h"
#include "common/settings_enums.h"
#include "frontend_common/settings_generator.h"
@@ -320,6 +321,7 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
constexpr int default_mouse_hide_timeout = 2500;
constexpr int default_input_update_timeout = 1;
+constexpr int EmuThreadStopTimeoutMs = 10'000;
constexpr size_t CopyBufferSize = 1_MiB;
@@ -457,6 +459,29 @@ MainWindow::MainWindow(bool has_broken_vulkan)
// Gen keys if necessary
OnCheckFirmwareDecryption(false);
+ auto* recommendations = new QMessageBox(
+ QMessageBox::Question, tr("Recommended Mario Kart World settings"),
+ tr("Use the recommended settings for Mario Kart World?\n\n"
+ "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, "
+ "reactive flushing off, FIFO Relaxed VSync, handheld mode, ScaleForce, and FXAA.\n\n"
+ "Yes replaces your saved settings. No or closing this window keeps your settings."),
+ QMessageBox::Yes | QMessageBox::No, this);
+ recommendations->setAttribute(Qt::WA_DeleteOnClose);
+ recommendations->setDefaultButton(QMessageBox::No);
+ recommendations->setEscapeButton(QMessageBox::No);
+ recommendations->setWindowModality(Qt::NonModal);
+ connect(recommendations, &QMessageBox::finished, this, [this](int result) {
+ use_recommended_mkw_settings = result == QMessageBox::Yes;
+ if (use_recommended_mkw_settings && !emulation_running && !boot_in_progress) {
+ Settings::SetMarioKartWorldRecommendedSettings(Settings::values, true);
+ config->SaveAllValues();
+ QtCommon::system->ApplySettings();
+ UpdateStatusButtons();
+ }
+ LOG_INFO(Config, "MKW recommended settings accepted: {}", use_recommended_mkw_settings);
+ });
+ recommendations->show();
+
#ifdef __unix__
OnCheckGraphicsBackend();
#endif
@@ -1964,6 +1989,12 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa
QtCommon::system->ApplySettings();
}
+ if (Settings::ApplyMarioKartWorldPreset(Settings::values, title_id,
+ use_recommended_mkw_settings)) {
+ QtCommon::system->ApplySettings();
+ Settings::UpdateGPUAccuracy();
+ LOG_INFO(Config, "Applied recommended MKW settings after per-game configuration");
+ }
Settings::LogSettings();
if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) {
@@ -2209,6 +2240,23 @@ void MainWindow::OnShutdownBeginDialog() {
void MainWindow::OnEmulationStopTimeExpired() {
if (QtCommon::emu_thread) {
QtCommon::emu_thread->ForceStop();
+ emu_terminate_timer.setSingleShot(true);
+ emu_terminate_timer.start(EmuThreadStopTimeoutMs);
+ connect(&emu_terminate_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeout,
+ Qt::UniqueConnection);
+ }
+}
+
+void MainWindow::OnEmulationStopTimeout() {
+ TerminateEmulationThread();
+ OnEmulationStopped();
+}
+
+void MainWindow::TerminateEmulationThread() {
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
+ LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it");
+ QtCommon::emu_thread->terminate();
+ QtCommon::emu_thread->wait();
}
}
@@ -2220,6 +2268,7 @@ void MainWindow::OnEmulationStopped() {
return;
}
shutdown_timer.stop();
+ emu_terminate_timer.stop();
if (QtCommon::emu_thread) {
QtCommon::emu_thread->disconnect();
QtCommon::emu_thread->wait();
@@ -2309,13 +2358,17 @@ void MainWindow::ShutdownGame() {
play_time_manager->Stop();
OnShutdownBegin();
OnEmulationStopTimeExpired();
- if (QtCommon::emu_thread) {
+ emu_terminate_timer.stop();
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
QEventLoop loop;
+ QTimer timeout;
+ timeout.setSingleShot(true);
connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit);
- if (QtCommon::emu_thread->isRunning()) {
- loop.exec(QEventLoop::ExcludeUserInputEvents);
- }
+ connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
+ timeout.start(EmuThreadStopTimeoutMs);
+ loop.exec(QEventLoop::ExcludeUserInputEvents);
}
+ TerminateEmulationThread();
OnEmulationStopped();
}
diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h
index e2c5a0f6db..d6b0e5b312 100644
--- a/src/yuzu/main_window.h
+++ b/src/yuzu/main_window.h
@@ -279,6 +279,7 @@ private:
StartGameType with_config = StartGameType::Normal);
void BootGameFromList(const QString& filename, StartGameType with_config);
void ShutdownGame();
+ void TerminateEmulationThread();
void SetDiscordEnabled(bool state);
void LoadAmiibo(const QString& filename);
@@ -437,6 +438,7 @@ private slots:
void OnShutdownBeginDialog();
void OnEmulationStopped();
void OnEmulationStopTimeExpired();
+ void OnEmulationStopTimeout();
#ifdef ENABLE_UPDATE_CHECKER
void OnEmulatorUpdateAvailable();
@@ -501,11 +503,13 @@ private:
#endif
MultiplayerState* multiplayer_state = nullptr;
+ bool use_recommended_mkw_settings = false;
GRenderWindow* render_window = nullptr;
GameList* game_list = nullptr;
LoadingScreen* loading_screen = nullptr;
QTimer shutdown_timer;
+ QTimer emu_terminate_timer;
OverlayDialog* shutdown_dialog{};
PerformanceOverlay* perf_overlay = nullptr;
#ifdef HAS_RESHADE