Compare commits

..

6 Commits

Author SHA1 Message Date
wildcard 0cda02e91e potential fix for totk and smo for amd 2026-06-23 05:31:28 +02:00
lizzie f8facda35f [vk] Prevent OOM due to increased staging buffer on *nix (#4116)
*nix drivers for NVIDIA doesn't seem to like the increased size.
Windows is fine with it however.

Signed-off-by: lizzie <lizzie@eden-emu.dev>

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4116
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-06-23 01:48:11 +02:00
crueter 39be450fa3 [desktop] Add basic carousel view (#4112)
Adds a basic carousel view, or essentially a horizontal list a la Android/Qt Quick.

Lacks a lot of niceties like autoscroll, smooth shifts, etc. Will work on those later

Also fixed a bug introduced recently that capped game icon size to 8 at the low end, breaking the None option

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4112
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-06-22 21:04:47 +02:00
MaranBr 68aaea6085 [buffer_cache] Refactor buffer synchronization to use bounding-box download region (#4094)
Implement bounding-box buffer synchronization to minimize downloads and preserve GPU-modified regions during uploads.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4094
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
2026-06-22 17:05:59 +02:00
MaranBr ba608d2b57 [video_core] Add in-flight flush throttling to prevent CPU/GPU desync (#4085)
Prevent the CPU from getting too far ahead of the GPU by limiting pending flushes.

This fixes graphical issues in games that rely on better CPU and GPU synchronization in the Scheduler, such as the flickering bug inside shrines in The Legend of Zelda: Tears of the Kingdom.

This is enabled when the GPU is in Accurate mode.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4085
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
2026-06-22 17:05:18 +02:00
MaranBr c22f57bc4e [video_core] Improve StagingBufferPool stream allocation (#4101)
This fixes (I really hope so) the random graphical corruption issue that can occur in Mario Kart 8 Deluxe and possibly other games as well.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4101
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
2026-06-22 17:04:52 +02:00
25 changed files with 385 additions and 105 deletions
+10 -10
View File
@@ -9,8 +9,8 @@
},
"openssl-cmake": {
"repo": "jimmy-park/openssl-cmake",
"hash": "bc73a6c035f299711bb90e20b16fa3521e8ffdab32d8d17f9e7f5cde1ff5def37ec6f9e03de25c3466d18885815c0874331f56e592b8a2f7deeb484c69c5c621",
"git_version": "4.0.1",
"hash": "2cc185c924fd70e7d886257ca0caa42b3b8f7f712f2052b4f94dde74759e27022de76178460e18c9bdfc57c366583999e198fbb6052d4e7d91c099d15a0ca63e",
"git_version": "3.6.2",
"tag": "%VERSION%",
"bundled": true,
"options": [
@@ -28,8 +28,8 @@
"package": "OpenSSL",
"min_version": "3",
"version": "3",
"hash": "2405891660b67e4cc01c7868e100f2766e1c6290120213ae62f775ef3d9610c3527bdc43d4c831260417450c74353b0693ae3e24cca3e99b02e4daa548b93904",
"git_version": "4.0.1",
"hash": "29002ce50cb95a4f4f1d0e9d3f684401fbd4eac34203dc2eef3b6334af5d44aa46bf788b63a6f5c139c383eafb7269ae87a58a9a3ad5912903b9773e545ccc0a",
"git_version": "3.6.2",
"tag": "openssl-%VERSION%",
"patches": [
"0001-add-bundled-cert.patch"
@@ -40,8 +40,8 @@
"repo": "boostorg/boost",
"tag": "boost-%VERSION%",
"artifact": "%TAG%-cmake.tar.xz",
"hash": "935b3d516e996f6d25948ba8a54c1b7f70f7f0e3f517e36481fdf0196c2c5cfc2841f86e891f3df9517746b7fb605db47cdded1b8ff78d9482ddaa621db43a34",
"git_version": "1.91.0",
"hash": "6ae6e94664fe7f2fb01976b59b276ac5df8085c7503fa829d810fbfe495960cfec44fa2c36e2cb23480bc19c956ed199d4952b02639a00a6c07625d4e7130c2d",
"git_version": "1.90.0",
"version": "1.57",
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
"patches": [
@@ -51,9 +51,9 @@
"fmt": {
"repo": "fmtlib/fmt",
"tag": "%VERSION%",
"hash": "5ac2ba0f54a484999ed5407d82b77aad170cea49a267decd2c0eedadf3b14413e2a83fcc8e9ca9c16640595e019b8636e160f72314d8be50653324e82ac745eb",
"hash": "f0da82c545b01692e9fd30fdfb613dbb8dd9716983dcd0ff19ac2a8d36f74beb5540ef38072fdecc1e34191b3682a8542ecbf3a61ef287dbba0a2679d4e023f2",
"version": "8",
"git_version": "12.2.0"
"git_version": "12.1.0"
},
"lz4": {
"name": "lz4",
@@ -126,9 +126,9 @@
"package": "VVL",
"repo": "KhronosGroup/Vulkan-ValidationLayers",
"tag": "vulkan-sdk-%VERSION%",
"git_version": "1.4.350.1",
"git_version": "1.4.341.0",
"artifact": "android-binaries-%VERSION%.zip",
"hash": "1888eaaa4f0ccd7f3d1f0c6485ae5508f39e7b402ea8486d8243756d63073def569ddee75ea40b403cbfcb2ead515c410d2ff16ae48d8a4d6b4fbacbd78e24e4"
"hash": "8812ae84cbe49e6a3418ade9c458d3be6d74a3dffd319d4502007b564d580998056e8190414368ec11b27bc83993c7a0dad713c31bcc3d9553b51243efee3753"
},
"quazip": {
"package": "QuaZip-Qt6",
+18 -18
View File
@@ -3,9 +3,9 @@
"package": "VulkanMemoryAllocator",
"repo": "GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator",
"tag": "v%VERSION%",
"hash": "563acbcd8912d10d92c23715eba7acf0e7c1683af36021f415b36f359c2cce065f3906e395c32282a8410ec5c8179fbcb6412935c6629a49357475d4b4410e2a",
"hash": "deb5902ef8db0e329fbd5f3f4385eb0e26bdd9f14f3a2334823fb3fe18f36bc5d235d620d6e5f6fe3551ec3ea7038638899db8778c09f6d5c278f5ff95c3344b",
"find_args": "CONFIG",
"git_version": "3.4.0"
"git_version": "3.3.0"
},
"sirit": {
"repo": "eden-emulator/sirit",
@@ -28,8 +28,8 @@
"httplib": {
"repo": "yhirose/cpp-httplib",
"tag": "v%VERSION%",
"hash": "bc9474b0069b84b33f96790d48cee5b52eb1b9555ea6321ce878679c4469f1c56b87b34e74786e47d09cad5a00539cdd6726058bd0ffb2087255f02d7bdabe51",
"git_version": "0.48.0",
"hash": "159ed94965018f2a371d45a3bfc1961e5fb1549e501ded70a6b4532d7fe99d0579c18b5195aff6e35f96f399b426cea2650ec9fb75ef80d4c9edeccb51f2e6c9",
"git_version": "0.46.0",
"find_args": "MODULE GLOBAL",
"patches": [
"0001-mingw.patch"
@@ -56,8 +56,8 @@
"package": "xbyak",
"repo": "herumi/xbyak",
"tag": "v%VERSION%",
"hash": "14a0a8e3a768e6256e220b5edfd783de5e89f36736ef62674c8532f2564df03719311db923a08d2c930491632f2f83e486c8264e9997b6394ef59d32cae3070b",
"git_version": "7.37.4"
"hash": "b6475276b2faaeb315734ea8f4f8bd87ededcee768961b39679bee547e7f3e98884d8b7851e176d861dab30a80a76e6ea302f8c111483607dde969b4797ea95a",
"git_version": "7.35.2"
},
"oaknut": {
"repo": "eden-emulator/oaknut",
@@ -139,9 +139,9 @@
"package": "Catch2",
"repo": "catchorg/Catch2",
"tag": "v%VERSION%",
"hash": "2e32d3d41e4ef84d927eaaaff0a75eba8fd1e47a5ddeb6007b73934f9d1ac747bf9818d02d2cdfb6343830b5091ba4f3d66efc281fc376a4f0716753dd7c9a97",
"hash": "7eea385d79d88a5690cde131fe7ccda97d5c54ea09d6f515000d7bf07c828809d61c1ac99912c1ee507cf933f61c1c47ecdcc45df7850ffa82714034b0fccf35",
"version": "3.0.1",
"git_version": "3.15.1",
"git_version": "3.13.0",
"patches": [
"0001-solaris-isnan-fix.patch"
]
@@ -157,16 +157,16 @@
"package": "SimpleIni",
"repo": "brofield/simpleini",
"tag": "v%VERSION%",
"hash": "a62c5748efe2473aae5bddab96ba9114d981a72f5b0d1a44d563daa085d5c231ed8c447794691d9bd67e1e0c6bfb44e4a8736be75fee59967d0c67ce3a59bb6e",
"hash": "b937c18a7b6277d77ca7ebfb216af4984810f77af4c32d101b7685369a4bd5eb61406223f82698e167e6311a728d07415ab59639fdf19eff71ad6dc2abfda989",
"find_args": "MODULE",
"git_version": "4.26"
"git_version": "4.25"
},
"sdl3": {
"package": "SDL3",
"repo": "libsdl-org/SDL",
"tag": "release-%VERSION%",
"hash": "10a9c1fa4d6891c7387fa694b786a168f86b6e5447c6c522f85b739e33720e106ee879dfa9e6e537d5521b983cfbd4908927c555c4d3412735129a4231e16fe6",
"git_version": "3.4.10",
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
"git_version": "3.4.8",
"version": "3.2.10"
},
"moltenvk": {
@@ -194,9 +194,9 @@
"libusb": {
"repo": "libusb/libusb",
"tag": "v%VERSION%",
"hash": "3251c9f41e900efa13caf981483f46886c8434bf4c30f3fd3073921b06be9977cebcf4a18f7bc46db86e33d7f19752296377be9050abfb8d887ffeb377864647",
"hash": "98c5f7940ff06b25c9aa65aa98e23de4c79a4c1067595f4c73cc145af23a1c286639e1ba11185cd91bab702081f307b973f08a4c9746576dc8d01b3620a3aeb5",
"find_args": "MODULE",
"git_version": "1.0.30",
"git_version": "1.0.29",
"patches": [
"0001-netbsd-gettime.patch"
]
@@ -229,15 +229,15 @@
"repo": "KhronosGroup/Vulkan-Headers",
"package": "VulkanHeaders",
"version": "1.4.317",
"hash": "9f92c0a5fee856be4104e72180d1914ed819b77f680f8add13282c88239e3d4bf8891a738920b1e38b2f00b121c77120834a87b8c9aae6bf60c11a01b2a58492",
"git_version": "1.4.354",
"hash": "d2846ea228415772645eea4b52a9efd33e6a563043dd3de059e798be6391a8f0ca089f455ae420ff22574939ed0f48ed7c6ff3d5a9987d5231dbf3b3f89b484b",
"git_version": "1.4.345",
"tag": "v%VERSION%"
},
"vulkan-utility-libraries": {
"repo": "KhronosGroup/Vulkan-Utility-Libraries",
"package": "VulkanUtilityLibraries",
"hash": "17a282a5eb72f905254eb62e2a4a4d6cfc3acca79f8323365e55cb9be903f75d752b6112bd016a6bec22690dcef91554cbcb2656130eccfc094ece05db0a445f",
"git_version": "1.4.354",
"hash": "114f6b237a6dcba923ccc576befb5dea3f1c9b3a30de7dc741f234a831d1c2d52d8a224afb37dd57dffca67ac0df461eaaab6a5ab5e503b393f91c166680c3e1",
"git_version": "1.4.345",
"tag": "v%VERSION%"
},
"frozen": {
+1 -1
View File
@@ -158,7 +158,7 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(GameListMode, TreeView, GridView);
ENUM(GameListMode, TreeView, GridView, CarouselView);
ENUM(SpeedMode, Standard, Turbo, Slow);
template <typename Type>
+1 -1
View File
@@ -207,7 +207,7 @@ struct Values {
// Game List
Setting<bool> show_add_ons{linkage, true, "show_add_ons", Category::UiGameList};
Setting<u32, true> game_icon_size{linkage, 64, 8, 512, "game_icon_size", Category::UiGameList};
Setting<u32, true> game_icon_size{linkage, 64, 0, 512, "game_icon_size", Category::UiGameList};
Setting<u32, true> folder_icon_size{linkage, 48, 8, 512, "folder_icon_size", Category::UiGameList};
Setting<u8> row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList};
Setting<u8> row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList};
+1
View File
@@ -165,6 +165,7 @@ public:
return QStringLiteral("%1\n %2").arg(row1, row2);
}
case Settings::GameListMode::GridView:
case Settings::GameListMode::CarouselView:
return row1;
default:
break;
+22 -26
View File
@@ -1619,36 +1619,33 @@ void BufferCache<P>::TouchBuffer(Buffer& buffer, BufferId buffer_id) noexcept {
template <class P>
bool BufferCache<P>::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 size) {
upload_copies.clear();
u64 staging_offset = 0;
u64 total_size_bytes = 0;
u64 largest_copy = 0;
DAddr buffer_start = buffer.CpuAddr();
auto push = [&](u64 start, u64 end) {
if (start >= end) {
return;
}
u64 sz = end - start;
upload_copies.push_back({
.src_offset = staging_offset,
.dst_offset = start - buffer_start,
.size = sz
const DAddr buffer_start = buffer.cpu_addr_cached;
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 device_addr_out, u64 range_size) {
upload_copies.push_back(BufferCopy{
.src_offset = total_size_bytes,
.dst_offset = device_addr_out - buffer_start,
.size = range_size,
});
staging_offset += sz;
largest_copy = (std::max)(largest_copy, sz);
};
memory_tracker.ForEachUploadRange(device_addr, size, [&](u64 addr, u64 range_size) {
u64 start = addr;
u64 end = addr + range_size;
gpu_modified_ranges.ForEachInRange(start, range_size, [&](u64 gstart, u64 gsize) {
u64 gend = gstart + gsize;
push(start, gstart);
start = (std::max)(start, gend);
});
push(start, end);
total_size_bytes += range_size;
largest_copy = (std::max)(largest_copy, range_size);
});
if (upload_copies.empty()) {
if (total_size_bytes == 0) {
return true;
}
UploadMemory(buffer, staging_offset, largest_copy, std::span(upload_copies));
u64 min_offset = (std::numeric_limits<u64>::max)();
u64 max_offset = 0;
for (const auto& copy : upload_copies) {
min_offset = (std::min)(min_offset, copy.dst_offset);
max_offset = (std::max)(max_offset, copy.dst_offset + copy.size);
}
const DAddr sync_addr = buffer.CpuAddr() + min_offset;
const u64 sync_size = max_offset - min_offset;
DownloadBufferMemory(buffer, sync_addr, sync_size);
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
any_buffer_uploaded = true;
return false;
}
@@ -1699,7 +1696,6 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
// Apply the staging offset
copy.src_offset += upload_staging.offset;
}
@@ -62,6 +62,9 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
extended_dynamic_state_2_logic_op.Assign(features.has_extended_dynamic_state_2_logic_op ? 1 : 0);
extended_dynamic_state_3_blend.Assign(features.has_extended_dynamic_state_3_blend ? 1 : 0);
extended_dynamic_state_3_enables.Assign(features.has_extended_dynamic_state_3_enables ? 1 : 0);
dynamic_state3_depth_clamp_enable.Assign(features.has_dynamic_state3_depth_clamp_enable ? 1 : 0);
dynamic_state3_logic_op_enable.Assign(features.has_dynamic_state3_logic_op_enable ? 1 : 0);
dynamic_state3_line_stipple_enable.Assign(features.has_dynamic_state3_line_stipple_enable ? 1 : 0);
dynamic_vertex_input.Assign(features.has_dynamic_vertex_input ? 1 : 0);
xfb_enabled.Assign(regs.transform_feedback_enabled != 0);
ndc_minus_one_to_one.Assign(regs.depth_mode == Maxwell::DepthMode::MinusOneToOne ? 1 : 0);
@@ -208,6 +208,9 @@ struct FixedPipelineState {
BitField<12, 2, u32> tessellation_spacing;
BitField<14, 1, u32> tessellation_clockwise;
BitField<15, 5, u32> patch_control_points_minus_one;
BitField<20, 1, u32> dynamic_state3_depth_clamp_enable;
BitField<21, 1, u32> dynamic_state3_logic_op_enable;
BitField<22, 1, u32> dynamic_state3_line_stipple_enable;
BitField<24, 4, Maxwell::PrimitiveTopology> topology;
BitField<28, 4, Tegra::Texture::MsaaMode> msaa_mode;
@@ -907,10 +907,10 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
// EDS3 - Enables (composite: per-feature)
if (key.state.extended_dynamic_state_3_enables) {
if (device.SupportsDynamicState3DepthClampEnable()) {
if (key.state.dynamic_state3_depth_clamp_enable) {
dynamic_states.push_back(VK_DYNAMIC_STATE_DEPTH_CLAMP_ENABLE_EXT);
}
if (device.SupportsDynamicState3LogicOpEnable()) {
if (key.state.dynamic_state3_logic_op_enable) {
dynamic_states.push_back(VK_DYNAMIC_STATE_LOGIC_OP_ENABLE_EXT);
}
if (device.SupportsDynamicState3LineRasterizationMode()) {
@@ -919,7 +919,7 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
if (device.SupportsDynamicState3ConservativeRasterizationMode()) {
dynamic_states.push_back(VK_DYNAMIC_STATE_CONSERVATIVE_RASTERIZATION_MODE_EXT);
}
if (device.SupportsDynamicState3LineStippleEnable()) {
if (key.state.dynamic_state3_line_stipple_enable) {
dynamic_states.push_back(VK_DYNAMIC_STATE_LINE_STIPPLE_ENABLE_EXT);
}
if (device.SupportsDynamicState3AlphaToCoverageEnable()) {
@@ -27,6 +27,7 @@
namespace Vulkan {
constexpr u64 MAX_PENDING_FLUSHES = 5;
void Scheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf,
vk::CommandBuffer upload_cmdbuf) {
@@ -115,7 +116,18 @@ Scheduler::Scheduler(const Device& device_, StateTracker& state_tracker_)
Scheduler::~Scheduler() = default;
u64 Scheduler::Flush(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
// When flushing, we only send data to the worker thread; no waiting is necessary.
// Prevent the CPU from getting too far ahead of the GPU by limiting pending flushes.
const bool should_throttle = Settings::IsGPULevelHigh();
if (should_throttle) {
const u64 current_tick = master_semaphore->CurrentTick();
const u64 gap = current_tick > last_submitted_tick ? current_tick - last_submitted_tick : 0;
const u64 step = (std::min)(MAX_PENDING_FLUSHES, gap);
const u64 new_tick = last_submitted_tick + step;
if (new_tick < current_tick) {
last_submitted_tick = new_tick;
master_semaphore->Wait(last_submitted_tick);
}
}
const u64 signal_value = SubmitExecution(signal_semaphore, wait_semaphore);
AllocateNewContext();
return signal_value;
@@ -296,6 +296,8 @@ private:
double last_target_fps{};
u64 max_frame_count{};
u64 frame_counter{};
u64 last_submitted_tick = 0;
};
} // namespace Vulkan
@@ -27,8 +27,17 @@ using namespace Common::Literals;
// Maximum potential alignment of a Vulkan buffer
constexpr VkDeviceSize MAX_ALIGNMENT = 256;
// Stream buffer size in bytes
// *NIX drivers are more sensitive to increased buffers for streaming.
// Windows ones however, can intake bigger buffers and generally do not OOM.
// - GTX 960 on Windows will not OOM with 256mib
// - GT 1030 on ^NIX will OOM with 256mib
#ifdef _WIN32
constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 256_MiB;
#else
constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB;
#endif
size_t GetStreamBufferSize(const Device& device) {
if (!device.HasDebuggingToolAttached()) {
@@ -46,13 +55,13 @@ size_t GetStreamBufferSize(const Device& device) {
// If rebar is not supported, cut the max heap size to 40%. This will allow 2 captures to be
// loaded at the same time in RenderDoc. If rebar is supported, this shouldn't be an issue
// as the heap will be much larger.
if (size <= 256_MiB) {
if (size <= MAX_STREAM_BUFFER_SIZE) {
size = size * 40 / 100;
}
} else {
size = MAX_STREAM_BUFFER_SIZE;
}
return (std::min)(Common::AlignUp(size, MAX_ALIGNMENT), MAX_STREAM_BUFFER_SIZE);
return Common::AlignUp(size, MAX_ALIGNMENT);
}
} // Anonymous namespace
+3 -1
View File
@@ -240,7 +240,9 @@ add_executable(yuzu
render/performance_overlay.h render/performance_overlay.cpp render/performance_overlay.ui
updater/update_dialog.h updater/update_dialog.cpp updater/update_dialog.ui
game/common.h)
game/common.h
game/carousel.h game/carousel.cpp
)
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
+107
View File
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/config/uisettings.h"
#include "qt_common/game_list/model.h"
#include "yuzu/game/common.h"
#include "yuzu/game/game_card.h"
#include "yuzu/game/carousel.h"
GameCarousel::GameCarousel(QWidget* parent) : QListView{parent} {
m_gameCard = new GameCard(this);
setItemDelegate(m_gameCard);
setViewMode(QListView::IconMode);
setMovement(QListView::Static);
setUniformItemSizes(true);
setSelectionMode(QAbstractItemView::SingleSelection);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setContextMenuPolicy(Qt::CustomContextMenu);
setSpacing(10);
setWordWrap(true);
setTextElideMode(Qt::ElideRight);
setFlow(QListView::LeftToRight);
setWrapping(false);
}
void GameCarousel::SetModel(GameListModel* model) {
QListView::setModel(model);
UpdateIconSize();
}
void GameCarousel::ApplyFilter(const QString& edit_filter_text, GameListModel* model) {
int row_count = model->rowCount();
for (int i = 0; i < row_count; ++i) {
QStandardItem* item = model->item(i, 0);
if (!item)
continue;
if (Yuzu::FilterMatches(edit_filter_text, item)) {
setRowHidden(i, false);
} else {
setRowHidden(i, true);
}
}
}
void GameCarousel::UpdateIconSize() {
const u32 icon_size = UISettings::values.game_icon_size.GetValue();
int heightMargin = 0;
int widthMargin = 80;
// TODO(crueter): get rid of this nonsense
if (UISettings::values.show_game_name) {
switch (icon_size) {
case 128:
heightMargin = 65;
break;
case 0:
widthMargin = 120;
heightMargin = 120;
break;
case 64:
heightMargin = 77;
break;
case 32:
case 256:
heightMargin = 81;
break;
}
} else {
widthMargin = 24;
heightMargin = 24;
}
const int min_item_width = icon_size + widthMargin;
const int min_item_height = icon_size + heightMargin;
const int grid_height = std::max(min_item_height, viewport()->height());
QSize content_size(min_item_width, min_item_height);
QSize grid_size(min_item_width, grid_height);
if (gridSize() != grid_size) {
setUpdatesEnabled(false);
setGridSize(grid_size);
m_gameCard->setSize(grid_size, content_size, 0, 0);
setUpdatesEnabled(true);
}
}
QModelIndex GameCarousel::indexAt(const QPoint& point) const {
QModelIndex index = QListView::indexAt(point);
if (!index.isValid())
return {};
if (m_gameCard && !m_gameCard->hitTest(point, index, this, visualRect(index)))
return {};
return index;
}
+27
View File
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QListView>
#include <QString>
class GameCard;
class GameListModel;
class QResizeEvent;
class GameCarousel : public QListView {
Q_OBJECT
public:
explicit GameCarousel(QWidget* parent = nullptr);
void SetModel(GameListModel* model);
void ApplyFilter(const QString& edit_filter_text, GameListModel* model);
void UpdateIconSize();
QModelIndex indexAt(const QPoint& point) const override;
private:
GameCard* m_gameCard = nullptr;
};
+54 -15
View File
@@ -18,27 +18,16 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
constexpr int cardMargin = 8;
constexpr int cardCornerRadius = 10;
const int column = index.row() % m_columns;
const int cell_width = option.rect.width();
const int card_width = cell_width - m_padding;
const int row_width = m_columns * cell_width;
const int total_gap = row_width - cardMargin * 2 - m_columns * card_width;
const int gap = (m_columns > 1) ? (total_gap / (m_columns - 1)) : 0;
const int card_left = option.rect.left() - column * cell_width + cardMargin + column * (card_width + gap) + 4;
const QRect cardRect(card_left, option.rect.top() + 4, card_width - 8,
option.rect.height() - cardMargin);
const QRect cardRect = getCardRect(option, index);
QPalette palette = option.palette;
QColor backgroundColor = palette.window().color();
QColor borderColor = palette.dark().color();
QColor textColor = palette.text().color();
// highlight blue on select
// highlight on select or hover
if (option.state & QStyle::State_Selected) {
backgroundColor = palette.highlight().color();
borderColor = palette.highlight().color().lighter(150);
@@ -60,7 +49,7 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
scaled.scale(icon_size, icon_size, Qt::KeepAspectRatio);
iconRect = {cardRect.left() + (cardRect.width() - scaled.width()) / 2,
cardRect.top() + cardMargin, scaled.width(), scaled.height()};
cardRect.top() + cardMargin - 1, scaled.width(), scaled.height()};
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
@@ -94,12 +83,62 @@ void GameCard::paint(QPainter* painter, const QStyleOptionViewItem& option,
painter->restore();
}
QRect GameCard::getCardRect(const QStyleOptionViewItem& option, const QModelIndex& index) const {
const int cell_width = option.rect.width();
const int card_width = cell_width - m_padding;
int card_left, card_top, card_height;
if (m_columns >= 1) {
// grid mode
// center everything in-line, such that the leftmost and rightmost cards
// have ~ equal padding to the edge of the viewport
// spacing between each card is larger, but equal to each other
const int column = index.row() % m_columns;
const int row_width = m_columns * cell_width;
const int total_gap = row_width - cardMargin * 2 - m_columns * card_width;
const int gap = (m_columns > 1) ? (total_gap / (m_columns - 1)) : 0;
card_left =
option.rect.left() - column * cell_width + cardMargin + column * (card_width + gap) + 4;
// fill cell vertically
card_top = option.rect.top() + cardMargin;
card_height = option.rect.height() - cardMargin - 1;
} else {
// carousel mode
card_left = option.rect.left() + cardMargin + 4;
// the delegate itself takes up the full height, but the card itself
// gets centered
const int content_height = m_contentSize.height() - cardMargin;
const int cell_height = option.rect.height();
card_height = std::min(content_height, cell_height - cardMargin * 2) - 1;
card_top = option.rect.top() + (cell_height - card_height) / 2;
}
return QRect(card_left, card_top, card_width - cardMargin, card_height);
}
bool GameCard::hitTest(const QPoint& point, const QModelIndex& index, const QWidget* widget,
const QRect& cellRect) const {
QStyleOptionViewItem option;
option.initFrom(widget);
option.rect = cellRect;
return getCardRect(option, index).contains(point);
}
QSize GameCard::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const {
return m_size;
}
void GameCard::setSize(const QSize& newSize, const int padding, const int columns) {
void GameCard::setSize(const QSize& newSize, const QSize& contentSize, const int padding,
const int columns) {
m_size = newSize;
m_contentSize = contentSize;
m_padding = padding;
m_columns = columns;
}
+9 -1
View File
@@ -17,11 +17,19 @@ public:
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
QRect getCardRect(const QStyleOptionViewItem &option, const QModelIndex &index) const;
bool hitTest(const QPoint& point, const QModelIndex& index,
const QWidget* widget, const QRect& cellRect) const;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void setSize(const QSize& newSize, const int padding, const int columns);
void setSize(const QSize& newSize, const QSize& contentSize, const int padding, const int columns);
private:
static constexpr int cardMargin = 8;
QSize m_size;
QSize m_contentSize;
int m_padding;
int m_columns;
};
+10 -3
View File
@@ -25,8 +25,6 @@ GameGrid::GameGrid(QWidget* parent) : QListView{parent} {
setEditTriggers(QAbstractItemView::NoEditTriggers);
setContextMenuPolicy(Qt::CustomContextMenu);
setGridSize(QSize(140, 160));
m_gameCard->setSize(gridSize(), 0, 4);
setSpacing(10);
setWordWrap(true);
@@ -97,8 +95,17 @@ void GameGrid::UpdateIconSize() {
setUpdatesEnabled(false);
setGridSize(grid_size);
m_gameCard->setSize(grid_size, stretched_width - min_item_width, columns);
m_gameCard->setSize(grid_size, grid_size, stretched_width - min_item_width, columns);
setUpdatesEnabled(true);
}
}
QModelIndex GameGrid::indexAt(const QPoint& point) const {
QModelIndex index = QListView::indexAt(point);
if (!index.isValid())
return {};
if (m_gameCard && !m_gameCard->hitTest(point, index, this, visualRect(index)))
return {};
return index;
}
+2 -2
View File
@@ -6,8 +6,6 @@
#include <QListView>
#include <QString>
#include "common/common_types.h"
class GameCard;
class GameListModel;
@@ -21,6 +19,8 @@ public:
void ApplyFilter(const QString& edit_filter_text, GameListModel* model);
void UpdateIconSize();
QModelIndex indexAt(const QPoint& point) const override;
private:
GameCard* m_gameCard = nullptr;
};
+58 -13
View File
@@ -20,6 +20,7 @@
#include <QScrollerProperties>
#include <QToolButton>
#include <QVariantAnimation>
#include <qlayoutitem.h>
#include "common/common_types.h"
#include "core/core.h"
@@ -31,6 +32,7 @@
#include "qt_common/qt_common.h"
#include "qt_common/util/game.h"
#include "yuzu/compatibility_list.h"
#include "yuzu/game/carousel.h"
#include "yuzu/game/game_grid.h"
#include "yuzu/game/game_list.h"
#include "yuzu/game/game_tree.h"
@@ -67,6 +69,9 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
connect(grid_view, &QListView::activated, this, &GameList::ValidateEntry);
connect(grid_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(carousel_view, &QListView::activated, this, &GameList::ValidateEntry);
connect(carousel_view, &QListView::customContextMenuRequested, this, &GameList::PopupContextMenu);
connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, this,
[this](Qt::Key key) {
if (system.IsPoweredOn()) {
@@ -86,6 +91,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
connect(item_model, &GameListModel::SaveConfig, this, &GameList::SaveConfig);
connect(item_model, &GameListModel::PopulatingStarted, this, &GameList::OnPopulate);
// TODO: impl on grid/carousel
connect(tree_view, &GameTree::FilterResultReady, search_field,
[this](int visible, int total) { search_field->setFilterResult(visible, total); });
@@ -101,12 +107,11 @@ GameList::~GameList() {
void GameList::SetupViews() {
tree_view = new GameTree(this);
grid_view = new GameGrid(this);
carousel_view = new GameCarousel(this);
tree_view->SetModel(item_model);
grid_view->SetModel(item_model);
layout->addWidget(tree_view);
layout->addWidget(grid_view);
carousel_view->SetModel(item_model);
}
QString GameList::GetLastFilterResultItem() const {
@@ -138,10 +143,16 @@ void GameList::LoadCompatibilityList() {
void GameList::OnPopulate() {
m_currentView->setEnabled(false);
if (m_isTreeMode) {
grid_view->UpdateIconSize();
} else {
switch (game_list_mode) {
case Settings::GameListMode::TreeView:
tree_view->UpdateColumnVisibility(item_model);
break;
case Settings::GameListMode::GridView:
grid_view->UpdateIconSize();
break;
case Settings::GameListMode::CarouselView:
carousel_view->UpdateIconSize();
break;
}
}
@@ -166,26 +177,46 @@ void GameList::UnloadController() {
}
void GameList::ResetViewMode() {
auto& setting = UISettings::values.game_list_mode;
const auto mode = UISettings::values.game_list_mode.GetValue();
game_list_mode = mode;
if (m_currentView)
layout->removeWidget(m_currentView);
bool newTreeMode = false;
switch (setting.GetValue()) {
switch (mode) {
case Settings::GameListMode::TreeView:
m_currentView = tree_view;
newTreeMode = true;
tree_view->setVisible(true);
grid_view->setVisible(false);
break;
case Settings::GameListMode::GridView:
m_currentView = grid_view;
newTreeMode = false;
grid_view->setVisible(true);
tree_view->setVisible(false);
break;
case Settings::GameListMode::CarouselView:
m_currentView = carousel_view;
newTreeMode = false;
break;
default:
UNREACHABLE();
}
tree_view->setVisible(false);
grid_view->setVisible(false);
carousel_view->setVisible(false);
tree_view->setEnabled(false);
grid_view->setEnabled(false);
carousel_view->setEnabled(false);
m_currentView->setVisible(true);
m_currentView->setEnabled(true);
layout->insertWidget(0, m_currentView);
auto view = m_currentView->viewport();
view->installEventFilter(this);
@@ -297,11 +328,13 @@ void GameList::OnPopulatingCompleted(const QStringList& watch_list) {
for (int i = 1; i < item_model->rowCount() - 1; ++i) {
children_total += item_model->item(i, 0)->rowCount();
}
search_field->setFilterResult(children_total, children_total);
if (children_total > 0) {
search_field->setFocus();
}
// TODO: carousel/grid impl.
item_model->sort(tree_view->header()->sortIndicatorSection(),
tree_view->header()->sortIndicatorOrder());
@@ -317,8 +350,15 @@ void GameList::RefreshExternalContent() {
}
void GameList::UpdateIconSizes() {
if (!m_isTreeMode) {
switch (game_list_mode) {
case Settings::GameListMode::GridView:
grid_view->UpdateIconSize();
break;
case Settings::GameListMode::CarouselView:
carousel_view->UpdateIconSize();
break;
case Settings::GameListMode::TreeView:
break;
}
}
@@ -712,6 +752,11 @@ bool GameList::eventFilter(QObject* obj, QEvent* event) {
return true;
}
if (obj == carousel_view->viewport() && event->type() == QEvent::Resize) {
carousel_view->UpdateIconSize();
return true;
}
return QWidget::eventFilter(obj, event);
}
+6 -1
View File
@@ -15,12 +15,12 @@
#include <QVBoxLayout>
#include <QVector>
#include <QWidget>
#include <qabstractitemview.h>
#include "common/common_types.h"
#include "core/core.h"
#include "frontend_common/play_time_manager.h"
#include "qt_common/config/uisettings.h"
#include "qt_common/game_list/model.h"
#include "qt_common/util/game.h"
#include "yuzu/compatibility_list.h"
@@ -38,6 +38,9 @@ class MainWindow;
enum class AmLaunchType;
enum class StartGameType;
class GameCarousel;
class QAbstractItemView;
namespace Core {
class System;
}
@@ -151,7 +154,9 @@ private:
GameTree* tree_view = nullptr;
GameGrid* grid_view = nullptr;
GameCarousel* carousel_view = nullptr;
GameListModel* item_model = nullptr;
Settings::GameListMode game_list_mode;
ControllerNavigation* controller_navigation = nullptr;
-2
View File
@@ -3,8 +3,6 @@
#pragma once
#include <QHeaderView>
#include <QString>
#include <QTreeView>
class GameListModel;
+10 -1
View File
@@ -45,7 +45,7 @@
<x>0</x>
<y>0</y>
<width>1280</width>
<height>22</height>
<height>23</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
@@ -111,6 +111,7 @@
</property>
<addaction name="action_Tree_View"/>
<addaction name="action_Grid_View"/>
<addaction name="action_Carousel_View"/>
</widget>
<widget class="QMenu" name="menuGame_Icon_Size">
<property name="title">
@@ -620,6 +621,14 @@
<string>Show &amp;Performance Overlay</string>
</property>
</action>
<action name="action_Carousel_View">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Carousel View</string>
</property>
</action>
</widget>
<resources>
<include location="yuzu.qrc"/>
+10 -4
View File
@@ -1529,6 +1529,7 @@ void MainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Grid_View, &MainWindow::SetGridView);
connect_menu(ui->action_Tree_View, &MainWindow::SetTreeView);
connect_menu(ui->action_Carousel_View, &MainWindow::SetCarouselView);
game_size_actions = new QActionGroup(this);
game_size_actions->setExclusive(true);
@@ -3317,9 +3318,10 @@ void MainWindow::ResetWindowSize1080() {
void MainWindow::SetGameListMode(Settings::GameListMode mode) {
ui->action_Grid_View->setChecked(mode == Settings::GameListMode::GridView);
ui->action_Tree_View->setChecked(mode == Settings::GameListMode::TreeView);
ui->action_Carousel_View->setChecked(mode == Settings::GameListMode::CarouselView);
UISettings::values.game_list_mode = mode;
ui->action_Show_Game_Name->setEnabled(mode == Settings::GameListMode::GridView);
ui->action_Show_Game_Name->setEnabled(mode != Settings::GameListMode::TreeView);
CheckIconSize();
game_list->ResetViewMode();
@@ -3333,11 +3335,15 @@ void MainWindow::SetTreeView() {
SetGameListMode(Settings::GameListMode::TreeView);
}
void MainWindow::SetCarouselView() {
SetGameListMode(Settings::GameListMode::CarouselView);
}
void MainWindow::CheckIconSize() {
// When in grid view mode, with text off
// there is no point in having icons turned off..
// When in grid/carousel view mode, with text off
// there is no point in having icons turned off
auto actions = game_size_actions->actions();
if (UISettings::values.game_list_mode.GetValue() == Settings::GameListMode::GridView &&
if (UISettings::values.game_list_mode.GetValue() != Settings::GameListMode::TreeView &&
!UISettings::values.show_game_name.GetValue()) {
u32 newSize = UISettings::values.game_icon_size.GetValue();
if (newSize == 0) {
+1
View File
@@ -410,6 +410,7 @@ private slots:
void SetGameListMode(Settings::GameListMode mode);
void SetGridView();
void SetTreeView();
void SetCarouselView();
void CheckIconSize();
void ToggleShowGameName();