Compare commits

...

9 Commits

Author SHA1 Message Date
PavelBARABANOV 355f9d84ef [shader_recompiler, renderer_vulkan] draw sprites whose texture handle comes from a vertex attribute 2026-10-05 05:14:18 +03:00
MaranBr d16735f5b6 [am] Properly handle None frontend applet (#4500)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Properly handle the `AppletId::None` case by returning a null frontend applet instead of falling back to the stub implementation.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4500
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-10-02 02:24:58 +02:00
lizzie d3550c4571 [qt] Filter collates accents (Unicode NFD) (#4380)
Now "POKEMON" and "pokemon" are the same, and search's no longer case sensitive.
Maybe we could transliterate? But that would require icu which is a bit overkill.
Main point is to be able to search "Pokemon" (accent on 'e') without actually typing the accent

https://unicode.org/reports/tr15/

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

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4380
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-10-01 07:39:44 +02:00
John 963119e7cc [ffmpeg]Gamescope / Gamemode NVDEC Emulation - GPU Decoding Patches (#4501)
Switch to ffmpeg with liz's gamescope patches.

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4501
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-10-01 03:34:57 +02:00
xbzk 2a7e9b53b1 [android,ui] fixed shader cache clearing when changing drivers (#4496)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------
Trivial bug pending on backlog, where changing drivers was not clearing up sharder cache as expected.

The driver flow (broken) was deleting externalFilesDir/shader/<game>, while the visible cache-size control (working) DirectoryInitialization.userDirectory/cache/shader/<game>.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4496
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-10-01 00:28:18 +02:00
xbzk 8e2d26c272 [video_core] buffer cache readback for nce invalidations (#4490)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

This is a follow up of 4473, a fix for the next Ender Magnolia known crash, the shackled beast's vaper crash, but specific for NCE.

Imma try to be short:

NCE invalidation was dooming whole guest pages when it shouldn't. If the GPU buffer cache owns some bytes on a page, invalidating first discards their ownership, and the retrying CPU can then observe stale data. On Android, this was corrupting some indirect draw packets and causing huge invalid index count reach staging memory allocation.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4490
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-09-30 22:13:37 +02:00
Eden CI f8b7651104 [dist, android] Update translations from Transifex for Sep 28 (#4494)
Automatic translation update for Sep 28

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4494
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-09-30 02:00:44 +02:00
xbzk 3f78512c83 [ro] add TLS patching for runtime NROs (#4492)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------

Aimed to fix Biomutant which stucks in a bootloop due to guest being unable to reach correct thread local storage (TLS)
This fixes runtime NRO loading by properly patching the guest TLS (TPIDR_EL0) path. Steps:
-Read the loaded NRO image into a temporary buffer.
-Run the NCE patcher on it.
-Produce a patch section / relocated code.
-Map that patch section as executable memory in the target process.
Game still has intermittent visual issues which can be solved with GPU level accurate, but that causes a huge performance drop in exchange. Someone else will resume this one.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4492
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: lizzie <lizzie@eden-emu.dev>
2026-09-30 00:58:29 +02:00
xbzk a82f31a310 [settings,qt] merge of "disable controller applet" toggle with "controller applet mode" picker (#4498)
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md)
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

-------------------
Small good simplification to nuke a toggle which costed me almost an entire day of work when fixing controller applet issues:

There were two settings related to Controller applet:
-"disable controller applet", which is a boolean toggle;
-"controller configuration" applet mode, which is a "Real"/"Custom" picker;

All i did was to nuke the toggle and add a third option "Disabled" to the picker.
Users which used the disable toggle will have to pick Disabled;
Real/Custom settings wont be affected since their indexes remains.
All options tested against diablo 3. All working as intended.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4498
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-09-30 00:57:23 +02:00
49 changed files with 12145 additions and 12412 deletions
+1 -1
View File
@@ -79,7 +79,7 @@
"name": "ffmpeg",
"package": "FFmpeg",
"repo": "crueter-ci/FFmpeg",
"version": "9.0.1-1788120736-bf1b838f2a"
"version": "9.0.2-1790726865-946fcce07b"
},
"fmt": {
"hash": "f0da82c545b01692e9fd30fdfb613dbb8dd9716983dcd0ff19ac2a8d36f74beb5540ef38072fdecc1e34191b3682a8542ecbf3a61ef287dbba0a2679d4e023f2",
+455 -473
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+440 -463
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+440 -463
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+440 -463
View File
File diff suppressed because it is too large Load Diff
+440 -463
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
+440 -463
View File
File diff suppressed because it is too large Load Diff
+439 -456
View File
File diff suppressed because it is too large Load Diff
@@ -23,6 +23,7 @@ import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.NativeConfig
import java.io.File
@@ -146,11 +147,11 @@ class DriverViewModel : ViewModel() {
private fun wipeGameShaders(game: Game) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val externalFilesDir = YuzuApplication.appContext.getExternalFilesDir(null)
val userDirectory = DirectoryInitialization.userDirectory
?: return@withContext
val shaderDir = File(
externalFilesDir.absolutePath +
"/shader/" + game.settingsName.lowercase()
userDirectory +
"/cache/shader/" + game.settingsName.lowercase()
)
if (shaderDir.exists()) {
shaderDir.deleteRecursively()
+1 -1
View File
@@ -150,7 +150,7 @@ ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, Scale
ENUM(AntiAliasing, None, Fxaa, Smaa);
ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
ENUM(ConsoleMode, Handheld, Docked);
ENUM(AppletMode, HLE, LLE);
ENUM(AppletMode, HLE, LLE, Disabled);
ENUM(SpirvOptimizeMode, Never, OnLoad, Always);
ENUM(GpuClock, Normal, Boost, Overclock)
ENUM(GpuUnswizzleSize, VerySmall, Small, Normal, Large, VeryLarge)
+3 -2
View File
@@ -250,9 +250,10 @@ std::shared_ptr<FrontendApplet> FrontendAppletHolder::GetApplet(std::shared_ptr<
return std::make_shared<PhotoViewer>(system, applet, mode, *frontend.photo_viewer);
case AppletId::NetConnect:
return std::make_shared<NetConnect>(system, applet, mode, *frontend.net_connect);
case AppletId::None:
return nullptr;
default:
LOG_ERROR(Service_AM, "No backend implementation exists for applet_id={:02X} program_id={:016X}"
"Falling back to stub applet", static_cast<u8>(id), applet->program_id);
UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:02X} program_id={:016X}! Using stub applet.", static_cast<u8>(id), applet->program_id);
return std::make_shared<StubApplet>(system, applet, id, mode);
}
}
+30 -2
View File
@@ -16,6 +16,10 @@
#include "core/hle/service/ro/ro_types.h"
#include "core/hle/service/server_manager.h"
#include "core/hle/service/service.h"
#ifdef HAS_NCE
#include "core/arm/nce/patcher.h"
#include "core/hle/kernel/k_shared_memory.h"
#endif
namespace Service::RO {
@@ -386,7 +390,7 @@ public:
R_SUCCEED();
}
Result MapManualLoadModuleMemory(u64* out_address, size_t context_id, u64 nro_address,
Result MapManualLoadModuleMemory(Kernel::KernelCore& kernel, u64* out_address, size_t context_id, u64 nro_address,
u64 nro_size, u64 bss_address, u64 bss_size) {
// Get context.
ProcessContext* context = this->GetContextById(context_id);
@@ -421,7 +425,31 @@ public:
R_TRY(context->ValidateNro(std::addressof(nro_info->module_id), std::addressof(rx_size),
std::addressof(ro_size), std::addressof(rw_size),
nro_info->base_address, nro_size, bss_size));
#ifdef HAS_NCE
if (Settings::IsNceEnabled()) {
auto* process = context->GetProcess();
auto& memory = process->GetMemory();
std::vector<u8> image(total_size);
memory.ReadBlock(nro_info->base_address, image.data(), rx_size);
Kernel::CodeSet::Segment code{.size = static_cast<u32>(rx_size)};
Core::NCE::Patcher patch;
patch.PatchText(image, code);
patch.RelocateAndCopy(nro_info->base_address, code, image, nullptr);
const u64 patch_address = nro_info->base_address + total_size;
const size_t patch_size = patch.GetSectionSize();
constexpr auto permission = Kernel::Svc::MemoryPermission::ReadExecute;
auto* patch_memory = Kernel::KSharedMemory::Create(kernel);
R_TRY(patch_memory->Initialize(kernel, kernel.System().DeviceMemory(), process, permission, permission, patch_size));
std::memcpy(patch_memory->GetPointer(), image.data() + total_size, patch_size);
R_TRY(process->AddSharedMemory(kernel, patch_memory, patch_address, patch_size));
R_TRY(patch_memory->Map(*process, patch_address, patch_size, permission));
memory.WriteBlock(nro_info->base_address, image.data(), rx_size);
}
#endif
// Set NRO perms.
R_TRY(SetNroPerms(context->GetProcess(), nro_info->base_address, rx_size, ro_size,
rw_size + bss_size));
@@ -531,7 +559,7 @@ public:
Result MapManualLoadModuleMemory(Out<u64> out_load_address, ClientProcessId client_pid,
u64 nro_address, u64 nro_size, u64 bss_address, u64 bss_size) {
R_TRY(m_ro->ValidateProcess(m_context_id, *client_pid));
R_RETURN(m_ro->MapManualLoadModuleMemory(out_load_address.Get(), m_context_id, nro_address,
R_RETURN(m_ro->MapManualLoadModuleMemory(system.Kernel(), out_load_address.Get(), m_context_id, nro_address,
nro_size, bss_address, bss_size));
}
+1 -1
View File
@@ -757,7 +757,7 @@ struct Memory::Impl {
};
auto& gpu = system.GPU();
gpu_device_memory->ApplyOpOnPointer(
p, scratch_buffers[core], [&](DAddr address) { gpu.InvalidateRegion(address, size); });
p, scratch_buffers[core], [&](DAddr address) { gpu.InvalidateRegion(address, size, true); });
}
Core::System& system;
+1 -3
View File
@@ -342,9 +342,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
"it bypasses such prompts and directly exits the emulation."));
INSERT(UISettings, hide_mouse, tr("Hide mouse on inactivity"),
tr("Hides the mouse after 2.5s of inactivity."));
INSERT(UISettings, controller_applet_disabled, tr("Disable controller applet"),
tr("Forcibly disables the use of the controller applet in emulated programs.\n"
"When a program attempts to open the controller applet, it is immediately closed."));
INSERT(UISettings, check_for_updates, tr("Check for updates"),
tr("Whether or not to check for updates upon startup."));
@@ -380,6 +377,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
{
PAIR(AppletMode, HLE, tr("Custom frontend")),
PAIR(AppletMode, LLE, tr("Real applet")),
PAIR(AppletMode, Disabled, tr("Disabled")),
}});
translations->insert({Settings::EnumMetadata<Settings::SpirvOptimizeMode>::Index(),
-2
View File
@@ -127,8 +127,6 @@ struct Values {
Setting<bool> hide_mouse{
linkage, true, "hideInactiveMouse", Category::UiGeneral, Settings::Specialization::Default,
true, true};
Setting<bool> controller_applet_disabled{linkage, false, "disableControllerApplet",
Category::UiGeneral};
// Set when Vulkan is known to crash the application
bool has_broken_vulkan = false;
@@ -38,6 +38,8 @@ struct HostTranslateInfo {
///< passthrough shaders
bool support_conditional_barrier{}; ///< True when the device supports barriers in conditional
///< control flow
bool support_attribute_texture_handle{}; ///< True when the host binds texture handles that
///< arrive as vertex attributes
void ApplyDescriptorLimitPolicy() noexcept {
if (min_ssbo_alignment == 0) {
+44 -3
View File
@@ -268,6 +268,10 @@ static inline u32 ReadCbufCached(Environment& env, u32 index, u32 offset) {
return v;
}
static inline bool IsAttributeHandle(const ConstBufferAddr& cbuf) {
return cbuf.index == ATTRIBUTE_HANDLE_CBUF_INDEX;
}
static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr& cbuf) {
// Must all be uniquely different variables
// If has secondary, then it will be cbuf.secondary_{index|offset}, else its 0.
@@ -287,14 +291,23 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
// Cached variants of existing helpers
static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return TextureType::Color2D;
}
return env.ReadTextureType(GetTextureHandleCached(env, cbuf));
}
static inline TexturePixelFormat ReadTexturePixelFormatCached(Environment& env,
const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return TexturePixelFormat::A8B8G8R8_UNORM;
}
return env.ReadTexturePixelFormat(GetTextureHandleCached(env, cbuf));
}
static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
const ConstBufferAddr& cbuf) {
if (IsAttributeHandle(cbuf)) {
return false;
}
return env.IsTexturePixelFormatInteger(GetTextureHandleCached(env, cbuf));
}
@@ -466,15 +479,43 @@ std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environme
};
}
bool IsAttributeSourcedHandle(const IR::Value& handle) {
if (handle.IsImmediate()) {
return false;
}
const IR::Inst* inst{handle.InstRecursive()};
if (inst->GetOpcode() == IR::Opcode::BitCastU32F32 && !inst->Arg(0).IsImmediate()) {
inst = inst->Arg(0).InstRecursive();
}
return inst->GetOpcode() == IR::Opcode::GetAttribute ||
inst->GetOpcode() == IR::Opcode::GetAttributeU32;
}
TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const HostTranslateInfo& host_info) {
ConstBufferAddr addr;
if (IsBindless(inst)) {
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info)};
if (!track_addr) {
throw NotImplementedException("Failed to track bindless texture constant buffer");
} else {
if (track_addr) {
addr = *track_addr;
} else if (host_info.support_attribute_texture_handle &&
IsAttributeSourcedHandle(inst.Arg(0))) {
// The handle travels with the vertices (sprite batches pick the texture per quad), so
// there is no constant buffer to read it from. The host binds the handle of each run
// of vertices that share one.
addr = ConstBufferAddr{
.index = ATTRIBUTE_HANDLE_CBUF_INDEX,
.offset = 0,
.shift_left = 0,
.secondary_index = 0,
.secondary_offset = 0,
.secondary_shift_left = 0,
.dynamic_offset = {},
.count = 1,
.has_secondary = false,
};
} else {
throw NotImplementedException("Failed to track bindless texture constant buffer");
}
} else {
addr = ConstBufferAddr{
+4
View File
@@ -38,6 +38,10 @@ enum class TextureType : u32 {
};
constexpr u32 NUM_TEXTURE_TYPES = 9;
/// Constant buffer index marking a texture handle that the shader receives as a vertex attribute
/// instead of reading it from a constant buffer. The host supplies the handle of each draw run.
constexpr u32 ATTRIBUTE_HANDLE_CBUF_INDEX = 0xFFFF;
enum class TexturePixelFormat {
A8B8G8R8_UNORM,
A8B8G8R8_SNORM,
+13 -3
View File
@@ -226,7 +226,17 @@ struct GPU::Impl {
}
/// Notify rasterizer that any caches of the specified region should be invalidated
void InvalidateRegion(DAddr addr, u64 size) {
void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) {
VideoCore::RasterizerInterface* rasterizer = renderer->ReadRasterizer();
if (preserve_gpu_writes && rasterizer->MustFlushRegion(addr, size, VideoCommon::CacheType::BufferCache)) {
const u64 fence = RequestSyncOperation([rasterizer, addr, size] {
rasterizer->FlushRegion(addr, size, VideoCommon::CacheType::BufferCache);
rasterizer->OnCacheInvalidation(addr, size);
});
gpu_thread.TickGPU(is_async);
WaitForSyncOperation(fence);
return;
}
gpu_thread.InvalidateRegion(addr, size);
}
@@ -518,8 +528,8 @@ void GPU::FlushRegion(DAddr addr, u64 size) {
impl->FlushRegion(addr, size);
}
void GPU::InvalidateRegion(DAddr addr, u64 size) {
impl->InvalidateRegion(addr, size);
void GPU::InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) {
impl->InvalidateRegion(addr, size, preserve_gpu_writes);
}
bool GPU::OnCPUWrite(DAddr addr, u64 size) {
+1 -1
View File
@@ -248,7 +248,7 @@ public:
void FlushRegion(DAddr addr, u64 size);
/// Notify rasterizer that any caches of the specified region should be invalidated
void InvalidateRegion(DAddr addr, u64 size);
void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes = false);
/// Notify rasterizer that CPU is trying to write this area. It returns true if the area is
/// sensible, false otherwise, addr and size must be a valid combination
@@ -276,6 +276,9 @@ GraphicsPipeline::GraphicsPipeline(
num_textures += Shader::NumDescriptors(info->texture_descriptors);
num_image_elements += Shader::NumDescriptors(info->texture_descriptors);
num_image_elements += Shader::NumDescriptors(info->image_descriptors);
for (const auto& desc : info->texture_descriptors) {
uses_attribute_handle |= desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX;
}
num_descriptor_entries += NumDescriptorEntries(*info);
}
fragment_has_color0_output = stage_infos[NUM_STAGES - 1].stores_frag_color[0];
@@ -375,6 +378,9 @@ 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 (desc.cbuf_index == Shader::ATTRIBUTE_HANDLE_CBUF_INDEX) {
return TexturePair(attribute_handle, via_header_index);
}
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
@@ -110,6 +110,16 @@ public:
return configure_func(this, is_indexed);
}
/// True when a shader samples a texture whose handle arrives as a vertex attribute.
[[nodiscard]] bool UsesAttributeHandle() const noexcept {
return uses_attribute_handle;
}
/// Handle bound to those textures by the next Configure.
void SetAttributeHandle(u32 handle) noexcept {
attribute_handle = handle;
}
[[nodiscard]] GraphicsPipeline* Next(const GraphicsPipelineCacheKey& current_key) noexcept {
if (key == current_key) {
return this;
@@ -168,6 +178,8 @@ private:
u32 num_descriptor_entries{};
size_t num_image_elements{};
u32 num_textures{};
u32 attribute_handle{};
bool uses_attribute_handle{};
bool fragment_has_color0_output{};
vk::DescriptorSetLayout descriptor_set_layout;
@@ -479,6 +479,7 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
.support_viewport_index_layer = device.IsExtShaderViewportIndexLayerSupported(),
.support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
.support_conditional_barrier = device.SupportsConditionalBarriers(),
.support_attribute_texture_handle = true,
};
host_info.ApplyDescriptorLimitPolicy();
@@ -248,6 +248,15 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
// update engine as channel may be different.
pipeline->SetEngine(maxwell3d, gpu_memory);
attribute_handle_pipeline = nullptr;
attribute_handle_runs.clear();
if (split_attribute_handles && pipeline->UsesAttributeHandle()) {
CollectAttributeHandleRuns(is_indexed);
if (!attribute_handle_runs.empty()) {
attribute_handle_pipeline = pipeline;
pipeline->SetAttributeHandle(attribute_handle_runs.front().handle);
}
}
if (!pipeline->Configure(is_indexed))
return;
@@ -259,22 +268,110 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
draw_func();
}
void RasterizerVulkan::CollectAttributeHandleRuns(bool is_indexed) {
using VertexAttribute = Maxwell::VertexAttribute;
const auto& regs = maxwell3d->regs;
const auto& draw_state = maxwell3d->draw_manager.draw_state;
const u32 total = is_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count;
if (total == 0) {
return;
}
// The handle is an integer attribute; sprite batches repeat it on every vertex of a quad.
const VertexAttribute* source{};
for (const auto& attr : regs.vertex_attrib_format) {
if (attr.constant || attr.size == VertexAttribute::Size::Invalid) {
continue;
}
if (attr.type == VertexAttribute::Type::UInt || attr.type == VertexAttribute::Type::SInt) {
source = &attr;
break;
}
}
const auto& stream = regs.vertex_streams[source ? source->buffer.Value() : 0];
if (!source || !stream.IsEnabled()) {
attribute_handle_runs.push_back({0, total, 0});
return;
}
// Only plain triangle lists are split; anything else keeps the handle of its first vertex.
const u32 primitive_size =
draw_state.topology == Maxwell::PrimitiveTopology::Triangles ? 3 : total;
const GPUVAddr base_address = stream.Address() + source->offset;
const auto read_handle = [&](u32 position) {
u32 vertex;
if (is_indexed) {
const auto& index_buffer = draw_state.index_buffer;
const GPUVAddr address =
index_buffer.IndexStart() + size_t{position} * index_buffer.FormatSizeInBytes();
u32 index{};
gpu_memory->ReadBlockUnsafe(address, &index, index_buffer.FormatSizeInBytes());
vertex = static_cast<u32>(draw_state.base_index + index);
} else {
vertex = draw_state.vertex_buffer.first + position;
}
return gpu_memory->Read<u32>(base_address + static_cast<GPUVAddr>(vertex) * stream.stride);
};
for (u32 position = 0; position + primitive_size <= total; position += primitive_size) {
const u32 handle = read_handle(position);
if (!attribute_handle_runs.empty() && attribute_handle_runs.back().handle == handle) {
attribute_handle_runs.back().count += primitive_size;
} else {
attribute_handle_runs.push_back({position, primitive_size, handle});
}
}
if (attribute_handle_runs.empty()) {
attribute_handle_runs.push_back({0, total, read_handle(0)});
} else {
const u32 covered = attribute_handle_runs.back().first + attribute_handle_runs.back().count;
if (covered < total) {
attribute_handle_runs.back().count += total - covered;
}
}
}
void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
split_attribute_handles = true;
SCOPE_EXIT {
split_attribute_handles = false;
};
PrepareDraw(is_indexed, [this, is_indexed, instance_count] {
const auto& draw_state = maxwell3d->draw_manager.draw_state;
const u32 num_instances{instance_count};
const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)};
scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) {
if (draw_params.is_indexed) {
cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances,
draw_params.first_index, draw_params.base_vertex,
draw_params.base_instance);
} else {
cmdbuf.Draw(draw_params.num_vertices, draw_params.num_instances,
draw_params.base_vertex, draw_params.base_instance);
const auto record_draw = [this](const DrawParams& params) {
scheduler.Record([params](vk::CommandBuffer cmdbuf) {
if (params.is_indexed) {
cmdbuf.DrawIndexed(params.num_vertices, params.num_instances,
params.first_index, params.base_vertex,
params.base_instance);
} else {
cmdbuf.Draw(params.num_vertices, params.num_instances, params.base_vertex,
params.base_instance);
}
});
};
if (attribute_handle_pipeline && attribute_handle_runs.size() > 1) {
// The first run is already configured, each further one binds its own texture.
for (size_t i = 0; i < attribute_handle_runs.size(); ++i) {
const AttributeHandleRun& run = attribute_handle_runs[i];
if (i != 0) {
attribute_handle_pipeline->SetAttributeHandle(run.handle);
if (!attribute_handle_pipeline->Configure(is_indexed)) {
continue;
}
}
DrawParams run_params = draw_params;
run_params.num_vertices = run.count;
if (is_indexed) {
run_params.first_index += run.first;
} else {
run_params.base_vertex += run.first;
}
record_draw(run_params);
}
});
} else {
record_draw(draw_params);
}
// Log draw call
if (GPU::Logging::IsActive() &&
@@ -157,6 +157,20 @@ private:
template <typename Func>
void PrepareDraw(bool is_indexed, Func&&);
/// A stretch of a draw whose vertices all carry the same texture handle.
struct AttributeHandleRun {
u32 first;
u32 count;
u32 handle;
};
/// Splits the current draw wherever the texture handle carried in the vertex data changes.
void CollectAttributeHandleRuns(bool is_indexed);
std::vector<AttributeHandleRun> attribute_handle_runs;
GraphicsPipeline* attribute_handle_pipeline{};
bool split_attribute_handles{};
void FlushWork();
void UpdateDynamicStates();
+3 -1
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -8,7 +11,6 @@
class QCheckBox;
class QLineEdit;
class QComboBox;
class QDateTimeEdit;
namespace Core {
class System;
+8 -4
View File
@@ -140,7 +140,12 @@ QWidget* Widget::CreateCombobox(std::function<std::string()>& serializer,
if (combobox_enumerations.contains(type)) {
enumeration = &combobox_enumerations.at(type);
for (const auto& [id, name] : *enumeration) {
combobox->addItem(name);
if (type == Settings::EnumMetadata<Settings::AppletMode>::Index() &&
id == static_cast<u32>(Settings::AppletMode::Disabled) &&
setting.Id() != Settings::values.controller_applet_mode.Id()) {
continue;
}
combobox->addItem(name, id);
}
} else {
return combobox;
@@ -158,9 +163,8 @@ QWidget* Widget::CreateCombobox(std::function<std::string()>& serializer,
const u32 setting_value = std::strtoul(setting.ToString().c_str(), nullptr, 0);
combobox->setCurrentIndex(find_index(setting_value));
serializer = [this, enumeration]() {
int current = combobox->currentIndex();
return std::to_string(enumeration->at(current).first);
serializer = [this]() {
return std::to_string(combobox->currentData().toUInt());
};
restore_func = [this, find_index]() {
+15 -11
View File
@@ -12,25 +12,29 @@ namespace Yuzu {
inline bool ContainsAllWords(const QString& haystack, const QString& userinput) {
const QStringList userinput_split = userinput.split(QLatin1Char{' '}, Qt::SkipEmptyParts);
return std::all_of(userinput_split.begin(), userinput_split.end(),
[&haystack](const QString& s) { return haystack.contains(s); });
return std::all_of(userinput_split.begin(), userinput_split.end(), [&haystack](const QString& s) {
return haystack.contains(s);
});
}
inline bool FilterMatches(const QString& filter, const QStandardItem* item) {
if (filter.isEmpty())
return true;
auto const sluggify = [](const QString& s) {
QString o(s.normalized(QString::NormalizationForm_D).toLower());
return o.replace(QRegularExpression(QStringLiteral("[^a-z\\s]")), QStringLiteral(""));
};
const auto norm_filter = sluggify(filter);
const auto program_id = item->data(GameListItemPath::ProgramIdRole).toULongLong();
const QString file_path = item->data(GameListItemPath::FullPathRole).toString().toLower();
const QString file_title = item->data(GameListItemPath::TitleRole).toString().toLower();
const QString file_path = sluggify(item->data(GameListItemPath::FullPathRole).toString());
const QString file_title = sluggify(item->data(GameListItemPath::TitleRole).toString());
const QString file_program_id = QStringLiteral("%1").arg(program_id, 16, 16, QLatin1Char{'0'});
const QString file_name =
file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + file_title;
return Yuzu::ContainsAllWords(file_name, filter) ||
(file_program_id.size() == 16 && file_program_id.contains(filter));
const QString file_name = sluggify(file_path.mid(file_path.lastIndexOf(QLatin1Char{'/'}) + 1) + QLatin1Char{' '} + file_title);
return (ContainsAllWords(file_name, norm_filter)
|| ContainsAllWords(file_title, norm_filter))
|| (file_program_id.size() == 16 && file_program_id.contains(norm_filter));
}
} // namespace Yuzu
+1 -1
View File
@@ -1786,7 +1786,7 @@ bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPar
QtCommon::system->SetFrontendAppletSet({
std::make_unique<QtAmiiboSettings>(*this), // Amiibo Settings
(UISettings::values.controller_applet_disabled.GetValue() == true)
(Settings::values.controller_applet_mode.GetValue() == Settings::AppletMode::Disabled)
? nullptr
: std::make_unique<QtControllerSelector>(*this), // Controller Selector
std::make_unique<QtErrorDisplay>(*this), // Error Display