From 7eea1fc4d694d5d6626087f8d5bffb1a3ccf39ad Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 10 May 2025 23:29:23 +0300 Subject: [PATCH 001/141] log error for videodec ,videodec2 (#2900) --- src/core/libraries/videodec/videodec.cpp | 11 +++++++++++ src/core/libraries/videodec/videodec2.cpp | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/core/libraries/videodec/videodec.cpp b/src/core/libraries/videodec/videodec.cpp index 02ea61509..ae7d17560 100644 --- a/src/core/libraries/videodec/videodec.cpp +++ b/src/core/libraries/videodec/videodec.cpp @@ -17,10 +17,12 @@ int PS4_SYSV_ABI sceVideodecCreateDecoder(const OrbisVideodecConfigInfo* pCfgInf LOG_INFO(Lib_Videodec, "called"); if (!pCfgInfoIn || !pRsrcInfoIn || !pCtrlOut) { + LOG_ERROR(Lib_Videodec, "Invalid arguments"); return ORBIS_VIDEODEC_ERROR_ARGUMENT_POINTER; } if (pCfgInfoIn->thisSize != sizeof(OrbisVideodecConfigInfo) || pRsrcInfoIn->thisSize != sizeof(OrbisVideodecResourceInfo)) { + LOG_ERROR(Lib_Videodec, "Invalid struct size"); return ORBIS_VIDEODEC_ERROR_STRUCT_SIZE; } @@ -37,15 +39,18 @@ int PS4_SYSV_ABI sceVideodecDecode(OrbisVideodecCtrl* pCtrlIn, OrbisVideodecPictureInfo* pPictureInfoOut) { LOG_TRACE(Lib_Videodec, "called"); if (!pCtrlIn || !pInputDataIn || !pPictureInfoOut) { + LOG_ERROR(Lib_Videodec, "Invalid arguments"); return ORBIS_VIDEODEC_ERROR_ARGUMENT_POINTER; } if (pCtrlIn->thisSize != sizeof(OrbisVideodecCtrl) || pFrameBufferInOut->thisSize != sizeof(OrbisVideodecFrameBuffer)) { + LOG_ERROR(Lib_Videodec, "Invalid struct size"); return ORBIS_VIDEODEC_ERROR_STRUCT_SIZE; } VdecDecoder* decoder = (VdecDecoder*)pCtrlIn->handle; if (!decoder) { + LOG_ERROR(Lib_Videodec, "Invalid decoder handle"); return ORBIS_VIDEODEC_ERROR_HANDLE; } return decoder->Decode(*pInputDataIn, *pFrameBufferInOut, *pPictureInfoOut); @@ -56,6 +61,7 @@ int PS4_SYSV_ABI sceVideodecDeleteDecoder(OrbisVideodecCtrl* pCtrlIn) { VdecDecoder* decoder = (VdecDecoder*)pCtrlIn->handle; if (!decoder) { + LOG_ERROR(Lib_Videodec, "Invalid decoder handle"); return ORBIS_VIDEODEC_ERROR_HANDLE; } delete decoder; @@ -68,15 +74,18 @@ int PS4_SYSV_ABI sceVideodecFlush(OrbisVideodecCtrl* pCtrlIn, LOG_INFO(Lib_Videodec, "called"); if (!pFrameBufferInOut || !pPictureInfoOut) { + LOG_ERROR(Lib_Videodec, "Invalid arguments"); return ORBIS_VIDEODEC_ERROR_ARGUMENT_POINTER; } if (pFrameBufferInOut->thisSize != sizeof(OrbisVideodecFrameBuffer) || pPictureInfoOut->thisSize != sizeof(OrbisVideodecPictureInfo)) { + LOG_ERROR(Lib_Videodec, "Invalid struct size"); return ORBIS_VIDEODEC_ERROR_STRUCT_SIZE; } VdecDecoder* decoder = (VdecDecoder*)pCtrlIn->handle; if (!decoder) { + LOG_ERROR(Lib_Videodec, "Invalid decoder handle"); return ORBIS_VIDEODEC_ERROR_HANDLE; } return decoder->Flush(*pFrameBufferInOut, *pPictureInfoOut); @@ -92,10 +101,12 @@ int PS4_SYSV_ABI sceVideodecQueryResourceInfo(const OrbisVideodecConfigInfo* pCf LOG_INFO(Lib_Videodec, "called"); if (!pCfgInfoIn || !pRsrcInfoOut) { + LOG_ERROR(Lib_Videodec, "Invalid arguments"); return ORBIS_VIDEODEC_ERROR_ARGUMENT_POINTER; } if (pCfgInfoIn->thisSize != sizeof(OrbisVideodecConfigInfo) || pRsrcInfoOut->thisSize != sizeof(OrbisVideodecResourceInfo)) { + LOG_ERROR(Lib_Videodec, "Invalid struct size"); return ORBIS_VIDEODEC_ERROR_STRUCT_SIZE; } diff --git a/src/core/libraries/videodec/videodec2.cpp b/src/core/libraries/videodec/videodec2.cpp index a7e520b41..4f9379151 100644 --- a/src/core/libraries/videodec/videodec2.cpp +++ b/src/core/libraries/videodec/videodec2.cpp @@ -17,9 +17,11 @@ sceVideodec2QueryComputeMemoryInfo(OrbisVideodec2ComputeMemoryInfo* computeMemIn LOG_INFO(Lib_Vdec2, "called"); if (!computeMemInfo) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (computeMemInfo->thisSize != sizeof(OrbisVideodec2ComputeMemoryInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -47,10 +49,12 @@ sceVideodec2QueryDecoderMemoryInfo(const OrbisVideodec2DecoderConfigInfo* decode LOG_INFO(Lib_Vdec2, "called"); if (!decoderCfgInfo || !decoderMemInfo) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (decoderCfgInfo->thisSize != sizeof(OrbisVideodec2DecoderConfigInfo) || decoderMemInfo->thisSize != sizeof(OrbisVideodec2DecoderMemoryInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -74,10 +78,12 @@ s32 PS4_SYSV_ABI sceVideodec2CreateDecoder(const OrbisVideodec2DecoderConfigInfo LOG_INFO(Lib_Vdec2, "called"); if (!decoderCfgInfo || !decoderMemInfo || !decoder) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (decoderCfgInfo->thisSize != sizeof(OrbisVideodec2DecoderConfigInfo) || decoderMemInfo->thisSize != sizeof(OrbisVideodec2DecoderMemoryInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -89,6 +95,7 @@ s32 PS4_SYSV_ABI sceVideodec2DeleteDecoder(OrbisVideodec2Decoder decoder) { LOG_INFO(Lib_Vdec2, "called"); if (!decoder) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_DECODER_INSTANCE; } @@ -103,13 +110,16 @@ s32 PS4_SYSV_ABI sceVideodec2Decode(OrbisVideodec2Decoder decoder, LOG_TRACE(Lib_Vdec2, "called"); if (!decoder) { + LOG_ERROR(Lib_Vdec2, "Invalid decoder instance"); return ORBIS_VIDEODEC2_ERROR_DECODER_INSTANCE; } if (!inputData || !frameBuffer || !outputInfo) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (inputData->thisSize != sizeof(OrbisVideodec2InputData) || frameBuffer->thisSize != sizeof(OrbisVideodec2FrameBuffer)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -122,13 +132,16 @@ s32 PS4_SYSV_ABI sceVideodec2Flush(OrbisVideodec2Decoder decoder, LOG_INFO(Lib_Vdec2, "called"); if (!decoder) { + LOG_ERROR(Lib_Vdec2, "Invalid decoder instance"); return ORBIS_VIDEODEC2_ERROR_DECODER_INSTANCE; } if (!frameBuffer || !outputInfo) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (frameBuffer->thisSize != sizeof(OrbisVideodec2FrameBuffer) || outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -139,6 +152,7 @@ s32 PS4_SYSV_ABI sceVideodec2Reset(OrbisVideodec2Decoder decoder) { LOG_INFO(Lib_Vdec2, "called"); if (!decoder) { + LOG_ERROR(Lib_Vdec2, "Invalid decoder instance"); return ORBIS_VIDEODEC2_ERROR_DECODER_INSTANCE; } @@ -150,12 +164,15 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp LOG_TRACE(Lib_Vdec2, "called"); if (!outputInfo) { + LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } if (outputInfo->pictureCount == 0 || gPictureInfos.empty()) { + LOG_ERROR(Lib_Vdec2, "No picture info available"); return ORBIS_OK; } @@ -163,6 +180,7 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp OrbisVideodec2AvcPictureInfo* picInfo = static_cast(p1stPictureInfoOut); if (picInfo->thisSize != sizeof(OrbisVideodec2AvcPictureInfo)) { + LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } *picInfo = gPictureInfos.back(); From 6ece91c7632798474a2110ca0379985f183907eb Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 10 May 2025 22:02:00 -0500 Subject: [PATCH 002/141] sceKernelVirtualQuery Fixes VI (#2904) * Reduce bitfield size Linux compilers automatically convert this, Windows not so much. * Static assert for VirtualQueryInfo struct size Since compilers can be weird, having a static assert for this will be helpful. Granted, this probably wont need changing after this PR. --- src/core/libraries/kernel/memory.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/libraries/kernel/memory.h b/src/core/libraries/kernel/memory.h index 6acb559d1..2ca7f2931 100644 --- a/src/core/libraries/kernel/memory.h +++ b/src/core/libraries/kernel/memory.h @@ -61,13 +61,15 @@ struct OrbisVirtualQueryInfo { size_t offset; s32 protection; s32 memory_type; - u32 is_flexible : 1; - u32 is_direct : 1; - u32 is_stack : 1; - u32 is_pooled : 1; - u32 is_committed : 1; + u8 is_flexible : 1; + u8 is_direct : 1; + u8 is_stack : 1; + u8 is_pooled : 1; + u8 is_committed : 1; char name[ORBIS_KERNEL_MAXIMUM_NAME_LENGTH]; }; +static_assert(sizeof(OrbisVirtualQueryInfo) == 72, + "OrbisVirtualQueryInfo struct size is incorrect"); struct OrbisKernelBatchMapEntry { void* start; From afcf3a12a3af90bd45114c8317fc8b56b28c611d Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 11 May 2025 04:59:14 -0500 Subject: [PATCH 003/141] Core: Pooled Memory Fixes (#2895) * Update sceKernelMemoryPoolExpand Hardware tests show that this function is basically the same as sceKernelAllocateDirectMemory, with some minor differences. Update the memory searching code to match my updated AllocateDirectMemory code, with appropriate error conditions. * Update MemoryPoolReserve Only difference between real hw and our code is behavior with addr = 0. * Don't coalesce PoolReserved areas. Real hardware doesn't coalesce them. * Update PoolCommit Plenty of edge case behaviors to handle here. Addresses are treated as fixed, EINVAL is returned for bad mappings, name should be preserved from PoolReserving, committed areas should coalesce, reserved areas get their phys_base updated * Formatting * Adjust fixed PoolReserve path Hardware tests suggest this will overwrite all VMAs in the range. Run UnmapMemoryImpl on the full area, then reserve. Same logic applies to normal reservations too. Also adjusts logic of the non-fixed path to more closely align with hardware observations. * Remove phys_base modifications This can be handled later. Doing the logic properly would likely take work in MergeAdjacent, and would probably need to be applied to normal dmem mappings too. * Use VMAHandle.Contains() Why do extra math when we have a function specifically for this? * Update memory.cpp * Remove unnecessary code Since I've removed those two asserts, these two lines of code effectively do nothing. * Clang * Fix names * Fix PoolDecommit Should fix the address space regressions in UE titles on Windows. * Fix error log Should make the cause of this clearer? * Clang * Oops * Remove coalesce on PoolCommit Windows makes this more difficult. * Track pool budgets If you try to commit more pooled memory than is allocated, PoolCommit returns ENOMEM. Also fixes error conditions for PoolDecommit, that should return EINVAL if given an address that isn't part of the pool. Note: Seems like the pool budget can't hit zero? I used a <= comparison based on hardware tests, otherwise we're able to make more mappings than real hardware can. --- src/core/libraries/kernel/memory.cpp | 27 +++-- src/core/memory.cpp | 146 +++++++++++++++++---------- src/core/memory.h | 3 +- 3 files changed, 111 insertions(+), 65 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 495ddc52f..9fcaa2439 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -383,13 +383,12 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, size_ LOG_ERROR(Kernel_Vmm, "Provided address range is invalid!"); return ORBIS_KERNEL_ERROR_EINVAL; } - const bool is_in_range = searchEnd - searchStart >= len; - if (len <= 0 || !Common::Is64KBAligned(len) || !is_in_range) { - LOG_ERROR(Kernel_Vmm, "Provided address range is invalid!"); + if (len <= 0 || !Common::Is64KBAligned(len)) { + LOG_ERROR(Kernel_Vmm, "Provided length {:#x} is invalid!", len); return ORBIS_KERNEL_ERROR_EINVAL; } if (alignment != 0 && !Common::Is64KBAligned(alignment)) { - LOG_ERROR(Kernel_Vmm, "Alignment value is invalid!"); + LOG_ERROR(Kernel_Vmm, "Alignment {:#x} is invalid!", alignment); return ORBIS_KERNEL_ERROR_EINVAL; } if (physAddrOut == nullptr) { @@ -397,8 +396,21 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, size_ return ORBIS_KERNEL_ERROR_EINVAL; } + const bool is_in_range = searchEnd - searchStart >= len; + if (searchEnd <= searchStart || searchEnd < len || !is_in_range) { + LOG_ERROR(Kernel_Vmm, + "Provided address range is too small!" + " searchStart = {:#x}, searchEnd = {:#x}, length = {:#x}", + searchStart, searchEnd, len); + return ORBIS_KERNEL_ERROR_ENOMEM; + } + auto* memory = Core::Memory::Instance(); PAddr phys_addr = memory->PoolExpand(searchStart, searchEnd, len, alignment); + if (phys_addr == -1) { + return ORBIS_KERNEL_ERROR_ENOMEM; + } + *physAddrOut = static_cast(phys_addr); LOG_INFO(Kernel_Vmm, @@ -413,10 +425,6 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addrIn, size_t len, size_t ali LOG_INFO(Kernel_Vmm, "addrIn = {}, len = {:#x}, alignment = {:#x}, flags = {:#x}", fmt::ptr(addrIn), len, alignment, flags); - if (addrIn == nullptr) { - LOG_ERROR(Kernel_Vmm, "Address is invalid!"); - return ORBIS_KERNEL_ERROR_EINVAL; - } if (len == 0 || !Common::Is2MBAligned(len)) { LOG_ERROR(Kernel_Vmm, "Map size is either zero or not 2MB aligned!"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -469,9 +477,8 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) const VAddr pool_addr = reinterpret_cast(addr); auto* memory = Core::Memory::Instance(); - memory->PoolDecommit(pool_addr, len); - return ORBIS_OK; + return memory->PoolDecommit(pool_addr, len); } int PS4_SYSV_ABI sceKernelMmap(void* addr, u64 len, int prot, int flags, int fd, size_t offset, diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 9861e813a..8fef8d102 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -109,31 +109,42 @@ bool MemoryManager::TryWriteBacking(void* address, const void* data, u32 num_byt PAddr MemoryManager::PoolExpand(PAddr search_start, PAddr search_end, size_t size, u64 alignment) { std::scoped_lock lk{mutex}; + alignment = alignment > 0 ? alignment : 64_KB; auto dmem_area = FindDmemArea(search_start); + auto mapping_start = search_start > dmem_area->second.base + ? Common::AlignUp(search_start, alignment) + : Common::AlignUp(dmem_area->second.base, alignment); + auto mapping_end = mapping_start + size; - const auto is_suitable = [&] { - const auto aligned_base = alignment > 0 ? Common::AlignUp(dmem_area->second.base, alignment) - : dmem_area->second.base; - const auto alignment_size = aligned_base - dmem_area->second.base; - const auto remaining_size = - dmem_area->second.size >= alignment_size ? dmem_area->second.size - alignment_size : 0; - return dmem_area->second.is_free && remaining_size >= size; - }; - while (!is_suitable() && dmem_area->second.GetEnd() <= search_end) { + // Find the first free, large enough dmem area in the range. + while (!dmem_area->second.is_free || dmem_area->second.GetEnd() < mapping_end) { + // The current dmem_area isn't suitable, move to the next one. dmem_area++; - } - ASSERT_MSG(is_suitable(), "Unable to find free direct memory area: size = {:#x}", size); + if (dmem_area == dmem_map.end()) { + break; + } - // Align free position - PAddr free_addr = dmem_area->second.base; - free_addr = alignment > 0 ? Common::AlignUp(free_addr, alignment) : free_addr; + // Update local variables based on the new dmem_area + mapping_start = Common::AlignUp(dmem_area->second.base, alignment); + mapping_end = mapping_start + size; + } + + if (dmem_area == dmem_map.end()) { + // There are no suitable mappings in this range + LOG_ERROR(Kernel_Vmm, "Unable to find free direct memory area: size = {:#x}", size); + return -1; + } // Add the allocated region to the list and commit its pages. - auto& area = CarveDmemArea(free_addr, size)->second; + auto& area = CarveDmemArea(mapping_start, size)->second; area.is_free = false; area.is_pooled = true; - return free_addr; + + // Track how much dmem was allocated for pools. + pool_budget += size; + + return mapping_start; } PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, u64 alignment, @@ -206,27 +217,27 @@ void MemoryManager::Free(PAddr phys_addr, size_t size) { int MemoryManager::PoolReserve(void** out_addr, VAddr virtual_addr, size_t size, MemoryMapFlags flags, u64 alignment) { std::scoped_lock lk{mutex}; - - virtual_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; alignment = alignment > 0 ? alignment : 2_MB; - VAddr mapped_addr = alignment > 0 ? Common::AlignUp(virtual_addr, alignment) : virtual_addr; + VAddr min_address = Common::AlignUp(impl.SystemManagedVirtualBase(), alignment); + VAddr mapped_addr = Common::AlignUp(virtual_addr, alignment); // Fixed mapping means the virtual address must exactly match the provided one. if (True(flags & MemoryMapFlags::Fixed)) { - auto& vma = FindVMA(mapped_addr)->second; - // If the VMA is mapped, unmap the region first. - if (vma.IsMapped()) { + // Make sure we're mapping to a valid address + mapped_addr = mapped_addr > min_address ? mapped_addr : min_address; + auto vma = FindVMA(mapped_addr)->second; + size_t remaining_size = vma.base + vma.size - mapped_addr; + // If the VMA is mapped or there's not enough space, unmap the region first. + if (vma.IsMapped() || remaining_size < size) { UnmapMemoryImpl(mapped_addr, size); vma = FindVMA(mapped_addr)->second; } - const size_t remaining_size = vma.base + vma.size - mapped_addr; - ASSERT_MSG(vma.type == VMAType::Free && remaining_size >= size, - "Memory region {:#x} to {:#x} is not large enough to reserve {:#x} to {:#x}", - vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); } - // Find the first free area starting with provided virtual address. if (False(flags & MemoryMapFlags::Fixed)) { + // When MemoryMapFlags::Fixed is not specified, and mapped_addr is 0, + // search from address 0x200000000 instead. + mapped_addr = mapped_addr == 0 ? 0x200000000 : mapped_addr; mapped_addr = SearchFree(mapped_addr, size, alignment); if (mapped_addr == -1) { // No suitable memory areas to map to @@ -241,7 +252,6 @@ int MemoryManager::PoolReserve(void** out_addr, VAddr virtual_addr, size_t size, new_vma.prot = MemoryProt::NoAccess; new_vma.name = "anon"; new_vma.type = VMAType::PoolReserved; - MergeAdjacent(vma_map, new_vma_handle); *out_addr = std::bit_cast(mapped_addr); return ORBIS_OK; @@ -258,15 +268,12 @@ int MemoryManager::Reserve(void** out_addr, VAddr virtual_addr, size_t size, Mem // Fixed mapping means the virtual address must exactly match the provided one. if (True(flags & MemoryMapFlags::Fixed)) { auto vma = FindVMA(mapped_addr)->second; - // If the VMA is mapped, unmap the region first. - if (vma.IsMapped()) { + size_t remaining_size = vma.base + vma.size - mapped_addr; + // If the VMA is mapped or there's not enough space, unmap the region first. + if (vma.IsMapped() || remaining_size < size) { UnmapMemoryImpl(mapped_addr, size); vma = FindVMA(mapped_addr)->second; } - const size_t remaining_size = vma.base + vma.size - mapped_addr; - ASSERT_MSG(vma.type == VMAType::Free && remaining_size >= size, - "Memory region {:#x} to {:#x} is not large enough to reserve {:#x} to {:#x}", - vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); } // Find the first free area starting with provided virtual address. @@ -296,30 +303,47 @@ int MemoryManager::PoolCommit(VAddr virtual_addr, size_t size, MemoryProt prot) const u64 alignment = 64_KB; - // When virtual addr is zero, force it to virtual_base. The guest cannot pass Fixed - // flag so we will take the branch that searches for free (or reserved) mappings. - virtual_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; + // Input addresses to PoolCommit are treated as fixed. VAddr mapped_addr = Common::AlignUp(virtual_addr, alignment); - // This should return SCE_KERNEL_ERROR_ENOMEM but shouldn't normally happen. - const auto& vma = FindVMA(mapped_addr)->second; - const size_t remaining_size = vma.base + vma.size - mapped_addr; - ASSERT_MSG(!vma.IsMapped() && remaining_size >= size, - "Memory region {:#x} to {:#x} isn't free enough to map region {:#x} to {:#x}", - vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); + auto& vma = FindVMA(mapped_addr)->second; + if (vma.type != VMAType::PoolReserved) { + // If we're attempting to commit non-pooled memory, return EINVAL + LOG_ERROR(Kernel_Vmm, "Attempting to commit non-pooled memory at {:#x}", mapped_addr); + return ORBIS_KERNEL_ERROR_EINVAL; + } - // Perform the mapping. - void* out_addr = impl.Map(mapped_addr, size, alignment, -1, false); - TRACK_ALLOC(out_addr, size, "VMEM"); + if (!vma.Contains(mapped_addr, size)) { + // If there's not enough space to commit, return EINVAL + LOG_ERROR(Kernel_Vmm, + "Pooled region {:#x} to {:#x} is not large enough to commit from {:#x} to {:#x}", + vma.base, vma.base + vma.size, mapped_addr, mapped_addr + size); + return ORBIS_KERNEL_ERROR_EINVAL; + } - auto& new_vma = CarveVMA(mapped_addr, size)->second; + if (pool_budget <= size) { + // If there isn't enough pooled memory to perform the mapping, return ENOMEM + LOG_ERROR(Kernel_Vmm, "Not enough pooled memory to perform mapping"); + return ORBIS_KERNEL_ERROR_ENOMEM; + } else { + // Track how much pooled memory this commit will take + pool_budget -= size; + } + + // Carve out the new VMA representing this mapping + const auto new_vma_handle = CarveVMA(mapped_addr, size); + auto& new_vma = new_vma_handle->second; new_vma.disallow_merge = false; new_vma.prot = prot; - new_vma.name = ""; + new_vma.name = "anon"; new_vma.type = Core::VMAType::Pooled; new_vma.is_exec = false; new_vma.phys_base = 0; + // Perform the mapping + void* out_addr = impl.Map(mapped_addr, size, alignment, -1, false); + TRACK_ALLOC(out_addr, size, "VMEM"); + if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } @@ -438,7 +462,7 @@ int MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, size_t size, Mem return ORBIS_OK; } -void MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { +s32 MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { std::scoped_lock lk{mutex}; const auto it = FindVMA(virtual_addr); @@ -453,6 +477,16 @@ void MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { const auto start_in_vma = virtual_addr - vma_base_addr; const auto type = vma_base.type; + if (type != VMAType::PoolReserved && type != VMAType::Pooled) { + LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); + return ORBIS_KERNEL_ERROR_EINVAL; + } + + if (type == VMAType::Pooled) { + // Track how much pooled memory is decommitted + pool_budget += size; + } + if (IsValidGpuMapping(virtual_addr, size)) { rasterizer->UnmapMemory(virtual_addr, size); } @@ -464,13 +498,17 @@ void MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { vma.prot = MemoryProt::NoAccess; vma.phys_base = 0; vma.disallow_merge = false; - vma.name = ""; + vma.name = "anon"; MergeAdjacent(vma_map, new_it); - // Unmap the memory region. - impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + size, phys_base, is_exec, - false, false); - TRACK_FREE(virtual_addr, "VMEM"); + if (type != VMAType::PoolReserved) { + // Unmap the memory region. + impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + size, phys_base, + is_exec, false, false); + TRACK_FREE(virtual_addr, "VMEM"); + } + + return ORBIS_OK; } s32 MemoryManager::UnmapMemory(VAddr virtual_addr, size_t size) { diff --git a/src/core/memory.h b/src/core/memory.h index 3a204eb96..4920aa397 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -198,7 +198,7 @@ public: int MapFile(void** out_addr, VAddr virtual_addr, size_t size, MemoryProt prot, MemoryMapFlags flags, uintptr_t fd, size_t offset); - void PoolDecommit(VAddr virtual_addr, size_t size); + s32 PoolDecommit(VAddr virtual_addr, size_t size); s32 UnmapMemory(VAddr virtual_addr, size_t size); @@ -274,6 +274,7 @@ private: size_t total_direct_size{}; size_t total_flexible_size{}; size_t flexible_usage{}; + size_t pool_budget{}; Vulkan::Rasterizer* rasterizer{}; friend class ::Core::Devtools::Widget::MemoryMapViewer; From ff1339b0b663fe45c3376083bb3377b3deb99dbb Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sun, 11 May 2025 19:03:55 +0300 Subject: [PATCH 004/141] [Libs] Camera (#2902) * stubbed camera lib * function definations * added error codes --- CMakeLists.txt | 6 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/camera/camera.cpp | 517 +++++++++++++++++++++++ src/core/libraries/camera/camera.h | 308 ++++++++++++++ src/core/libraries/camera/camera_error.h | 29 ++ src/core/libraries/libs.cpp | 2 + 7 files changed, 864 insertions(+) create mode 100644 src/core/libraries/camera/camera.cpp create mode 100644 src/core/libraries/camera/camera.h create mode 100644 src/core/libraries/camera/camera_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b10d0e5b..c182e0658 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -601,6 +601,11 @@ set(MISC_LIBS src/core/libraries/screenshot/screenshot.cpp src/core/libraries/signin_dialog/signindialog.h ) +set(CAMERA_LIBS src/core/libraries/camera/camera.cpp + src/core/libraries/camera/camera.h + src/core/libraries/camera/camera_error.h +) + set(DEV_TOOLS src/core/devtools/layer.cpp src/core/devtools/layer.h src/core/devtools/options.cpp @@ -764,6 +769,7 @@ set(CORE src/core/aerolib/stubs.cpp ${FIBER_LIB} ${VDEC_LIB} ${VR_LIBS} + ${CAMERA_LIBS} ${DEV_TOOLS} src/core/debug_state.cpp src/core/debug_state.h diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 622af93cc..1b605e9ed 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -138,6 +138,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, Zlib) \ SUB(Lib, Hmd) \ SUB(Lib, SigninDialog) \ + SUB(Lib, Camera) \ CLS(Frontend) \ CLS(Render) \ SUB(Render, Vulkan) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 27a87e082..5746b648e 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -105,6 +105,7 @@ enum class Class : u8 { Lib_Zlib, ///< The LibSceZlib implementation. Lib_Hmd, ///< The LibSceHmd implementation. Lib_SigninDialog, ///< The LibSigninDialog implementation. + Lib_Camera, ///< The LibCamera implementation. Frontend, ///< Emulator UI Render, ///< Video Core Render_Vulkan, ///< Vulkan backend diff --git a/src/core/libraries/camera/camera.cpp b/src/core/libraries/camera/camera.cpp new file mode 100644 index 000000000..996d1c895 --- /dev/null +++ b/src/core/libraries/camera/camera.cpp @@ -0,0 +1,517 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/camera/camera.h" +#include "core/libraries/camera/camera_error.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::Camera { + +s32 PS4_SYSV_ABI sceCameraAccGetData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraAudioClose() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraAudioGetData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraAudioGetData2() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraAudioOpen() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraAudioReset() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraChangeAppModuleState() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraClose(s32 handle) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraCloseByHandle() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraDeviceOpen() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetAttribute(s32 handle, OrbisCameraAttribute* pAttribute) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetAutoExposureGain(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetAutoWhiteBalance(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetCalibData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetCalibDataFromDevice() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetCalibrationData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetConfig(s32 handle, OrbisCameraConfig* pConfig) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetContrast(s32 handle, OrbisCameraChannel channel, u32* pContrast, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDefectivePixelCancellation(s32 handle, OrbisCameraChannel channel, + u32* pEnable, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDeviceConfig() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDeviceConfigWithoutHandle() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDeviceID() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDeviceIDWithoutOpen() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetDeviceInfo(s32 reserved, OrbisCameraDeviceInfo* pDeviceInfo) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetExposureGain(s32 handle, OrbisCameraChannel channel, + OrbisCameraExposureGain* pExposureGain, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetFrameData(int handle, OrbisCameraFrameData* pFrameData) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* pGamma, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetHue(s32 handle, OrbisCameraChannel channel, s32* pHue, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetLensCorrection(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetMmapConnectedCount() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetProductInfo() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetRegister() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetRegistryInfo() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetSaturation(s32 handle, OrbisCameraChannel channel, u32* pSaturation, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetSharpness(s32 handle, OrbisCameraChannel channel, u32* pSharpness, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetVrCaptureInfo() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraGetWhiteBalance(s32 handle, OrbisCameraChannel channel, + OrbisCameraWhiteBalance* pWhiteBalance, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraInitializeRegistryCalibData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraIsAttached(s32 index) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraIsConfigChangeDone() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraIsValidFrameData(int handle, OrbisCameraFrameData* pFrameData) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, OrbisCameraOpenParameter* pParam) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraOpenByModuleId() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraRemoveAppModuleFocus() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetAppModuleFocus() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetAttribute(s32 handle, OrbisCameraAttribute* pAttribute) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetAttributeInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetAutoExposureGain(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetAutoWhiteBalance(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetCalibData() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetConfig(s32 handle, OrbisCameraConfig* pConfig) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetConfigInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetContrast(s32 handle, OrbisCameraChannel channel, u32 contrast, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetDebugStop() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetDefectivePixelCancellation(s32 handle, OrbisCameraChannel channel, + u32 enable, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetDefectivePixelCancellationInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetExposureGain(s32 handle, OrbisCameraChannel channel, + OrbisCameraExposureGain* pExposureGain, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetForceActivate() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* pGamma, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetHue(s32 handle, OrbisCameraChannel channel, s32 hue, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetLensCorrection(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetLensCorrectionInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetProcessFocus() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetProcessFocusByHandle() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetRegister() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetSaturation(s32 handle, OrbisCameraChannel channel, u32 saturation, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetSharpness(s32 handle, OrbisCameraChannel channel, u32 sharpness, + void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetTrackerMode() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetUacModeInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetVideoSync(s32 handle, OrbisCameraVideoSyncParameter* pVideoSync) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetVideoSyncInternal() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraSetWhiteBalance(s32 handle, OrbisCameraChannel channel, + OrbisCameraWhiteBalance* pWhiteBalance, void* pOption) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* pParam) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraStartByHandle() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraStop(s32 handle) { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCameraStopByHandle() { + LOG_ERROR(Lib_Camera, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterlibSceCamera(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("QhjrPkRPUZQ", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAccGetData); + LIB_FUNCTION("UFonL7xopFM", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAudioClose); + LIB_FUNCTION("fkZE7Hup2ro", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAudioGetData); + LIB_FUNCTION("hftC5A1C8OQ", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAudioGetData2); + LIB_FUNCTION("DhqqFiBU+6g", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAudioOpen); + LIB_FUNCTION("wyU98EXAYxU", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraAudioReset); + LIB_FUNCTION("Y0pCDajzkVQ", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraChangeAppModuleState); + LIB_FUNCTION("OMS9LlcrvBo", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraClose); + LIB_FUNCTION("ztqH5qNTpTk", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraCloseByHandle); + LIB_FUNCTION("nBH6i2s4Glc", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraDeviceOpen); + LIB_FUNCTION("0btIPD5hg5A", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetAttribute); + LIB_FUNCTION("oEi6vM-3E2c", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetAutoExposureGain); + LIB_FUNCTION("qTPRMh4eY60", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetAutoWhiteBalance); + LIB_FUNCTION("hHA1frlMxYE", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetCalibData); + LIB_FUNCTION("5Oie5RArfWs", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetCalibDataFromDevice); + LIB_FUNCTION("RHYJ7GKOSMg", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetCalibrationData); + LIB_FUNCTION("ZaqmGEtYuL0", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetConfig); + LIB_FUNCTION("a5xFueMZIMs", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetContrast); + LIB_FUNCTION("tslCukqFE+E", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetDefectivePixelCancellation); + LIB_FUNCTION("DSOLCrc3Kh8", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetDeviceConfig); + LIB_FUNCTION("n+rFeP1XXyM", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetDeviceConfigWithoutHandle); + LIB_FUNCTION("jTJCdyv9GLU", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetDeviceID); + LIB_FUNCTION("-H3UwGQvNZI", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetDeviceIDWithoutOpen); + LIB_FUNCTION("WZpxnSAM-ds", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetDeviceInfo); + LIB_FUNCTION("ObIste7hqdk", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetExposureGain); + LIB_FUNCTION("mxgMmR+1Kr0", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetFrameData); + LIB_FUNCTION("WVox2rwGuSc", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetGamma); + LIB_FUNCTION("zrIUDKZx0iE", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetHue); + LIB_FUNCTION("XqYRHc4aw3w", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetLensCorrection); + LIB_FUNCTION("B260o9pSzM8", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraGetMmapConnectedCount); + LIB_FUNCTION("ULxbwqiYYuU", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetProductInfo); + LIB_FUNCTION("olojYZKYiYs", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetRegister); + LIB_FUNCTION("hawKak+Auw4", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetRegistryInfo); + LIB_FUNCTION("RTDOsWWqdME", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetSaturation); + LIB_FUNCTION("c6Fp9M1EXXc", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetSharpness); + LIB_FUNCTION("IAz2HgZQWzE", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetVrCaptureInfo); + LIB_FUNCTION("HX5524E5tMY", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraGetWhiteBalance); + LIB_FUNCTION("0wnf2a60FqI", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraInitializeRegistryCalibData); + LIB_FUNCTION("p6n3Npi3YY4", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraIsAttached); + LIB_FUNCTION("wQfd7kfRZvo", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraIsConfigChangeDone); + LIB_FUNCTION("U3BVwQl2R5Q", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraIsValidFrameData); + LIB_FUNCTION("BHn83xrF92E", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraOpen); + LIB_FUNCTION("eTywOSWsEiI", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraOpenByModuleId); + LIB_FUNCTION("py8p6kZcHmA", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraRemoveAppModuleFocus); + LIB_FUNCTION("j5isFVIlZLk", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetAppModuleFocus); + LIB_FUNCTION("doPlf33ab-U", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetAttribute); + LIB_FUNCTION("96F7zp1Xo+k", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetAttributeInternal); + LIB_FUNCTION("yfSdswDaElo", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetAutoExposureGain); + LIB_FUNCTION("zIKL4kZleuc", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetAutoWhiteBalance); + LIB_FUNCTION("LEMk5cTHKEA", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetCalibData); + LIB_FUNCTION("VQ+5kAqsE2Q", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetConfig); + LIB_FUNCTION("9+SNhbctk64", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetConfigInternal); + LIB_FUNCTION("3i5MEzrC1pg", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetContrast); + LIB_FUNCTION("vejouEusC7g", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetDebugStop); + LIB_FUNCTION("jMv40y2A23g", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetDefectivePixelCancellation); + LIB_FUNCTION("vER3cIMBHqI", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetDefectivePixelCancellationInternal); + LIB_FUNCTION("wgBMXJJA6K4", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetExposureGain); + LIB_FUNCTION("jeTpU0MqKU0", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetForceActivate); + LIB_FUNCTION("lhEIsHzB8r4", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetGamma); + LIB_FUNCTION("QI8GVJUy2ZY", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetHue); + LIB_FUNCTION("K7W7H4ZRwbc", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetLensCorrection); + LIB_FUNCTION("eHa3vhGu2rQ", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetLensCorrectionInternal); + LIB_FUNCTION("lS0tM6n+Q5E", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetProcessFocus); + LIB_FUNCTION("NVITuK83Z7o", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetProcessFocusByHandle); + LIB_FUNCTION("8MjO05qk5hA", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetRegister); + LIB_FUNCTION("bSKEi2PzzXI", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetSaturation); + LIB_FUNCTION("P-7MVfzvpsM", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetSharpness); + LIB_FUNCTION("3VJOpzKoIeM", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetTrackerMode); + LIB_FUNCTION("nnR7KAIDPv8", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetUacModeInternal); + LIB_FUNCTION("wpeyFwJ+UEI", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetVideoSync); + LIB_FUNCTION("8WtmqmE4edw", "libSceCamera", 1, "libSceCamera", 1, 1, + sceCameraSetVideoSyncInternal); + LIB_FUNCTION("k3zPIcgFNv0", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraSetWhiteBalance); + LIB_FUNCTION("9EpRYMy7rHU", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraStart); + LIB_FUNCTION("cLxF1QtHch0", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraStartByHandle); + LIB_FUNCTION("2G2C0nmd++M", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraStop); + LIB_FUNCTION("+X1Kgnn3bzg", "libSceCamera", 1, "libSceCamera", 1, 1, sceCameraStopByHandle); +}; + +} // namespace Libraries::Camera \ No newline at end of file diff --git a/src/core/libraries/camera/camera.h b/src/core/libraries/camera/camera.h new file mode 100644 index 000000000..51aa8b729 --- /dev/null +++ b/src/core/libraries/camera/camera.h @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Camera { + +constexpr int ORBIS_CAMERA_MAX_DEVICE_NUM = 2; +constexpr int ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM = 4; + +enum OrbisCameraChannel { + ORBIS_CAMERA_CHANNEL_0 = 1, + ORBIS_CAMERA_CHANNEL_1 = 2, + ORBIS_CAMERA_CHANNEL_BOTH = 3, +}; + +struct OrbisCameraOpenParameter { + u32 sizeThis; + u32 reserved1; + u32 reserved2; + u32 reserved3; +}; + +enum OrbisCameraConfigType { + ORBIS_CAMERA_CONFIG_TYPE1 = 0x01, + ORBIS_CAMERA_CONFIG_TYPE2 = 0x02, + ORBIS_CAMERA_CONFIG_TYPE3 = 0x03, + ORBIS_CAMERA_CONFIG_TYPE4 = 0x04, + ORBIS_CAMERA_CONFIG_TYPE5 = 0x05, + ORBIS_CAMERA_CONFIG_EXTENTION = 0x10, +}; + +enum OrbisCameraResolution { + ORBIS_CAMERA_RESOLUTION_1280X800 = 0x0, + ORBIS_CAMERA_RESOLUTION_640X400 = 0x1, + ORBIS_CAMERA_RESOLUTION_320X200 = 0x2, + ORBIS_CAMERA_RESOLUTION_160X100 = 0x3, + ORBIS_CAMERA_RESOLUTION_320X192 = 0x4, + ORBIS_CAMERA_RESOLUTION_SPECIFIED_WIDTH_HEIGHT, + ORBIS_CAMERA_RESOLUTION_UNKNOWN = 0xFF, +}; + +enum OrbisCameraFramerate { + ORBIS_CAMERA_FRAMERATE_UNKNOWN = 0, + ORBIS_CAMERA_FRAMERATE_7_5 = 7, + ORBIS_CAMERA_FRAMERATE_15 = 15, + ORBIS_CAMERA_FRAMERATE_30 = 30, + ORBIS_CAMERA_FRAMERATE_60 = 60, + ORBIS_CAMERA_FRAMERATE_120 = 120, + ORBIS_CAMERA_FRAMERATE_240 = 240, +}; + +enum OrbisCameraBaseFormat { + ORBIS_CAMERA_FORMAT_YUV422 = 0x0, + ORBIS_CAMERA_FORMAT_RAW16, + ORBIS_CAMERA_FORMAT_RAW8, + ORBIS_CAMERA_FORMAT_NO_USE = 0x10, + ORBIS_CAMERA_FORMAT_UNKNOWN = 0xFF, +}; + +enum OrbisCameraScaleFormat { + ORBIS_CAMERA_SCALE_FORMAT_YUV422 = 0x0, + ORBIS_CAMERA_SCALE_FORMAT_Y16 = 0x3, + ORBIS_CAMERA_SCALE_FORMAT_Y8, + ORBIS_CAMERA_SCALE_FORMAT_NO_USE = 0x10, + ORBIS_CAMERA_SCALE_FORMAT_UNKNOWN = 0xFF, +}; + +struct OrbisCameraFormat { + OrbisCameraBaseFormat formatLevel0; + OrbisCameraScaleFormat formatLevel1; + OrbisCameraScaleFormat formatLevel2; + OrbisCameraScaleFormat formatLevel3; +}; + +struct OrbisCameraConfigExtention { + OrbisCameraFormat format; + OrbisCameraResolution resolution; + OrbisCameraFramerate framerate; + u32 width; + u32 height; + u32 reserved1; + void* pBaseOption; +}; + +struct OrbisCameraConfig { + u32 sizeThis; + OrbisCameraConfigType configType; + OrbisCameraConfigExtention configExtention[ORBIS_CAMERA_MAX_DEVICE_NUM]; +}; + +enum OrbisCameraAecAgcTarget { + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_DEF = 0x00, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_2_0 = 0x20, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_1_6 = 0x16, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_1_4 = 0x14, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_1_2 = 0x12, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_1_0 = 0x10, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_0_8 = 0x08, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_0_6 = 0x06, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_0_4 = 0x04, + ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_0_2 = 0x02, +}; + +struct OrbisCameraDeviceInfo { + u32 sizeThis; + u32 infoRevision; + u32 deviceRevision; + u32 padding; +}; + +struct OrbisCameraStartParameter { + u32 sizeThis; + u32 formatLevel[ORBIS_CAMERA_MAX_DEVICE_NUM]; + void* pStartOption; +}; + +struct OrbisCameraVideoSyncParameter { + u32 sizeThis; + u32 videoSyncMode; + void* pModeOption; +}; + +struct OrbisCameraFramePosition { + u32 x; + u32 y; + u32 xSize; + u32 ySize; +}; + +struct OrbisCameraAutoExposureGainTarget { + u32 sizeThis; + OrbisCameraAecAgcTarget target; +}; + +struct OrbisCameraExposureGain { + u32 exposureControl; + u32 exposure; + u32 gain; + u32 mode; +}; + +struct OrbisCameraWhiteBalance { + u32 whiteBalanceControl; + u32 gainRed; + u32 gainBlue; + u32 gainGreen; +}; + +struct OrbisCameraGamma { + u32 gammaControl; + u32 value; + u8 reserved[16]; +}; + +struct OrbisCameraMeta { + u32 metaMode; + u32 format[ORBIS_CAMERA_MAX_DEVICE_NUM][ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM]; + u64 frame[ORBIS_CAMERA_MAX_DEVICE_NUM]; + u64 timestamp[ORBIS_CAMERA_MAX_DEVICE_NUM]; + u32 deviceTimestamp[ORBIS_CAMERA_MAX_DEVICE_NUM]; + OrbisCameraExposureGain exposureGain[ORBIS_CAMERA_MAX_DEVICE_NUM]; + OrbisCameraWhiteBalance whiteBalance[ORBIS_CAMERA_MAX_DEVICE_NUM]; + OrbisCameraGamma gamma[ORBIS_CAMERA_MAX_DEVICE_NUM]; + u32 luminance[ORBIS_CAMERA_MAX_DEVICE_NUM]; + float acceleration_x; + float acceleration_y; + float acceleration_z; + u64 vcounter; + u32 reserved[14]; +}; + +struct OrbisCameraFrameData { + u32 sizeThis; + u32 readMode; + OrbisCameraFramePosition framePosition[ORBIS_CAMERA_MAX_DEVICE_NUM] + [ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM]; + void* pFramePointerList[ORBIS_CAMERA_MAX_DEVICE_NUM][ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM]; + u32 frameSize[ORBIS_CAMERA_MAX_DEVICE_NUM][ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM]; + u32 status[ORBIS_CAMERA_MAX_DEVICE_NUM]; + OrbisCameraMeta meta; + void* pFramePointerListGarlic[ORBIS_CAMERA_MAX_DEVICE_NUM][ORBIS_CAMERA_MAX_FORMAT_LEVEL_NUM]; +}; + +struct OrbisCameraAttribute { + u32 sizeThis; + OrbisCameraChannel channel; + OrbisCameraFramePosition framePosition; + OrbisCameraExposureGain exposureGain; + OrbisCameraWhiteBalance whiteBalance; + OrbisCameraGamma gamma; + u32 saturation; + u32 contrast; + u32 sharpness; + s32 hue; + u32 reserved1; + u32 reserved2; + u32 reserved3; + u32 reserved4; +}; + +s32 PS4_SYSV_ABI sceCameraAccGetData(); +s32 PS4_SYSV_ABI sceCameraAudioClose(); +s32 PS4_SYSV_ABI sceCameraAudioGetData(); +s32 PS4_SYSV_ABI sceCameraAudioGetData2(); +s32 PS4_SYSV_ABI sceCameraAudioOpen(); +s32 PS4_SYSV_ABI sceCameraAudioReset(); +s32 PS4_SYSV_ABI sceCameraChangeAppModuleState(); +s32 PS4_SYSV_ABI sceCameraClose(s32 handle); +s32 PS4_SYSV_ABI sceCameraCloseByHandle(); +s32 PS4_SYSV_ABI sceCameraDeviceOpen(); +s32 PS4_SYSV_ABI sceCameraGetAttribute(s32 handle, OrbisCameraAttribute* pAttribute); +s32 PS4_SYSV_ABI sceCameraGetAutoExposureGain(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetAutoWhiteBalance(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetCalibData(); +s32 PS4_SYSV_ABI sceCameraGetCalibDataFromDevice(); +s32 PS4_SYSV_ABI sceCameraGetCalibrationData(); +s32 PS4_SYSV_ABI sceCameraGetConfig(s32 handle, OrbisCameraConfig* pConfig); +s32 PS4_SYSV_ABI sceCameraGetContrast(s32 handle, OrbisCameraChannel channel, u32* pContrast, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetDefectivePixelCancellation(s32 handle, OrbisCameraChannel channel, + u32* pEnable, void* pOption); +s32 PS4_SYSV_ABI sceCameraGetDeviceConfig(); +s32 PS4_SYSV_ABI sceCameraGetDeviceConfigWithoutHandle(); +s32 PS4_SYSV_ABI sceCameraGetDeviceID(); +s32 PS4_SYSV_ABI sceCameraGetDeviceIDWithoutOpen(); +s32 PS4_SYSV_ABI sceCameraGetDeviceInfo(s32 reserved, OrbisCameraDeviceInfo* pDeviceInfo); +s32 PS4_SYSV_ABI sceCameraGetExposureGain(s32 handle, OrbisCameraChannel channel, + OrbisCameraExposureGain* pExposureGain, void* pOption); +s32 PS4_SYSV_ABI sceCameraGetFrameData(int handle, OrbisCameraFrameData* pFrameData); +s32 PS4_SYSV_ABI sceCameraGetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* pGamma, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetHue(s32 handle, OrbisCameraChannel channel, s32* pHue, void* pOption); +s32 PS4_SYSV_ABI sceCameraGetLensCorrection(s32 handle, OrbisCameraChannel channel, u32* pEnable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetMmapConnectedCount(); +s32 PS4_SYSV_ABI sceCameraGetProductInfo(); +s32 PS4_SYSV_ABI sceCameraGetRegister(); +s32 PS4_SYSV_ABI sceCameraGetRegistryInfo(); +s32 PS4_SYSV_ABI sceCameraGetSaturation(s32 handle, OrbisCameraChannel channel, u32* pSaturation, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetSharpness(s32 handle, OrbisCameraChannel channel, u32* pSharpness, + void* pOption); +s32 PS4_SYSV_ABI sceCameraGetVrCaptureInfo(); +s32 PS4_SYSV_ABI sceCameraGetWhiteBalance(s32 handle, OrbisCameraChannel channel, + OrbisCameraWhiteBalance* pWhiteBalance, void* pOption); +s32 PS4_SYSV_ABI sceCameraInitializeRegistryCalibData(); +s32 PS4_SYSV_ABI sceCameraIsAttached(s32 index); +s32 PS4_SYSV_ABI sceCameraIsConfigChangeDone(); +s32 PS4_SYSV_ABI sceCameraIsValidFrameData(int handle, OrbisCameraFrameData* pFrameData); +s32 PS4_SYSV_ABI sceCameraOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type, + s32 index, OrbisCameraOpenParameter* pParam); +s32 PS4_SYSV_ABI sceCameraOpenByModuleId(); +s32 PS4_SYSV_ABI sceCameraRemoveAppModuleFocus(); +s32 PS4_SYSV_ABI sceCameraSetAppModuleFocus(); +s32 PS4_SYSV_ABI sceCameraSetAttribute(s32 handle, OrbisCameraAttribute* pAttribute); +s32 PS4_SYSV_ABI sceCameraSetAttributeInternal(); +s32 PS4_SYSV_ABI sceCameraSetAutoExposureGain(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetAutoWhiteBalance(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetCalibData(); +s32 PS4_SYSV_ABI sceCameraSetConfig(s32 handle, OrbisCameraConfig* pConfig); +s32 PS4_SYSV_ABI sceCameraSetConfigInternal(); +s32 PS4_SYSV_ABI sceCameraSetContrast(s32 handle, OrbisCameraChannel channel, u32 contrast, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetDebugStop(); +s32 PS4_SYSV_ABI sceCameraSetDefectivePixelCancellation(s32 handle, OrbisCameraChannel channel, + u32 enable, void* pOption); +s32 PS4_SYSV_ABI sceCameraSetDefectivePixelCancellationInternal(); +s32 PS4_SYSV_ABI sceCameraSetExposureGain(s32 handle, OrbisCameraChannel channel, + OrbisCameraExposureGain* pExposureGain, void* pOption); +s32 PS4_SYSV_ABI sceCameraSetForceActivate(); +s32 PS4_SYSV_ABI sceCameraSetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* pGamma, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetHue(s32 handle, OrbisCameraChannel channel, s32 hue, void* pOption); +s32 PS4_SYSV_ABI sceCameraSetLensCorrection(s32 handle, OrbisCameraChannel channel, u32 enable, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetLensCorrectionInternal(); +s32 PS4_SYSV_ABI sceCameraSetProcessFocus(); +s32 PS4_SYSV_ABI sceCameraSetProcessFocusByHandle(); +s32 PS4_SYSV_ABI sceCameraSetRegister(); +s32 PS4_SYSV_ABI sceCameraSetSaturation(s32 handle, OrbisCameraChannel channel, u32 saturation, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetSharpness(s32 handle, OrbisCameraChannel channel, u32 sharpness, + void* pOption); +s32 PS4_SYSV_ABI sceCameraSetTrackerMode(); +s32 PS4_SYSV_ABI sceCameraSetUacModeInternal(); +s32 PS4_SYSV_ABI sceCameraSetVideoSync(s32 handle, OrbisCameraVideoSyncParameter* pVideoSync); +s32 PS4_SYSV_ABI sceCameraSetVideoSyncInternal(); +s32 PS4_SYSV_ABI sceCameraSetWhiteBalance(s32 handle, OrbisCameraChannel channel, + OrbisCameraWhiteBalance* pWhiteBalance, void* pOption); +s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* pParam); +s32 PS4_SYSV_ABI sceCameraStartByHandle(); +s32 PS4_SYSV_ABI sceCameraStop(s32 handle); +s32 PS4_SYSV_ABI sceCameraStopByHandle(); + +void RegisterlibSceCamera(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Camera \ No newline at end of file diff --git a/src/core/libraries/camera/camera_error.h b/src/core/libraries/camera/camera_error.h new file mode 100644 index 000000000..acb04dd02 --- /dev/null +++ b/src/core/libraries/camera/camera_error.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +constexpr int ORBIS_CAMERA_ERROR_PARAM = 0x802E0000; +constexpr int ORBIS_CAMERA_ERROR_ALREADY_INIT = 0x802E0001; +constexpr int ORBIS_CAMERA_ERROR_NOT_INIT = 0x802E0002; +constexpr int ORBIS_CAMERA_ERROR_ALREADY_OPEN = 0x802E0003; +constexpr int ORBIS_CAMERA_ERROR_NOT_OPEN = 0x802E0004; +constexpr int ORBIS_CAMERA_ERROR_ALREADY_START = 0x802E0005; +constexpr int ORBIS_CAMERA_ERROR_NOT_START = 0x802E0006; +constexpr int ORBIS_CAMERA_ERROR_FORMAT_UNKNOWN = 0x802E0007; +constexpr int ORBIS_CAMERA_ERROR_RESOLUTION_UNKNOWN = 0x802E0008; +constexpr int ORBIS_CAMERA_ERROR_BAD_FRAMERATE = 0x802E0009; +constexpr int ORBIS_CAMERA_ERROR_TIMEOUT = 0x802E000A; +constexpr int ORBIS_CAMERA_ERROR_ATTRIBUTE_UNKNOWN = 0x802E000B; +constexpr int ORBIS_CAMERA_ERROR_BUSY = 0x802E000C; +constexpr int ORBIS_CAMERA_ERROR_UNKNOWN_CONFIG = 0x802E000D; +constexpr int ORBIS_CAMERA_ERROR_ALREADY_READ = 0x802E000F; +constexpr int ORBIS_CAMERA_ERROR_NOT_CONNECTED = 0x802E0010; +constexpr int ORBIS_CAMERA_ERROR_NOT_SUPPORTED = 0x802E0011; +constexpr int ORBIS_CAMERA_ERROR_INVALID_CONFIG = 0x802E0013; +constexpr int ORBIS_CAMERA_ERROR_MAX_HANDLE = 0x802E0014; +constexpr int ORBIS_CAMERA_ERROR_MAX_PROCESS = 0x802E00FB; +constexpr int ORBIS_CAMERA_ERROR_COPYOUT_FAILED = 0x802E00FC; +constexpr int ORBIS_CAMERA_ERROR_COPYIN_FAILED = 0x802E00FD; +constexpr int ORBIS_CAMERA_ERROR_KPROC_CREATE = 0x802E00FE; +constexpr int ORBIS_CAMERA_ERROR_FATAL = 0x802E00FF; \ No newline at end of file diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 3826ff793..5ef4b259d 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -8,6 +8,7 @@ #include "core/libraries/audio/audioout.h" #include "core/libraries/audio3d/audio3d.h" #include "core/libraries/avplayer/avplayer.h" +#include "core/libraries/camera/camera.h" #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/game_live_streaming/gamelivestreaming.h" #include "core/libraries/gnmdriver/gnmdriver.h" @@ -122,6 +123,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::DiscMap::RegisterlibSceDiscMap(sym); Libraries::Ulobjmgr::RegisterlibSceUlobjmgr(sym); Libraries::SigninDialog::RegisterlibSceSigninDialog(sym); + Libraries::Camera::RegisterlibSceCamera(sym); } } // namespace Libraries From 3a090e988cf66131b1af4b7309c0a59c6ea45581 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 11 May 2025 14:22:17 -0700 Subject: [PATCH 005/141] kernel: Clean up and fix some mistakes. (#2907) --- src/common/va_ctx.h | 2 +- src/core/libraries/kernel/aio.cpp | 2 +- src/core/libraries/kernel/kernel.h | 18 +++++------------- src/core/libraries/kernel/threads/pthread.cpp | 11 ++++------- src/core/libraries/kernel/threads/pthread.h | 3 ++- .../libraries/kernel/threads/pthread_attr.cpp | 19 ++++++++++--------- src/core/libraries/libs.h | 10 ++-------- src/core/linker.cpp | 5 +++-- 8 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/common/va_ctx.h b/src/common/va_ctx.h index e0b8c0bab..cffe468ff 100644 --- a/src/common/va_ctx.h +++ b/src/common/va_ctx.h @@ -8,7 +8,7 @@ #define VA_ARGS \ uint64_t rdi, uint64_t rsi, uint64_t rdx, uint64_t rcx, uint64_t r8, uint64_t r9, \ uint64_t overflow_arg_area, __m128 xmm0, __m128 xmm1, __m128 xmm2, __m128 xmm3, \ - __m128 xmm4, __m128 xmm5, __m128 xmm6, __m128 xmm7, ... + __m128 xmm4, __m128 xmm5, __m128 xmm6, __m128 xmm7 #define VA_CTX(ctx) \ alignas(16)::Common::VaCtx ctx{}; \ diff --git a/src/core/libraries/kernel/aio.cpp b/src/core/libraries/kernel/aio.cpp index e017010cb..1d746860b 100644 --- a/src/core/libraries/kernel/aio.cpp +++ b/src/core/libraries/kernel/aio.cpp @@ -19,7 +19,7 @@ namespace Libraries::Kernel { static s32* id_state; static s32 id_index; -s32 sceKernelAioInitializeImpl(void* p, s32 size) { +s32 PS4_SYSV_ABI sceKernelAioInitializeImpl(void* p, s32 size) { return 0; } diff --git a/src/core/libraries/kernel/kernel.h b/src/core/libraries/kernel/kernel.h index 4d68aa357..aaa22aec1 100644 --- a/src/core/libraries/kernel/kernel.h +++ b/src/core/libraries/kernel/kernel.h @@ -3,9 +3,6 @@ #pragma once -#include -#include -#include "common/string_literal.h" #include "common/types.h" #include "core/libraries/kernel/orbis_error.h" @@ -20,26 +17,21 @@ int ErrnoToSceKernelError(int e); void SetPosixErrno(int e); int* PS4_SYSV_ABI __Error(); -template -struct WrapperImpl; +template +struct OrbisWrapperImpl; -template -struct WrapperImpl { - static constexpr StringLiteral Name{name}; +template +struct OrbisWrapperImpl { static R PS4_SYSV_ABI wrap(Args... args) { u32 ret = f(args...); if (ret != 0) { - // LOG_ERROR(Lib_Kernel, "Function {} returned {}", std::string_view{name.value}, ret); ret += ORBIS_KERNEL_ERROR_UNKNOWN; } return ret; } }; -template -constexpr auto OrbisWrapper = WrapperImpl::wrap; - -#define ORBIS(func) WrapperImpl<#func, decltype(&func), func>::wrap +#define ORBIS(func) (Libraries::Kernel::OrbisWrapperImpl::wrap) int* PS4_SYSV_ABI __Error(); diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index e791e74bf..a51f1f6e8 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -581,12 +581,9 @@ int PS4_SYSV_ABI posix_pthread_setaffinity_np(PthreadT thread, size_t cpusetsize return thread->SetAffinity(cpusetp); } -int PS4_SYSV_ABI scePthreadSetaffinity(PthreadT thread, const Cpuset mask) { - int result = posix_pthread_setaffinity_np(thread, 0x10, &mask); - if (result != 0) { - return ErrnoToSceKernelError(result); - } - return 0; +int PS4_SYSV_ABI scePthreadSetaffinity(PthreadT thread, const u64 mask) { + const Cpuset cpuset = {.bits = mask}; + return posix_pthread_setaffinity_np(thread, sizeof(Cpuset), &cpuset); } void RegisterThread(Core::Loader::SymbolsResolver* sym) { @@ -635,7 +632,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("W0Hpm2X0uPE", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_setprio)); LIB_FUNCTION("rNhWz+lvOMU", "libkernel", 1, "libkernel", 1, 1, _sceKernelSetThreadDtors); LIB_FUNCTION("6XG4B33N09g", "libkernel", 1, "libkernel", 1, 1, sched_yield); - LIB_FUNCTION("bt3CTBKmGyI", "libkernel", 1, "libkernel", 1, 1, scePthreadSetaffinity) + LIB_FUNCTION("bt3CTBKmGyI", "libkernel", 1, "libkernel", 1, 1, ORBIS(scePthreadSetaffinity)); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/threads/pthread.h b/src/core/libraries/kernel/threads/pthread.h index 09eed11b8..ebcc4aed3 100644 --- a/src/core/libraries/kernel/threads/pthread.h +++ b/src/core/libraries/kernel/threads/pthread.h @@ -159,6 +159,7 @@ enum class SchedPolicy : u32 { struct Cpuset { u64 bits; + u64 _reserved; }; struct PthreadAttr { @@ -269,7 +270,7 @@ struct Pthread { bool no_cancel; bool cancel_async; bool cancelling; - Cpuset sigmask; + u64 sigmask; bool unblock_sigcancel; bool in_sigsuspend; bool force_exit; diff --git a/src/core/libraries/kernel/threads/pthread_attr.cpp b/src/core/libraries/kernel/threads/pthread_attr.cpp index a8e60ccf8..02a8cb1c7 100644 --- a/src/core/libraries/kernel/threads/pthread_attr.cpp +++ b/src/core/libraries/kernel/threads/pthread_attr.cpp @@ -243,7 +243,7 @@ int PS4_SYSV_ABI posix_pthread_attr_getaffinity_np(const PthreadAttrT* pattr, si if (attr->cpuset != nullptr) memcpy(cpusetp, attr->cpuset, std::min(cpusetsize, attr->cpusetsize)); else - memset(cpusetp, -1, sizeof(Cpuset)); + memset(cpusetp, -1, cpusetsize); return 0; } @@ -259,30 +259,31 @@ int PS4_SYSV_ABI posix_pthread_attr_setaffinity_np(PthreadAttrT* pattr, size_t c if (cpusetsize == 0 || cpusetp == nullptr) { if (attr->cpuset != nullptr) { free(attr->cpuset); - attr->cpuset = NULL; + attr->cpuset = nullptr; attr->cpusetsize = 0; } return 0; } if (attr->cpuset == nullptr) { - attr->cpuset = (Cpuset*)calloc(1, sizeof(Cpuset)); + attr->cpuset = static_cast(calloc(1, sizeof(Cpuset))); attr->cpusetsize = sizeof(Cpuset); } - memcpy(attr->cpuset, cpusetp, sizeof(Cpuset)); + memcpy(attr->cpuset, cpusetp, cpusetsize); return 0; } -int PS4_SYSV_ABI scePthreadAttrGetaffinity(PthreadAttrT* param_1, Cpuset* mask) { +int PS4_SYSV_ABI scePthreadAttrGetaffinity(PthreadAttrT* param_1, u64* mask) { Cpuset cpuset; - const int ret = posix_pthread_attr_getaffinity_np(param_1, 0x10, &cpuset); + const int ret = posix_pthread_attr_getaffinity_np(param_1, sizeof(Cpuset), &cpuset); if (ret == 0) { - *mask = cpuset; + *mask = cpuset.bits; } return ret; } -int PS4_SYSV_ABI scePthreadAttrSetaffinity(PthreadAttrT* attr, const Cpuset mask) { - return posix_pthread_attr_setaffinity_np(attr, 0x10, &mask); +int PS4_SYSV_ABI scePthreadAttrSetaffinity(PthreadAttrT* attr, const u64 mask) { + const Cpuset cpuset = {.bits = mask}; + return posix_pthread_attr_setaffinity_np(attr, sizeof(Cpuset), &cpuset); } void RegisterThreadAttr(Core::Loader::SymbolsResolver* sym) { diff --git a/src/core/libraries/libs.h b/src/core/libraries/libs.h index aa5ba4a97..7e073db8e 100644 --- a/src/core/libraries/libs.h +++ b/src/core/libraries/libs.h @@ -3,14 +3,9 @@ #pragma once -#include - -#include "common/logging/log.h" #include "core/loader/elf.h" #include "core/loader/symbols_resolver.h" -#define W(foo) foo - #define LIB_FUNCTION(nid, lib, libversion, mod, moduleVersionMajor, moduleVersionMinor, function) \ { \ Core::Loader::SymbolResolver sr{}; \ @@ -25,7 +20,7 @@ sym->AddSymbol(sr, func); \ } -#define LIB_OBJ(nid, lib, libversion, mod, moduleVersionMajor, moduleVersionMinor, function) \ +#define LIB_OBJ(nid, lib, libversion, mod, moduleVersionMajor, moduleVersionMinor, obj) \ { \ Core::Loader::SymbolResolver sr{}; \ sr.name = nid; \ @@ -35,8 +30,7 @@ sr.module_version_major = moduleVersionMajor; \ sr.module_version_minor = moduleVersionMinor; \ sr.type = Core::Loader::SymbolType::Object; \ - auto func = reinterpret_cast(function); \ - sym->AddSymbol(sr, func); \ + sym->AddSymbol(sr, reinterpret_cast(obj)); \ } namespace Libraries { diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 0f86376af..eced87968 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -127,7 +127,7 @@ void Linker::Execute(const std::vector args) { } } params.entry_addr = module->GetEntryAddress(); - RunMainEntry(¶ms); + ExecuteGuest(RunMainEntry, ¶ms); }); } @@ -366,7 +366,8 @@ void* Linker::TlsGetAddr(u64 module_index, u64 offset) { if (!addr) { // Module was just loaded by above code. Allocate TLS block for it. const u32 init_image_size = module->tls.init_image_size; - u8* dest = reinterpret_cast(heap_api->heap_malloc(module->tls.image_size)); + u8* dest = reinterpret_cast( + Core::ExecuteGuest(heap_api->heap_malloc, module->tls.image_size)); const u8* src = reinterpret_cast(module->tls.image_virtual_addr); std::memcpy(dest, src, init_image_size); std::memset(dest + init_image_size, 0, module->tls.image_size - init_image_size); From c0562a6b1bb1bfb13ee80c061058682fca02edd6 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 11 May 2025 14:23:49 -0700 Subject: [PATCH 006/141] qt: Delay physical device enumeration to settings open. (#2908) --- src/qt_gui/main_window.cpp | 19 ++----------------- src/qt_gui/main_window.h | 3 --- src/qt_gui/settings_dialog.cpp | 23 ++++++++++++++++++++--- src/qt_gui/settings_dialog.h | 3 +-- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 36037fd4c..8eeec3536 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -24,7 +24,6 @@ #include "main_window.h" #include "settings_dialog.h" -#include "video_core/renderer_vulkan/vk_instance.h" #ifdef ENABLE_DISCORD_RPC #include "common/discord_rpc_handler.h" #endif @@ -53,7 +52,6 @@ bool MainWindow::Init() { CreateConnects(); SetLastUsedTheme(); SetLastIconSizeBullet(); - GetPhysicalDevices(); // show ui setMinimumSize(720, 405); std::string window_title = ""; @@ -368,19 +366,6 @@ void MainWindow::CheckUpdateMain(bool checkSave) { } #endif -void MainWindow::GetPhysicalDevices() { - Vulkan::Instance instance(false, false); - auto physical_devices = instance.GetPhysicalDevices(); - for (const vk::PhysicalDevice physical_device : physical_devices) { - auto prop = physical_device.getProperties(); - QString name = QString::fromUtf8(prop.deviceName, -1); - if (prop.apiVersion < Vulkan::TargetVulkanApiVersion) { - name += tr(" * Unsupported Vulkan Version"); - } - m_physical_devices.push_back(name); - } -} - void MainWindow::CreateConnects() { connect(this, &MainWindow::WindowResized, this, &MainWindow::HandleResize); connect(ui->mw_searchbar, &QLineEdit::textChanged, this, &MainWindow::SearchGameTable); @@ -421,7 +406,7 @@ void MainWindow::CreateConnects() { &MainWindow::StartGame); connect(ui->configureAct, &QAction::triggered, this, [this]() { - auto settingsDialog = new SettingsDialog(m_physical_devices, m_compat_info, this); + auto settingsDialog = new SettingsDialog(m_compat_info, this); connect(settingsDialog, &SettingsDialog::LanguageChanged, this, &MainWindow::OnLanguageChanged); @@ -454,7 +439,7 @@ void MainWindow::CreateConnects() { }); connect(ui->settingsButton, &QPushButton::clicked, this, [this]() { - auto settingsDialog = new SettingsDialog(m_physical_devices, m_compat_info, this); + auto settingsDialog = new SettingsDialog(m_compat_info, this); connect(settingsDialog, &SettingsDialog::LanguageChanged, this, &MainWindow::OnLanguageChanged); diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index 5d05bfca4..a5ec08d36 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -60,7 +60,6 @@ private: void toggleFullscreen(); void CreateRecentGameActions(); void CreateDockWindows(); - void GetPhysicalDevices(); void LoadGameLists(); #ifdef ENABLE_UPDATER @@ -96,8 +95,6 @@ private: QScopedPointer m_elf_viewer; // Status Bar. QScopedPointer statusBar; - // Available GPU devices - std::vector m_physical_devices; PSF psf; diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 5ee802b0c..914cc5470 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -25,6 +26,7 @@ #include "common/logging/filter.h" #include "settings_dialog.h" #include "ui_settings_dialog.h" +#include "video_core/renderer_vulkan/vk_instance.h" QStringList languageNames = {"Arabic", "Czech", "Danish", @@ -67,8 +69,9 @@ QMap chooseHomeTabMap; int backgroundImageOpacitySlider_backup; int bgm_volume_backup; -SettingsDialog::SettingsDialog(std::span physical_devices, - std::shared_ptr m_compat_info, +static std::vector m_physical_devices; + +SettingsDialog::SettingsDialog(std::shared_ptr m_compat_info, QWidget* parent) : QDialog(parent), ui(new Ui::SettingsDialog) { ui->setupUi(this); @@ -89,9 +92,23 @@ SettingsDialog::SettingsDialog(std::span physical_devices, {tr("Input"), "Input"}, {tr("Paths"), "Paths"}, {tr("Debug"), "Debug"}}; + if (m_physical_devices.empty()) { + // Populate cache of physical devices. + Vulkan::Instance instance(false, false); + auto physical_devices = instance.GetPhysicalDevices(); + for (const vk::PhysicalDevice physical_device : physical_devices) { + auto prop = physical_device.getProperties(); + QString name = QString::fromUtf8(prop.deviceName, -1); + if (prop.apiVersion < Vulkan::TargetVulkanApiVersion) { + name += tr(" * Unsupported Vulkan Version"); + } + m_physical_devices.push_back(name); + } + } + // Add list of available GPUs ui->graphicsAdapterBox->addItem(tr("Auto Select")); // -1, auto selection - for (const auto& device : physical_devices) { + for (const auto& device : m_physical_devices) { ui->graphicsAdapterBox->addItem(device); } diff --git a/src/qt_gui/settings_dialog.h b/src/qt_gui/settings_dialog.h index 09aa2b855..cdf9be80e 100644 --- a/src/qt_gui/settings_dialog.h +++ b/src/qt_gui/settings_dialog.h @@ -20,8 +20,7 @@ class SettingsDialog; class SettingsDialog : public QDialog { Q_OBJECT public: - explicit SettingsDialog(std::span physical_devices, - std::shared_ptr m_compat_info, + explicit SettingsDialog(std::shared_ptr m_compat_info, QWidget* parent = nullptr); ~SettingsDialog(); From 6206986914314ebb9318ded5c2c4d9d6bd0fe9c3 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 11 May 2025 21:51:03 -0500 Subject: [PATCH 007/141] libkernel: Implement sceKernelMemoryPoolBatch (#2909) * Implement sceKernelMemoryPoolBatch I've tested Commit and Decommit on real hardware, haven't tested Protect or TypeProtect yet. Implementation is primarily based on our sceKernelBatchMap implementation. * Clang --- src/core/libraries/kernel/memory.cpp | 54 ++++++++++++++++++++++++++++ src/core/libraries/kernel/memory.h | 44 +++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 9fcaa2439..dd0e07302 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -481,6 +481,59 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) return memory->PoolDecommit(pool_addr, len); } +s32 PS4_SYSV_ABI sceKernelMemoryPoolBatch(const OrbisKernelMemoryPoolBatchEntry* entries, s32 count, + s32* num_processed, s32 flags) { + if (entries == nullptr) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + s32 result = ORBIS_OK; + s32 processed = 0; + + for (s32 i = 0; i < count; i++, processed++) { + OrbisKernelMemoryPoolBatchEntry entry = entries[i]; + switch (entry.opcode) { + case OrbisKernelMemoryPoolOpcode::Commit: { + result = sceKernelMemoryPoolCommit(entry.commit_params.addr, entry.commit_params.len, + entry.commit_params.type, entry.commit_params.prot, + entry.flags); + break; + } + case OrbisKernelMemoryPoolOpcode::Decommit: { + result = sceKernelMemoryPoolDecommit(entry.decommit_params.addr, + entry.decommit_params.len, entry.flags); + break; + } + case OrbisKernelMemoryPoolOpcode::Protect: { + result = sceKernelMProtect(entry.protect_params.addr, entry.protect_params.len, + entry.protect_params.prot); + break; + } + case OrbisKernelMemoryPoolOpcode::TypeProtect: { + result = sceKernelMTypeProtect( + entry.type_protect_params.addr, entry.type_protect_params.len, + entry.type_protect_params.type, entry.type_protect_params.prot); + break; + } + case OrbisKernelMemoryPoolOpcode::Move: { + UNREACHABLE_MSG("Unimplemented sceKernelMemoryPoolBatch opcode Move"); + } + default: { + result = ORBIS_KERNEL_ERROR_EINVAL; + break; + } + } + + if (result != ORBIS_OK) { + break; + } + } + + if (num_processed != nullptr) { + *num_processed = processed; + } + return result; +} + int PS4_SYSV_ABI sceKernelMmap(void* addr, u64 len, int prot, int flags, int fd, size_t offset, void** res) { LOG_INFO(Kernel_Vmm, "called addr = {}, len = {}, prot = {}, flags = {}, fd = {}, offset = {}", @@ -612,6 +665,7 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("pU-QydtGcGY", "libkernel", 1, "libkernel", 1, 1, sceKernelMemoryPoolReserve); LIB_FUNCTION("Vzl66WmfLvk", "libkernel", 1, "libkernel", 1, 1, sceKernelMemoryPoolCommit); LIB_FUNCTION("LXo1tpFqJGs", "libkernel", 1, "libkernel", 1, 1, sceKernelMemoryPoolDecommit); + LIB_FUNCTION("YN878uKRBbE", "libkernel", 1, "libkernel", 1, 1, sceKernelMemoryPoolBatch); LIB_FUNCTION("BPE9s9vQQXo", "libkernel", 1, "libkernel", 1, 1, posix_mmap); LIB_FUNCTION("BPE9s9vQQXo", "libScePosix", 1, "libkernel", 1, 1, posix_mmap); diff --git a/src/core/libraries/kernel/memory.h b/src/core/libraries/kernel/memory.h index 2ca7f2931..3e2bf8de5 100644 --- a/src/core/libraries/kernel/memory.h +++ b/src/core/libraries/kernel/memory.h @@ -81,6 +81,48 @@ struct OrbisKernelBatchMapEntry { int operation; }; +enum class OrbisKernelMemoryPoolOpcode : u32 { + Commit = 1, + Decommit = 2, + Protect = 3, + TypeProtect = 4, + Move = 5, +}; + +struct OrbisKernelMemoryPoolBatchEntry { + OrbisKernelMemoryPoolOpcode opcode; + u32 flags; + union { + struct { + void* addr; + u64 len; + u8 prot; + u8 type; + } commit_params; + struct { + void* addr; + u64 len; + } decommit_params; + struct { + void* addr; + u64 len; + u8 prot; + } protect_params; + struct { + void* addr; + u64 len; + u8 prot; + u8 type; + } type_protect_params; + struct { + void* dest_addr; + void* src_addr; + u64 len; + } move_params; + uintptr_t padding[3]; + }; +}; + u64 PS4_SYSV_ABI sceKernelGetDirectMemorySize(); int PS4_SYSV_ABI sceKernelAllocateDirectMemory(s64 searchStart, s64 searchEnd, u64 len, u64 alignment, int memoryType, s64* physAddrOut); @@ -130,6 +172,8 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addrIn, size_t len, size_t ali void** addrOut); s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags); s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags); +s32 PS4_SYSV_ABI sceKernelMemoryPoolBatch(const OrbisKernelMemoryPoolBatchEntry* entries, s32 count, + s32* num_processed, s32 flags); int PS4_SYSV_ABI sceKernelMunmap(void* addr, size_t len); From 02d3ed4973960e8a4a2ac454dc3e46d5d804a743 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 11 May 2025 20:27:43 -0700 Subject: [PATCH 008/141] liverpool: Log more information on SetQueueReg. (#2912) --- src/video_core/amdgpu/liverpool.cpp | 5 +++++ src/video_core/amdgpu/pm4_cmds.h | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 4c8e3367a..598288085 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -864,6 +864,11 @@ Liverpool::Task Liverpool::ProcessCompute(const u32* acb, u32 acb_dwords, u32 vq } break; } + case PM4ItOpcode::SetQueueReg: { + const auto* set_data = reinterpret_cast(header); + UNREACHABLE_MSG("Encountered compute SetQueueReg: vqid = {}, reg_offset = {:#x}", + set_data->vqid.Value(), set_data->reg_offset.Value()); + } case PM4ItOpcode::DispatchDirect: { const auto* dispatch_direct = reinterpret_cast(header); auto& cs_program = GetCsRegs(); diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 6b55f5b65..cd175f6c9 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -211,6 +211,21 @@ struct PM4CmdSetData { } }; +struct PM4CmdSetQueueReg { + PM4Type3Header header; + union { + u32 raw; + BitField<0, 8, u32> reg_offset; ///< Offset in DWords from the register base address + BitField<15, 1, u32> defer_exec; ///< Defer execution + BitField<16, 10, u32> vqid; ///< Queue ID + }; + u32 data[0]; + + [[nodiscard]] u32 Size() const { + return header.count << 2u; + } +}; + struct PM4CmdNop { PM4Type3Header header; u32 data_block[0]; From 678f18ddb95773186877504b1b668e2a3c8d8785 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 11 May 2025 20:27:54 -0700 Subject: [PATCH 009/141] core: Introduce host call wrapper. (#2913) --- src/core/libraries/kernel/threads.h | 2 +- src/core/libraries/libs.h | 3 ++- src/core/tls.h | 12 ++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/threads.h b/src/core/libraries/kernel/threads.h index 409136968..7b75d54bf 100644 --- a/src/core/libraries/kernel/threads.h +++ b/src/core/libraries/kernel/threads.h @@ -35,7 +35,7 @@ public: this->func = std::move(func); PthreadAttrT attr{}; posix_pthread_attr_init(&attr); - posix_pthread_create(&thread, &attr, RunWrapper, this); + posix_pthread_create(&thread, &attr, HOST_CALL(RunWrapper), this); posix_pthread_attr_destroy(&attr); } diff --git a/src/core/libraries/libs.h b/src/core/libraries/libs.h index 7e073db8e..d9c8216a5 100644 --- a/src/core/libraries/libs.h +++ b/src/core/libraries/libs.h @@ -5,6 +5,7 @@ #include "core/loader/elf.h" #include "core/loader/symbols_resolver.h" +#include "core/tls.h" #define LIB_FUNCTION(nid, lib, libversion, mod, moduleVersionMajor, moduleVersionMinor, function) \ { \ @@ -16,7 +17,7 @@ sr.module_version_major = moduleVersionMajor; \ sr.module_version_minor = moduleVersionMinor; \ sr.type = Core::Loader::SymbolType::Function; \ - auto func = reinterpret_cast(function); \ + auto func = reinterpret_cast(HOST_CALL(function)); \ sym->AddSymbol(sr, func); \ } diff --git a/src/core/tls.h b/src/core/tls.h index 46ca8153b..d1d490465 100644 --- a/src/core/tls.h +++ b/src/core/tls.h @@ -58,4 +58,16 @@ ReturnType ExecuteGuest(PS4_SYSV_ABI ReturnType (*func)(FuncArgs...), CallArgs&& return func(std::forward(args)...); } +template +struct HostCallWrapperImpl; + +template +struct HostCallWrapperImpl { + static ReturnType PS4_SYSV_ABI wrap(Args... args) { + return func(args...); + } +}; + +#define HOST_CALL(func) (Core::HostCallWrapperImpl::wrap) + } // namespace Core From 8909d9bb891a8167e33f7de0aca5eae649b4ec4d Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 10:46:40 -0700 Subject: [PATCH 010/141] shader_recompiler: Always mark buffers as storage buffers. (#2914) --- src/shader_recompiler/info.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index 12e48c8e4..ba28d7e43 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -62,7 +62,14 @@ struct BufferResource { } bool IsStorage(const AmdGpu::Buffer& buffer, const Profile& profile) const noexcept { - return buffer.GetSize() > profile.max_ubo_size || is_written; + // When using uniform buffers, a size is required at compilation time, so we need to + // either compile a lot of shader specializations to handle each size or just force it to + // the maximum possible size always. However, for some vendors the shader-supplied size is + // used for bounds checking uniform buffer accesses, so the latter would effectively turn + // off buffer robustness behavior. Instead, force storage buffers which are bounds checked + // using the actual buffer size. We are assuming the performance hit from this is + // acceptable. + return true; // buffer.GetSize() > profile.max_ubo_size || is_written; } [[nodiscard]] constexpr AmdGpu::Buffer GetSharp(const Info& info) const noexcept; From f94c7e52b7d687e89c06a7dd2fe159a7070d4492 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 10:46:53 -0700 Subject: [PATCH 011/141] kernel: Implement scePthreadGetaffinity (#2916) --- src/core/libraries/kernel/threads.h | 6 ++++ src/core/libraries/kernel/threads/pthread.cpp | 30 ++++++++++++++++--- .../libraries/kernel/threads/pthread_attr.cpp | 6 ++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/core/libraries/kernel/threads.h b/src/core/libraries/kernel/threads.h index 7b75d54bf..bcccf1695 100644 --- a/src/core/libraries/kernel/threads.h +++ b/src/core/libraries/kernel/threads.h @@ -17,6 +17,12 @@ int PS4_SYSV_ABI posix_pthread_attr_init(PthreadAttrT* attr); int PS4_SYSV_ABI posix_pthread_attr_destroy(PthreadAttrT* attr); +int PS4_SYSV_ABI posix_pthread_attr_getaffinity_np(const PthreadAttrT* pattr, size_t cpusetsize, + Cpuset* cpusetp); + +int PS4_SYSV_ABI posix_pthread_attr_setaffinity_np(PthreadAttrT* pattr, size_t cpusetsize, + const Cpuset* cpusetp); + int PS4_SYSV_ABI posix_pthread_create(PthreadT* thread, const PthreadAttrT* attr, PthreadEntryFunc start_routine, void* arg); diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index a51f1f6e8..61310bfb5 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -6,6 +6,7 @@ #include "core/debug_state.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/posix_error.h" +#include "core/libraries/kernel/threads.h" #include "core/libraries/kernel/threads/pthread.h" #include "core/libraries/kernel/threads/thread_state.h" #include "core/libraries/libs.h" @@ -535,8 +536,6 @@ int Pthread::SetAffinity(const Cpuset* cpuset) { return POSIX_EINVAL; } - u64 mask = cpuset->bits; - uintptr_t handle = native_thr.GetHandle(); if (handle == 0) { return POSIX_ESRCH; @@ -545,6 +544,7 @@ int Pthread::SetAffinity(const Cpuset* cpuset) { // We don't use this currently because some games gets performance problems // when applying affinity even on strong hardware /* + u64 mask = cpuset->bits; #ifdef _WIN64 DWORD_PTR affinity_mask = static_cast(mask); if (!SetThreadAffinityMask(reinterpret_cast(handle), affinity_mask)) { @@ -572,13 +572,33 @@ int Pthread::SetAffinity(const Cpuset* cpuset) { return 0; } +int PS4_SYSV_ABI posix_pthread_getaffinity_np(PthreadT thread, size_t cpusetsize, Cpuset* cpusetp) { + if (thread == nullptr || cpusetp == nullptr) { + return POSIX_EINVAL; + } + auto* attr_ptr = &thread->attr; + return posix_pthread_attr_getaffinity_np(&attr_ptr, cpusetsize, cpusetp); +} + int PS4_SYSV_ABI posix_pthread_setaffinity_np(PthreadT thread, size_t cpusetsize, const Cpuset* cpusetp) { if (thread == nullptr || cpusetp == nullptr) { return POSIX_EINVAL; } - thread->attr.cpusetsize = cpusetsize; - return thread->SetAffinity(cpusetp); + auto* attr_ptr = &thread->attr; + if (const auto ret = posix_pthread_attr_setaffinity_np(&attr_ptr, cpusetsize, cpusetp)) { + return ret; + } + return thread->SetAffinity(thread->attr.cpuset); +} + +int PS4_SYSV_ABI scePthreadGetaffinity(PthreadT thread, u64* mask) { + Cpuset cpuset; + const int ret = posix_pthread_getaffinity_np(thread, sizeof(Cpuset), &cpuset); + if (ret == 0) { + *mask = cpuset.bits; + } + return ret; } int PS4_SYSV_ABI scePthreadSetaffinity(PthreadT thread, const u64 mask) { @@ -609,6 +629,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("Z4QosVuAsA0", "libkernel", 1, "libkernel", 1, 1, posix_pthread_once); LIB_FUNCTION("EotR8a3ASf4", "libkernel", 1, "libkernel", 1, 1, posix_pthread_self); LIB_FUNCTION("OxhIB8LB-PQ", "libkernel", 1, "libkernel", 1, 1, posix_pthread_create); + LIB_FUNCTION("Jb2uGFMr688", "libkernel", 1, "libkernel", 1, 1, posix_pthread_getaffinity_np); LIB_FUNCTION("5KWrg7-ZqvE", "libkernel", 1, "libkernel", 1, 1, posix_pthread_setaffinity_np); // Orbis @@ -632,6 +653,7 @@ void RegisterThread(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("W0Hpm2X0uPE", "libkernel", 1, "libkernel", 1, 1, ORBIS(posix_pthread_setprio)); LIB_FUNCTION("rNhWz+lvOMU", "libkernel", 1, "libkernel", 1, 1, _sceKernelSetThreadDtors); LIB_FUNCTION("6XG4B33N09g", "libkernel", 1, "libkernel", 1, 1, sched_yield); + LIB_FUNCTION("rcrVFJsQWRY", "libkernel", 1, "libkernel", 1, 1, ORBIS(scePthreadGetaffinity)); LIB_FUNCTION("bt3CTBKmGyI", "libkernel", 1, "libkernel", 1, 1, ORBIS(scePthreadSetaffinity)); } diff --git a/src/core/libraries/kernel/threads/pthread_attr.cpp b/src/core/libraries/kernel/threads/pthread_attr.cpp index 02a8cb1c7..71f6438a6 100644 --- a/src/core/libraries/kernel/threads/pthread_attr.cpp +++ b/src/core/libraries/kernel/threads/pthread_attr.cpp @@ -268,13 +268,13 @@ int PS4_SYSV_ABI posix_pthread_attr_setaffinity_np(PthreadAttrT* pattr, size_t c attr->cpuset = static_cast(calloc(1, sizeof(Cpuset))); attr->cpusetsize = sizeof(Cpuset); } - memcpy(attr->cpuset, cpusetp, cpusetsize); + memcpy(attr->cpuset, cpusetp, std::min(cpusetsize, sizeof(Cpuset))); return 0; } -int PS4_SYSV_ABI scePthreadAttrGetaffinity(PthreadAttrT* param_1, u64* mask) { +int PS4_SYSV_ABI scePthreadAttrGetaffinity(PthreadAttrT* attr, u64* mask) { Cpuset cpuset; - const int ret = posix_pthread_attr_getaffinity_np(param_1, sizeof(Cpuset), &cpuset); + const int ret = posix_pthread_attr_getaffinity_np(attr, sizeof(Cpuset), &cpuset); if (ret == 0) { *mask = cpuset.bits; } From b3abb83fc5f7888fab4191447fcf97f0a9adac3d Mon Sep 17 00:00:00 2001 From: ringolol <57327672+ringolol@users.noreply.github.com> Date: Mon, 12 May 2025 22:39:50 +0300 Subject: [PATCH 012/141] fix rough mouse movement due to incorrect check (#2911) Co-authored-by: rnglol --- src/input/input_mouse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/input_mouse.cpp b/src/input/input_mouse.cpp index c84d14b3f..5eb0aab3e 100644 --- a/src/input/input_mouse.cpp +++ b/src/input/input_mouse.cpp @@ -60,7 +60,7 @@ Uint32 MousePolling(void* param, Uint32 id, Uint32 interval) { float angle = atan2(d_y, d_x); float a_x = cos(angle) * output_speed, a_y = sin(angle) * output_speed; - if (d_x != 0 && d_y != 0) { + if (d_x != 0 || d_y != 0) { controller->Axis(0, axis_x, GetAxis(-0x80, 0x7f, a_x)); controller->Axis(0, axis_y, GetAxis(-0x80, 0x7f, a_y)); } else { From 5ab5fa70245d65d32a0e836055b3dba9f042f119 Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Mon, 12 May 2025 18:32:26 -0300 Subject: [PATCH 013/141] hotfix: replace memset declaration by cstring include fixes the Arch package build (https://aur.archlinux.org/packages/shadps4-git#comment-1023984) --- src/core/tls.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/tls.h b/src/core/tls.h index d1d490465..e9e2b9e6a 100644 --- a/src/core/tls.h +++ b/src/core/tls.h @@ -3,10 +3,9 @@ #pragma once +#include #include "common/types.h" -void* memset(void* ptr, int value, size_t num); - namespace Xbyak { class CodeGenerator; } From 9baa58dd9278baefb09645e39e9f205d94ec2131 Mon Sep 17 00:00:00 2001 From: Randomuser8219 <168323856+Randomuser8219@users.noreply.github.com> Date: Mon, 12 May 2025 15:26:55 -0700 Subject: [PATCH 014/141] renderer_vulkan: Properly enable dualSrcBlend feature. (#2921) * renderer_vulkan: Properly enable dualSrcBlend feature * whoops --- src/video_core/renderer_vulkan/vk_instance.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 1004d850f..f6625fbef 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -337,6 +337,7 @@ bool Instance::CreateDevice() { .independentBlend = features.independentBlend, .geometryShader = features.geometryShader, .tessellationShader = features.tessellationShader, + .dualSrcBlend = features.dualSrcBlend, .logicOp = features.logicOp, .multiDrawIndirect = features.multiDrawIndirect, .depthBiasClamp = features.depthBiasClamp, From b23f6fdc1d039906abc229c1997cc320e25913b7 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 15:27:47 -0700 Subject: [PATCH 015/141] buffer_cache: Split updateBuffer calls into 65536 byte chunks. (#2915) --- src/video_core/buffer_cache/buffer_cache.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index fb9fd755e..c993ef3e5 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -238,7 +238,15 @@ void BufferCache::InlineData(VAddr address, const void* value, u32 num_bytes, bo .bufferMemoryBarrierCount = 1, .pBufferMemoryBarriers = &pre_barrier, }); - cmdbuf.updateBuffer(buffer->Handle(), buffer->Offset(address), num_bytes, value); + // vkCmdUpdateBuffer can only copy up to 65536 bytes at a time. + static constexpr u32 UpdateBufferMaxSize = 65536; + const auto dst_offset = buffer->Offset(address); + for (u32 offset = 0; offset < num_bytes; offset += UpdateBufferMaxSize) { + const auto* update_src = static_cast(value) + offset; + const auto update_dst = dst_offset + offset; + const auto update_size = std::min(num_bytes - offset, UpdateBufferMaxSize); + cmdbuf.updateBuffer(buffer->Handle(), update_dst, update_size, update_src); + } cmdbuf.pipelineBarrier2(vk::DependencyInfo{ .dependencyFlags = vk::DependencyFlagBits::eByRegion, .bufferMemoryBarrierCount = 1, From 3a3a6d8e450557efa2ed4151db02c6c62cc8af2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 01:55:19 +0200 Subject: [PATCH 016/141] Mprotect only over whole pages (#2918) * Mprotect only over whole pages * Fix aligned_size error and clang-format. --------- Co-authored-by: squidbus <175574877+squidbus@users.noreply.github.com> --- src/core/memory.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 8fef8d102..6438670d3 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -649,19 +649,23 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s s32 MemoryManager::Protect(VAddr addr, size_t size, MemoryProt prot) { std::scoped_lock lk{mutex}; s64 protected_bytes = 0; + + auto aligned_addr = Common::AlignDown(addr, 16_KB); + auto aligned_size = Common::AlignUp(size + addr - aligned_addr, 16_KB); do { - auto it = FindVMA(addr + protected_bytes); + auto it = FindVMA(aligned_addr + protected_bytes); auto& vma_base = it->second; ASSERT_MSG(vma_base.Contains(addr + protected_bytes, 0), "Address {:#x} is out of bounds", addr + protected_bytes); auto result = 0; - result = ProtectBytes(addr + protected_bytes, vma_base, size - protected_bytes, prot); + result = ProtectBytes(aligned_addr + protected_bytes, vma_base, + aligned_size - protected_bytes, prot); if (result < 0) { // ProtectBytes returned an error, return it return result; } protected_bytes += result; - } while (protected_bytes < size); + } while (protected_bytes < aligned_size); return ORBIS_OK; } From 6bbb424c286559298bb5a84d8a97229720c41b49 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 18:03:55 -0700 Subject: [PATCH 017/141] vk_instance: Enable robustImageAccess2 (#2922) --- src/video_core/renderer_vulkan/vk_instance.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index f6625fbef..e31b95844 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -261,6 +261,8 @@ bool Instance::CreateDevice() { robustness2_features = feature_chain.get(); LOG_INFO(Render_Vulkan, "- robustBufferAccess2: {}", robustness2_features.robustBufferAccess2); + LOG_INFO(Render_Vulkan, "- robustImageAccess2: {}", + robustness2_features.robustImageAccess2); LOG_INFO(Render_Vulkan, "- nullDescriptor: {}", robustness2_features.nullDescriptor); } custom_border_color = add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME); @@ -395,6 +397,7 @@ bool Instance::CreateDevice() { }, vk::PhysicalDeviceRobustness2FeaturesEXT{ .robustBufferAccess2 = robustness2_features.robustBufferAccess2, + .robustImageAccess2 = robustness2_features.robustImageAccess2, .nullDescriptor = robustness2_features.nullDescriptor, }, vk::PhysicalDeviceVertexInputDynamicStateFeaturesEXT{ From 2a3a701115f583ad6af1ca540536c5daaf8fb2eb Mon Sep 17 00:00:00 2001 From: Roman <38257989+MrJohnDev@users.noreply.github.com> Date: Tue, 13 May 2025 12:10:33 +0700 Subject: [PATCH 018/141] kernel: macos/linux Implement sceKernelUuidCreate (#2923) * kernel: macos/linux Implement sceKernelUuidCreate * Fix clang-format * Fix Linux build * Fix Linux build (2) --------- Co-authored-by: squidbus <175574877+squidbus@users.noreply.github.com> --- CMakeLists.txt | 10 +++++++--- src/core/libraries/kernel/kernel.cpp | 14 +++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c182e0658..6780db417 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1094,9 +1094,13 @@ if (ENABLE_DISCORD_RPC) target_compile_definitions(shadps4 PRIVATE ENABLE_DISCORD_RPC) endif() -# Optional due to https://github.com/shadps4-emu/shadPS4/issues/1704 -if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND ENABLE_USERFAULTFD) - target_compile_definitions(shadps4 PRIVATE ENABLE_USERFAULTFD) +if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # Optional due to https://github.com/shadps4-emu/shadPS4/issues/1704 + if (ENABLE_USERFAULTFD) + target_compile_definitions(shadps4 PRIVATE ENABLE_USERFAULTFD) + endif() + + target_link_libraries(shadps4 PRIVATE uuid) endif() if (APPLE) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 959a8605a..6289af113 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -28,6 +28,8 @@ #ifdef _WIN64 #include +#else +#include #endif #include #include "aio.h" @@ -164,7 +166,17 @@ int PS4_SYSV_ABI sceKernelUuidCreate(OrbisKernelUuid* orbisUuid) { orbisUuid->node[i] = uuid.Data4[2 + i]; } #else - LOG_ERROR(Kernel, "sceKernelUuidCreate: Add linux"); + uuid_t uuid; + uuid_generate(uuid); + orbisUuid->timeLow = + ((u32)uuid[0] << 24) | ((u32)uuid[1] << 16) | ((u32)uuid[2] << 8) | (u32)uuid[3]; + orbisUuid->timeMid = ((u16)uuid[4] << 8) | uuid[5]; + orbisUuid->timeHiAndVersion = ((u16)uuid[6] << 8) | uuid[7]; + orbisUuid->clockSeqHiAndReserved = uuid[8]; + orbisUuid->clockSeqLow = uuid[9]; + for (int i = 0; i < 6; i++) { + orbisUuid->node[i] = uuid[10 + i]; + } #endif return 0; } From 3a10fda008fc4277a4f9e478512734a317df3de2 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 22:15:04 -0700 Subject: [PATCH 019/141] kernel: Simplify sceKernelUuidCreate --- src/core/libraries/kernel/kernel.cpp | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 6289af113..81b6dfe50 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -152,33 +152,23 @@ struct OrbisKernelUuid { u8 clockSeqLow; u8 node[6]; }; +static_assert(sizeof(OrbisKernelUuid) == 0x10); int PS4_SYSV_ABI sceKernelUuidCreate(OrbisKernelUuid* orbisUuid) { + if (!orbisUuid) { + return ORBIS_KERNEL_ERROR_EINVAL; + } #ifdef _WIN64 UUID uuid; - UuidCreate(&uuid); - orbisUuid->timeLow = uuid.Data1; - orbisUuid->timeMid = uuid.Data2; - orbisUuid->timeHiAndVersion = uuid.Data3; - orbisUuid->clockSeqHiAndReserved = uuid.Data4[0]; - orbisUuid->clockSeqLow = uuid.Data4[1]; - for (int i = 0; i < 6; i++) { - orbisUuid->node[i] = uuid.Data4[2 + i]; + if (UuidCreate(&uuid) != RPC_S_OK) { + return ORBIS_KERNEL_ERROR_EFAULT; } #else uuid_t uuid; uuid_generate(uuid); - orbisUuid->timeLow = - ((u32)uuid[0] << 24) | ((u32)uuid[1] << 16) | ((u32)uuid[2] << 8) | (u32)uuid[3]; - orbisUuid->timeMid = ((u16)uuid[4] << 8) | uuid[5]; - orbisUuid->timeHiAndVersion = ((u16)uuid[6] << 8) | uuid[7]; - orbisUuid->clockSeqHiAndReserved = uuid[8]; - orbisUuid->clockSeqLow = uuid[9]; - for (int i = 0; i < 6; i++) { - orbisUuid->node[i] = uuid[10 + i]; - } #endif - return 0; + std::memcpy(orbisUuid, &uuid, sizeof(OrbisKernelUuid)); + return ORBIS_OK; } int PS4_SYSV_ABI kernel_ioctl(int fd, u64 cmd, VA_ARGS) { From e6a144ddb05028110014bc9f2d6046f390131b5c Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 13 May 2025 08:54:38 +0300 Subject: [PATCH 020/141] [Libs] sceNet IV (#2867) * dummy returns in p2p sockets * added logging for sceNetSetsockopt * possible fix for ORBIS_NET_SO_LINGER set * logging for getsockoption as well * disable kernel getsockname (seems to create issues with cyberpunk) * some fixes with SetSocketOptions * arggg * posix_getsockname try * mutex protection * removed duplicated include (diegolix29) * posix_getsockname appears to have issues in cyberpunk , comment it for now --- src/core/libraries/kernel/kernel.cpp | 23 ++++++++++- src/core/libraries/network/net.cpp | 2 + src/core/libraries/network/p2p_sockets.cpp | 14 +++---- src/core/libraries/network/posix_sockets.cpp | 40 +++++++++++++++++++- src/core/libraries/network/sockets.h | 10 ++++- src/core/libraries/network/sys_net.cpp | 1 - 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 81b6dfe50..c7eafe799 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -32,6 +32,8 @@ #include #endif #include +#include +#include #include "aio.h" namespace Libraries::Kernel { @@ -207,6 +209,24 @@ int PS4_SYSV_ABI posix_getpagesize() { return 16_KB; } +int PS4_SYSV_ABI posix_getsockname(Libraries::Net::OrbisNetId s, + Libraries::Net::OrbisNetSockaddr* addr, u32* paddrlen) { + auto* netcall = Common::Singleton::Instance(); + auto sock = netcall->FindSocket(s); + if (!sock) { + *Libraries::Kernel::__Error() = ORBIS_NET_ERROR_EBADF; + LOG_ERROR(Lib_Net, "socket id is invalid = {}", s); + return -1; + } + int returncode = sock->GetSocketAddress(addr, paddrlen); + if (returncode >= 0) { + LOG_ERROR(Lib_Net, "return code : {:#x}", (u32)returncode); + return 0; + } + *Libraries::Kernel::__Error() = 0x20; + LOG_ERROR(Lib_Net, "error code returned : {:#x}", (u32)returncode); + return -1; +} void RegisterKernel(Core::Loader::SymbolsResolver* sym) { service_thread = std::jthread{KernelServiceThread}; @@ -244,8 +264,7 @@ void RegisterKernel(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("lUk6wrGXyMw", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_recvfrom); LIB_FUNCTION("fFxGkxF2bVo", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_setsockopt); - LIB_FUNCTION("RenI1lL1WFk", "libScePosix", 1, "libkernel", 1, 1, - Libraries::Net::sys_getsockname); + // LIB_FUNCTION("RenI1lL1WFk", "libScePosix", 1, "libkernel", 1, 1, posix_getsockname); LIB_FUNCTION("KuOmgKoqCdY", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_bind); LIB_FUNCTION("5jRCs2axtr4", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sceNetInetNtop); // TODO fix it to sys_ ... diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index 1f024277f..0ef4a84f5 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -886,6 +886,7 @@ int PS4_SYSV_ABI sceNetGetsockname(OrbisNetId s, OrbisNetSockaddr* addr, u32* pa } int PS4_SYSV_ABI sceNetGetsockopt(OrbisNetId s, int level, int optname, void* optval, u32* optlen) { + LOG_INFO(Lib_Net, "s={} level={} optname={}", s, level, optname); if (!g_isNetInitialized) { return ORBIS_NET_ERROR_ENOTINIT; } @@ -1449,6 +1450,7 @@ int PS4_SYSV_ABI sceNetSetDnsInfoToKernel() { int PS4_SYSV_ABI sceNetSetsockopt(OrbisNetId s, int level, int optname, const void* optval, u32 optlen) { + LOG_INFO(Lib_Net, "s={} level={} optname={} optlen={}", s, level, optname, optlen); if (!g_isNetInitialized) { return ORBIS_NET_ERROR_ENOTINIT; } diff --git a/src/core/libraries/network/p2p_sockets.cpp b/src/core/libraries/network/p2p_sockets.cpp index e9b710bb3..4f678dace 100644 --- a/src/core/libraries/network/p2p_sockets.cpp +++ b/src/core/libraries/network/p2p_sockets.cpp @@ -10,25 +10,25 @@ namespace Libraries::Net { int P2PSocket::Close() { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::SetSocketOptions(int level, int optname, const void* optval, u32 optlen) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::GetSocketOptions(int level, int optname, void* optval, u32* optlen) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::Bind(const OrbisNetSockaddr* addr, u32 addrlen) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::Listen(int backlog) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to, @@ -49,12 +49,12 @@ SocketPtr P2PSocket::Accept(OrbisNetSockaddr* addr, u32* addrlen) { int P2PSocket::Connect(const OrbisNetSockaddr* addr, u32 namelen) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } int P2PSocket::GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) { LOG_ERROR(Lib_Net, "(STUBBED) called"); - return -1; + return 0; } } // namespace Libraries::Net \ No newline at end of file diff --git a/src/core/libraries/network/posix_sockets.cpp b/src/core/libraries/network/posix_sockets.cpp index 140e4fd22..2df375262 100644 --- a/src/core/libraries/network/posix_sockets.cpp +++ b/src/core/libraries/network/posix_sockets.cpp @@ -143,6 +143,7 @@ static void convertPosixSockaddrToOrbis(sockaddr* src, OrbisNetSockaddr* dst) { } int PosixSocket::Close() { + std::scoped_lock lock{m_mutex}; #ifdef _WIN32 auto out = closesocket(sock); #else @@ -152,17 +153,20 @@ int PosixSocket::Close() { } int PosixSocket::Bind(const OrbisNetSockaddr* addr, u32 addrlen) { + std::scoped_lock lock{m_mutex}; sockaddr addr2; convertOrbisNetSockaddrToPosix(addr, &addr2); return ConvertReturnErrorCode(::bind(sock, &addr2, sizeof(sockaddr_in))); } int PosixSocket::Listen(int backlog) { + std::scoped_lock lock{m_mutex}; return ConvertReturnErrorCode(::listen(sock, backlog)); } int PosixSocket::SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to, u32 tolen) { + std::scoped_lock lock{m_mutex}; if (to != nullptr) { sockaddr addr; convertOrbisNetSockaddrToPosix(to, &addr); @@ -175,6 +179,7 @@ int PosixSocket::SendPacket(const void* msg, u32 len, int flags, const OrbisNetS int PosixSocket::ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) { + std::scoped_lock lock{m_mutex}; if (from != nullptr) { sockaddr addr; int res = recvfrom(sock, (char*)buf, len, flags, &addr, (socklen_t*)fromlen); @@ -187,6 +192,7 @@ int PosixSocket::ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* } SocketPtr PosixSocket::Accept(OrbisNetSockaddr* addr, u32* addrlen) { + std::scoped_lock lock{m_mutex}; sockaddr addr2; net_socket new_socket = ::accept(sock, &addr2, (socklen_t*)addrlen); #ifdef _WIN32 @@ -202,12 +208,14 @@ SocketPtr PosixSocket::Accept(OrbisNetSockaddr* addr, u32* addrlen) { } int PosixSocket::Connect(const OrbisNetSockaddr* addr, u32 namelen) { + std::scoped_lock lock{m_mutex}; sockaddr addr2; convertOrbisNetSockaddrToPosix(addr, &addr2); return ::connect(sock, &addr2, sizeof(sockaddr_in)); } int PosixSocket::GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) { + std::scoped_lock lock{m_mutex}; sockaddr addr; convertOrbisNetSockaddrToPosix(name, &addr); if (name != nullptr) { @@ -234,13 +242,15 @@ int PosixSocket::GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) { return 0 int PosixSocket::SetSocketOptions(int level, int optname, const void* optval, u32 optlen) { + std::scoped_lock lock{m_mutex}; level = ConvertLevels(level); + ::linger native_linger; if (level == SOL_SOCKET) { switch (optname) { CASE_SETSOCKOPT(SO_REUSEADDR); CASE_SETSOCKOPT(SO_KEEPALIVE); CASE_SETSOCKOPT(SO_BROADCAST); - CASE_SETSOCKOPT(SO_LINGER); + // CASE_SETSOCKOPT(SO_LINGER); CASE_SETSOCKOPT(SO_SNDBUF); CASE_SETSOCKOPT(SO_RCVBUF); CASE_SETSOCKOPT(SO_SNDTIMEO); @@ -251,6 +261,24 @@ int PosixSocket::SetSocketOptions(int level, int optname, const void* optval, u3 CASE_SETSOCKOPT_VALUE(ORBIS_NET_SO_ONESBCAST, &sockopt_so_onesbcast); CASE_SETSOCKOPT_VALUE(ORBIS_NET_SO_USECRYPTO, &sockopt_so_usecrypto); CASE_SETSOCKOPT_VALUE(ORBIS_NET_SO_USESIGNATURE, &sockopt_so_usesignature); + case ORBIS_NET_SO_LINGER: { + if (socket_type != ORBIS_NET_SOCK_STREAM) { + return ORBIS_NET_EPROCUNAVAIL; + } + if (optlen < sizeof(OrbisNetLinger)) { + LOG_ERROR(Lib_Net, "size missmatched! optlen = {} OrbisNetLinger={}", optlen, + sizeof(OrbisNetLinger)); + return ORBIS_NET_ERROR_EINVAL; + } + + const void* native_val = &native_linger; + u32 native_len = sizeof(native_linger); + native_linger.l_onoff = reinterpret_cast(optval)->l_onoff; + native_linger.l_linger = reinterpret_cast(optval)->l_linger; + return ConvertReturnErrorCode( + setsockopt(sock, level, SO_LINGER, (const char*)native_val, native_len)); + } + case ORBIS_NET_SO_NAME: return ORBIS_NET_ERROR_EINVAL; // don't support set for name case ORBIS_NET_SO_NBIO: { @@ -269,7 +297,7 @@ int PosixSocket::SetSocketOptions(int level, int optname, const void* optval, u3 } } else if (level == IPPROTO_IP) { switch (optname) { - CASE_SETSOCKOPT(IP_HDRINCL); + // CASE_SETSOCKOPT(IP_HDRINCL); CASE_SETSOCKOPT(IP_TOS); CASE_SETSOCKOPT(IP_TTL); CASE_SETSOCKOPT(IP_MULTICAST_IF); @@ -279,6 +307,13 @@ int PosixSocket::SetSocketOptions(int level, int optname, const void* optval, u3 CASE_SETSOCKOPT(IP_DROP_MEMBERSHIP); CASE_SETSOCKOPT_VALUE(ORBIS_NET_IP_TTLCHK, &sockopt_ip_ttlchk); CASE_SETSOCKOPT_VALUE(ORBIS_NET_IP_MAXTTL, &sockopt_ip_maxttl); + case ORBIS_NET_IP_HDRINCL: { + if (socket_type != ORBIS_NET_SOCK_RAW) { + return ORBIS_NET_EPROCUNAVAIL; + } + return ConvertReturnErrorCode( + setsockopt(sock, level, optname, (const char*)optval, optlen)); + } } } else if (level == IPPROTO_TCP) { switch (optname) { @@ -311,6 +346,7 @@ int PosixSocket::SetSocketOptions(int level, int optname, const void* optval, u3 return 0; int PosixSocket::GetSocketOptions(int level, int optname, void* optval, u32* optlen) { + std::scoped_lock lock{m_mutex}; level = ConvertLevels(level); if (level == SOL_SOCKET) { switch (optname) { diff --git a/src/core/libraries/network/sockets.h b/src/core/libraries/network/sockets.h index e41671d88..c54e11e66 100644 --- a/src/core/libraries/network/sockets.h +++ b/src/core/libraries/network/sockets.h @@ -32,6 +32,10 @@ struct Socket; typedef std::shared_ptr SocketPtr; +struct OrbisNetLinger { + s32 l_onoff; + s32 l_linger; +}; struct Socket { explicit Socket(int domain, int type, int protocol) {} virtual ~Socket() = default; @@ -47,6 +51,7 @@ struct Socket { u32* fromlen) = 0; virtual int Connect(const OrbisNetSockaddr* addr, u32 namelen) = 0; virtual int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) = 0; + std::mutex m_mutex; }; struct PosixSocket : public Socket { @@ -59,8 +64,11 @@ struct PosixSocket : public Socket { int sockopt_ip_ttlchk = 0; int sockopt_ip_maxttl = 0; int sockopt_tcp_mss_to_advertise = 0; + int socket_type; explicit PosixSocket(int domain, int type, int protocol) - : Socket(domain, type, protocol), sock(socket(domain, type, protocol)) {} + : Socket(domain, type, protocol), sock(socket(domain, type, protocol)) { + socket_type = type; + } explicit PosixSocket(net_socket sock) : Socket(0, 0, 0), sock(sock) {} int Close() override; int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override; diff --git a/src/core/libraries/network/sys_net.cpp b/src/core/libraries/network/sys_net.cpp index fbf2a2456..087632159 100644 --- a/src/core/libraries/network/sys_net.cpp +++ b/src/core/libraries/network/sys_net.cpp @@ -1,4 +1,3 @@ -#include "sys_net.h" // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later From 4cd13ea9d8a402d41b68a73b58fe839d91243c3e Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 12 May 2025 23:10:43 -0700 Subject: [PATCH 021/141] fix: Disable emitting bounds checks until fixed. --- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 0a0c81d4c..d7ad47a3c 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -205,7 +205,8 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .supports_image_load_store_lod = instance_.IsImageLoadStoreLodSupported(), .supports_native_cube_calc = instance_.IsAmdGcnShaderSupported(), .supports_trinary_minmax = instance_.IsAmdShaderTrinaryMinMaxSupported(), - .supports_robust_buffer_access = instance_.IsRobustBufferAccess2Supported(), + // TODO: Emitted bounds checks cause problems with phi control flow; needs to be fixed. + .supports_robust_buffer_access = true, // instance_.IsRobustBufferAccess2Supported(), .supports_image_fp32_atomic_min_max = instance_.IsShaderAtomicFloatImage32MinMaxSupported(), .needs_manual_interpolation = instance.IsFragmentShaderBarycentricSupported() && instance.GetDriverID() == vk::DriverId::eNvidiaProprietary, From f97c0deea966e200dc5d675dfaec90f3148a3865 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Tue, 13 May 2025 21:29:47 +0300 Subject: [PATCH 022/141] ngs2: removed possible nullptr value from logging (#2924) --- src/core/libraries/ngs2/ngs2.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/libraries/ngs2/ngs2.cpp b/src/core/libraries/ngs2/ngs2.cpp index 743be5fd6..9bb73536c 100644 --- a/src/core/libraries/ngs2/ngs2.cpp +++ b/src/core/libraries/ngs2/ngs2.cpp @@ -380,8 +380,7 @@ s32 PS4_SYSV_ABI sceNgs2GeomApply(const OrbisNgs2GeomListenerWork* listener, s32 PS4_SYSV_ABI sceNgs2PanInit(OrbisNgs2PanWork* work, const float* aSpeakerAngle, float unitAngle, u32 numSpeakers) { - LOG_ERROR(Lib_Ngs2, "aSpeakerAngle = {}, unitAngle = {}, numSpeakers = {}", *aSpeakerAngle, - unitAngle, numSpeakers); + LOG_ERROR(Lib_Ngs2, "unitAngle = {}, numSpeakers = {}", unitAngle, numSpeakers); return ORBIS_OK; } From 7334fb620bb760e998d10a3dce8d2c726440d906 Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Tue, 13 May 2025 21:16:53 +0200 Subject: [PATCH 023/141] sceKernelAddTimerEvent implementation (#2906) * implementation * add backend (WIP) * now should be good - fix implementation based on homebrew tests - demote log to debug - make squidbus happy (hopefully) * fix moved m_name * fix clang * replace existing event when its same id and filter * run timercallback after addEvent and remove useless code * move KernelSignalRequest to the end * clang (i hate you) --- src/core/libraries/kernel/equeue.cpp | 88 ++++++++++++++++++++++++++++ src/core/libraries/kernel/equeue.h | 2 + 2 files changed, 90 insertions(+) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index a4916208a..7e579614f 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -152,6 +152,16 @@ int EqueueInternal::WaitForSmallTimer(SceKernelEvent* ev, int num, u32 micros) { return count; } +bool EqueueInternal::EventExists(u64 id, s16 filter) { + std::scoped_lock lock{m_mutex}; + + const auto& it = std::ranges::find_if(m_events, [id, filter](auto& ev) { + return ev.event.ident == id && ev.event.filter == filter; + }); + + return it != m_events.cend(); +} + extern boost::asio::io_context io_context; extern void KernelSignalRequest(); @@ -300,6 +310,82 @@ int PS4_SYSV_ABI sceKernelDeleteHRTimerEvent(SceKernelEqueue eq, int id) { } } +static void TimerCallback(const boost::system::error_code& error, SceKernelEqueue eq, + SceKernelEvent kevent, SceKernelUseconds interval_ms) { + if (error) { + LOG_ERROR(Kernel_Event, "Timer callback error: {}", error.message()); + return; + } + + if (eq->EventExists(kevent.ident, kevent.filter)) { + eq->TriggerEvent(kevent.ident, SceKernelEvent::Filter::Timer, kevent.udata); + + if (!(kevent.flags & SceKernelEvent::Flags::OneShot)) { + auto timer = std::make_shared( + io_context, std::chrono::milliseconds(interval_ms)); + + timer->async_wait( + [eq, kevent, interval_ms, timer](const boost::system::error_code& ec) { + TimerCallback(ec, eq, kevent, interval_ms); + }); + } + } +} + +int PS4_SYSV_ABI sceKernelAddTimerEvent(SceKernelEqueue eq, int id, SceKernelUseconds usec, + void* udata) { + if (eq == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + const u64 interval_ms = static_cast(usec & 0xFFFFFFFF) / 1000; + + EqueueEvent event{}; + event.event.ident = static_cast(id); + event.event.filter = SceKernelEvent::Filter::Timer; + event.event.flags = SceKernelEvent::Flags::Add; + event.event.fflags = 0; + event.event.data = interval_ms; + event.event.udata = udata; + event.time_added = std::chrono::steady_clock::now(); + + if (eq->EventExists(event.event.ident, event.event.filter)) { + eq->RemoveEvent(id, SceKernelEvent::Filter::Timer); + LOG_DEBUG(Kernel_Event, + "Timer event already exists, removing it: queue name={}, queue id={}", + eq->GetName(), event.event.ident); + } + + LOG_DEBUG(Kernel_Event, + "Added timing event: queue name={}, queue id={}, ms-intevall={}, pointer={:x}", + eq->GetName(), event.event.ident, interval_ms, reinterpret_cast(udata)); + + auto timer = std::make_shared( + io_context, std::chrono::milliseconds(interval_ms)); + + if (!eq->AddEvent(event)) { + return ORBIS_KERNEL_ERROR_ENOMEM; + } + + timer->async_wait( + [eq, event_data = event.event, interval_ms, timer](const boost::system::error_code& ec) { + TimerCallback(ec, eq, event_data, interval_ms); + }); + + KernelSignalRequest(); + + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceKernelDeleteTimerEvent(SceKernelEqueue eq, int id) { + if (eq == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + return eq->RemoveEvent(id, SceKernelEvent::Filter::Timer) ? ORBIS_OK + : ORBIS_KERNEL_ERROR_ENOENT; +} + int PS4_SYSV_ABI sceKernelAddUserEvent(SceKernelEqueue eq, int id) { if (eq == nullptr) { return ORBIS_KERNEL_ERROR_EBADF; @@ -380,6 +466,8 @@ void RegisterEventQueue(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("WDszmSbWuDk", "libkernel", 1, "libkernel", 1, 1, sceKernelAddUserEventEdge); LIB_FUNCTION("R74tt43xP6k", "libkernel", 1, "libkernel", 1, 1, sceKernelAddHRTimerEvent); LIB_FUNCTION("J+LF6LwObXU", "libkernel", 1, "libkernel", 1, 1, sceKernelDeleteHRTimerEvent); + LIB_FUNCTION("57ZK+ODEXWY", "libkernel", 1, "libkernel", 1, 1, sceKernelAddTimerEvent); + LIB_FUNCTION("YWQFUyXIVdU", "libkernel", 1, "libkernel", 1, 1, sceKernelDeleteTimerEvent); LIB_FUNCTION("F6e0kwo4cnk", "libkernel", 1, "libkernel", 1, 1, sceKernelTriggerUserEvent); LIB_FUNCTION("LJDwdSNTnDg", "libkernel", 1, "libkernel", 1, 1, sceKernelDeleteUserEvent); LIB_FUNCTION("mJ7aghmgvfc", "libkernel", 1, "libkernel", 1, 1, sceKernelGetEventId); diff --git a/src/core/libraries/kernel/equeue.h b/src/core/libraries/kernel/equeue.h index 2bd7ef510..636496604 100644 --- a/src/core/libraries/kernel/equeue.h +++ b/src/core/libraries/kernel/equeue.h @@ -152,6 +152,8 @@ public: int WaitForSmallTimer(SceKernelEvent* ev, int num, u32 micros); + bool EventExists(u64 id, s16 filter); + private: std::string m_name; std::mutex m_mutex; From 484fbcc3209a2b3248f4f23a2354f5d5ad53eb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 22:19:56 +0200 Subject: [PATCH 024/141] Handle -1 as V_CMP_NE_U64 argument (#2919) --- .../frontend/translate/vector_alu.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 3ce86c131..6171cca07 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -989,13 +989,22 @@ void Translator::V_CMP_NE_U64(const GcnInst& inst) { } }; const IR::U1 src0{get_src(inst.src[0])}; - ASSERT(inst.src[1].field == OperandField::ConstZero); // src0 != 0 + auto op = [&inst, this](auto x) { + switch (inst.src[1].field) { + case OperandField::ConstZero: + return x; + case OperandField::SignedConstIntNeg: + return ir.LogicalNot(x); + default: + UNREACHABLE_MSG("unhandled V_CMP_NE_U64 source argument {}", u32(inst.src[1].field)); + } + }; switch (inst.dst[1].field) { case OperandField::VccLo: - ir.SetVcc(src0); + ir.SetVcc(op(src0)); break; case OperandField::ScalarGPR: - ir.SetThreadBitScalarReg(IR::ScalarReg(inst.dst[1].code), src0); + ir.SetThreadBitScalarReg(IR::ScalarReg(inst.dst[1].code), op(src0)); break; default: UNREACHABLE(); From 1832ec2ac2f9fe1a507a34fb2734cd6b6f3490a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 22:54:22 +0200 Subject: [PATCH 025/141] Implement sceKernelIsStack (#2917) --- src/core/libraries/kernel/memory.cpp | 8 ++++++++ src/core/libraries/kernel/memory.h | 1 + src/core/memory.cpp | 29 ++++++++++++++++++++++++++++ src/core/memory.h | 2 ++ 4 files changed, 40 insertions(+) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index dd0e07302..7af67d6d3 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -290,6 +290,13 @@ int PS4_SYSV_ABI sceKernelGetDirectMemoryType(u64 addr, int* directMemoryTypeOut directMemoryEndOut); } +int PS4_SYSV_ABI sceKernelIsStack(void* addr, void** start, void** end) { + LOG_DEBUG(Kernel_Vmm, "called, addr = {:#x}, start = {:#x}, end = {:#x}", fmt::ptr(addr), + fmt::ptr(start), fmt::ptr(end)); + auto* memory = Core::Memory::Instance(); + return memory->IsStack(std::bit_cast(addr), start, end); +} + s32 PS4_SYSV_ABI sceKernelBatchMap(OrbisKernelBatchMapEntry* entries, int numEntries, int* numEntriesOut) { return sceKernelBatchMap2(entries, numEntries, numEntriesOut, @@ -636,6 +643,7 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("7oxv3PPCumo", "libkernel", 1, "libkernel", 1, 1, sceKernelReserveVirtualRange); LIB_FUNCTION("BC+OG5m9+bw", "libkernel", 1, "libkernel", 1, 1, sceKernelGetDirectMemoryType); LIB_FUNCTION("pO96TwzOm5E", "libkernel", 1, "libkernel", 1, 1, sceKernelGetDirectMemorySize); + LIB_FUNCTION("yDBwVAolDgg", "libkernel", 1, "libkernel", 1, 1, sceKernelIsStack); LIB_FUNCTION("NcaWUxfMNIQ", "libkernel", 1, "libkernel", 1, 1, sceKernelMapNamedDirectMemory); LIB_FUNCTION("L-Q3LEjIbgA", "libkernel", 1, "libkernel", 1, 1, sceKernelMapDirectMemory); LIB_FUNCTION("WFcfL2lzido", "libkernel", 1, "libkernel", 1, 1, sceKernelQueryMemoryProtection); diff --git a/src/core/libraries/kernel/memory.h b/src/core/libraries/kernel/memory.h index 3e2bf8de5..92e158a00 100644 --- a/src/core/libraries/kernel/memory.h +++ b/src/core/libraries/kernel/memory.h @@ -158,6 +158,7 @@ void PS4_SYSV_ABI _sceKernelRtldSetApplicationHeapAPI(void* func[]); int PS4_SYSV_ABI sceKernelGetDirectMemoryType(u64 addr, int* directMemoryTypeOut, void** directMemoryStartOut, void** directMemoryEndOut); +int PS4_SYSV_ABI sceKernelIsStack(void* addr, void** start, void** end); s32 PS4_SYSV_ABI sceKernelBatchMap(OrbisKernelBatchMapEntry* entries, int numEntries, int* numEntriesOut); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 6438670d3..ec03d6c5e 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -949,4 +949,33 @@ int MemoryManager::GetDirectMemoryType(PAddr addr, int* directMemoryTypeOut, return ORBIS_OK; } +int MemoryManager::IsStack(VAddr addr, void** start, void** end) { + auto vma_handle = FindVMA(addr); + if (vma_handle == vma_map.end()) { + return ORBIS_KERNEL_ERROR_EINVAL; + } + + const VirtualMemoryArea& vma = vma_handle->second; + if (!vma.Contains(addr, 0) || vma.IsFree()) { + return ORBIS_KERNEL_ERROR_EACCES; + } + + auto stack_start = 0ul; + auto stack_end = 0ul; + if (vma.type == VMAType::Stack) { + stack_start = vma.base; + stack_end = vma.base + vma.size; + } + + if (start != nullptr) { + *start = reinterpret_cast(stack_start); + } + + if (end != nullptr) { + *end = reinterpret_cast(stack_end); + } + + return ORBIS_OK; +} + } // namespace Core diff --git a/src/core/memory.h b/src/core/memory.h index 4920aa397..4c143ff6f 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -223,6 +223,8 @@ public: void InvalidateMemory(VAddr addr, u64 size) const; + int IsStack(VAddr addr, void** start, void** end); + private: VMAHandle FindVMA(VAddr target) { return std::prev(vma_map.upper_bound(target)); From e5b675d607502bbe8d78204794a95c22ef2c50af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 22:56:20 +0200 Subject: [PATCH 026/141] Handle IT_WAIT_REG_MEM with Register argument (#2927) --- src/core/libraries/gnmdriver/gnmdriver.cpp | 2 +- src/video_core/amdgpu/liverpool.cpp | 6 ++--- src/video_core/amdgpu/pm4_cmds.h | 26 +++++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/core/libraries/gnmdriver/gnmdriver.cpp b/src/core/libraries/gnmdriver/gnmdriver.cpp index f2f40e0e3..9cf340050 100644 --- a/src/core/libraries/gnmdriver/gnmdriver.cpp +++ b/src/core/libraries/gnmdriver/gnmdriver.cpp @@ -179,7 +179,7 @@ s32 PS4_SYSV_ABI sceGnmComputeWaitOnAddress(u32* cmdbuf, u32 size, uintptr_t add auto* wait_reg_mem = reinterpret_cast(cmdbuf); wait_reg_mem->header = PM4Type3Header{PM4ItOpcode::WaitRegMem, 5}; wait_reg_mem->raw = (is_mem << 4u) | (cmp_func & 7u); - wait_reg_mem->poll_addr_lo = u32(addr & addr_mask); + wait_reg_mem->poll_addr_lo_raw = u32(addr & addr_mask); wait_reg_mem->poll_addr_hi = u32(addr >> 32u); wait_reg_mem->ref = ref; wait_reg_mem->mask = mask; diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 598288085..0fbfa8b9b 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -696,10 +696,10 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spanAddress(); if (vo_port->IsVoLabel(wait_addr) && num_submits == mapped_queues[GfxQueueId].submits.size()) { - vo_port->WaitVoLabel([&] { return wait_reg_mem->Test(); }); + vo_port->WaitVoLabel([&] { return wait_reg_mem->Test(regs.reg_array); }); break; } - while (!wait_reg_mem->Test()) { + while (!wait_reg_mem->Test(regs.reg_array)) { YIELD_GFX(); } break; @@ -934,7 +934,7 @@ Liverpool::Task Liverpool::ProcessCompute(const u32* acb, u32 acb_dwords, u32 vq case PM4ItOpcode::WaitRegMem: { const auto* wait_reg_mem = reinterpret_cast(header); ASSERT(wait_reg_mem->engine.Value() == PM4CmdWaitRegMem::Engine::Me); - while (!wait_reg_mem->Test()) { + while (!wait_reg_mem->Test(regs.reg_array)) { YIELD_ASC(vqid); } break; diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index cd175f6c9..066fa4b62 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -474,7 +474,12 @@ struct PM4CmdWaitRegMem { BitField<8, 1, Engine> engine; u32 raw; }; - u32 poll_addr_lo; + union { + BitField<0, 16, u32> reg; + BitField<2, 30, u32> poll_addr_lo; + BitField<0, 2, u32> swap; + u32 poll_addr_lo_raw; + }; u32 poll_addr_hi; u32 ref; u32 mask; @@ -485,28 +490,33 @@ struct PM4CmdWaitRegMem { return std::bit_cast((uintptr_t(poll_addr_hi) << 32) | poll_addr_lo); } - bool Test() const { + u32 Reg() const { + return reg.Value(); + } + + bool Test(const std::array& regs) const { + u32 value = mem_space.Value() == MemSpace::Memory ? *Address() : regs[Reg()]; switch (function.Value()) { case Function::Always: { return true; } case Function::LessThan: { - return (*Address() & mask) < ref; + return (value & mask) < ref; } case Function::LessThanEqual: { - return (*Address() & mask) <= ref; + return (value & mask) <= ref; } case Function::Equal: { - return (*Address() & mask) == ref; + return (value & mask) == ref; } case Function::NotEqual: { - return (*Address() & mask) != ref; + return (value & mask) != ref; } case Function::GreaterThanEqual: { - return (*Address() & mask) >= ref; + return (value & mask) >= ref; } case Function::GreaterThan: { - return (*Address() & mask) > ref; + return (value & mask) > ref; } case Function::Reserved: [[fallthrough]]; From 0d127a82dda5dc655592bab5a662d5820cf97b78 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 14:05:29 -0700 Subject: [PATCH 027/141] equeue: Clean up timers implementation. (#2925) --- src/core/libraries/kernel/equeue.cpp | 128 +++++++++++++++------------ src/core/libraries/kernel/equeue.h | 9 +- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 7e579614f..8b3ae90cb 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -12,12 +12,25 @@ namespace Libraries::Kernel { +extern boost::asio::io_context io_context; +extern void KernelSignalRequest(); + +static constexpr auto HrTimerSpinlockThresholdUs = 1200u; + // Events are uniquely identified by id and filter. bool EqueueInternal::AddEvent(EqueueEvent& event) { std::scoped_lock lock{m_mutex}; event.time_added = std::chrono::steady_clock::now(); + if (event.event.filter == SceKernelEvent::Filter::Timer || + event.event.filter == SceKernelEvent::Filter::HrTimer) { + // HrTimer events are offset by the threshold of time at the end that we spinlock for + // greater accuracy. + const auto offset = + event.event.filter == SceKernelEvent::Filter::HrTimer ? HrTimerSpinlockThresholdUs : 0u; + event.timer_interval = std::chrono::microseconds(event.event.data - offset); + } const auto& it = std::ranges::find(m_events, event); if (it != m_events.cend()) { @@ -29,6 +42,47 @@ bool EqueueInternal::AddEvent(EqueueEvent& event) { return true; } +bool EqueueInternal::ScheduleEvent(u64 id, s16 filter, + void (*callback)(SceKernelEqueue, const SceKernelEvent&)) { + std::scoped_lock lock{m_mutex}; + + const auto& it = std::ranges::find_if(m_events, [id, filter](auto& ev) { + return ev.event.ident == id && ev.event.filter == filter; + }); + if (it == m_events.cend()) { + return false; + } + + const auto& event = *it; + ASSERT(event.event.filter == SceKernelEvent::Filter::Timer || + event.event.filter == SceKernelEvent::Filter::HrTimer); + + if (!it->timer) { + it->timer = std::make_unique(io_context, event.timer_interval); + } else { + // If the timer already exists we are scheduling a reoccurrence after the next period. + // Set the expiration time to the previous occurrence plus the period. + it->timer->expires_at(it->timer->expires_at() + event.timer_interval); + } + + it->timer->async_wait( + [this, event_data = event.event, callback](const boost::system::error_code& ec) { + if (ec) { + if (ec.value() != boost::system::errc::operation_canceled) { + LOG_ERROR(Kernel_Event, "Timer callback error: {}", ec.message()); + } else { + // Timer was cancelled (removed) before it triggered + LOG_DEBUG(Kernel_Event, "Timer cancelled"); + } + return; + } + callback(this, event_data); + }); + KernelSignalRequest(); + + return true; +} + bool EqueueInternal::RemoveEvent(u64 id, s16 filter) { bool has_found = false; std::scoped_lock lock{m_mutex}; @@ -162,20 +216,6 @@ bool EqueueInternal::EventExists(u64 id, s16 filter) { return it != m_events.cend(); } -extern boost::asio::io_context io_context; -extern void KernelSignalRequest(); - -static constexpr auto HrTimerSpinlockThresholdUs = 1200u; - -static void SmallTimerCallback(const boost::system::error_code& error, SceKernelEqueue eq, - SceKernelEvent kevent) { - static EqueueEvent event; - event.event = kevent; - event.event.data = HrTimerSpinlockThresholdUs; - eq->AddSmallTimer(event); - eq->TriggerEvent(kevent.ident, SceKernelEvent::Filter::HrTimer, kevent.udata); -} - int PS4_SYSV_ABI sceKernelCreateEqueue(SceKernelEqueue* eq, const char* name) { if (eq == nullptr) { LOG_ERROR(Kernel_Event, "Event queue is null!"); @@ -253,6 +293,14 @@ int PS4_SYSV_ABI sceKernelWaitEqueue(SceKernelEqueue eq, SceKernelEvent* ev, int return ORBIS_OK; } +static void HrTimerCallback(SceKernelEqueue eq, const SceKernelEvent& kevent) { + static EqueueEvent event; + event.event = kevent; + event.event.data = HrTimerSpinlockThresholdUs; + eq->AddSmallTimer(event); + eq->TriggerEvent(kevent.ident, SceKernelEvent::Filter::HrTimer, kevent.udata); +} + s32 PS4_SYSV_ABI sceKernelAddHRTimerEvent(SceKernelEqueue eq, int id, timespec* ts, void* udata) { if (eq == nullptr) { return ORBIS_KERNEL_ERROR_EBADF; @@ -283,17 +331,10 @@ s32 PS4_SYSV_ABI sceKernelAddHRTimerEvent(SceKernelEqueue eq, int id, timespec* return eq->AddSmallTimer(event) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOMEM; } - event.timer = std::make_unique( - io_context, std::chrono::microseconds(total_us - HrTimerSpinlockThresholdUs)); - - event.timer->async_wait(std::bind(SmallTimerCallback, std::placeholders::_1, eq, event.event)); - - if (!eq->AddEvent(event)) { + if (!eq->AddEvent(event) || + !eq->ScheduleEvent(id, SceKernelEvent::Filter::HrTimer, HrTimerCallback)) { return ORBIS_KERNEL_ERROR_ENOMEM; } - - KernelSignalRequest(); - return ORBIS_OK; } @@ -310,24 +351,13 @@ int PS4_SYSV_ABI sceKernelDeleteHRTimerEvent(SceKernelEqueue eq, int id) { } } -static void TimerCallback(const boost::system::error_code& error, SceKernelEqueue eq, - SceKernelEvent kevent, SceKernelUseconds interval_ms) { - if (error) { - LOG_ERROR(Kernel_Event, "Timer callback error: {}", error.message()); - return; - } - +static void TimerCallback(SceKernelEqueue eq, const SceKernelEvent& kevent) { if (eq->EventExists(kevent.ident, kevent.filter)) { eq->TriggerEvent(kevent.ident, SceKernelEvent::Filter::Timer, kevent.udata); if (!(kevent.flags & SceKernelEvent::Flags::OneShot)) { - auto timer = std::make_shared( - io_context, std::chrono::milliseconds(interval_ms)); - - timer->async_wait( - [eq, kevent, interval_ms, timer](const boost::system::error_code& ec) { - TimerCallback(ec, eq, kevent, interval_ms); - }); + // Reschedule the event for its next period. + eq->ScheduleEvent(kevent.ident, kevent.filter, TimerCallback); } } } @@ -338,16 +368,13 @@ int PS4_SYSV_ABI sceKernelAddTimerEvent(SceKernelEqueue eq, int id, SceKernelUse return ORBIS_KERNEL_ERROR_EBADF; } - const u64 interval_ms = static_cast(usec & 0xFFFFFFFF) / 1000; - EqueueEvent event{}; event.event.ident = static_cast(id); event.event.filter = SceKernelEvent::Filter::Timer; event.event.flags = SceKernelEvent::Flags::Add; event.event.fflags = 0; - event.event.data = interval_ms; + event.event.data = usec; event.event.udata = udata; - event.time_added = std::chrono::steady_clock::now(); if (eq->EventExists(event.event.ident, event.event.filter)) { eq->RemoveEvent(id, SceKernelEvent::Filter::Timer); @@ -356,24 +383,13 @@ int PS4_SYSV_ABI sceKernelAddTimerEvent(SceKernelEqueue eq, int id, SceKernelUse eq->GetName(), event.event.ident); } - LOG_DEBUG(Kernel_Event, - "Added timing event: queue name={}, queue id={}, ms-intevall={}, pointer={:x}", - eq->GetName(), event.event.ident, interval_ms, reinterpret_cast(udata)); + LOG_DEBUG(Kernel_Event, "Added timing event: queue name={}, queue id={}, usec={}, pointer={:x}", + eq->GetName(), event.event.ident, usec, reinterpret_cast(udata)); - auto timer = std::make_shared( - io_context, std::chrono::milliseconds(interval_ms)); - - if (!eq->AddEvent(event)) { + if (!eq->AddEvent(event) || + !eq->ScheduleEvent(id, SceKernelEvent::Filter::Timer, TimerCallback)) { return ORBIS_KERNEL_ERROR_ENOMEM; } - - timer->async_wait( - [eq, event_data = event.event, interval_ms, timer](const boost::system::error_code& ec) { - TimerCallback(ec, eq, event_data, interval_ms); - }); - - KernelSignalRequest(); - return ORBIS_OK; } diff --git a/src/core/libraries/kernel/equeue.h b/src/core/libraries/kernel/equeue.h index 636496604..a0367c66a 100644 --- a/src/core/libraries/kernel/equeue.h +++ b/src/core/libraries/kernel/equeue.h @@ -21,6 +21,9 @@ namespace Libraries::Kernel { class EqueueInternal; struct EqueueEvent; +using SceKernelUseconds = u32; +using SceKernelEqueue = EqueueInternal*; + struct SceKernelEvent { enum Filter : s16 { None = 0, @@ -77,6 +80,7 @@ struct EqueueEvent { SceKernelEvent event; void* data = nullptr; std::chrono::steady_clock::time_point time_added; + std::chrono::microseconds timer_interval; std::unique_ptr timer; void ResetTriggerState() { @@ -133,6 +137,8 @@ public: } bool AddEvent(EqueueEvent& event); + bool ScheduleEvent(u64 id, s16 filter, + void (*callback)(SceKernelEqueue, const SceKernelEvent&)); bool RemoveEvent(u64 id, s16 filter); int WaitForEvents(SceKernelEvent* ev, int num, u32 micros); bool TriggerEvent(u64 ident, s16 filter, void* trigger_data); @@ -162,9 +168,6 @@ private: std::condition_variable m_cond; }; -using SceKernelUseconds = u32; -using SceKernelEqueue = EqueueInternal*; - u64 PS4_SYSV_ABI sceKernelGetEventData(const SceKernelEvent* ev); void RegisterEventQueue(Core::Loader::SymbolsResolver* sym); From 073f93172985480961ba09fece0048d94d763d6c Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 14:14:11 -0700 Subject: [PATCH 028/141] fix: PM4CmdWaitRegMem memory address --- src/video_core/amdgpu/pm4_cmds.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 066fa4b62..6dc7d97a6 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -487,7 +487,7 @@ struct PM4CmdWaitRegMem { template T Address() const { - return std::bit_cast((uintptr_t(poll_addr_hi) << 32) | poll_addr_lo); + return std::bit_cast((uintptr_t(poll_addr_hi) << 32) | (poll_addr_lo << 2)); } u32 Reg() const { From 3ab69e24db5f976d7d675fea395b550e1ccdfc1d Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 14:22:44 -0700 Subject: [PATCH 029/141] fix: Compiling with newer Boost --- src/core/libraries/kernel/equeue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 8b3ae90cb..ec3b50cd5 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -62,7 +62,7 @@ bool EqueueInternal::ScheduleEvent(u64 id, s16 filter, } else { // If the timer already exists we are scheduling a reoccurrence after the next period. // Set the expiration time to the previous occurrence plus the period. - it->timer->expires_at(it->timer->expires_at() + event.timer_interval); + it->timer->expires_at(it->timer->expiry() + event.timer_interval); } it->timer->async_wait( From 6abda17532fbf1e44e4ced877f79db33845a347c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 23:31:14 +0200 Subject: [PATCH 030/141] Avoid post-increment of SGPR in S_*_LOAD_DWORD (#2928) --- src/shader_recompiler/frontend/translate/scalar_memory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/translate/scalar_memory.cpp b/src/shader_recompiler/frontend/translate/scalar_memory.cpp index 89426e080..376cc304e 100644 --- a/src/shader_recompiler/frontend/translate/scalar_memory.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_memory.cpp @@ -53,7 +53,7 @@ void Translator::S_LOAD_DWORD(int num_dwords, const GcnInst& inst) { ir.CompositeConstruct(ir.GetScalarReg(sbase), ir.GetScalarReg(sbase + 1)); IR::ScalarReg dst_reg{inst.dst[0].code}; for (u32 i = 0; i < num_dwords; i++) { - ir.SetScalarReg(dst_reg++, ir.ReadConst(base, ir.Imm32(dword_offset + i))); + ir.SetScalarReg(dst_reg + i, ir.ReadConst(base, ir.Imm32(dword_offset + i))); } } @@ -75,7 +75,7 @@ void Translator::S_BUFFER_LOAD_DWORD(int num_dwords, const GcnInst& inst) { IR::ScalarReg dst_reg{inst.dst[0].code}; for (u32 i = 0; i < num_dwords; i++) { const IR::U32 index = ir.IAdd(dword_offset, ir.Imm32(i)); - ir.SetScalarReg(dst_reg++, ir.ReadConstBuffer(vsharp, index)); + ir.SetScalarReg(dst_reg + i, ir.ReadConstBuffer(vsharp, index)); } } From 647b1d3ee476ce15fbf542e8b0f57e6debb12d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 13 May 2025 23:34:22 +0200 Subject: [PATCH 031/141] Handle VgtStreamoutFlush event (#2929) --- src/video_core/amdgpu/liverpool.cpp | 21 ++++- src/video_core/amdgpu/liverpool.h | 13 +++- src/video_core/amdgpu/pm4_cmds.h | 114 ++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 0fbfa8b9b..686e8e84f 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -584,7 +584,16 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); + const auto* event = reinterpret_cast(header); + LOG_DEBUG(Render_Vulkan, + "Encountered EventWrite: event_type = {}, event_index = {}", + magic_enum::enum_name(event->event_type.Value()), + magic_enum::enum_name(event->event_index.Value())); + if (event->event_type.Value() == EventType::SoVgtStreamoutFlush) { + // TODO: handle proper synchronization, for now signal that update is done + // immediately + regs.cp_strmout_cntl.offset_update_done = 1; + } break; } case PM4ItOpcode::EventWriteEos: { @@ -732,6 +741,16 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); + LOG_WARNING(Render_Vulkan, + "Unimplemented IT_STRMOUT_BUFFER_UPDATE, update_memory = {}, " + "source_select = {}, buffer_select = {}", + strmout->update_memory.Value(), + magic_enum::enum_name(strmout->source_select.Value()), + strmout->buffer_select.Value()); + break; + } default: UNREACHABLE_MSG("Unknown PM4 type 3 opcode {:#x} with count {}", static_cast(opcode), count); diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index c4bebd05f..a62141099 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -1175,6 +1175,14 @@ struct Liverpool { BitField<22, 2, u32> onchip; }; + union StreamOutControl { + u32 raw; + struct { + u32 offset_update_done : 1; + u32 : 31; + }; + }; + union StreamOutConfig { u32 raw; struct { @@ -1378,7 +1386,9 @@ struct Liverpool { AaConfig aa_config; INSERT_PADDING_WORDS(0xA318 - 0xA2F8 - 1); ColorBuffer color_buffers[NumColorBuffers]; - INSERT_PADDING_WORDS(0xC242 - 0xA390); + INSERT_PADDING_WORDS(0xC03F - 0xA390); + StreamOutControl cp_strmout_cntl; + INSERT_PADDING_WORDS(0xC242 - 0xC040); PrimitiveType primitive_type; INSERT_PADDING_WORDS(0xC24C - 0xC243); u32 num_indices; @@ -1668,6 +1678,7 @@ static_assert(GFX6_3D_REG_INDEX(color_buffers[0].base_address) == 0xA318); static_assert(GFX6_3D_REG_INDEX(color_buffers[0].pitch) == 0xA319); static_assert(GFX6_3D_REG_INDEX(color_buffers[0].slice) == 0xA31A); static_assert(GFX6_3D_REG_INDEX(color_buffers[7].base_address) == 0xA381); +static_assert(GFX6_3D_REG_INDEX(cp_strmout_cntl) == 0xC03F); static_assert(GFX6_3D_REG_INDEX(primitive_type) == 0xC242); static_assert(GFX6_3D_REG_INDEX(num_instances) == 0xC24D); static_assert(GFX6_3D_REG_INDEX(vgt_tf_memory_base) == 0xc250); diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 6dc7d97a6..58ecda93e 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -246,6 +246,46 @@ struct PM4CmdNop { }; }; +enum class SourceSelect : u32 { + BufferOffset = 0, + VgtStrmoutBufferFilledSize = 1, + SrcAddress = 2, + None = 3, +}; + +struct PM4CmdStrmoutBufferUpdate { + PM4Type3Header header; + union { + BitField<0, 1, u32> update_memory; + BitField<1, 2, SourceSelect> source_select; + BitField<8, 2, u32> buffer_select; + u32 control; + }; + union { + BitField<2, 30, u32> dst_address_lo; + BitField<0, 2, u32> swap_dst; + }; + u32 dst_address_hi; + union { + u32 buffer_offset; + BitField<2, 30, u32> src_address_lo; + BitField<0, 2, u32> swap_src; + }; + u32 src_address_hi; + + template + T DstAddress() const { + ASSERT(update_memory.Value() == 1); + return reinterpret_cast(dst_address_lo.Value() | u64(dst_address_hi & 0xFFFF) << 32); + } + + template + T SrcAddress() const { + ASSERT(source_select.Value() == SourceSelect::SrcAddress); + return reinterpret_cast(src_address_lo.Value() | u64(src_address_hi & 0xFFFF) << 32); + } +}; + struct PM4CmdDrawIndexOffset2 { PM4Type3Header header; u32 max_size; ///< Maximum number of indices @@ -303,6 +343,80 @@ static u64 GetGpuClock64() { return static_cast(ticks); } +// VGT_EVENT_INITIATOR.EVENT_TYPE +enum class EventType : u32 { + SampleStreamoutStats1 = 1, + SampleStreamoutStats2 = 2, + SampleStreamoutStats3 = 3, + CacheFlushTs = 4, + ContextDone = 5, + CacheFlush = 6, + CsPartialFlush = 7, + VgtStreamoutSync = 8, + VgtStreamoutReset = 10, + EndOfPipeIncrDe = 11, + EndOfPipeIbEnd = 12, + RstPixCnt = 13, + VsPartialFlush = 15, + PsPartialFlush = 16, + FlushHsOutput = 17, + FlushLsOutput = 18, + CacheFlushAndInvTsEvent = 20, + ZpassDone = 21, + CacheFlushAndInvEvent = 22, + PerfcounterStart = 23, + PerfcounterStop = 24, + PipelineStatStart = 25, + PipelineStatStop = 26, + PerfcounterSample = 27, + FlushEsOutput = 28, + FlushGsOutput = 29, + SamplePipelineStat = 30, + SoVgtStreamoutFlush = 31, + SampleStreamoutStats = 32, + ResetVtxCnt = 33, + VgtFlush = 36, + ScSendDbVpz = 39, + BottomOfPipeTs = 40, + DbCacheFlushAndInv = 42, + FlushAndInvDbDataTs = 43, + FlushAndInvDbMeta = 44, + FlushAndInvCbDataTs = 45, + FlushAndInvCbMeta = 46, + CsDone = 47, + PsDone = 48, + FlushAndInvCbPixelData = 49, + ThreadTraceStart = 51, + ThreadTraceStop = 52, + ThreadTraceFlush = 54, + ThreadTraceFinish = 55, + PixelPipeStatControl = 56, + PixelPipeStatDump = 57, + PixelPipeStatReset = 58, +}; + +enum class EventIndex : u32 { + Other = 0, + ZpassDone = 1, + SamplePipelineStat = 2, + SampleStreamoutStatSx = 3, + CsVsPsPartialFlush = 4, + EopReserved = 5, + EosReserved = 6, + CacheFlush = 7, +}; + +struct PM4CmdEventWrite { + PM4Type3Header header; + union { + u32 event_control; + BitField<0, 6, EventType> event_type; ///< Event type written to VGT_EVENT_INITIATOR + BitField<8, 4, EventIndex> event_index; ///< Event index + BitField<20, 1, u32> inv_l2; ///< Send WBINVL2 op to the TC L2 cache when EVENT_INDEX = 0111 + }; + u32 address[]; +}; + struct PM4CmdEventWriteEop { PM4Type3Header header; union { From 1639640902f5e4aa5cc088a6becb36c3c8c57864 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 14:46:59 -0700 Subject: [PATCH 032/141] event_flag: Lower error logs to debug. --- src/core/libraries/kernel/threads/event_flag.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/threads/event_flag.cpp b/src/core/libraries/kernel/threads/event_flag.cpp index 24ddcb927..91b17bd49 100644 --- a/src/core/libraries/kernel/threads/event_flag.cpp +++ b/src/core/libraries/kernel/threads/event_flag.cpp @@ -315,7 +315,7 @@ int PS4_SYSV_ABI sceKernelPollEventFlag(OrbisKernelEventFlag ef, u64 bitPattern, auto result = ef->Poll(bitPattern, wait, clear, pResultPat); if (result != ORBIS_OK && result != ORBIS_KERNEL_ERROR_EBUSY) { - LOG_ERROR(Kernel_Event, "returned {}", result); + LOG_DEBUG(Kernel_Event, "returned {:#x}", result); } return result; @@ -361,7 +361,7 @@ int PS4_SYSV_ABI sceKernelWaitEventFlag(OrbisKernelEventFlag ef, u64 bitPattern, u32 result = ef->Wait(bitPattern, wait, clear, pResultPat, pTimeout); if (result != ORBIS_OK && result != ORBIS_KERNEL_ERROR_ETIMEDOUT) { - LOG_ERROR(Kernel_Event, "returned {:#x}", result); + LOG_DEBUG(Kernel_Event, "returned {:#x}", result); } return result; From 99eaba7c963797b4282af4a4b78ec8d8ea358d80 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 16:08:25 -0700 Subject: [PATCH 033/141] liverpool: Lower SetQueueReg to warning log. (#2930) --- src/video_core/amdgpu/liverpool.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 686e8e84f..d1cd98634 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -885,8 +885,9 @@ Liverpool::Task Liverpool::ProcessCompute(const u32* acb, u32 acb_dwords, u32 vq } case PM4ItOpcode::SetQueueReg: { const auto* set_data = reinterpret_cast(header); - UNREACHABLE_MSG("Encountered compute SetQueueReg: vqid = {}, reg_offset = {:#x}", - set_data->vqid.Value(), set_data->reg_offset.Value()); + LOG_WARNING(Render, "Encountered compute SetQueueReg: vqid = {}, reg_offset = {:#x}", + set_data->vqid.Value(), set_data->reg_offset.Value()); + break; } case PM4ItOpcode::DispatchDirect: { const auto* dispatch_direct = reinterpret_cast(header); From 26c965cf4aee39903067015c2759f38f7f4444b0 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 13 May 2025 17:31:23 -0700 Subject: [PATCH 034/141] equeue: Fix timer cancel error code check. --- src/core/libraries/kernel/equeue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index ec3b50cd5..958019cd3 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -68,7 +68,7 @@ bool EqueueInternal::ScheduleEvent(u64 id, s16 filter, it->timer->async_wait( [this, event_data = event.event, callback](const boost::system::error_code& ec) { if (ec) { - if (ec.value() != boost::system::errc::operation_canceled) { + if (ec != boost::system::errc::operation_canceled) { LOG_ERROR(Kernel_Event, "Timer callback error: {}", ec.message()); } else { // Timer was cancelled (removed) before it triggered From 6d38100a4118f161fdbf50cc0a1012d863d475d0 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 14 May 2025 09:09:23 -0500 Subject: [PATCH 035/141] Avoid logging nulls in sceKernelIsStack (#2933) Games would crash if providing nullptr to start or end. Also don't need the :# part when logging pointers, as they're automatically formatted. --- src/core/libraries/kernel/memory.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 7af67d6d3..bddfcfc84 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -291,8 +291,7 @@ int PS4_SYSV_ABI sceKernelGetDirectMemoryType(u64 addr, int* directMemoryTypeOut } int PS4_SYSV_ABI sceKernelIsStack(void* addr, void** start, void** end) { - LOG_DEBUG(Kernel_Vmm, "called, addr = {:#x}, start = {:#x}, end = {:#x}", fmt::ptr(addr), - fmt::ptr(start), fmt::ptr(end)); + LOG_DEBUG(Kernel_Vmm, "called, addr = {}", fmt::ptr(addr)); auto* memory = Core::Memory::Instance(); return memory->IsStack(std::bit_cast(addr), start, end); } From 98faff425ea325c693ce6e6d00f65d32e15d6665 Mon Sep 17 00:00:00 2001 From: MajorP93 Date: Wed, 14 May 2025 19:37:16 +0200 Subject: [PATCH 036/141] build: Target x86-64-v3 CPU architecture (#2934) --- CMakeLists.txt | 6 +++--- documents/Quickstart/Quickstart.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6780db417..8daa98dea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,9 +54,9 @@ else() endif() if (ARCHITECTURE STREQUAL "x86_64") - # Target the same CPU architecture as the PS4, to maintain the same level of compatibility. - # Exclude SSE4a as it is only available on AMD CPUs. - add_compile_options(-march=btver2 -mtune=generic -mno-sse4a) + # Target x86-64-v3 CPU architecture as this is a good balance between supporting performance critical + # instructions like AVX2 and maintaining support for older CPUs. + add_compile_options(-march=x86-64-v3) endif() if (APPLE AND ARCHITECTURE STREQUAL "x86_64" AND CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "arm64") diff --git a/documents/Quickstart/Quickstart.md b/documents/Quickstart/Quickstart.md index 62df95e71..e2145ebbd 100644 --- a/documents/Quickstart/Quickstart.md +++ b/documents/Quickstart/Quickstart.md @@ -21,9 +21,9 @@ SPDX-License-Identifier: GPL-2.0-or-later - A processor with at least 4 cores and 6 threads - Above 2.5 GHz frequency -- A CPU supporting the following instruction sets: MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, AVX, F16C, CLMUL, AES, BMI1, MOVBE, XSAVE, ABM +- A CPU supporting the x86-64-v3 baseline. - **Intel**: Haswell generation or newer - - **AMD**: Jaguar generation or newer + - **AMD**: Excavator generation or newer - **Apple**: Rosetta 2 on macOS 15.4 or newer ### GPU @@ -55,4 +55,4 @@ To configure the emulator, you can go through the interface and go to "settings" You can also configure the emulator by editing the `config.toml` file located in the `user` folder created after the application is started (Mostly useful if you are using the SDL version). Some settings may be related to more technical development and debugging.\ -For more information on this, see [**Debugging**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Debugging/Debugging.md#configuration). \ No newline at end of file +For more information on this, see [**Debugging**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Debugging/Debugging.md#configuration). From aeb453698821f5d346de9602d9d7e8114f94a89a Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 15 May 2025 13:59:34 -0700 Subject: [PATCH 037/141] externals: Remove winpthreads. (#2932) --- .gitmodules | 4 - CMakeLists.txt | 9 +- externals/CMakeLists.txt | 6 - externals/winpthreads | 1 - src/common/thread.cpp | 39 +- src/common/thread.h | 3 + src/core/libraries/kernel/kernel.cpp | 3 + src/core/libraries/kernel/time.cpp | 560 +++++++++++++++------------ src/core/libraries/kernel/time.h | 10 +- 9 files changed, 362 insertions(+), 273 deletions(-) delete mode 160000 externals/winpthreads diff --git a/.gitmodules b/.gitmodules index 065a4570f..25b5d307b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,10 +30,6 @@ path = externals/xbyak url = https://github.com/herumi/xbyak.git shallow = true -[submodule "externals/winpthreads"] - path = externals/winpthreads - url = https://github.com/shadps4-emu/winpthreads.git - shallow = true [submodule "externals/magic_enum"] path = externals/magic_enum url = https://github.com/Neargye/magic_enum.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8daa98dea..6c5dde7bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,13 +239,6 @@ if (APPLE) endif() list(POP_BACK CMAKE_MODULE_PATH) -# Note: Windows always has these functions through winpthreads -include(CheckSymbolExists) -check_symbol_exists(pthread_mutex_timedlock "pthread.h" HAVE_PTHREAD_MUTEX_TIMEDLOCK) -if(HAVE_PTHREAD_MUTEX_TIMEDLOCK OR WIN32) - add_compile_options(-DHAVE_PTHREAD_MUTEX_TIMEDLOCK) -endif() - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") # libc++ requires -fexperimental-library to enable std::jthread and std::stop_token support. include(CheckCXXSymbolExists) @@ -1156,7 +1149,7 @@ if (ENABLE_QT_GUI) endif() if (WIN32) - target_link_libraries(shadps4 PRIVATE mincore winpthreads) + target_link_libraries(shadps4 PRIVATE mincore) if (MSVC) # MSVC likes putting opinions on what people can use, disable: diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index b92e13795..89b0fbfdd 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -137,12 +137,6 @@ if (NOT TARGET Zydis::Zydis) add_subdirectory(zydis) endif() -# Winpthreads -if (WIN32) - add_subdirectory(winpthreads) - target_include_directories(winpthreads INTERFACE winpthreads/include) -endif() - # sirit add_subdirectory(sirit) if (WIN32) diff --git a/externals/winpthreads b/externals/winpthreads deleted file mode 160000 index f35b0948d..000000000 --- a/externals/winpthreads +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f35b0948d36a736e6a2d052ae295a3ffde09703f diff --git a/src/common/thread.cpp b/src/common/thread.cpp index 9ef1e86d8..982041ebb 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include @@ -104,14 +105,24 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { SetThreadPriority(handle, windows_priority); } -static void AccurateSleep(std::chrono::nanoseconds duration) { +bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanoseconds* remaining, + const bool interruptible) { + const auto begin_sleep = std::chrono::high_resolution_clock::now(); + LARGE_INTEGER interval{ .QuadPart = -1 * (duration.count() / 100u), }; HANDLE timer = ::CreateWaitableTimer(NULL, TRUE, NULL); SetWaitableTimer(timer, &interval, 0, NULL, NULL, 0); - WaitForSingleObject(timer, INFINITE); + const auto ret = WaitForSingleObjectEx(timer, INFINITE, interruptible); ::CloseHandle(timer); + + if (remaining) { + const auto end_sleep = std::chrono::high_resolution_clock::now(); + const auto sleep_time = end_sleep - begin_sleep; + *remaining = duration > sleep_time ? duration - sleep_time : std::chrono::nanoseconds(0); + } + return ret == WAIT_OBJECT_0; } #else @@ -134,8 +145,24 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { pthread_setschedparam(this_thread, scheduling_type, ¶ms); } -static void AccurateSleep(std::chrono::nanoseconds duration) { - std::this_thread::sleep_for(duration); +bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanoseconds* remaining, + const bool interruptible) { + timespec request = { + .tv_sec = duration.count() / 1'000'000'000, + .tv_nsec = duration.count() % 1'000'000'000, + }; + timespec remain; + int ret; + while ((ret = nanosleep(&request, &remain)) < 0 && errno == EINTR) { + if (interruptible) { + break; + } + request = remain; + } + if (remaining) { + *remaining = std::chrono::nanoseconds(remain.tv_sec * 1'000'000'000 + remain.tv_nsec); + } + return ret == 0 || errno != EINTR; } #endif @@ -196,9 +223,9 @@ AccurateTimer::AccurateTimer(std::chrono::nanoseconds target_interval) : target_interval(target_interval) {} void AccurateTimer::Start() { - auto begin_sleep = std::chrono::high_resolution_clock::now(); + const auto begin_sleep = std::chrono::high_resolution_clock::now(); if (total_wait.count() > 0) { - AccurateSleep(total_wait); + AccurateSleep(total_wait, nullptr, false); } start_time = std::chrono::high_resolution_clock::now(); total_wait -= std::chrono::duration_cast(start_time - begin_sleep); diff --git a/src/common/thread.h b/src/common/thread.h index 92cc0c59d..5bd83d35c 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -25,6 +25,9 @@ void SetCurrentThreadName(const char* name); void SetThreadName(void* thread, const char* name); +bool AccurateSleep(std::chrono::nanoseconds duration, std::chrono::nanoseconds* remaining, + bool interruptible); + class AccurateTimer { std::chrono::nanoseconds target_interval{}; std::chrono::nanoseconds total_wait{}; diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index c7eafe799..180850217 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -108,6 +108,9 @@ void SetPosixErrno(int e) { case EACCES: g_posix_errno = POSIX_EACCES; break; + case EFAULT: + g_posix_errno = POSIX_EFAULT; + break; case EINVAL: g_posix_errno = POSIX_EINVAL; break; diff --git a/src/core/libraries/kernel/time.cpp b/src/core/libraries/kernel/time.cpp index b7e4c1756..2fe74d0a3 100644 --- a/src/core/libraries/kernel/time.cpp +++ b/src/core/libraries/kernel/time.cpp @@ -5,24 +5,23 @@ #include "common/assert.h" #include "common/native_clock.h" +#include "common/thread.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/posix_error.h" #include "core/libraries/kernel/time.h" #include "core/libraries/libs.h" #ifdef _WIN64 -#include #include - #include "common/ntapi.h" - #else #if __APPLE__ #include #endif +#include #include #include -#include #include #endif @@ -52,88 +51,116 @@ u64 PS4_SYSV_ABI sceKernelReadTsc() { return clock->GetUptime(); } -int PS4_SYSV_ABI sceKernelUsleep(u32 microseconds) { -#ifdef _WIN64 - const auto start_time = std::chrono::high_resolution_clock::now(); - auto total_wait_time = std::chrono::microseconds(microseconds); +static s32 posix_nanosleep_impl(const OrbisKernelTimespec* rqtp, OrbisKernelTimespec* rmtp, + const bool interruptible) { + if (!rqtp || rqtp->tv_sec < 0 || rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1'000'000'000) { + SetPosixErrno(EINVAL); + return -1; + } + const auto duration = std::chrono::nanoseconds(rqtp->tv_sec * 1'000'000'000 + rqtp->tv_nsec); + std::chrono::nanoseconds remain; + const auto uninterrupted = Common::AccurateSleep(duration, &remain, interruptible); + if (rmtp) { + rmtp->tv_sec = remain.count() / 1'000'000'000; + rmtp->tv_nsec = remain.count() % 1'000'000'000; + } + if (!uninterrupted) { + SetPosixErrno(EINTR); + return -1; + } + return 0; +} - while (total_wait_time.count() > 0) { - auto wait_time = std::chrono::ceil(total_wait_time).count(); - u64 res = SleepEx(static_cast(wait_time), true); - if (res == WAIT_IO_COMPLETION) { - auto elapsedTime = std::chrono::high_resolution_clock::now() - start_time; - auto elapsedMicroseconds = - std::chrono::duration_cast(elapsedTime).count(); - total_wait_time = std::chrono::microseconds(microseconds - elapsedMicroseconds); - } else { - break; - } +s32 PS4_SYSV_ABI posix_nanosleep(const OrbisKernelTimespec* rqtp, OrbisKernelTimespec* rmtp) { + return posix_nanosleep_impl(rqtp, rmtp, true); +} + +s32 PS4_SYSV_ABI sceKernelNanosleep(const OrbisKernelTimespec* rqtp, OrbisKernelTimespec* rmtp) { + if (const auto ret = posix_nanosleep_impl(rqtp, rmtp, false); ret < 0) { + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI posix_usleep(u32 microseconds) { + const OrbisKernelTimespec ts = { + .tv_sec = microseconds / 1'000'000, + .tv_nsec = (microseconds % 1'000'000) * 1'000, + }; + return posix_nanosleep(&ts, nullptr); +} + +s32 PS4_SYSV_ABI sceKernelUsleep(u32 microseconds) { + const OrbisKernelTimespec ts = { + .tv_sec = microseconds / 1'000'000, + .tv_nsec = (microseconds % 1'000'000) * 1'000, + }; + return sceKernelNanosleep(&ts, nullptr); +} + +u32 PS4_SYSV_ABI posix_sleep(u32 seconds) { + const OrbisKernelTimespec ts = { + .tv_sec = seconds, + .tv_nsec = 0, + }; + OrbisKernelTimespec rm; + if (const auto ret = posix_nanosleep(&ts, &rm); ret < 0) { + return *__Error() == POSIX_EINTR ? rm.tv_sec + (rm.tv_nsec == 0 ? 0 : 1) : seconds; + } + return 0; +} + +s32 PS4_SYSV_ABI sceKernelSleep(u32 seconds) { + return sceKernelUsleep(seconds * 1'000'000); +} + +s32 PS4_SYSV_ABI posix_clock_gettime(u32 clock_id, OrbisKernelTimespec* ts) { + if (ts == nullptr) { + SetPosixErrno(EFAULT); + return -1; } - return 0; -#else - timespec start; - timespec remain; - start.tv_sec = microseconds / 1000000; - start.tv_nsec = (microseconds % 1000000) * 1000; - timespec* requested = &start; - int ret = 0; - do { - ret = nanosleep(requested, &remain); - requested = &remain; - } while (ret != 0); - return ret; -#endif -} + if (clock_id == ORBIS_CLOCK_PROCTIME) { + const auto us = sceKernelGetProcessTime(); + ts->tv_sec = static_cast(us / 1'000'000); + ts->tv_nsec = static_cast((us % 1'000'000) * 1000); + return 0; + } + if (clock_id == ORBIS_CLOCK_EXT_NETWORK || clock_id == ORBIS_CLOCK_EXT_DEBUG_NETWORK || + clock_id == ORBIS_CLOCK_EXT_AD_NETWORK || clock_id == ORBIS_CLOCK_EXT_RAW_NETWORK) { + LOG_ERROR(Lib_Kernel, "Unsupported clock type {}, using CLOCK_MONOTONIC", clock_id); + clock_id = ORBIS_CLOCK_MONOTONIC; + } -int PS4_SYSV_ABI posix_usleep(u32 microseconds) { - return sceKernelUsleep(microseconds); -} - -u32 PS4_SYSV_ABI sceKernelSleep(u32 seconds) { - std::this_thread::sleep_for(std::chrono::seconds(seconds)); - return 0; -} - -#ifdef _WIN64 -#ifndef CLOCK_REALTIME -#define CLOCK_REALTIME 0 -#endif -#ifndef CLOCK_MONOTONIC -#define CLOCK_MONOTONIC 1 -#endif -#ifndef CLOCK_PROCESS_CPUTIME_ID -#define CLOCK_PROCESS_CPUTIME_ID 2 -#endif -#ifndef CLOCK_THREAD_CPUTIME_ID -#define CLOCK_THREAD_CPUTIME_ID 3 -#endif -#ifndef CLOCK_REALTIME_COARSE -#define CLOCK_REALTIME_COARSE 5 -#endif -#ifndef CLOCK_MONOTONIC_COARSE -#define CLOCK_MONOTONIC_COARSE 6 -#endif - -#define DELTA_EPOCH_IN_100NS 116444736000000000ULL - -static u64 FileTimeTo100Ns(FILETIME& ft) { - return *reinterpret_cast(&ft); -} - -static s32 clock_gettime(u32 clock_id, struct timespec* ts) { +#ifdef _WIN32 + static const auto FileTimeTo100Ns = [](FILETIME& ft) { return *reinterpret_cast(&ft); }; switch (clock_id) { - case CLOCK_REALTIME: - case CLOCK_REALTIME_COARSE: { + case ORBIS_CLOCK_REALTIME: + case ORBIS_CLOCK_REALTIME_PRECISE: { FILETIME ft; - GetSystemTimeAsFileTime(&ft); - const u64 ns = FileTimeTo100Ns(ft) - DELTA_EPOCH_IN_100NS; + GetSystemTimePreciseAsFileTime(&ft); + static constexpr u64 DeltaEpochIn100ns = 116444736000000000ULL; + const u64 ns = FileTimeTo100Ns(ft) - DeltaEpochIn100ns; ts->tv_sec = ns / 10'000'000; ts->tv_nsec = (ns % 10'000'000) * 100; return 0; } - case CLOCK_MONOTONIC: - case CLOCK_MONOTONIC_COARSE: { + case ORBIS_CLOCK_SECOND: + case ORBIS_CLOCK_REALTIME_FAST: { + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + static constexpr u64 DeltaEpochIn100ns = 116444736000000000ULL; + const u64 ns = FileTimeTo100Ns(ft) - DeltaEpochIn100ns; + ts->tv_sec = ns / 10'000'000; + ts->tv_nsec = (ns % 10'000'000) * 100; + return 0; + } + case ORBIS_CLOCK_UPTIME: + case ORBIS_CLOCK_UPTIME_PRECISE: + case ORBIS_CLOCK_MONOTONIC: + case ORBIS_CLOCK_MONOTONIC_PRECISE: + case ORBIS_CLOCK_UPTIME_FAST: + case ORBIS_CLOCK_MONOTONIC_FAST: { static LARGE_INTEGER pf = [] { LARGE_INTEGER res{}; QueryPerformanceFrequency(&pf); @@ -141,43 +168,53 @@ static s32 clock_gettime(u32 clock_id, struct timespec* ts) { }(); LARGE_INTEGER pc{}; - QueryPerformanceCounter(&pc); + if (!QueryPerformanceCounter(&pc)) { + SetPosixErrno(EFAULT); + return -1; + } ts->tv_sec = pc.QuadPart / pf.QuadPart; ts->tv_nsec = ((pc.QuadPart % pf.QuadPart) * 1000'000'000) / pf.QuadPart; return 0; } - case CLOCK_PROCESS_CPUTIME_ID: { + case ORBIS_CLOCK_THREAD_CPUTIME_ID: { FILETIME ct, et, kt, ut; - if (!GetProcessTimes(GetCurrentProcess(), &ct, &et, &kt, &ut)) { - return EFAULT; + if (!GetThreadTimes(GetCurrentThread(), &ct, &et, &kt, &ut)) { + SetPosixErrno(EFAULT); + return -1; } const u64 ns = FileTimeTo100Ns(ut) + FileTimeTo100Ns(kt); ts->tv_sec = ns / 10'000'000; ts->tv_nsec = (ns % 10'000'000) * 100; return 0; } - case CLOCK_THREAD_CPUTIME_ID: { + case ORBIS_CLOCK_VIRTUAL: { FILETIME ct, et, kt, ut; - if (!GetThreadTimes(GetCurrentThread(), &ct, &et, &kt, &ut)) { - return EFAULT; + if (!GetProcessTimes(GetCurrentProcess(), &ct, &et, &kt, &ut)) { + SetPosixErrno(EFAULT); + return -1; } - const u64 ns = FileTimeTo100Ns(ut) + FileTimeTo100Ns(kt); + const u64 ns = FileTimeTo100Ns(ut); + ts->tv_sec = ns / 10'000'000; + ts->tv_nsec = (ns % 10'000'000) * 100; + return 0; + } + case ORBIS_CLOCK_PROF: { + FILETIME ct, et, kt, ut; + if (!GetProcessTimes(GetCurrentProcess(), &ct, &et, &kt, &ut)) { + SetPosixErrno(EFAULT); + return -1; + } + const u64 ns = FileTimeTo100Ns(kt); ts->tv_sec = ns / 10'000'000; ts->tv_nsec = (ns % 10'000'000) * 100; return 0; } default: - return EINVAL; + SetPosixErrno(EFAULT); + return -1; } -} -#endif - -int PS4_SYSV_ABI orbis_clock_gettime(s32 clock_id, struct OrbisKernelTimespec* ts) { - if (ts == nullptr) { - return ORBIS_KERNEL_ERROR_EFAULT; - } - - clockid_t pclock_id = CLOCK_MONOTONIC; +#else + clockid_t pclock_id; switch (clock_id) { case ORBIS_CLOCK_REALTIME: case ORBIS_CLOCK_REALTIME_PRECISE: @@ -185,7 +222,7 @@ int PS4_SYSV_ABI orbis_clock_gettime(s32 clock_id, struct OrbisKernelTimespec* t break; case ORBIS_CLOCK_SECOND: case ORBIS_CLOCK_REALTIME_FAST: -#ifndef __APPLE__ +#ifdef CLOCK_REALTIME_COARSE pclock_id = CLOCK_REALTIME_COARSE; #else pclock_id = CLOCK_REALTIME; @@ -199,7 +236,7 @@ int PS4_SYSV_ABI orbis_clock_gettime(s32 clock_id, struct OrbisKernelTimespec* t break; case ORBIS_CLOCK_UPTIME_FAST: case ORBIS_CLOCK_MONOTONIC_FAST: -#ifndef __APPLE__ +#ifdef CLOCK_MONOTONIC_COARSE pclock_id = CLOCK_MONOTONIC_COARSE; #else pclock_id = CLOCK_MONOTONIC; @@ -208,196 +245,226 @@ int PS4_SYSV_ABI orbis_clock_gettime(s32 clock_id, struct OrbisKernelTimespec* t case ORBIS_CLOCK_THREAD_CPUTIME_ID: pclock_id = CLOCK_THREAD_CPUTIME_ID; break; - case ORBIS_CLOCK_PROCTIME: { - const auto us = sceKernelGetProcessTime(); - ts->tv_sec = us / 1'000'000; - ts->tv_nsec = (us % 1'000'000) * 1000; - return 0; - } case ORBIS_CLOCK_VIRTUAL: { -#ifdef _WIN64 - FILETIME ct, et, kt, ut; - if (!GetProcessTimes(GetCurrentProcess(), &ct, &et, &kt, &ut)) { - return EFAULT; - } - const u64 ns = FileTimeTo100Ns(ut); - ts->tv_sec = ns / 10'000'000; - ts->tv_nsec = (ns % 10'000'000) * 100; -#else - struct rusage ru; + rusage ru; const auto res = getrusage(RUSAGE_SELF, &ru); if (res < 0) { - return res; + SetPosixErrno(EFAULT); + return -1; } ts->tv_sec = ru.ru_utime.tv_sec; ts->tv_nsec = ru.ru_utime.tv_usec * 1000; -#endif return 0; } case ORBIS_CLOCK_PROF: { -#ifdef _WIN64 - FILETIME ct, et, kt, ut; - if (!GetProcessTimes(GetCurrentProcess(), &ct, &et, &kt, &ut)) { - return EFAULT; - } - const u64 ns = FileTimeTo100Ns(kt); - ts->tv_sec = ns / 10'000'000; - ts->tv_nsec = (ns % 10'000'000) * 100; -#else - struct rusage ru; + rusage ru; const auto res = getrusage(RUSAGE_SELF, &ru); if (res < 0) { - return res; + SetPosixErrno(EFAULT); + return -1; } ts->tv_sec = ru.ru_stime.tv_sec; ts->tv_nsec = ru.ru_stime.tv_usec * 1000; -#endif return 0; } - case ORBIS_CLOCK_EXT_NETWORK: - case ORBIS_CLOCK_EXT_DEBUG_NETWORK: - case ORBIS_CLOCK_EXT_AD_NETWORK: - case ORBIS_CLOCK_EXT_RAW_NETWORK: - pclock_id = CLOCK_MONOTONIC; - LOG_ERROR(Lib_Kernel, "unsupported = {} using CLOCK_MONOTONIC", clock_id); - break; default: - return EINVAL; + SetPosixErrno(EFAULT); + return -1; } timespec t{}; - int result = clock_gettime(pclock_id, &t); + const auto result = clock_gettime(pclock_id, &t); ts->tv_sec = t.tv_sec; ts->tv_nsec = t.tv_nsec; - return result; -} - -int PS4_SYSV_ABI sceKernelClockGettime(s32 clock_id, OrbisKernelTimespec* tp) { - const auto res = orbis_clock_gettime(clock_id, tp); - if (res < 0) { - return ErrnoToSceKernelError(res); + if (result < 0) { + SetPosixErrno(errno); + return -1; } - return ORBIS_OK; -} - -int PS4_SYSV_ABI posix_nanosleep(const OrbisKernelTimespec* rqtp, OrbisKernelTimespec* rmtp) { - const auto* request = reinterpret_cast(rqtp); - auto* remain = reinterpret_cast(rmtp); - return nanosleep(request, remain); -} - -int PS4_SYSV_ABI sceKernelNanosleep(const OrbisKernelTimespec* rqtp, OrbisKernelTimespec* rmtp) { - if (!rqtp || !rmtp) { - return ORBIS_KERNEL_ERROR_EFAULT; - } - - if (rqtp->tv_sec < 0 || rqtp->tv_nsec < 0) { - return ORBIS_KERNEL_ERROR_EINVAL; - } - - return posix_nanosleep(rqtp, rmtp); -} - -int PS4_SYSV_ABI sceKernelGettimeofday(OrbisKernelTimeval* tp) { - if (!tp) { - return ORBIS_KERNEL_ERROR_EFAULT; - } - -#ifdef _WIN64 - FILETIME filetime; - GetSystemTimePreciseAsFileTime(&filetime); - - constexpr u64 UNIX_TIME_START = 0x295E9648864000; - constexpr u64 TICKS_PER_SECOND = 1000000; - - u64 ticks = filetime.dwHighDateTime; - ticks <<= 32; - ticks |= filetime.dwLowDateTime; - ticks /= 10; - ticks -= UNIX_TIME_START; - - tp->tv_sec = ticks / TICKS_PER_SECOND; - tp->tv_usec = ticks % TICKS_PER_SECOND; -#else - timeval tv; - gettimeofday(&tv, nullptr); - tp->tv_sec = tv.tv_sec; - tp->tv_usec = tv.tv_usec; + return 0; #endif +} + +s32 PS4_SYSV_ABI sceKernelClockGettime(const u32 clock_id, OrbisKernelTimespec* ts) { + if (const auto ret = posix_clock_gettime(clock_id, ts); ret < 0) { + return ErrnoToSceKernelError(*__Error()); + } return ORBIS_OK; } -int PS4_SYSV_ABI gettimeofday(OrbisKernelTimeval* tp, OrbisKernelTimezone* tz) { - // FreeBSD docs mention that the kernel generally does not track these values - // and they are usually returned as zero. - if (tz) { - tz->tz_minuteswest = 0; - tz->tz_dsttime = 0; - } - return sceKernelGettimeofday(tp); -} - -s32 PS4_SYSV_ABI sceKernelGettimezone(OrbisKernelTimezone* tz) { -#ifdef _WIN64 - ASSERT(tz); - static int tzflag = 0; - if (!tzflag) { - _tzset(); - tzflag++; - } - tz->tz_minuteswest = _timezone / 60; - tz->tz_dsttime = _daylight; -#else - struct timezone tzz; - struct timeval tv; - gettimeofday(&tv, &tzz); - tz->tz_dsttime = tzz.tz_dsttime; - tz->tz_minuteswest = tzz.tz_minuteswest; -#endif - return ORBIS_OK; -} - -int PS4_SYSV_ABI posix_clock_getres(u32 clock_id, OrbisKernelTimespec* res) { +s32 PS4_SYSV_ABI posix_clock_getres(u32 clock_id, OrbisKernelTimespec* res) { if (res == nullptr) { - return ORBIS_KERNEL_ERROR_EFAULT; + SetPosixErrno(EFAULT); + return -1; } - clockid_t pclock_id = CLOCK_REALTIME; + + if (clock_id == ORBIS_CLOCK_EXT_NETWORK || clock_id == ORBIS_CLOCK_EXT_DEBUG_NETWORK || + clock_id == ORBIS_CLOCK_EXT_AD_NETWORK || clock_id == ORBIS_CLOCK_EXT_RAW_NETWORK) { + LOG_ERROR(Lib_Kernel, "Unsupported clock type {}, using CLOCK_MONOTONIC", clock_id); + clock_id = ORBIS_CLOCK_MONOTONIC; + } + +#ifdef _WIN32 + switch (clock_id) { + case ORBIS_CLOCK_SECOND: + case ORBIS_CLOCK_REALTIME_FAST: { + DWORD timeAdjustment; + DWORD timeIncrement; + BOOL isTimeAdjustmentDisabled; + if (!GetSystemTimeAdjustment(&timeAdjustment, &timeIncrement, &isTimeAdjustmentDisabled)) { + SetPosixErrno(EFAULT); + return -1; + } + res->tv_sec = 0; + res->tv_nsec = timeIncrement * 100; + return 0; + } + case ORBIS_CLOCK_REALTIME: + case ORBIS_CLOCK_REALTIME_PRECISE: + case ORBIS_CLOCK_UPTIME: + case ORBIS_CLOCK_UPTIME_PRECISE: + case ORBIS_CLOCK_MONOTONIC: + case ORBIS_CLOCK_MONOTONIC_PRECISE: + case ORBIS_CLOCK_UPTIME_FAST: + case ORBIS_CLOCK_MONOTONIC_FAST: { + LARGE_INTEGER pf; + if (!QueryPerformanceFrequency(&pf)) { + SetPosixErrno(EFAULT); + return -1; + } + res->tv_sec = 0; + res->tv_nsec = + std::max(static_cast((1000000000 + (pf.QuadPart >> 1)) / pf.QuadPart), 1); + return 0; + } + default: + UNREACHABLE(); + } +#else + clockid_t pclock_id; switch (clock_id) { case ORBIS_CLOCK_REALTIME: case ORBIS_CLOCK_REALTIME_PRECISE: - case ORBIS_CLOCK_REALTIME_FAST: pclock_id = CLOCK_REALTIME; break; case ORBIS_CLOCK_SECOND: + case ORBIS_CLOCK_REALTIME_FAST: +#ifdef CLOCK_REALTIME_COARSE + pclock_id = CLOCK_REALTIME_COARSE; +#else + pclock_id = CLOCK_REALTIME; +#endif + break; + case ORBIS_CLOCK_UPTIME: + case ORBIS_CLOCK_UPTIME_PRECISE: case ORBIS_CLOCK_MONOTONIC: case ORBIS_CLOCK_MONOTONIC_PRECISE: - case ORBIS_CLOCK_MONOTONIC_FAST: pclock_id = CLOCK_MONOTONIC; break; + case ORBIS_CLOCK_UPTIME_FAST: + case ORBIS_CLOCK_MONOTONIC_FAST: +#ifdef CLOCK_MONOTONIC_COARSE + pclock_id = CLOCK_MONOTONIC_COARSE; +#else + pclock_id = CLOCK_MONOTONIC; +#endif + break; default: UNREACHABLE(); } timespec t{}; - int result = clock_getres(pclock_id, &t); + const auto result = clock_getres(pclock_id, &t); res->tv_sec = t.tv_sec; res->tv_nsec = t.tv_nsec; - if (result == 0) { - return ORBIS_OK; + if (result < 0) { + SetPosixErrno(errno); + return -1; } - return ORBIS_KERNEL_ERROR_EINVAL; + return 0; +#endif } -int PS4_SYSV_ABI sceKernelConvertLocaltimeToUtc(time_t param_1, int64_t param_2, time_t* seconds, - OrbisKernelTimezone* timezone, int* dst_seconds) { +s32 PS4_SYSV_ABI sceKernelClockGetres(const u32 clock_id, OrbisKernelTimespec* res) { + if (const auto ret = posix_clock_getres(clock_id, res); ret < 0) { + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI posix_gettimeofday(OrbisKernelTimeval* tp, OrbisKernelTimezone* tz) { +#ifdef _WIN64 + if (tp) { + FILETIME filetime; + GetSystemTimePreciseAsFileTime(&filetime); + + constexpr u64 UNIX_TIME_START = 0x295E9648864000; + constexpr u64 TICKS_PER_SECOND = 1000000; + + u64 ticks = filetime.dwHighDateTime; + ticks <<= 32; + ticks |= filetime.dwLowDateTime; + ticks /= 10; + ticks -= UNIX_TIME_START; + + tp->tv_sec = ticks / TICKS_PER_SECOND; + tp->tv_usec = ticks % TICKS_PER_SECOND; + } + if (tz) { + static int tzflag = 0; + if (!tzflag) { + _tzset(); + tzflag++; + } + tz->tz_minuteswest = _timezone / 60; + tz->tz_dsttime = _daylight; + } + return 0; +#else + struct timezone tzz; + timeval tv; + const auto ret = gettimeofday(&tv, &tzz); + if (tp) { + tp->tv_sec = tv.tv_sec; + tp->tv_usec = tv.tv_usec; + } + if (tz) { + tz->tz_dsttime = tzz.tz_dsttime; + tz->tz_minuteswest = tzz.tz_minuteswest; + } + if (ret < 0) { + SetPosixErrno(errno); + return -1; + } + return 0; +#endif +} + +s32 PS4_SYSV_ABI sceKernelGettimeofday(OrbisKernelTimeval* tp) { + if (const auto ret = posix_gettimeofday(tp, nullptr); ret < 0) { + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceKernelGettimezone(OrbisKernelTimezone* tz) { + if (const auto ret = posix_gettimeofday(nullptr, tz); ret < 0) { + return ErrnoToSceKernelError(*__Error()); + } + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceKernelConvertLocaltimeToUtc(time_t param_1, int64_t param_2, time_t* seconds, + OrbisKernelTimezone* timezone, s32* dst_seconds) { LOG_INFO(Kernel, "called"); if (timezone) { sceKernelGettimezone(timezone); param_1 -= (timezone->tz_minuteswest + timezone->tz_dsttime) * 60; - if (seconds) + if (seconds) { *seconds = param_1; - if (dst_seconds) + } + if (dst_seconds) { *dst_seconds = timezone->tz_dsttime * 60; + } } else { return ORBIS_KERNEL_ERROR_EINVAL; } @@ -415,7 +482,7 @@ Common::NativeClock* GetClock() { } // namespace Dev -int PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, +s32 PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, struct OrbisTimesec* st, u64* dst_sec) { LOG_TRACE(Kernel, "Called"); #ifdef __APPLE__ @@ -444,28 +511,35 @@ int PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, void RegisterTime(Core::Loader::SymbolsResolver* sym) { clock = std::make_unique(); initial_ptc = clock->GetUptime(); + + // POSIX + LIB_FUNCTION("yS8U2TGCe1A", "libkernel", 1, "libkernel", 1, 1, posix_nanosleep); + LIB_FUNCTION("yS8U2TGCe1A", "libScePosix", 1, "libkernel", 1, 1, posix_nanosleep); + LIB_FUNCTION("QcteRwbsnV0", "libkernel", 1, "libkernel", 1, 1, posix_usleep); + LIB_FUNCTION("QcteRwbsnV0", "libScePosix", 1, "libkernel", 1, 1, posix_usleep); + LIB_FUNCTION("0wu33hunNdE", "libkernel", 1, "libkernel", 1, 1, posix_sleep); + LIB_FUNCTION("0wu33hunNdE", "libScePosix", 1, "libkernel", 1, 1, posix_sleep); + LIB_FUNCTION("lLMT9vJAck0", "libkernel", 1, "libkernel", 1, 1, posix_clock_gettime); + LIB_FUNCTION("lLMT9vJAck0", "libScePosix", 1, "libkernel", 1, 1, posix_clock_gettime); + LIB_FUNCTION("smIj7eqzZE8", "libkernel", 1, "libkernel", 1, 1, posix_clock_getres); + LIB_FUNCTION("smIj7eqzZE8", "libScePosix", 1, "libkernel", 1, 1, posix_clock_getres); + LIB_FUNCTION("n88vx3C5nW8", "libkernel", 1, "libkernel", 1, 1, posix_gettimeofday); + LIB_FUNCTION("n88vx3C5nW8", "libScePosix", 1, "libkernel", 1, 1, posix_gettimeofday); + + // Orbis LIB_FUNCTION("4J2sUJmuHZQ", "libkernel", 1, "libkernel", 1, 1, sceKernelGetProcessTime); LIB_FUNCTION("fgxnMeTNUtY", "libkernel", 1, "libkernel", 1, 1, sceKernelGetProcessTimeCounter); LIB_FUNCTION("BNowx2l588E", "libkernel", 1, "libkernel", 1, 1, sceKernelGetProcessTimeCounterFrequency); LIB_FUNCTION("-2IRUCO--PM", "libkernel", 1, "libkernel", 1, 1, sceKernelReadTsc); LIB_FUNCTION("1j3S3n-tTW4", "libkernel", 1, "libkernel", 1, 1, sceKernelGetTscFrequency); - LIB_FUNCTION("ejekcaNQNq0", "libkernel", 1, "libkernel", 1, 1, sceKernelGettimeofday); - LIB_FUNCTION("n88vx3C5nW8", "libkernel", 1, "libkernel", 1, 1, gettimeofday); - LIB_FUNCTION("n88vx3C5nW8", "libScePosix", 1, "libkernel", 1, 1, gettimeofday); LIB_FUNCTION("QvsZxomvUHs", "libkernel", 1, "libkernel", 1, 1, sceKernelNanosleep); LIB_FUNCTION("1jfXLRVzisc", "libkernel", 1, "libkernel", 1, 1, sceKernelUsleep); - LIB_FUNCTION("QcteRwbsnV0", "libkernel", 1, "libkernel", 1, 1, posix_usleep); - LIB_FUNCTION("QcteRwbsnV0", "libScePosix", 1, "libkernel", 1, 1, posix_usleep); LIB_FUNCTION("-ZR+hG7aDHw", "libkernel", 1, "libkernel", 1, 1, sceKernelSleep); - LIB_FUNCTION("0wu33hunNdE", "libScePosix", 1, "libkernel", 1, 1, sceKernelSleep); - LIB_FUNCTION("yS8U2TGCe1A", "libkernel", 1, "libkernel", 1, 1, posix_nanosleep); - LIB_FUNCTION("yS8U2TGCe1A", "libScePosix", 1, "libkernel", 1, 1, posix_nanosleep); LIB_FUNCTION("QBi7HCK03hw", "libkernel", 1, "libkernel", 1, 1, sceKernelClockGettime); + LIB_FUNCTION("wRYVA5Zolso", "libkernel", 1, "libkernel", 1, 1, sceKernelClockGetres); + LIB_FUNCTION("ejekcaNQNq0", "libkernel", 1, "libkernel", 1, 1, sceKernelGettimeofday); LIB_FUNCTION("kOcnerypnQA", "libkernel", 1, "libkernel", 1, 1, sceKernelGettimezone); - LIB_FUNCTION("lLMT9vJAck0", "libkernel", 1, "libkernel", 1, 1, orbis_clock_gettime); - LIB_FUNCTION("lLMT9vJAck0", "libScePosix", 1, "libkernel", 1, 1, orbis_clock_gettime); - LIB_FUNCTION("smIj7eqzZE8", "libScePosix", 1, "libkernel", 1, 1, posix_clock_getres); LIB_FUNCTION("0NTHN1NKONI", "libkernel", 1, "libkernel", 1, 1, sceKernelConvertLocaltimeToUtc); LIB_FUNCTION("-o5uEDpN+oY", "libkernel", 1, "libkernel", 1, 1, sceKernelConvertUtcToLocaltime); } diff --git a/src/core/libraries/kernel/time.h b/src/core/libraries/kernel/time.h index 407b6f9ed..c80de7bc4 100644 --- a/src/core/libraries/kernel/time.h +++ b/src/core/libraries/kernel/time.h @@ -75,14 +75,14 @@ u64 PS4_SYSV_ABI sceKernelGetProcessTime(); u64 PS4_SYSV_ABI sceKernelGetProcessTimeCounter(); u64 PS4_SYSV_ABI sceKernelGetProcessTimeCounterFrequency(); u64 PS4_SYSV_ABI sceKernelReadTsc(); -int PS4_SYSV_ABI sceKernelClockGettime(s32 clock_id, OrbisKernelTimespec* tp); +s32 PS4_SYSV_ABI sceKernelClockGettime(u32 clock_id, OrbisKernelTimespec* tp); s32 PS4_SYSV_ABI sceKernelGettimezone(OrbisKernelTimezone* tz); -int PS4_SYSV_ABI sceKernelConvertLocaltimeToUtc(time_t param_1, int64_t param_2, time_t* seconds, - OrbisKernelTimezone* timezone, int* dst_seconds); +s32 PS4_SYSV_ABI sceKernelConvertLocaltimeToUtc(time_t param_1, int64_t param_2, time_t* seconds, + OrbisKernelTimezone* timezone, s32* dst_seconds); -int PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, OrbisTimesec* st, +s32 PS4_SYSV_ABI sceKernelConvertUtcToLocaltime(time_t time, time_t* local_time, OrbisTimesec* st, u64* dst_sec); -int PS4_SYSV_ABI sceKernelUsleep(u32 microseconds); +s32 PS4_SYSV_ABI sceKernelUsleep(u32 microseconds); void RegisterTime(Core::Loader::SymbolsResolver* sym); From 18ff65efc81bce0c2bb731418aac157a67f347fb Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Fri, 16 May 2025 07:38:40 -0500 Subject: [PATCH 038/141] Implement sceKernelMapDirectMemory2 (#2940) * Implement sceKernelMapDirectMemory2 Behaves similarly to sceKernelMapDirectMemory, but has a type parameter. * Simplify No need to copy all the MapDirectMemory code over, can just call the function, then do the SetDirectMemoryType call * Clang --- src/core/libraries/kernel/memory.cpp | 14 ++++++++++++++ src/core/memory.cpp | 13 +++++++++++++ src/core/memory.h | 2 ++ 3 files changed, 29 insertions(+) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index bddfcfc84..cb41a664a 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -209,6 +209,19 @@ int PS4_SYSV_ABI sceKernelMapDirectMemory(void** addr, u64 len, int prot, int fl "anon"); } +s32 PS4_SYSV_ABI sceKernelMapDirectMemory2(void** addr, u64 len, s32 type, s32 prot, s32 flags, + s64 phys_addr, u64 alignment) { + LOG_INFO(Kernel_Vmm, "called, redirected to sceKernelMapNamedDirectMemory"); + const s32 ret = + sceKernelMapNamedDirectMemory(addr, len, prot, flags, phys_addr, alignment, "anon"); + + if (ret == 0) { + auto* memory = Core::Memory::Instance(); + memory->SetDirectMemoryType(phys_addr, type); + } + return ret; +} + s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, std::size_t len, int prot, int flags, const char* name) { @@ -645,6 +658,7 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("yDBwVAolDgg", "libkernel", 1, "libkernel", 1, 1, sceKernelIsStack); LIB_FUNCTION("NcaWUxfMNIQ", "libkernel", 1, "libkernel", 1, 1, sceKernelMapNamedDirectMemory); LIB_FUNCTION("L-Q3LEjIbgA", "libkernel", 1, "libkernel", 1, 1, sceKernelMapDirectMemory); + LIB_FUNCTION("BQQniolj9tQ", "libkernel", 1, "libkernel", 1, 1, sceKernelMapDirectMemory2); LIB_FUNCTION("WFcfL2lzido", "libkernel", 1, "libkernel", 1, 1, sceKernelQueryMemoryProtection); LIB_FUNCTION("BHouLQzh0X0", "libkernel", 1, "libkernel", 1, 1, sceKernelDirectMemoryQuery); LIB_FUNCTION("MBuItvba6z8", "libkernel", 1, "libkernel", 1, 1, sceKernelReleaseDirectMemory); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index ec03d6c5e..13290336c 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -783,6 +783,19 @@ int MemoryManager::DirectQueryAvailable(PAddr search_start, PAddr search_end, si return ORBIS_OK; } +s32 MemoryManager::SetDirectMemoryType(s64 phys_addr, s32 memory_type) { + std::scoped_lock lk{mutex}; + + auto& dmem_area = FindDmemArea(phys_addr)->second; + + ASSERT_MSG(phys_addr <= dmem_area.GetEnd() && !dmem_area.is_free, + "Direct memory area is not mapped"); + + dmem_area.memory_type = memory_type; + + return ORBIS_OK; +} + void MemoryManager::NameVirtualRange(VAddr virtual_addr, size_t size, std::string_view name) { auto it = FindVMA(virtual_addr); diff --git a/src/core/memory.h b/src/core/memory.h index 4c143ff6f..883b48854 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -219,6 +219,8 @@ public: int GetDirectMemoryType(PAddr addr, int* directMemoryTypeOut, void** directMemoryStartOut, void** directMemoryEndOut); + s32 SetDirectMemoryType(s64 phys_addr, s32 memory_type); + void NameVirtualRange(VAddr virtual_addr, size_t size, std::string_view name); void InvalidateMemory(VAddr addr, u64 size) const; From 7de6aec337b68d01c88fa2696e4f3cd7e27ed7a6 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 16 May 2025 16:17:37 +0300 Subject: [PATCH 039/141] [Libs] Companion httpd (#2942) * stubbed companion httpd * sceCompanionHttpdGetEvent returns disconnected device * more function definations * added error codes file --- CMakeLists.txt | 5 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + .../libraries/companion/companion_error.h | 20 +++ .../libraries/companion/companion_httpd.cpp | 142 ++++++++++++++++++ .../libraries/companion/companion_httpd.h | 91 +++++++++++ src/core/libraries/libs.cpp | 2 + 7 files changed, 262 insertions(+) create mode 100644 src/core/libraries/companion/companion_error.h create mode 100644 src/core/libraries/companion/companion_httpd.cpp create mode 100644 src/core/libraries/companion/companion_httpd.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5dde7bf..0e14b8467 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -599,6 +599,10 @@ set(CAMERA_LIBS src/core/libraries/camera/camera.cpp src/core/libraries/camera/camera_error.h ) +set(COMPANION_LIBS src/core/libraries/companion/companion_httpd.cpp + src/core/libraries/companion/companion_httpd.h + src/core/libraries/companion/companion_error.h +) set(DEV_TOOLS src/core/devtools/layer.cpp src/core/devtools/layer.h src/core/devtools/options.cpp @@ -763,6 +767,7 @@ set(CORE src/core/aerolib/stubs.cpp ${VDEC_LIB} ${VR_LIBS} ${CAMERA_LIBS} + ${COMPANION_LIBS} ${DEV_TOOLS} src/core/debug_state.cpp src/core/debug_state.h diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 1b605e9ed..231cbf849 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -139,6 +139,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, Hmd) \ SUB(Lib, SigninDialog) \ SUB(Lib, Camera) \ + SUB(Lib, CompanionHttpd) \ CLS(Frontend) \ CLS(Render) \ SUB(Render, Vulkan) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 5746b648e..e4eae59af 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -106,6 +106,7 @@ enum class Class : u8 { Lib_Hmd, ///< The LibSceHmd implementation. Lib_SigninDialog, ///< The LibSigninDialog implementation. Lib_Camera, ///< The LibCamera implementation. + Lib_CompanionHttpd, ///< The LibCompanionHttpd implementation. Frontend, ///< Emulator UI Render, ///< Video Core Render_Vulkan, ///< Vulkan backend diff --git a/src/core/libraries/companion/companion_error.h b/src/core/libraries/companion/companion_error.h new file mode 100644 index 000000000..2d1a3833c --- /dev/null +++ b/src/core/libraries/companion/companion_error.h @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +// companion_httpd error codes +constexpr int ORBIS_COMPANION_HTTPD_ERROR_UNKNOWN = 0x80E40001; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_FATAL = 0x80E40002; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOMEM = 0x80E40003; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_INVALID_PARAM = 0x80E40004; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_INVALID_OPERATION = 0x80E40005; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_INITIALIZED = 0x80E40006; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_ALREADY_INITIALIZED = 0x80E40007; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NO_EVENT = 0x80E40008; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_GENERATE_RESPONSE = 0x80E40009; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_ALREADY_STARTED = 0x80E4000A; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_STARTED = 0x80E4000B; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_ALREADY_REGISTERED = 0x80E4000; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_CONNECTED = 0x80E4000D; +constexpr int ORBIS_COMPANION_HTTPD_ERROR_USER_NOT_FOUND = 0x80E4000E; diff --git a/src/core/libraries/companion/companion_httpd.cpp b/src/core/libraries/companion/companion_httpd.cpp new file mode 100644 index 000000000..39081fa4e --- /dev/null +++ b/src/core/libraries/companion/companion_httpd.cpp @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "companion_error.h" +#include "core/libraries/companion/companion_httpd.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::CompanionHttpd { + +s32 PS4_SYSV_ABI sceCompanionHttpdAddHeader(const char* key, const char* value, + OrbisCompanionHttpdResponse* response) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI +sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdGetEvent(OrbisCompanionHttpdEvent* pEvent) { + pEvent->event = ORBIS_COMPANION_HTTPD_EVENT_DISCONNECT; // disconnected + LOG_DEBUG(Lib_CompanionHttpd, "device disconnected"); + return ORBIS_COMPANION_HTTPD_ERROR_NO_EVENT; // No events to obtain +} + +s32 PS4_SYSV_ABI +sceCompanionHttpdGetUserId(u32 addr, Libraries::UserService::OrbisUserServiceUserId* userId) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdInitialize() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdInitialize2() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdOptParamInitialize() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdRegisterRequestBodyReceptionCallback( + OrbisCompanionHttpdRequestBodyReceptionCallback function, void* param) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI +sceCompanionHttpdRegisterRequestCallback(OrbisCompanionHttpdRequestCallback function, void* param) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdRegisterRequestCallback2( + OrbisCompanionHttpdRequestCallback function, void* param) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdSetBody(const char* body, u64 bodySize, + OrbisCompanionHttpdResponse* response) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdSetStatus(s32 status, OrbisCompanionHttpdResponse* response) { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdStart() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdStop() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdTerminate() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionHttpdUnregisterRequestCallback() { + LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterlibSceCompanionHttpd(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("8pWltDG7h6A", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdAddHeader); + LIB_FUNCTION("B-QBMeFdNgY", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdGet2ndScreenStatus); + LIB_FUNCTION("Vku4big+IYM", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdGetEvent); + LIB_FUNCTION("0SySxcuVNG0", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdGetUserId); + LIB_FUNCTION("ykNpWs3ktLY", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdInitialize); + LIB_FUNCTION("OA6FbORefbo", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdInitialize2); + LIB_FUNCTION("r-2-a0c7Kfc", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdOptParamInitialize); + LIB_FUNCTION("fHNmij7kAUM", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdRegisterRequestBodyReceptionCallback); + LIB_FUNCTION("OaWw+IVEdbI", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdRegisterRequestCallback); + LIB_FUNCTION("-0c9TCTwnGs", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdRegisterRequestCallback2); + LIB_FUNCTION("h3OvVxzX4qM", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdSetBody); + LIB_FUNCTION("w7oz0AWHpT4", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdSetStatus); + LIB_FUNCTION("k7F0FcDM-Xc", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdStart); + LIB_FUNCTION("0SCgzfVQHpo", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdStop); + LIB_FUNCTION("+-du9tWgE9s", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdTerminate); + LIB_FUNCTION("ZSHiUfYK+QI", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdUnregisterRequestBodyReceptionCallback); + LIB_FUNCTION("xweOi2QT-BE", "libSceCompanionHttpd", 1, "libSceCompanionHttpd", 1, 1, + sceCompanionHttpdUnregisterRequestCallback); +}; + +} // namespace Libraries::CompanionHttpd \ No newline at end of file diff --git a/src/core/libraries/companion/companion_httpd.h b/src/core/libraries/companion/companion_httpd.h new file mode 100644 index 000000000..b6d441653 --- /dev/null +++ b/src/core/libraries/companion/companion_httpd.h @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/network/net.h" +#include "core/libraries/system/userservice.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::CompanionHttpd { + +// OrbisCompanionHttpdEvent event codes +constexpr int ORBIS_COMPANION_HTTPD_EVENT_CONNECT = 0x10000001; +constexpr int ORBIS_COMPANION_HTTPD_EVENT_DISCONNECT = 0x10000002; + +struct OrbisCompanionHttpdHeader { + char* key; + char* value; + struct OrbisCompanionHttpdHeader* header; +}; + +struct OrbisCompanionHttpdRequest { + s32 method; + char* url; + OrbisCompanionHttpdHeader* header; + char* body; + u64 bodySize; +}; + +struct OrbisCompanionHttpdResponse { + s32 status; + OrbisCompanionHttpdHeader* header; + char* body; + u64 bodySize; +}; + +using OrbisCompanionHttpdRequestBodyReceptionCallback = + PS4_SYSV_ABI s32 (*)(s32 event, Libraries::UserService::OrbisUserServiceUserId userId, + const OrbisCompanionHttpdRequest* httpRequest, void* param); + +using OrbisCompanionHttpdRequestCallback = + PS4_SYSV_ABI s32 (*)(Libraries::UserService::OrbisUserServiceUserId userId, + const OrbisCompanionHttpdRequest* httpRequest, + OrbisCompanionHttpdResponse* httpResponse, void* param); + +struct OrbisCompanionUtilDeviceInfo { + Libraries::UserService::OrbisUserServiceUserId userId; + Libraries::Net::OrbisNetSockaddrIn addr; + char reserved[236]; +}; + +struct OrbisCompanionHttpdEvent { + s32 event; + union { + OrbisCompanionUtilDeviceInfo deviceInfo; + Libraries::UserService::OrbisUserServiceUserId userId; + char reserved[256]; + } data; +}; + +s32 PS4_SYSV_ABI sceCompanionHttpdAddHeader(const char* key, const char* value, + OrbisCompanionHttpdResponse* response); +s32 PS4_SYSV_ABI +sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId userId); +s32 PS4_SYSV_ABI sceCompanionHttpdGetEvent(OrbisCompanionHttpdEvent* pEvent); +s32 PS4_SYSV_ABI sceCompanionHttpdGetUserId(u32 addr, + Libraries::UserService::OrbisUserServiceUserId* userId); +s32 PS4_SYSV_ABI sceCompanionHttpdInitialize(); +s32 PS4_SYSV_ABI sceCompanionHttpdInitialize2(); +s32 PS4_SYSV_ABI sceCompanionHttpdOptParamInitialize(); +s32 PS4_SYSV_ABI sceCompanionHttpdRegisterRequestBodyReceptionCallback( + OrbisCompanionHttpdRequestBodyReceptionCallback function, void* param); +s32 PS4_SYSV_ABI +sceCompanionHttpdRegisterRequestCallback(OrbisCompanionHttpdRequestCallback function, void* param); +s32 PS4_SYSV_ABI +sceCompanionHttpdRegisterRequestCallback2(OrbisCompanionHttpdRequestCallback function, void* param); +s32 PS4_SYSV_ABI sceCompanionHttpdSetBody(const char* body, u64 bodySize, + OrbisCompanionHttpdResponse* response); +s32 PS4_SYSV_ABI sceCompanionHttpdSetStatus(s32 status, OrbisCompanionHttpdResponse* response); +s32 PS4_SYSV_ABI sceCompanionHttpdStart(); +s32 PS4_SYSV_ABI sceCompanionHttpdStop(); +s32 PS4_SYSV_ABI sceCompanionHttpdTerminate(); +s32 PS4_SYSV_ABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback(); +s32 PS4_SYSV_ABI sceCompanionHttpdUnregisterRequestCallback(); + +void RegisterlibSceCompanionHttpd(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::CompanionHttpd \ No newline at end of file diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 5ef4b259d..2ab46d3a0 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -9,6 +9,7 @@ #include "core/libraries/audio3d/audio3d.h" #include "core/libraries/avplayer/avplayer.h" #include "core/libraries/camera/camera.h" +#include "core/libraries/companion/companion_httpd.h" #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/game_live_streaming/gamelivestreaming.h" #include "core/libraries/gnmdriver/gnmdriver.h" @@ -124,6 +125,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Ulobjmgr::RegisterlibSceUlobjmgr(sym); Libraries::SigninDialog::RegisterlibSceSigninDialog(sym); Libraries::Camera::RegisterlibSceCamera(sym); + Libraries::CompanionHttpd::RegisterlibSceCompanionHttpd(sym); } } // namespace Libraries From 4d769d9c7ebf14d8e993cb72c565f1d8209c92c0 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Fri, 16 May 2025 09:41:22 -0500 Subject: [PATCH 040/141] Proper error handling for MapMemory errors (#2938) * Properly handle ENOMEM error return in MapMemory Needed for Assassin's Creed Unity to behave properly. * Change error message If I left the message as-is, we'd probably see inexperienced people claiming the assert means your device needs more memory, which is completely false. * Clang You know you're doing something right when Clang complains. * Attempt to handle MemoryMapFlags::NoOverwrite Based on my interpretation of red_prig's descriptions. These changes are untested, as I'm not able to test right now. * Fix flag description * Move overwrite check to while condition --- src/core/memory.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 13290336c..a08f8b0e9 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -376,19 +376,22 @@ int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, M // To account for this, unmap any reserved areas within this mapping range first. auto unmap_addr = mapped_addr; auto unmap_size = size; - while (!vma.IsMapped() && unmap_addr < mapped_addr + size && remaining_size < size) { + // If flag NoOverwrite is provided, don't overwrite mapped VMAs. + // When it isn't provided, VMAs can be overwritten regardless of if they're mapped. + while ((False(flags & MemoryMapFlags::NoOverwrite) || !vma.IsMapped()) && + unmap_addr < mapped_addr + size && remaining_size < size) { auto unmapped = UnmapBytesFromEntry(unmap_addr, vma, unmap_size); unmap_addr += unmapped; unmap_size -= unmapped; vma = FindVMA(unmap_addr)->second; } - // This should return SCE_KERNEL_ERROR_ENOMEM but rarely happens. vma = FindVMA(mapped_addr)->second; remaining_size = vma.base + vma.size - mapped_addr; - ASSERT_MSG(!vma.IsMapped() && remaining_size >= size, - "Memory region {:#x} to {:#x} isn't free enough to map region {:#x} to {:#x}", - vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); + if (vma.IsMapped() || remaining_size < size) { + LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at address {:#x}", size, mapped_addr); + return ORBIS_KERNEL_ERROR_ENOMEM; + } } // Find the first free area starting with provided virtual address. From ffd31589cf2c51b744a97456ed3781d319a70f65 Mon Sep 17 00:00:00 2001 From: mailwl Date: Fri, 16 May 2025 19:22:47 +0300 Subject: [PATCH 041/141] Fix reading not existing savedata (#2941) * Fix reading not existing savedata * alloc save memory instead return error * save memory saze to save slot instead of global * remove unused enum * remove unneeded memory clean --- CMakeLists.txt | 1 + src/core/libraries/save_data/save_memory.cpp | 15 +++++----- src/core/libraries/save_data/save_memory.h | 7 +++-- src/core/libraries/save_data/savedata.cpp | 28 +++---------------- src/core/libraries/save_data/savedata_error.h | 27 ++++++++++++++++++ 5 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 src/core/libraries/save_data/savedata_error.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e14b8467..ef2425aff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,6 +409,7 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp src/core/libraries/save_data/save_memory.h src/core/libraries/save_data/savedata.cpp src/core/libraries/save_data/savedata.h + src/core/libraries/save_data/savedata_error.h src/core/libraries/save_data/dialog/savedatadialog.cpp src/core/libraries/save_data/dialog/savedatadialog.h src/core/libraries/save_data/dialog/savedatadialog_ui.cpp diff --git a/src/core/libraries/save_data/save_memory.cpp b/src/core/libraries/save_data/save_memory.cpp index 13e122c60..ab3ce2d4f 100644 --- a/src/core/libraries/save_data/save_memory.cpp +++ b/src/core/libraries/save_data/save_memory.cpp @@ -10,15 +10,14 @@ #include #include -#include - -#include "common/assert.h" +#include "boost/icl/concept/interval.hpp" #include "common/elf_info.h" #include "common/logging/log.h" #include "common/path_util.h" #include "common/singleton.h" #include "common/thread.h" #include "core/file_sys/fs.h" +#include "core/libraries/system/msgdialog_ui.h" #include "save_instance.h" using Common::FS::IOFile; @@ -35,11 +34,12 @@ namespace Libraries::SaveData::SaveMemory { static Core::FileSys::MntPoints* g_mnt = Common::Singleton::Instance(); struct SlotData { - OrbisUserServiceUserId user_id; + OrbisUserServiceUserId user_id{}; std::string game_serial; std::filesystem::path folder_path; PSF sfo; std::vector memory_cache; + size_t memory_cache_size{}; }; static std::mutex g_slot_mtx; @@ -97,7 +97,8 @@ std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id, return SaveInstance::MakeDirSavePath(user_id, Common::ElfInfo::Instance().GameSerial(), dir); } -size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial) { +size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial, + size_t memory_size) { std::lock_guard lck{g_slot_mtx}; const auto save_dir = GetSavePath(user_id, slot_id, game_serial); @@ -109,6 +110,7 @@ size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_ .folder_path = save_dir, .sfo = {}, .memory_cache = {}, + .memory_cache_size = memory_size, }; SaveInstance::SetupDefaultParamSFO(data.sfo, GetSaveDir(slot_id), std::string{game_serial}); @@ -196,9 +198,9 @@ void ReadMemory(u32 slot_id, void* buf, size_t buf_size, int64_t offset) { auto& data = g_attached_slots[slot_id]; auto& memory = data.memory_cache; if (memory.empty()) { // Load file + memory.resize(data.memory_cache_size); IOFile f{data.folder_path / FilenameSaveDataMemory, Common::FS::FileAccessMode::Read}; if (f.IsOpen()) { - memory.resize(f.GetSize()); f.Seek(0); f.ReadSpan(std::span{memory}); } @@ -222,5 +224,4 @@ void WriteMemory(u32 slot_id, void* buf, size_t buf_size, int64_t offset) { Backup::NewRequest(data.user_id, data.game_serial, GetSaveDir(slot_id), Backup::OrbisSaveDataEventType::__DO_NOT_SAVE); } - } // namespace Libraries::SaveData::SaveMemory \ No newline at end of file diff --git a/src/core/libraries/save_data/save_memory.h b/src/core/libraries/save_data/save_memory.h index 681865634..7765b04cd 100644 --- a/src/core/libraries/save_data/save_memory.h +++ b/src/core/libraries/save_data/save_memory.h @@ -4,13 +4,13 @@ #pragma once #include -#include "save_backup.h" +#include "core/libraries/save_data/save_backup.h" class PSF; namespace Libraries::SaveData { using OrbisUserServiceUserId = s32; -} +} // namespace Libraries::SaveData namespace Libraries::SaveData::SaveMemory { @@ -22,7 +22,8 @@ void PersistMemory(u32 slot_id, bool lock = true); std::string_view game_serial); // returns the size of the save memory if exists -size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial); +size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial, + size_t memory_size); // Write the icon. Set buf to null to read the standard icon. void SetIcon(u32 slot_id, void* buf = nullptr, size_t buf_size = 0); diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index e9ad77d69..932bcc1ec 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include "common/assert.h" @@ -20,7 +19,9 @@ #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" #include "core/libraries/save_data/savedata.h" +#include "core/libraries/save_data/savedata_error.h" #include "core/libraries/system/msgdialog.h" +#include "core/libraries/system/msgdialog_ui.h" #include "save_backup.h" #include "save_instance.h" #include "save_memory.h" @@ -33,27 +34,6 @@ using Common::ElfInfo; namespace Libraries::SaveData { -enum class Error : u32 { - OK = 0, - USER_SERVICE_NOT_INITIALIZED = 0x80960002, - PARAMETER = 0x809F0000, - NOT_INITIALIZED = 0x809F0001, - OUT_OF_MEMORY = 0x809F0002, - BUSY = 0x809F0003, - NOT_MOUNTED = 0x809F0004, - EXISTS = 0x809F0007, - NOT_FOUND = 0x809F0008, - NO_SPACE_FS = 0x809F000A, - INTERNAL = 0x809F000B, - MOUNT_FULL = 0x809F000C, - BAD_MOUNTED = 0x809F000D, - BROKEN = 0x809F000F, - INVALID_LOGIN_USER = 0x809F0011, - MEMORY_NOT_READY = 0x809F0012, - BACKUP_BUSY = 0x809F0013, - BUSY_FOR_SAVING = 0x809F0016, -}; - enum class OrbisSaveDataSaveDataMemoryOption : u32 { NONE = 0, SET_PARAM = 1 << 0, @@ -1593,8 +1573,8 @@ Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetu } try { - size_t existed_size = - SaveMemory::SetupSaveMemory(setupParam->userId, slot_id, g_game_serial); + size_t existed_size = SaveMemory::SetupSaveMemory(setupParam->userId, slot_id, + g_game_serial, setupParam->memorySize); if (existed_size == 0) { // Just created if (g_fw_ver >= ElfInfo::FW_45 && setupParam->initParam != nullptr) { auto& sfo = SaveMemory::GetParamSFO(slot_id); diff --git a/src/core/libraries/save_data/savedata_error.h b/src/core/libraries/save_data/savedata_error.h new file mode 100644 index 000000000..ef347e855 --- /dev/null +++ b/src/core/libraries/save_data/savedata_error.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +namespace Libraries::SaveData { +enum class Error : u32 { + OK = 0, + USER_SERVICE_NOT_INITIALIZED = 0x80960002, + PARAMETER = 0x809F0000, + NOT_INITIALIZED = 0x809F0001, + OUT_OF_MEMORY = 0x809F0002, + BUSY = 0x809F0003, + NOT_MOUNTED = 0x809F0004, + EXISTS = 0x809F0007, + NOT_FOUND = 0x809F0008, + NO_SPACE_FS = 0x809F000A, + INTERNAL = 0x809F000B, + MOUNT_FULL = 0x809F000C, + BAD_MOUNTED = 0x809F000D, + BROKEN = 0x809F000F, + INVALID_LOGIN_USER = 0x809F0011, + MEMORY_NOT_READY = 0x809F0012, + BACKUP_BUSY = 0x809F0013, + BUSY_FOR_SAVING = 0x809F0016, +}; +} // namespace Libraries::SaveData From a0acb471850647193e1ace1c98de16573645fe15 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Fri, 16 May 2025 16:21:13 -0500 Subject: [PATCH 042/141] Only align when flags Fixed is not present (#2945) Should fix some remaining memory issues. --- src/core/memory.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index a08f8b0e9..ca6a0d6cd 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -362,11 +362,8 @@ int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, M return ORBIS_KERNEL_ERROR_ENOMEM; } - // When virtual addr is zero, force it to virtual_base. The guest cannot pass Fixed - // flag so we will take the branch that searches for free (or reserved) mappings. - virtual_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; - alignment = alignment > 0 ? alignment : 16_KB; - VAddr mapped_addr = alignment > 0 ? Common::AlignUp(virtual_addr, alignment) : virtual_addr; + // Limit the minumum address to SystemManagedVirtualBase to prevent hardware-specific issues. + VAddr mapped_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; // Fixed mapping means the virtual address must exactly match the provided one. if (True(flags & MemoryMapFlags::Fixed)) { @@ -396,7 +393,9 @@ int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, M // Find the first free area starting with provided virtual address. if (False(flags & MemoryMapFlags::Fixed)) { - mapped_addr = SearchFree(mapped_addr, size, alignment); + // Provided address needs to be aligned before we can map. + alignment = alignment > 0 ? alignment : 16_KB; + mapped_addr = SearchFree(Common::AlignUp(mapped_addr, alignment), size, alignment); if (mapped_addr == -1) { // No suitable memory areas to map to return ORBIS_KERNEL_ERROR_ENOMEM; From 251d0f0d7cc9bebc1ed80629abc075cdad3f386a Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 17 May 2025 11:38:04 +0300 Subject: [PATCH 043/141] [ci skip] Qt GUI: Update Translation. (#2910) Co-authored-by: georgemoralis <4313123+georgemoralis@users.noreply.github.com> --- src/qt_gui/translations/en_US.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qt_gui/translations/en_US.ts b/src/qt_gui/translations/en_US.ts index 780f089e8..cc854120f 100644 --- a/src/qt_gui/translations/en_US.ts +++ b/src/qt_gui/translations/en_US.ts @@ -1347,10 +1347,6 @@ Game List Game List - - * Unsupported Vulkan Version - * Unsupported Vulkan Version - Download Cheats For All Installed Games Download Cheats For All Installed Games @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer From a09f7158b899dfeb3a0e54d91d358f3f3bfa6643 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 17 May 2025 11:38:38 +0300 Subject: [PATCH 044/141] New translations en_us.ts (Korean) (#2943) --- src/qt_gui/translations/ko_KR.ts | 206 +++++++++++++++---------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/src/qt_gui/translations/ko_KR.ts b/src/qt_gui/translations/ko_KR.ts index b79959d38..b735a0d49 100644 --- a/src/qt_gui/translations/ko_KR.ts +++ b/src/qt_gui/translations/ko_KR.ts @@ -7,15 +7,15 @@ AboutDialog About shadPS4 - About shadPS4 + shadPS4에 관하여 shadPS4 is an experimental open-source emulator for the PlayStation 4. - shadPS4 is an experimental open-source emulator for the PlayStation 4. + shadPS4는 PlayStation 4용 실험적인 오픈 소스 에뮬레이터입니다. This software should not be used to play games you have not legally obtained. - This software should not be used to play games you have not legally obtained. + 이 소프트웨어는 합법적으로 얻지 않은 게임을 플레이하는 데 사용되어서는 안 됩니다. @@ -26,238 +26,238 @@ Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n - Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + 치트/패치는 실험적인 기능입니다.\n사용 시 주의하시기 바랍니다.\n\n치트를 개별적으로 다운로드하려면, 저장소를 선택한 후 다운로드 버튼을 클릭하세요.\n패치 탭에서는 모든 패치를 한 번에 다운로드할 수 있으며, 원하는 항목을 선택하고 저장할 수 있습니다.\n\n치트/패치는 저희가 개발한 것이 아니므로,\n문제가 발생하면 해당 치트 제작자에게 문의해 주세요.\n\n새로운 치트를 만들었나요? 방문해 주세요:\n No Image Available - No Image Available + 사용 가능한 이미지 없음 Serial: - Serial: + 시리얼: Version: - Version: + 버전: Size: - Size: + 사이즈: Select Cheat File: - Select Cheat File: + 치트 파일 선택: Repository: - Repository: + 저장소: Download Cheats - Download Cheats + 치트 다운로드 Delete File - Delete File + 파일 삭제 No files selected. - No files selected. + 파일 선택되지 않음. You can delete the cheats you don't want after downloading them. - You can delete the cheats you don't want after downloading them. + 다운로드한 후 원하지 않는 치트는 삭제할 수 있습니다. Do you want to delete the selected file?\n%1 - Do you want to delete the selected file?\n%1 + 선택한 파일을 삭제하시겠습니까?\n%1 Select Patch File: - Select Patch File: + 패치 파일 선택: Download Patches - Download Patches + 패치 다운로드 Save - Save + 저장 Cheats - Cheats + 치트 Patches - Patches + 패치 Error - Error + 오류 No patch selected. - No patch selected. + 패치 선택되지 않음. Unable to open files.json for reading. - Unable to open files.json for reading. + Files.json을 읽기 위해 열 수 없습니다. No patch file found for the current serial. - No patch file found for the current serial. + 현재 시리얼에 해당하는 패치 파일을 찾을 수 없습니다. Unable to open the file for reading. - Unable to open the file for reading. + 파일을 읽기 위해 열 수 없습니다. Unable to open the file for writing. - Unable to open the file for writing. + 파일을 쓰기 위해 열 수 없습니다. Failed to parse XML: - Failed to parse XML: + XML 구문 분석에 실패했습니다: Success - Success + 성공 Options saved successfully. - Options saved successfully. + 옵션이 성공적으로 저장되었습니다. Invalid Source - Invalid Source + 잘못된 출처 The selected source is invalid. - The selected source is invalid. + 선택한 출처가 올바르지 않습니다. File Exists - File Exists + 파일이 이미 존재합니다 File already exists. Do you want to replace it? - File already exists. Do you want to replace it? + 파일이 이미 존재합니다. 덮어쓰시겠습니까? Failed to save file: - Failed to save file: + 파일 저장 실패: Failed to download file: - Failed to download file: + 파일 다운로드 실패: Cheats Not Found - Cheats Not Found + 치트 찾을 수 없음 No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. - No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. + 선택한 저장소의 이 버전에서 해당 게임에 대한 치트를 찾을 수 없습니다. 다른 저장소나 게임의 다른 버전을 시도해 보세요. Cheats Downloaded Successfully - Cheats Downloaded Successfully + 치트가 성공적으로 다운로드되었습니다 You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. - You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. + 선택한 저장소에서 이 게임 버전의 치트를 성공적으로 다운로드했습니다. 다른 저장소에서 다운로드할 수 있는 경우, 목록에서 파일을 선택하여 사용할 수도 있습니다. Failed to save: - Failed to save: + 저장 실패: Failed to download: - Failed to download: + 다운로드 실패: Download Complete - Download Complete + 다운로드 완료 Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. - Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. + 패치가 성공적으로 다운로드되었습니다! 모든 게임에 적용 가능한 모든 패치가 다운로드되었으므로, 치트처럼 각 게임마다 개별적으로 다운로드할 필요가 없습니다. 만약 패치가 나타나지 않는다면, 해당 게임의 특정 시리얼 및 버전에 해당 패치가 없기 때문일 수 있습니다. Failed to parse JSON data from HTML. - Failed to parse JSON data from HTML. + HTML에서 JSON 데이터를 구문 분석하는 데 실패했습니다. Failed to retrieve HTML page. - Failed to retrieve HTML page. + HTML 페이지를 가져오지 못했습니다. The game is in version: %1 - The game is in version: %1 + 게임 버전: %1 The downloaded patch only works on version: %1 - The downloaded patch only works on version: %1 + 다운로드한 패치는 버전 %1 에서만 작동합니다. You may need to update your game. - You may need to update your game. + 게임을 업데이트해야 할 수도 있습니다. Incompatibility Notice - Incompatibility Notice + 호환성 경고 Failed to open file: - Failed to open file: + 파일을 열지 못했습니다: XML ERROR: - XML ERROR: + XML 오류: Failed to open files.json for writing - Failed to open files.json for writing + files.json 파일을 쓰기 위해 열지 못했습니다 Author: - Author: + 제작자: Directory does not exist: - Directory does not exist: + 디렉터리가 존재하지 않습니다: Failed to open files.json for reading. - Failed to open files.json for reading. + files.json 파일을 읽기 위해 열지 못했습니다. Name: - Name: + 이름: Can't apply cheats before the game is started - Can't apply cheats before the game is started + 게임이 시작되기 전에 치트를 적용할 수 없습니다 Close - Close + 닫기 CheckUpdate Auto Updater - Auto Updater + 자동 업데이트 Error - Error + 오류 Network error: - Network error: + 네트워크 오류: The Auto Updater allows up to 60 update checks per hour.\nYou have reached this limit. Please try again later. @@ -265,91 +265,91 @@ Failed to parse update information. - Failed to parse update information. + 업데이트 정보 구문 분석에 실패했습니다. No pre-releases found. - No pre-releases found. + 사전 릴리스가 없습니다. Invalid release data. - Invalid release data. + 잘못된 릴리스 데이터입니다. No download URL found for the specified asset. - No download URL found for the specified asset. + 지정된 자산에 대한 다운로드 URL을 찾을 수 없습니다. Your version is already up to date! - Your version is already up to date! + 버전이 이미 최신입니다! Update Available - Update Available + 업데이트 가능 Update Channel - Update Channel + 업데이트 채널 Current Version - Current Version + 현재 버전 Latest Version - Latest Version + 최신 버전 Do you want to update? - Do you want to update? + 업데이트하시겠습니까? Show Changelog - Show Changelog + 변경 사항 보기 Check for Updates at Startup - Check for Updates at Startup + 시작할 때 업데이트 확인 Update - Update + 업데이트 No - No + 아니요 Hide Changelog - Hide Changelog + 변경 사항 숨기기 Changes - Changes + 변경 사항 Network error occurred while trying to access the URL - Network error occurred while trying to access the URL + URL에 접근하는 동안 네트워크 오류가 발생했습니다 Download Complete - Download Complete + 다운로드 완료 The update has been downloaded, press OK to install. - The update has been downloaded, press OK to install. + 업데이트가 다운로드 되었습니다. 설치하려면 확인을 눌러주세요. Failed to save the update file at - Failed to save the update file at + 업데이트 파일을 다음 위치에 저장하지 못했습니다 Starting Update... - Starting Update... + 업데이트를 시작합니다... Failed to create the update script file - Failed to create the update script file + 업데이트 스크립트 파일을 생성하지 못했습니다 @@ -407,83 +407,83 @@ ControlSettings Configure Controls - Configure Controls + 컨트롤 설정 D-Pad - D-Pad + D-패드 Up - Up + Left - Left + 왼쪽 Right - Right + 오른쪽 Down - Down + 아래 Left Stick Deadzone (def:2 max:127) - Left Stick Deadzone (def:2 max:127) + 왼쪽 스틱 데드존 (기본값:2 최대값:127) Left Deadzone - Left Deadzone + 왼쪽 데드존 Left Stick - Left Stick + 왼쪽 스틱 Config Selection - Config Selection + 설정 선택 Common Config - Common Config + 공통 설정 Use per-game configs - Use per-game configs + 게임 별 설정 사용 L1 / LB - L1 / LB + L1 / LB L2 / LT - L2 / LT + L2 / LT Back - Back + 뒤로 R1 / RB - R1 / RB + R1 / RB R2 / RT - R2 / RT + R2 / RT L3 - L3 + L3 Options / Start - Options / Start + 옵션 / 시작 R3 - R3 + R3 Face Buttons From e2513d50bed1aea85d041425c1ce9891d08bac3b Mon Sep 17 00:00:00 2001 From: tlarok <116431383+tlarok@users.noreply.github.com> Date: Sat, 17 May 2025 10:39:35 +0200 Subject: [PATCH 045/141] New kbm icon (#2898) * Update kbm_gui.cpp * Update kbm_gui.cpp * Update kbm_gui.h * Update kbm_gui.cpp * lunix test * linux test * Update kbm_gui.h * Update kbm_gui.cpp * Update kbm_gui.cpp * Update kbm_gui.cpp * Update kbm_gui.cpp * Update kbm_gui.h * Update kbm_gui.cpp * Update kbm_gui.h * Update kbm_gui.cpp * Update kbm_gui.h * Update kbm_gui.cpp * kbm_gui.cpp's names fix just cleaning my code * name fix * Update kbm_gui.cpp * Update kbm_gui.h * Update kbm_gui.cpp * Update src/qt_gui/kbm_gui.cpp Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> * clean up from main * bruh, welp here we go again * Update KBM.png * resize kbm.png * Update kbm_gui.ui * test update kbm_gui.ui --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- src/images/KBM.png | Bin 241859 -> 136232 bytes src/qt_gui/kbm_gui.ui | 10 ++++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/images/KBM.png b/src/images/KBM.png index 37f52d549da567eb88eae45a60b174def40691b2..feab9fa0ff9803998cdb191d2a26243b843fd7a5 100644 GIT binary patch literal 136232 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 literal 241859 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 + + 48 + Qt::FocusPolicy::NoFocus @@ -841,6 +844,9 @@ 0 + + 48 + Qt::FocusPolicy::NoFocus @@ -983,8 +989,8 @@ - 500 - 200 + 424 + 250 From 5f447a815e1614b791f669c465a05c7279634b5d Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Sat, 17 May 2025 10:41:16 -0300 Subject: [PATCH 046/141] SaveDataDialog: fix possible null access (#2947) --- src/core/libraries/save_data/dialog/savedatadialog_ui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp index edb5caa07..05df67eeb 100644 --- a/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp +++ b/src/core/libraries/save_data/dialog/savedatadialog_ui.cpp @@ -155,7 +155,7 @@ SaveDialogState::SaveDialogState(const OrbisSaveDataDialogParam& param) { if (item->focusPos != FocusPos::DIRNAME) { this->focus_pos = item->focusPos; - } else { + } else if (item->focusPosDirName != nullptr) { this->focus_pos = item->focusPosDirName->data.to_string(); } this->style = item->itemStyle; From 6ee205d4e2b032ab7dfd19986bd9c45ac01bcd4b Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Sat, 17 May 2025 17:33:24 +0300 Subject: [PATCH 047/141] New Crowdin updates (#2946) * New translations en_us.ts (Korean) * New translations en_us.ts (Romanian) * New translations en_us.ts (French) * New translations en_us.ts (Spanish) * New translations en_us.ts (Arabic) * New translations en_us.ts (Danish) * New translations en_us.ts (German) * New translations en_us.ts (Greek) * New translations en_us.ts (Finnish) * New translations en_us.ts (Hungarian) * New translations en_us.ts (Italian) * New translations en_us.ts (Japanese) * New translations en_us.ts (Lithuanian) * New translations en_us.ts (Dutch) * New translations en_us.ts (Polish) * New translations en_us.ts (Portuguese) * New translations en_us.ts (Russian) * New translations en_us.ts (Slovenian) * New translations en_us.ts (Albanian) * New translations en_us.ts (Swedish) * New translations en_us.ts (Turkish) * New translations en_us.ts (Ukrainian) * New translations en_us.ts (Chinese Simplified) * New translations en_us.ts (Chinese Traditional) * New translations en_us.ts (Vietnamese) * New translations en_us.ts (Portuguese, Brazilian) * New translations en_us.ts (Indonesian) * New translations en_us.ts (Persian) * New translations en_us.ts (Norwegian Bokmal) * New translations en_us.ts (Chinese Simplified) * New translations en_us.ts (Italian) --- src/qt_gui/translations/ar_SA.ts | 8 ++++---- src/qt_gui/translations/da_DK.ts | 8 ++++---- src/qt_gui/translations/de_DE.ts | 8 ++++---- src/qt_gui/translations/el_GR.ts | 8 ++++---- src/qt_gui/translations/es_ES.ts | 8 ++++---- src/qt_gui/translations/fa_IR.ts | 8 ++++---- src/qt_gui/translations/fi_FI.ts | 8 ++++---- src/qt_gui/translations/fr_FR.ts | 8 ++++---- src/qt_gui/translations/hu_HU.ts | 8 ++++---- src/qt_gui/translations/id_ID.ts | 8 ++++---- src/qt_gui/translations/it_IT.ts | 8 ++++---- src/qt_gui/translations/ja_JP.ts | 8 ++++---- src/qt_gui/translations/ko_KR.ts | 8 ++++---- src/qt_gui/translations/lt_LT.ts | 8 ++++---- src/qt_gui/translations/nb_NO.ts | 8 ++++---- src/qt_gui/translations/nl_NL.ts | 8 ++++---- src/qt_gui/translations/pl_PL.ts | 8 ++++---- src/qt_gui/translations/pt_BR.ts | 8 ++++---- src/qt_gui/translations/pt_PT.ts | 8 ++++---- src/qt_gui/translations/ro_RO.ts | 8 ++++---- src/qt_gui/translations/ru_RU.ts | 8 ++++---- src/qt_gui/translations/sl_SI.ts | 8 ++++---- src/qt_gui/translations/sq_AL.ts | 8 ++++---- src/qt_gui/translations/sv_SE.ts | 8 ++++---- src/qt_gui/translations/tr_TR.ts | 8 ++++---- src/qt_gui/translations/uk_UA.ts | 8 ++++---- src/qt_gui/translations/vi_VN.ts | 8 ++++---- src/qt_gui/translations/zh_CN.ts | 8 ++++---- src/qt_gui/translations/zh_TW.ts | 8 ++++---- 29 files changed, 116 insertions(+), 116 deletions(-) diff --git a/src/qt_gui/translations/ar_SA.ts b/src/qt_gui/translations/ar_SA.ts index e434b3259..26e768720 100644 --- a/src/qt_gui/translations/ar_SA.ts +++ b/src/qt_gui/translations/ar_SA.ts @@ -1347,10 +1347,6 @@ Game List قائمة الألعاب - - * Unsupported Vulkan Version - * إصدار Vulkan غير مدعوم - Download Cheats For All Installed Games تحميل الشفرات لجميع الألعاب المثبتة @@ -2051,6 +2047,10 @@ Nightly: نُسخ تحتوي على أحدث الميزات، لكنها أقل Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. افتح مجلد الصور/الأصوات الخاصة بالجوائز المخصصة:\nيمكنك إضافة صور مخصصة للجوائز وصوت مرفق.\nأضف الملفات إلى مجلد custom_trophy بالأسماء التالية:\ntrophy.wav أو trophy.mp3، bronze.png، gold.png، platinum.png، silver.png\nملاحظة: الصوت سيعمل فقط في الإصدارات التي تستخدم QT. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/da_DK.ts b/src/qt_gui/translations/da_DK.ts index 131a989e1..1023c584b 100644 --- a/src/qt_gui/translations/da_DK.ts +++ b/src/qt_gui/translations/da_DK.ts @@ -1347,10 +1347,6 @@ Game List Spiloversigt - - * Unsupported Vulkan Version - * Ikke understøttet Vulkan-version - Download Cheats For All Installed Games Hent snyd til alle installerede spil @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/de_DE.ts b/src/qt_gui/translations/de_DE.ts index c7a18dd99..1d44eb717 100644 --- a/src/qt_gui/translations/de_DE.ts +++ b/src/qt_gui/translations/de_DE.ts @@ -1347,10 +1347,6 @@ Game List Spieleliste - - * Unsupported Vulkan Version - * Nicht unterstützte Vulkan-Version - Download Cheats For All Installed Games Cheats für alle installierten Spiele herunterladen @@ -2054,6 +2050,10 @@ Fügen Sie die Dateien dem Ordner custom_trophy mit folgenden Namen hinzu:\n trophy.wav ODER trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\n Hinweis: Der Sound funktioniert nur in Qt-Versionen. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/el_GR.ts b/src/qt_gui/translations/el_GR.ts index c91e0c731..765185c9e 100644 --- a/src/qt_gui/translations/el_GR.ts +++ b/src/qt_gui/translations/el_GR.ts @@ -1347,10 +1347,6 @@ Game List Λίστα παιχνιδιών - - * Unsupported Vulkan Version - * Μη υποστηριζόμενη έκδοση Vulkan - Download Cheats For All Installed Games Λήψη Cheats για όλα τα εγκατεστημένα παιχνίδια @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/es_ES.ts b/src/qt_gui/translations/es_ES.ts index 035aac6a3..e73386c96 100644 --- a/src/qt_gui/translations/es_ES.ts +++ b/src/qt_gui/translations/es_ES.ts @@ -1347,10 +1347,6 @@ Game List Lista de Juegos - - * Unsupported Vulkan Version - * Versión de Vulkan no soportada - Download Cheats For All Installed Games Descargar trucos para todos los juegos instalados @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Abre la carpeta de trofeos/sonidos personalizados:\nPuedes añadir imágenes y un audio personalizados a los trofeos.\nAñade los archivos a custom_trophy con los siguientes nombres:\ntrophy.wav o trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNota: El sonido sólo funcionará en versiones QT. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/fa_IR.ts b/src/qt_gui/translations/fa_IR.ts index 552a0ff23..b9c2282fa 100644 --- a/src/qt_gui/translations/fa_IR.ts +++ b/src/qt_gui/translations/fa_IR.ts @@ -1347,10 +1347,6 @@ Game List لیست بازی - - * Unsupported Vulkan Version - شما پشتیبانی نمیشود Vulkan ورژن * - Download Cheats For All Installed Games دانلود چیت برای همه بازی ها @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/fi_FI.ts b/src/qt_gui/translations/fi_FI.ts index 44c668560..c77c63b3e 100644 --- a/src/qt_gui/translations/fi_FI.ts +++ b/src/qt_gui/translations/fi_FI.ts @@ -1347,10 +1347,6 @@ Game List Pelilista - - * Unsupported Vulkan Version - * Ei Tuettu Vulkan-versio - Download Cheats For All Installed Games Lataa Huijaukset Kaikille Asennetuille Peleille @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/fr_FR.ts b/src/qt_gui/translations/fr_FR.ts index 13e1be9f5..7a3f1b51c 100644 --- a/src/qt_gui/translations/fr_FR.ts +++ b/src/qt_gui/translations/fr_FR.ts @@ -1347,10 +1347,6 @@ Game List Liste de jeux - - * Unsupported Vulkan Version - * Version de Vulkan non prise en charge - Download Cheats For All Installed Games Télécharger les Cheats pour tous les jeux installés @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Ouvrez le dossier des images/sons des trophées personnalisés:\nVous pouvez ajouter des images personnalisées aux trophées et aux sons.\nAjoutez les fichiers à custom_trophy avec les noms suivants:\ntrophy.wav OU trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote : Le son ne fonctionnera que dans les versions QT. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/hu_HU.ts b/src/qt_gui/translations/hu_HU.ts index 58857d0d7..e396cc4f5 100644 --- a/src/qt_gui/translations/hu_HU.ts +++ b/src/qt_gui/translations/hu_HU.ts @@ -1347,10 +1347,6 @@ Game List Játéklista - - * Unsupported Vulkan Version - * Nem támogatott Vulkan verzió - Download Cheats For All Installed Games Csalások letöltése minden telepített játékhoz @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/id_ID.ts b/src/qt_gui/translations/id_ID.ts index de19824f7..b4fe48637 100644 --- a/src/qt_gui/translations/id_ID.ts +++ b/src/qt_gui/translations/id_ID.ts @@ -1347,10 +1347,6 @@ Game List Daftar game - - * Unsupported Vulkan Version - * Versi Vulkan Tidak Didukung - Download Cheats For All Installed Games Unduh Cheat Untuk Semua Game Yang Terpasang @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/it_IT.ts b/src/qt_gui/translations/it_IT.ts index 8c9e53611..a1aa0bb3f 100644 --- a/src/qt_gui/translations/it_IT.ts +++ b/src/qt_gui/translations/it_IT.ts @@ -1347,10 +1347,6 @@ Game List Elenco giochi - - * Unsupported Vulkan Version - * Versione Vulkan non supportata - Download Cheats For All Installed Games Scarica Trucchi per tutti i giochi installati @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Apri la cartella personalizzata delle immagini/suoni trofei:\nÈ possibile aggiungere immagini personalizzate ai trofei e un audio.\nAggiungi i file a custom_trophy con i seguenti nomi:\ntrophy.wav OPPURE trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNota: Il suono funzionerà solo nelle versioni QT. + + * Unsupported Vulkan Version + * Versione Vulkan non supportata + TrophyViewer diff --git a/src/qt_gui/translations/ja_JP.ts b/src/qt_gui/translations/ja_JP.ts index 146caa515..7bd7fed8a 100644 --- a/src/qt_gui/translations/ja_JP.ts +++ b/src/qt_gui/translations/ja_JP.ts @@ -1347,10 +1347,6 @@ Game List ゲームリスト - - * Unsupported Vulkan Version - * サポートされていないVulkanバージョン - Download Cheats For All Installed Games すべてのインストール済みゲームのチートをダウンロード @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/ko_KR.ts b/src/qt_gui/translations/ko_KR.ts index b735a0d49..6e5b232a0 100644 --- a/src/qt_gui/translations/ko_KR.ts +++ b/src/qt_gui/translations/ko_KR.ts @@ -1347,10 +1347,6 @@ Game List Game List - - * Unsupported Vulkan Version - * Unsupported Vulkan Version - Download Cheats For All Installed Games Download Cheats For All Installed Games @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/lt_LT.ts b/src/qt_gui/translations/lt_LT.ts index 03ff5a003..a0b047dbb 100644 --- a/src/qt_gui/translations/lt_LT.ts +++ b/src/qt_gui/translations/lt_LT.ts @@ -1347,10 +1347,6 @@ Game List Žaidimų sąrašas - - * Unsupported Vulkan Version - * Nepalaikoma Vulkan versija - Download Cheats For All Installed Games Atsisiųsti sukčiavimus visiems įdiegtiems žaidimams @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/nb_NO.ts b/src/qt_gui/translations/nb_NO.ts index e937287fd..856755865 100644 --- a/src/qt_gui/translations/nb_NO.ts +++ b/src/qt_gui/translations/nb_NO.ts @@ -1347,10 +1347,6 @@ Game List Spilliste - - * Unsupported Vulkan Version - * Ustøttet Vulkan-versjon - Download Cheats For All Installed Games Last ned juks for alle installerte spill @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Åpne mappa med tilpassede bilder og lyder for trofé:\nDu kan legge til tilpassede bilder til trofeer og en lyd.\nLegg filene til custom_trophy med følgende navn:\ntrophy.wav ELLER trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nMerk: Lyden avspilles kun i Qt-versjonen. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/nl_NL.ts b/src/qt_gui/translations/nl_NL.ts index 2d75b74eb..ec676d360 100644 --- a/src/qt_gui/translations/nl_NL.ts +++ b/src/qt_gui/translations/nl_NL.ts @@ -1347,10 +1347,6 @@ Game List Lijst met spellen - - * Unsupported Vulkan Version - * Niet ondersteunde Vulkan-versie - Download Cheats For All Installed Games Download cheats voor alle geïnstalleerde spellen @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/pl_PL.ts b/src/qt_gui/translations/pl_PL.ts index bd59a1894..c1343cd2e 100644 --- a/src/qt_gui/translations/pl_PL.ts +++ b/src/qt_gui/translations/pl_PL.ts @@ -1347,10 +1347,6 @@ Game List Lista gier - - * Unsupported Vulkan Version - * Nieobsługiwana wersja Vulkan - Download Cheats For All Installed Games Pobierz kody do wszystkich zainstalowanych gier @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Otwórz niestandardowy folder obrazów/dźwięków:\nMożesz dodać własne obrazy dla trofeów i ich dźwięki.\nDodaj pliki do custom_trophy o następujących nazwach:\ntrophy.wav LUB trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nUwaga: Dźwięki działają tylko w wersji QT. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/pt_BR.ts b/src/qt_gui/translations/pt_BR.ts index 584d6dc19..34d31f240 100644 --- a/src/qt_gui/translations/pt_BR.ts +++ b/src/qt_gui/translations/pt_BR.ts @@ -1347,10 +1347,6 @@ Game List Lista de Jogos - - * Unsupported Vulkan Version - * Versão Vulkan não suportada - Download Cheats For All Installed Games Baixar Trapaças para Todos os Jogos Instalados @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Abrir a pasta de imagens e sons de troféus personalizados:\nVocê pode adicionar imagens personalizadas aos troféus e um áudio.\nAdicione os arquivos na pasta custom_trophy com os seguintes nomes:\ntrophy.wav OU trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nObservação: O som funcionará apenas em versões Qt. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/pt_PT.ts b/src/qt_gui/translations/pt_PT.ts index 70a73afe7..bfc3900dc 100644 --- a/src/qt_gui/translations/pt_PT.ts +++ b/src/qt_gui/translations/pt_PT.ts @@ -1347,10 +1347,6 @@ Game List Lista de Jogos - - * Unsupported Vulkan Version - * Versão do Vulkan não suportada - Download Cheats For All Installed Games Transferir Cheats para Todos os Jogos Instalados @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Abra a pasta de imagens/sons de troféus personalizados:\nPoderá adicionar imagens personalizadas aos troféus e um áudio.\nAdicione os arquivos na pasta custom_trophy com os seguintes nomes:\ntrophy.mp3 ou trophy.wav, bronze.png, gold.png, platinum.png, silver.png\nObservação: O som funcionará apenas nas versões Qt. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/ro_RO.ts b/src/qt_gui/translations/ro_RO.ts index 78dd79c53..a05bb06a8 100644 --- a/src/qt_gui/translations/ro_RO.ts +++ b/src/qt_gui/translations/ro_RO.ts @@ -1347,10 +1347,6 @@ Game List Lista jocurilor - - * Unsupported Vulkan Version - * Versiune Vulkan nesuportată - Download Cheats For All Installed Games Descarcă Cheats pentru toate jocurile instalate @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/ru_RU.ts b/src/qt_gui/translations/ru_RU.ts index 0f16efc2c..176c6e737 100644 --- a/src/qt_gui/translations/ru_RU.ts +++ b/src/qt_gui/translations/ru_RU.ts @@ -1347,10 +1347,6 @@ Game List Список игр - - * Unsupported Vulkan Version - * Неподдерживаемая версия Vulkan - Download Cheats For All Installed Games Скачать читы для всех установленных игр @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Открыть папку с пользовательскими изображениями/звуками трофеев:\nВы можете добавить пользовательские изображения к трофеям и аудио.\nДобавьте файлы в custom_trophy со следующими именами:\ntrophy.wav ИЛИ trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nПримечание: звук будет работать только в QT-версии. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/sl_SI.ts b/src/qt_gui/translations/sl_SI.ts index ab61a5d3a..47c5f5534 100644 --- a/src/qt_gui/translations/sl_SI.ts +++ b/src/qt_gui/translations/sl_SI.ts @@ -1347,10 +1347,6 @@ Game List Game List - - * Unsupported Vulkan Version - * Unsupported Vulkan Version - Download Cheats For All Installed Games Download Cheats For All Installed Games @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/sq_AL.ts b/src/qt_gui/translations/sq_AL.ts index 50314a9b2..003b089f6 100644 --- a/src/qt_gui/translations/sq_AL.ts +++ b/src/qt_gui/translations/sq_AL.ts @@ -1347,10 +1347,6 @@ Game List Lista e lojërave - - * Unsupported Vulkan Version - * Version i pambështetur i Vulkan - Download Cheats For All Installed Games Shkarko mashtrime për të gjitha lojërat e instaluara @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Hap dosjen e imazheve/tingujve të trofeve të personalizuar:\nMund të shtosh imazhe të personalizuara për trofetë dhe një audio.\nShto skedarët në dosjen custom_trophy me emrat që vijojnë:\ntrophy.wav ose trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nShënim: Tingulli do të punojë vetëm në versionet QT. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/sv_SE.ts b/src/qt_gui/translations/sv_SE.ts index ce0da785c..9533864b8 100644 --- a/src/qt_gui/translations/sv_SE.ts +++ b/src/qt_gui/translations/sv_SE.ts @@ -1347,10 +1347,6 @@ Game List Spellista - - * Unsupported Vulkan Version - * Vulkan-versionen stöds inte - Download Cheats For All Installed Games Hämta fusk för alla installerade spel @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Öppna mappen för anpassade trofébilder/ljud:\nDu kan lägga till egna bilder till troféerna och ett ljud.\nLägg till filerna i custom_trophy med följande namn:\ntrophy.wav ELLER trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nObservera: Ljudet fungerar endast i QT-versioner. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index f5f7b65e5..9539ca139 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -1347,10 +1347,6 @@ Game List Oyun Listesi - - * Unsupported Vulkan Version - * Desteklenmeyen Vulkan Sürümü - Download Cheats For All Installed Games Tüm Yüklenmiş Oyunlar İçin Hileleri İndir @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Özel kupa görüntüleri/sesleri klasörünü aç:\nKupalara özel görüntüler ve sesler ekleyebilirsiniz.\nDosyaları aşağıdaki adlarla custom_trophy'ye ekleyin:\ntrophy.wav ya da trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNot: Ses yalnızca QT sürümlerinde çalışacaktır. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/uk_UA.ts b/src/qt_gui/translations/uk_UA.ts index 3eb88bcab..8b83ae62f 100644 --- a/src/qt_gui/translations/uk_UA.ts +++ b/src/qt_gui/translations/uk_UA.ts @@ -1347,10 +1347,6 @@ Game List Список ігор - - * Unsupported Vulkan Version - * Непідтримувана версія Vulkan - Download Cheats For All Installed Games Завантажити чити для усіх встановлених ігор @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Відкрити папку користувацьких зображень трофеїв/звуків:\nВи можете додати користувацькі зображення до трофеїв та звук.\nДодайте файли до теки custom_trophy з такими назвами:\ntrophy.wav АБО trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nПримітка: Звук буде працювати лише у версіях ShadPS4 з графічним інтерфейсом. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/vi_VN.ts b/src/qt_gui/translations/vi_VN.ts index c657888bf..1e26f626c 100644 --- a/src/qt_gui/translations/vi_VN.ts +++ b/src/qt_gui/translations/vi_VN.ts @@ -1347,10 +1347,6 @@ Game List Danh sách trò chơi - - * Unsupported Vulkan Version - * Phiên bản Vulkan không được hỗ trợ - Download Cheats For All Installed Games Tải xuống cheat cho tất cả các trò chơi đã cài đặt @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer diff --git a/src/qt_gui/translations/zh_CN.ts b/src/qt_gui/translations/zh_CN.ts index 120310810..2bc635c41 100644 --- a/src/qt_gui/translations/zh_CN.ts +++ b/src/qt_gui/translations/zh_CN.ts @@ -1347,10 +1347,6 @@ Game List 游戏列表 - - * Unsupported Vulkan Version - * 不支持的 Vulkan 版本 - Download Cheats For All Installed Games 下载所有已安装游戏的作弊码 @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. 打开自定义奖杯图像/声音文件夹:\n您可以自定义奖杯图像和声音。\n将文件添加到 custom_trophy 文件夹中,文件名如下:\ntrophy.wav 或 trophy.mp3、bronze.png、gold.png、platinum.png、silver.png。\n注意:自定义声音只能在 QT 版本中生效。 + + * Unsupported Vulkan Version + * 不支持的 Vulkan 版本 + TrophyViewer diff --git a/src/qt_gui/translations/zh_TW.ts b/src/qt_gui/translations/zh_TW.ts index bd051651d..320f73c83 100644 --- a/src/qt_gui/translations/zh_TW.ts +++ b/src/qt_gui/translations/zh_TW.ts @@ -1347,10 +1347,6 @@ Game List 遊戲列表 - - * Unsupported Vulkan Version - * 不支援的 Vulkan 版本 - Download Cheats For All Installed Games 下載所有已安裝遊戲的金手指 @@ -2050,6 +2046,10 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. 開啟自訂獎盃影像/聲音資料夾:\n您可以將自訂影像新增至獎盃和音訊。 \n將檔案加入 custom_tropy,名稱如下:\ntropy.wav OR trophy.mp3、bronze.png、gold.png、platinum.png、silver.png\n注意:聲音僅在 QT 版本中有效。 + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + TrophyViewer From 5eb58799fe937443542f9dac231bb236969c7fa3 Mon Sep 17 00:00:00 2001 From: Vinicius Rangel Date: Sat, 17 May 2025 12:00:35 -0300 Subject: [PATCH 048/141] SaveData: respect install dir in param.sfo to select the game save folder (#2951) --- src/core/libraries/save_data/savedata.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 932bcc1ec..0731392cd 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -316,7 +316,9 @@ static std::array, 16> g_mount_slots; static void initialize() { g_initialized = true; - g_game_serial = ElfInfo::Instance().GameSerial(); + g_game_serial = Common::Singleton::Instance() + ->GetString("INSTALL_DIR_SAVEDATA") + .value_or(ElfInfo::Instance().GameSerial()); g_fw_ver = ElfInfo::Instance().FirmwareVer(); Backup::StartThread(); } From 034ae8cffac3bd7534f2a9481d06ccaa0e886938 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 18 May 2025 13:16:31 -0700 Subject: [PATCH 049/141] qt: Update save data dir open to use name from PSF. (#2954) --- src/qt_gui/game_info.h | 5 +++++ src/qt_gui/game_list_utils.h | 1 + src/qt_gui/gui_context_menus.h | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/qt_gui/game_info.h b/src/qt_gui/game_info.h index 09e5a4557..723142e1c 100644 --- a/src/qt_gui/game_info.h +++ b/src/qt_gui/game_info.h @@ -79,6 +79,11 @@ public: if (const auto play_time = psf.GetString("PLAY_TIME"); play_time.has_value()) { game.play_time = *play_time; } + if (const auto save_dir = psf.GetString("INSTALL_DIR_SAVEDATA"); save_dir.has_value()) { + game.save_dir = *save_dir; + } else { + game.save_dir = game.serial; + } } return game; } diff --git a/src/qt_gui/game_list_utils.h b/src/qt_gui/game_list_utils.h index 804f0e4b7..e19cf364e 100644 --- a/src/qt_gui/game_list_utils.h +++ b/src/qt_gui/game_list_utils.h @@ -25,6 +25,7 @@ struct GameInfo { std::string version = "Unknown"; std::string region = "Unknown"; std::string fw = "Unknown"; + std::string save_dir = "Unknown"; std::string play_time = "Unknown"; CompatibilityEntry compatibility = CompatibilityEntry{CompatibilityStatus::Unknown}; diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index 2fd4588d2..f435a3e38 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -160,7 +160,7 @@ public: Common::FS::PathToQString(userPath, Common::FS::GetUserPath(Common::FS::PathType::UserDir)); QString saveDataPath = - userPath + "/savedata/1/" + QString::fromStdString(m_games[itemID].serial); + userPath + "/savedata/1/" + QString::fromStdString(m_games[itemID].save_dir); QDir(saveDataPath).mkpath(saveDataPath); QDesktopServices::openUrl(QUrl::fromLocalFile(saveDataPath)); } From 60224e1d2278db88162c85ec93192be5da8ecb4d Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 21 May 2025 13:57:52 +0300 Subject: [PATCH 050/141] New Crowdin updates (#2953) * New translations en_us.ts (Portuguese) * New translations en_us.ts (Portuguese) * New translations en_us.ts (Albanian) * New translations en_us.ts (Russian) * New translations en_us.ts (Norwegian Bokmal) * New translations en_us.ts (French) * New translations en_us.ts (Swedish) * New translations en_us.ts (Norwegian Bokmal) --- src/qt_gui/translations/fr_FR.ts | 2 +- src/qt_gui/translations/nb_NO.ts | 12 +++---- src/qt_gui/translations/pt_PT.ts | 54 ++++++++++++++++---------------- src/qt_gui/translations/ru_RU.ts | 2 +- src/qt_gui/translations/sq_AL.ts | 2 +- src/qt_gui/translations/sv_SE.ts | 2 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/qt_gui/translations/fr_FR.ts b/src/qt_gui/translations/fr_FR.ts index 7a3f1b51c..75e424ad0 100644 --- a/src/qt_gui/translations/fr_FR.ts +++ b/src/qt_gui/translations/fr_FR.ts @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Version de Vulkan non prise en charge diff --git a/src/qt_gui/translations/nb_NO.ts b/src/qt_gui/translations/nb_NO.ts index 856755865..cb209cfb1 100644 --- a/src/qt_gui/translations/nb_NO.ts +++ b/src/qt_gui/translations/nb_NO.ts @@ -1253,11 +1253,11 @@ List View - Liste-visning + Listevisning Grid View - Rute-visning + Rutenettvisning Elf Viewer @@ -1756,7 +1756,7 @@ Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. - Loggfilter:\nFiltrerer loggen for å kun skrive ut spesifikk informasjon.\nEksempler: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical" \nNivåer: Trace, Debug, Info, Warning, Error, Critical - i denne rekkefølgen, et spesifikt nivå demper alle tidligere nivåer i lista og loggfører alle nivåer etter det. + Loggfilter:\nFiltrerer loggen for å kun skrive ut spesifikk informasjon.\nEksempler: «Core:Trace» «Lib.Pad:Debug Common.Filesystem:Error» «*:Critical» \nNivåer: Trace, Debug, Info, Warning, Error, Critical - i denne rekkefølgen, et spesifikt nivå demper alle tidligere nivåer i lista og loggfører alle nivåer etter det. Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. @@ -1788,7 +1788,7 @@ Display Compatibility Data:\nDisplays game compatibility information in table view. Enable "Update Compatibility On Startup" to get up-to-date information. - Vis kompatibilitets-data:\nViser informasjon om spillkompatibilitet i tabellvisning. Bruk "Oppdater kompatibilitets-data ved oppstart" for oppdatert informasjon. + Vis kompatibilitets-data:\nViser informasjon om spillkompatibilitet i tabellvisning. Bruk «Oppdater database ved oppstart» for oppdatert informasjon. Update Compatibility On Startup:\nAutomatically update the compatibility database when shadPS4 starts. @@ -1828,7 +1828,7 @@ Graphics Device:\nOn multiple GPU systems, select the GPU the emulator will use from the drop down list,\nor select "Auto Select" to automatically determine it. - Grafikkenhet:\nSystemer med flere GPU-er, kan emulatoren velge hvilken enhet som skal brukes fra rullegardinlista,\neller velg "Velg automatisk". + Grafikkenhet:\nSystemer med flere GPU-er, kan emulatoren velge hvilken enhet som skal brukes fra rullegardinlista,\neller bruk «Velg automatisk». Width/Height:\nSets the size of the emulator window at launch, which can be resized during gameplay.\nThis is different from the in-game resolution. @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + *Ustøttet Vulkan-versjon diff --git a/src/qt_gui/translations/pt_PT.ts b/src/qt_gui/translations/pt_PT.ts index bfc3900dc..a543d0ec3 100644 --- a/src/qt_gui/translations/pt_PT.ts +++ b/src/qt_gui/translations/pt_PT.ts @@ -543,7 +543,7 @@ Unable to Save - Não é possível salvar + Não foi possível guardar Cannot bind axis values more than once @@ -551,7 +551,7 @@ Save - Salvar + Guardar Apply @@ -559,7 +559,7 @@ Restore Defaults - Restaurar o Padrão + Restaurar Predefinições Cancel @@ -570,11 +570,11 @@ EditorDialog Edit Keyboard + Mouse and Controller input bindings - Editar comandos do Teclado + Mouse e do Controle + Editar configurações de entrada do Teclado + Rato e do Comando Use Per-Game configs - Use uma configuração para cada jogo + Utilizar configurações por jogo Error @@ -582,19 +582,19 @@ Could not open the file for reading - Não foi possível abrir o arquivo para ler + Não foi possível abrir o ficheiro para leitura Could not open the file for writing - Não foi possível abrir o arquivo para escrever + Não foi possível abrir o ficheiro para escrita Save Changes - Salvar mudanças + Guardar as alterações Do you want to save changes? - Salvar as mudanças? + Pretende guardar as alterações? Help @@ -610,7 +610,7 @@ Reset to Default - Resetar ao Padrão + Repor para o Padrão @@ -1150,7 +1150,7 @@ Unable to Save - Não é possível salvar + Não foi possível guardar Cannot bind any unique input more than once @@ -1166,11 +1166,11 @@ Mousewheel cannot be mapped to stick outputs - Roda do rato não pode ser mapeada para saídas empates + Roda do rato não pode ser mapeada para saídas dos manípulos Save - Salvar + Guardar Apply @@ -1178,7 +1178,7 @@ Restore Defaults - Restaurar Definições + Restaurar Predefinições Cancel @@ -1405,43 +1405,43 @@ Play - Play + Reproduzir Pause - Pause + Pausa Stop - Stop + Parar Restart - Restart + Reiniciar Full Screen - Full Screen + Ecrã Inteiro Controllers - Controllers + Comandos Keyboard - Keyboard + Teclado Refresh List - Refresh List + Atualizar Lista Resume - Resume + Continuar Show Labels Under Icons - Show Labels Under Icons + Mostrar Etiquetas Debaixo dos Ícones @@ -2028,7 +2028,7 @@ Cannot create portable user folder - Não é possível criar pasta de utilizador portátil + Não foi possível criar pasta de utilizador portátil %1 already exists @@ -2044,11 +2044,11 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. - Abra a pasta de imagens/sons de troféus personalizados:\nPoderá adicionar imagens personalizadas aos troféus e um áudio.\nAdicione os arquivos na pasta custom_trophy com os seguintes nomes:\ntrophy.mp3 ou trophy.wav, bronze.png, gold.png, platinum.png, silver.png\nObservação: O som funcionará apenas nas versões Qt. + Abra a pasta de imagens/sons de troféus personalizados:\nPoderá adicionar imagens personalizadas aos troféus e um áudio.\nAdicione os ficheiros na pasta custom_trophy com os seguintes nomes:\ntrophy.mp3 ou trophy.wav, bronze.png, gold.png, platinum.png, silver.png\nObservação: O som funcionará apenas nas versões Qt. * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Versão do Vulkan não suportada diff --git a/src/qt_gui/translations/ru_RU.ts b/src/qt_gui/translations/ru_RU.ts index 176c6e737..a56127ece 100644 --- a/src/qt_gui/translations/ru_RU.ts +++ b/src/qt_gui/translations/ru_RU.ts @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Неподдерживаемая версия Vulkan diff --git a/src/qt_gui/translations/sq_AL.ts b/src/qt_gui/translations/sq_AL.ts index 003b089f6..c554a283a 100644 --- a/src/qt_gui/translations/sq_AL.ts +++ b/src/qt_gui/translations/sq_AL.ts @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Version i pambështetur i Vulkan diff --git a/src/qt_gui/translations/sv_SE.ts b/src/qt_gui/translations/sv_SE.ts index 9533864b8..b8fab701c 100644 --- a/src/qt_gui/translations/sv_SE.ts +++ b/src/qt_gui/translations/sv_SE.ts @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Versionen av Vulkan stöds inte From 1b952bf1733de477975c432f22e4aff46e49268b Mon Sep 17 00:00:00 2001 From: Lander Gallastegi Date: Wed, 21 May 2025 18:08:27 +0200 Subject: [PATCH 051/141] ReadConst debug msg (#2964) --- .../backend/spirv/emit_spirv_context_get_set.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 83e8afd78..6442ae9f8 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" +#include "common/logging/log.h" #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" #include "shader_recompiler/ir/attribute.h" @@ -167,6 +168,7 @@ Id EmitReadConst(EmitContext& ctx, IR::Inst* inst) { const auto& srt_flatbuf = ctx.buffers.back(); ASSERT(srt_flatbuf.binding >= 0 && flatbuf_off_dw > 0 && srt_flatbuf.buffer_type == BufferType::ReadConstUbo); + LOG_DEBUG(Render_Recompiler, "ReadConst from flatbuf dword {}", flatbuf_off_dw); const auto [id, pointer_type] = srt_flatbuf[BufferAlias::U32]; const Id ptr{ ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, ctx.ConstU32(flatbuf_off_dw))}; From eb21083078ea81a750914fd5213a5a25635360bf Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 21 May 2025 21:22:38 +0200 Subject: [PATCH 052/141] Lower stack size to clear from 13 to 12 KB (#2967) --- src/core/tls.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/tls.h b/src/core/tls.h index e9e2b9e6a..470553d85 100644 --- a/src/core/tls.h +++ b/src/core/tls.h @@ -53,7 +53,7 @@ template ReturnType ExecuteGuest(PS4_SYSV_ABI ReturnType (*func)(FuncArgs...), CallArgs&&... args) { EnsureThreadInitialized(); // clear stack to avoid trash from EnsureThreadInitialized - ClearStack<13_KB>(); + ClearStack<12_KB>(); return func(std::forward(args)...); } From 786ad6f71e736955197155c5e11e0e6857daa88e Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Wed, 21 May 2025 22:00:11 +0200 Subject: [PATCH 053/141] Fork detection fixes IIIIX (#2966) * Fix local builds * Add remote fork windows title to release builds too * Remove trailing slah from remote links before processing --- CMakeLists.txt | 7 ++++--- src/emulator.cpp | 26 +++++++++++++++++--------- src/qt_gui/main_window.cpp | 25 ++++++++++++++++--------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef2425aff..e993061bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,7 @@ if (GIT_REMOTE_RESULT OR GIT_REMOTE_NAME STREQUAL "") ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) + message("got remote: ${GIT_REMOTE_NAME}") endif() # If running in GitHub Actions and the above fails @@ -177,7 +178,7 @@ if (GIT_REMOTE_RESULT OR GIT_REMOTE_NAME STREQUAL "") set(GIT_BRANCH "${GITHUB_BRANCH}") elseif ("${PR_NUMBER}" STREQUAL "" AND NOT "${GITHUB_REF}" STREQUAL "") set(GIT_BRANCH "${GITHUB_REF}") - else() + elseif("${GIT_BRANCH}" STREQUAL "") message("couldn't find branch") set(GIT_BRANCH "detached-head") endif() @@ -186,8 +187,8 @@ else() string(FIND "${GIT_REMOTE_NAME}" "/" INDEX) if (INDEX GREATER -1) string(SUBSTRING "${GIT_REMOTE_NAME}" 0 "${INDEX}" GIT_REMOTE_NAME) - else() - # If no remote is present (only a branch name), default to origin + elseif("${GIT_REMOTE_NAME}" STREQUAL "") + message("reset to origin") set(GIT_REMOTE_NAME "origin") endif() endif() diff --git a/src/emulator.cpp b/src/emulator.cpp index ebb34054b..9a0429d5d 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -190,16 +190,24 @@ void Emulator::Run(const std::filesystem::path& file, const std::vector", id, title, app_version); std::string window_title = ""; - if (Common::g_is_release) { - window_title = fmt::format("shadPS4 v{} | {}", Common::g_version, game_title); - } else { - std::string remote_url(Common::g_scm_remote_url); - std::string remote_host; - try { - remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); - } catch (...) { - remote_host = "unknown"; + std::string remote_url(Common::g_scm_remote_url); + std::string remote_host; + try { + if (*remote_url.rbegin() == '/') { + remote_url.pop_back(); } + remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); + } catch (...) { + remote_host = "unknown"; + } + if (Common::g_is_release) { + if (remote_host == "shadps4-emu" || remote_url.length() == 0) { + window_title = fmt::format("shadPS4 v{} | {}", Common::g_version, game_title); + } else { + window_title = + fmt::format("shadPS4 {}/v{} | {}", remote_host, Common::g_version, game_title); + } + } else { if (remote_host == "shadps4-emu" || remote_url.length() == 0) { window_title = fmt::format("shadPS4 v{} {} {} | {}", Common::g_version, Common::g_scm_branch, Common::g_scm_desc, game_title); diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 8eeec3536..1966aa52b 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -55,16 +55,23 @@ bool MainWindow::Init() { // show ui setMinimumSize(720, 405); std::string window_title = ""; - if (Common::g_is_release) { - window_title = fmt::format("shadPS4 v{}", Common::g_version); - } else { - std::string remote_url(Common::g_scm_remote_url); - std::string remote_host; - try { - remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); - } catch (...) { - remote_host = "unknown"; + std::string remote_url(Common::g_scm_remote_url); + std::string remote_host; + try { + if (*remote_url.rbegin() == '/') { + remote_url.pop_back(); } + remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); + } catch (...) { + remote_host = "unknown"; + } + if (Common::g_is_release) { + if (remote_host == "shadps4-emu" || remote_url.length() == 0) { + window_title = fmt::format("shadPS4 v{}", Common::g_version); + } else { + window_title = fmt::format("shadPS4 {}/v{}", remote_host, Common::g_version); + } + } else { if (remote_host == "shadps4-emu" || remote_url.length() == 0) { window_title = fmt::format("shadPS4 v{} {} {}", Common::g_version, Common::g_scm_branch, Common::g_scm_desc); From 6935b24440044fb093e029adf5402ef12c650cd4 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 22 May 2025 01:28:41 -0700 Subject: [PATCH 054/141] savedata: Fix missing uses of config based save data dir. (#2971) --- src/common/config.cpp | 2 +- src/common/path_util.cpp | 1 - src/common/path_util.h | 2 -- src/core/libraries/save_data/savedata.cpp | 3 ++- src/qt_gui/gui_context_menus.h | 11 ++++------- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 111c0cfa9..6bccd0f37 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -154,7 +154,7 @@ bool GetLoadGameSizeEnabled() { std::filesystem::path GetSaveDataPath() { if (save_data_path.empty()) { - return Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir); + return Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "savedata"; } return save_data_path; } diff --git a/src/common/path_util.cpp b/src/common/path_util.cpp index 1a6ff9ec8..3270c24dd 100644 --- a/src/common/path_util.cpp +++ b/src/common/path_util.cpp @@ -128,7 +128,6 @@ static auto UserPaths = [] { create_path(PathType::LogDir, user_dir / LOG_DIR); create_path(PathType::ScreenshotsDir, user_dir / SCREENSHOTS_DIR); create_path(PathType::ShaderDir, user_dir / SHADER_DIR); - create_path(PathType::SaveDataDir, user_dir / SAVEDATA_DIR); create_path(PathType::GameDataDir, user_dir / GAMEDATA_DIR); create_path(PathType::TempDataDir, user_dir / TEMPDATA_DIR); create_path(PathType::SysModuleDir, user_dir / SYSMODULES_DIR); diff --git a/src/common/path_util.h b/src/common/path_util.h index 2fd9b1588..b8053a229 100644 --- a/src/common/path_util.h +++ b/src/common/path_util.h @@ -18,7 +18,6 @@ enum class PathType { LogDir, // Where log files are stored. ScreenshotsDir, // Where screenshots are stored. ShaderDir, // Where shaders are stored. - SaveDataDir, // Where guest save data is stored. TempDataDir, // Where game temp data is stored. GameDataDir, // Where game data is stored. SysModuleDir, // Where system modules are stored. @@ -36,7 +35,6 @@ constexpr auto PORTABLE_DIR = "user"; constexpr auto LOG_DIR = "log"; constexpr auto SCREENSHOTS_DIR = "screenshots"; constexpr auto SHADER_DIR = "shader"; -constexpr auto SAVEDATA_DIR = "savedata"; constexpr auto GAMEDATA_DIR = "data"; constexpr auto TEMPDATA_DIR = "temp"; constexpr auto SYSMODULES_DIR = "sys_modules"; diff --git a/src/core/libraries/save_data/savedata.cpp b/src/core/libraries/save_data/savedata.cpp index 0731392cd..b25ebde6c 100644 --- a/src/core/libraries/save_data/savedata.cpp +++ b/src/core/libraries/save_data/savedata.cpp @@ -8,6 +8,7 @@ #include #include "common/assert.h" +#include "common/config.h" #include "common/cstring.h" #include "common/elf_info.h" #include "common/enum.h" @@ -438,7 +439,7 @@ static Error saveDataMount(const OrbisSaveDataMount2* mount_info, LOG_INFO(Lib_SaveData, "called with invalid block size"); } - const auto root_save = Common::FS::GetUserPath(Common::FS::PathType::SaveDataDir); + const auto root_save = Config::GetSaveDataPath(); fs::create_directories(root_save); const auto available = fs::space(root_save).available; diff --git a/src/qt_gui/gui_context_menus.h b/src/qt_gui/gui_context_menus.h index f435a3e38..46a40c5cd 100644 --- a/src/qt_gui/gui_context_menus.h +++ b/src/qt_gui/gui_context_menus.h @@ -156,11 +156,9 @@ public: } if (selected == openSaveDataFolder) { - QString userPath; - Common::FS::PathToQString(userPath, - Common::FS::GetUserPath(Common::FS::PathType::UserDir)); - QString saveDataPath = - userPath + "/savedata/1/" + QString::fromStdString(m_games[itemID].save_dir); + QString saveDataPath; + Common::FS::PathToQString(saveDataPath, + Config::GetSaveDataPath() / "1" / m_games[itemID].save_dir); QDir(saveDataPath).mkpath(saveDataPath); QDesktopServices::openUrl(QUrl::fromLocalFile(saveDataPath)); } @@ -485,8 +483,7 @@ public: dlc_path, Config::getAddonInstallDir() / Common::FS::PathFromQString(folder_path).parent_path().filename()); Common::FS::PathToQString(save_data_path, - Common::FS::GetUserPath(Common::FS::PathType::UserDir) / - "savedata/1" / m_games[itemID].serial); + Config::GetSaveDataPath() / "1" / m_games[itemID].save_dir); Common::FS::PathToQString(trophy_data_path, Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / From f4eb0b9b9e0626527b834f170ad14d5b0f190730 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 22 May 2025 03:03:24 -0700 Subject: [PATCH 055/141] shader_recompiler: Fix buffer type reading from step rate attribute. (#2973) --- .../spirv/emit_spirv_context_get_set.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 6442ae9f8..eff562955 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -175,19 +175,24 @@ Id EmitReadConst(EmitContext& ctx, IR::Inst* inst) { return ctx.OpLoad(ctx.U32[1], ptr); } -Id EmitReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { +template +Id ReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { const auto& buffer = ctx.buffers[handle]; index = ctx.OpIAdd(ctx.U32[1], index, buffer.offset_dwords); - const auto [id, pointer_type] = buffer[BufferAlias::U32]; + const auto [id, pointer_type] = buffer[alias]; + const auto value_type = alias == BufferAlias::U32 ? ctx.U32[1] : ctx.F32[1]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; - const Id result{ctx.OpLoad(ctx.U32[1], ptr)}; + const Id result{ctx.OpLoad(value_type, ptr)}; if (Sirit::ValidId(buffer.size_dwords)) { const Id in_bounds = ctx.OpULessThan(ctx.U1[1], index, buffer.size_dwords); - return ctx.OpSelect(ctx.U32[1], in_bounds, result, ctx.u32_zero_value); - } else { - return result; + return ctx.OpSelect(value_type, in_bounds, result, ctx.u32_zero_value); } + return result; +} + +Id EmitReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { + return ReadConstBuffer(ctx, handle, index); } Id EmitReadStepRate(EmitContext& ctx, int rate_idx) { @@ -246,7 +251,7 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, Id index) { ctx.OpUDiv(ctx.U32[1], ctx.OpLoad(ctx.U32[1], ctx.instance_id), step_rate), ctx.ConstU32(param.num_components)), ctx.ConstU32(comp)); - return EmitReadConstBuffer(ctx, param.buffer_handle, offset); + return ReadConstBuffer(ctx, param.buffer_handle, offset); } Id result; From 3f949d2b6cdb5fa4bfe6cc86defa977a435863bd Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 22 May 2025 03:16:20 -0700 Subject: [PATCH 056/141] amdgpu: Handle 32-bit Unorm formats. (#2974) --- src/shader_recompiler/ir/reinterpret.h | 10 ++++++++++ src/video_core/amdgpu/types.h | 27 ++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/ir/reinterpret.h b/src/shader_recompiler/ir/reinterpret.h index 99819cbb9..2a18f394a 100644 --- a/src/shader_recompiler/ir/reinterpret.h +++ b/src/shader_recompiler/ir/reinterpret.h @@ -46,6 +46,10 @@ inline F32 ApplyReadNumberConversion(IREmitter& ir, const F32& value, const IR::F32 max = ir.Imm32(float(std::numeric_limits::max())); return ir.FPDiv(left, max); } + case AmdGpu::NumberConversion::Uint32ToUnorm: { + const auto float_val = ir.ConvertUToF(32, 32, ir.BitCast(value)); + return ir.FPDiv(float_val, ir.Imm32(static_cast(std::numeric_limits::max()))); + } default: UNREACHABLE(); } @@ -92,6 +96,12 @@ inline F32 ApplyWriteNumberConversion(IREmitter& ir, const F32& value, const IR::U32 raw = ir.ConvertFToS(32, ir.FPDiv(left, ir.Imm32(2.f))); return ir.BitCast(raw); } + case AmdGpu::NumberConversion::Uint32ToUnorm: { + const auto clamped = ir.FPClamp(value, ir.Imm32(0.f), ir.Imm32(1.f)); + const auto unnormalized = + ir.FPMul(clamped, ir.Imm32(static_cast(std::numeric_limits::max()))); + return ir.BitCast(U32{ir.ConvertFToU(32, unnormalized)}); + } default: UNREACHABLE(); } diff --git a/src/video_core/amdgpu/types.h b/src/video_core/amdgpu/types.h index ab0df689e..f7536f7e2 100644 --- a/src/video_core/amdgpu/types.h +++ b/src/video_core/amdgpu/types.h @@ -197,8 +197,9 @@ enum class NumberConversion : u32 { UintToUscaled = 1, SintToSscaled = 2, UnormToUbnorm = 3, - Sint8ToSnormNz = 5, - Sint16ToSnormNz = 6, + Sint8ToSnormNz = 4, + Sint16ToSnormNz = 5, + Uint32ToUnorm = 6, }; struct CompMapping { @@ -286,6 +287,17 @@ inline DataFormat RemapDataFormat(const DataFormat format) { inline NumberFormat RemapNumberFormat(const NumberFormat format, const DataFormat data_format) { switch (format) { + case NumberFormat::Unorm: { + switch (data_format) { + case DataFormat::Format32: + case DataFormat::Format32_32: + case DataFormat::Format32_32_32: + case DataFormat::Format32_32_32_32: + return NumberFormat::Uint; + default: + return format; + } + } case NumberFormat::Uscaled: return NumberFormat::Uint; case NumberFormat::Sscaled: @@ -341,6 +353,17 @@ inline CompMapping RemapSwizzle(const DataFormat format, const CompMapping swizz inline NumberConversion MapNumberConversion(const NumberFormat num_fmt, const DataFormat data_fmt) { switch (num_fmt) { + case NumberFormat::Unorm: { + switch (data_fmt) { + case DataFormat::Format32: + case DataFormat::Format32_32: + case DataFormat::Format32_32_32: + case DataFormat::Format32_32_32_32: + return NumberConversion::Uint32ToUnorm; + default: + return NumberConversion::None; + } + } case NumberFormat::Uscaled: return NumberConversion::UintToUscaled; case NumberFormat::Sscaled: From b55c3f45559a0a5685b879adbe4c220c7304f859 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 22 May 2025 15:35:00 +0300 Subject: [PATCH 057/141] Trophy fix (#2975) * fixed invalid handle destroy * removed log --- src/core/libraries/np_trophy/np_trophy.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/libraries/np_trophy/np_trophy.cpp b/src/core/libraries/np_trophy/np_trophy.cpp index a951d5655..6de84bd93 100644 --- a/src/core/libraries/np_trophy/np_trophy.cpp +++ b/src/core/libraries/np_trophy/np_trophy.cpp @@ -206,6 +206,10 @@ s32 PS4_SYSV_ABI sceNpTrophyDestroyHandle(OrbisNpTrophyHandle handle) { if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + if (handle >= trophy_handles.size()) { + LOG_ERROR(Lib_NpTrophy, "Invalid handle {}", handle); + return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; + } if (!trophy_handles.is_allocated({static_cast(handle)})) { return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; } From 9c2f71326a0c09795864a138050ee87ffd2d010f Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 22 May 2025 18:52:44 +0300 Subject: [PATCH 058/141] tagged 0.9.0 release --- CMakeLists.txt | 8 ++++---- dist/net.shadps4.shadPS4.metainfo.xml | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e993061bd..440a4b185 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,13 +203,13 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") -set(EMULATOR_VERSION_MINOR "8") -set(EMULATOR_VERSION_PATCH "1") +set(EMULATOR_VERSION_MINOR "9") +set(EMULATOR_VERSION_PATCH "0") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") -set(APP_IS_RELEASE false) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") +set(APP_IS_RELEASE true) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}") diff --git a/dist/net.shadps4.shadPS4.metainfo.xml b/dist/net.shadps4.shadPS4.metainfo.xml index 9f7b4f9c5..493dc0df6 100644 --- a/dist/net.shadps4.shadPS4.metainfo.xml +++ b/dist/net.shadps4.shadPS4.metainfo.xml @@ -37,7 +37,10 @@ Game - + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.9.0 + + https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.8.0 From 37887e8fded641c0da06ac55b22e50200fb2876d Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 22 May 2025 19:11:25 +0300 Subject: [PATCH 059/141] starting 0.9.1 WIP --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 440a4b185..73fff2c73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,12 +204,12 @@ execute_process( # Set Version set(EMULATOR_VERSION_MAJOR "0") set(EMULATOR_VERSION_MINOR "9") -set(EMULATOR_VERSION_PATCH "0") +set(EMULATOR_VERSION_PATCH "1") set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}") -set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}") -set(APP_IS_RELEASE true) +set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP") +set(APP_IS_RELEASE false) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY) message("end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}") From f9bbde9c79e0cf225e4e919118ab81f42d63b42d Mon Sep 17 00:00:00 2001 From: Lander Gallastegi Date: Thu, 22 May 2025 20:00:15 +0200 Subject: [PATCH 060/141] video_core: Implement DMA. (#2819) * Import memory * 64K pages and fix memory mapping * Queue coverage * Buffer syncing, faulted readback adn BDA in Buffer * Base DMA implementation * Preparations for implementing SPV DMA access * Base impl (pending 16K pages and getbuffersize) * 16K pages and stack overflow fix * clang-format * clang-format but for real this time * Try to fix macOS build * Correct decltype * Add testing log * Fix stride and patch phi node blocks * No need to check if it is a deleted buffer * Clang format once more * Offset in bytes * Removed host buffers (may do it in another PR) Also some random barrier fixes * Add IR dumping from my read-const branch * clang-format * Correct size insteed of end * Fix incorrect assert * Possible fix for NieR deadlock * Copy to avoid deadlock * Use 2 mutexes insteed of copy * Attempt to range sync error * Revert "Attempt to range sync error" This reverts commit dd287b48682b50f215680bb0956e39c2809bf3fe. * Fix size truncated when syncing range And memory barrier * Some fixes (and async testing (doesn't work)) * Use compute to parse fault buffer * Process faults on submit * Only sync in the first time we see a readconst Thsi is partialy wrong. We need to save the state into the submission context itself, not the rasterizer since we can yield and process another sumission (if im not understanding wrong). * Use spec const and 32 bit atomic * 32 bit counter * Fix store_index * Better sync (WIP, breaks PR now) * Fixes for better sync * Better sync * Remove memory coveragte logic * Point sirit to upstream * Less waiting and barriers * Correctly checkout moltenvk * Bring back applying pending operations in wait * Sync the whole buffer insteed of only the range * Implement recursive shared/scoped locks * Iterators * Faster syncing with ranges * Some alignment fixes * fixed clang format * Fix clang-format again * Port page_manager from readbacks-poc * clang-format * Defer memory protect * Remove RENDERER_TRACE * Experiment: only sync on first readconst * Added profiling (will be removed) * Don't sync entire buffers * Added logging for testing * Updated temporary workaround to use 4k pages * clang.-format * Cleanup part 1 * Make ReadConst a SPIR-V function --------- Co-authored-by: georgemoralis --- CMakeLists.txt | 3 + externals/sirit | 2 +- src/common/recursive_lock.cpp | 37 ++ src/common/recursive_lock.h | 67 +++ src/common/slot_vector.h | 110 +++- .../backend/spirv/emit_spirv.cpp | 11 +- .../backend/spirv/emit_spirv_atomic.cpp | 6 +- .../spirv/emit_spirv_context_get_set.cpp | 81 ++- .../backend/spirv/emit_spirv_instructions.h | 2 +- .../backend/spirv/spirv_emit_context.cpp | 198 +++++++- .../backend/spirv/spirv_emit_context.h | 117 ++++- .../frontend/translate/scalar_memory.cpp | 13 +- src/shader_recompiler/info.h | 14 +- .../ir/abstract_syntax_list.cpp | 44 ++ .../ir/abstract_syntax_list.h | 5 + .../ir/passes/shader_info_collection_pass.cpp | 31 +- src/shader_recompiler/ir/program.cpp | 34 +- src/shader_recompiler/ir/program.h | 2 +- src/shader_recompiler/recompiler.cpp | 2 + src/video_core/amdgpu/liverpool.cpp | 1 + src/video_core/amdgpu/resource.h | 7 + src/video_core/buffer_cache/buffer.cpp | 14 +- src/video_core/buffer_cache/buffer.h | 6 + src/video_core/buffer_cache/buffer_cache.cpp | 470 +++++++++++++++--- src/video_core/buffer_cache/buffer_cache.h | 86 +++- .../buffer_cache/memory_tracker_base.h | 24 +- src/video_core/buffer_cache/range_set.h | 175 ++++++- src/video_core/buffer_cache/word_manager.h | 32 +- src/video_core/host_shaders/CMakeLists.txt | 1 + .../host_shaders/fault_buffer_process.comp | 42 ++ src/video_core/page_manager.cpp | 180 ++++--- src/video_core/page_manager.h | 32 +- src/video_core/renderdoc.cpp | 1 + .../renderer_vulkan/vk_instance.cpp | 3 + src/video_core/renderer_vulkan/vk_instance.h | 6 + .../renderer_vulkan/vk_rasterizer.cpp | 48 +- .../renderer_vulkan/vk_rasterizer.h | 15 +- .../renderer_vulkan/vk_scheduler.cpp | 14 + src/video_core/renderer_vulkan/vk_scheduler.h | 4 + .../texture_cache/texture_cache.cpp | 12 +- 40 files changed, 1641 insertions(+), 311 deletions(-) create mode 100644 src/common/recursive_lock.cpp create mode 100644 src/common/recursive_lock.h create mode 100644 src/shader_recompiler/ir/abstract_syntax_list.cpp create mode 100644 src/video_core/host_shaders/fault_buffer_process.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 73fff2c73..53a2281ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -674,6 +674,8 @@ set(COMMON src/common/logging/backend.cpp src/common/polyfill_thread.h src/common/rdtsc.cpp src/common/rdtsc.h + src/common/recursive_lock.cpp + src/common/recursive_lock.h src/common/sha1.h src/common/signal_context.h src/common/signal_context.cpp @@ -864,6 +866,7 @@ set(SHADER_RECOMPILER src/shader_recompiler/exception.h src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp + src/shader_recompiler/ir/abstract_syntax_list.cpp src/shader_recompiler/ir/abstract_syntax_list.h src/shader_recompiler/ir/attribute.cpp src/shader_recompiler/ir/attribute.h diff --git a/externals/sirit b/externals/sirit index 09a1416ab..6b450704f 160000 --- a/externals/sirit +++ b/externals/sirit @@ -1 +1 @@ -Subproject commit 09a1416ab1b59ddfebd2618412f118f2004f3b2c +Subproject commit 6b450704f6fedb9413d0c89a9eb59d028eb1e6c0 diff --git a/src/common/recursive_lock.cpp b/src/common/recursive_lock.cpp new file mode 100644 index 000000000..2471a2ee0 --- /dev/null +++ b/src/common/recursive_lock.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/assert.h" +#include "common/recursive_lock.h" + +namespace Common::Detail { + +struct RecursiveLockState { + RecursiveLockType type; + int count; +}; + +thread_local std::unordered_map g_recursive_locks; + +bool IncrementRecursiveLock(void* mutex, RecursiveLockType type) { + auto& state = g_recursive_locks[mutex]; + if (state.count == 0) { + ASSERT(state.type == RecursiveLockType::None); + state.type = type; + } + ASSERT(state.type == type); + return state.count++ == 0; +} + +bool DecrementRecursiveLock(void* mutex, RecursiveLockType type) { + auto& state = g_recursive_locks[mutex]; + ASSERT(state.type == type && state.count > 0); + if (--state.count == 0) { + g_recursive_locks.erase(mutex); + return true; + } + return false; +} + +} // namespace Common::Detail diff --git a/src/common/recursive_lock.h b/src/common/recursive_lock.h new file mode 100644 index 000000000..5a5fc6658 --- /dev/null +++ b/src/common/recursive_lock.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace Common { + +namespace Detail { + +enum class RecursiveLockType { None, Shared, Exclusive }; + +bool IncrementRecursiveLock(void* mutex, RecursiveLockType type); +bool DecrementRecursiveLock(void* mutex, RecursiveLockType type); + +} // namespace Detail + +template +class RecursiveScopedLock { +public: + explicit RecursiveScopedLock(MutexType& mutex) : m_mutex(mutex), m_locked(false) { + if (Detail::IncrementRecursiveLock(&m_mutex, Detail::RecursiveLockType::Exclusive)) { + m_locked = true; + m_lock.emplace(m_mutex); + } + } + + ~RecursiveScopedLock() { + Detail::DecrementRecursiveLock(&m_mutex, Detail::RecursiveLockType::Exclusive); + if (m_locked) { + m_lock.reset(); + } + } + +private: + MutexType& m_mutex; + std::optional> m_lock; + bool m_locked = false; +}; + +template +class RecursiveSharedLock { +public: + explicit RecursiveSharedLock(MutexType& mutex) : m_mutex(mutex), m_locked(false) { + if (Detail::IncrementRecursiveLock(&m_mutex, Detail::RecursiveLockType::Shared)) { + m_locked = true; + m_lock.emplace(m_mutex); + } + } + + ~RecursiveSharedLock() { + Detail::DecrementRecursiveLock(&m_mutex, Detail::RecursiveLockType::Shared); + if (m_locked) { + m_lock.reset(); + } + } + +private: + MutexType& m_mutex; + std::optional> m_lock; + bool m_locked = false; +}; + +} // namespace Common \ No newline at end of file diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h index d4ac51361..2f693fb28 100644 --- a/src/common/slot_vector.h +++ b/src/common/slot_vector.h @@ -14,6 +14,9 @@ namespace Common { struct SlotId { static constexpr u32 INVALID_INDEX = std::numeric_limits::max(); + SlotId() noexcept = default; + constexpr SlotId(u32 index) noexcept : index(index) {} + constexpr auto operator<=>(const SlotId&) const noexcept = default; constexpr explicit operator bool() const noexcept { @@ -28,6 +31,63 @@ class SlotVector { constexpr static std::size_t InitialCapacity = 2048; public: + template + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = ValueType; + using difference_type = std::ptrdiff_t; + using pointer = Pointer; + using reference = Reference; + + Iterator(SlotVector& vector_, SlotId index_) : vector(vector_), slot(index_) { + AdvanceToValid(); + } + + reference operator*() const { + return vector[slot]; + } + + pointer operator->() const { + return &vector[slot]; + } + + Iterator& operator++() { + ++slot.index; + AdvanceToValid(); + return *this; + } + + Iterator operator++(int) { + Iterator temp = *this; + ++(*this); + return temp; + } + + bool operator==(const Iterator& other) const { + return slot == other.slot; + } + + bool operator!=(const Iterator& other) const { + return !(*this == other); + } + + private: + void AdvanceToValid() { + while (slot < vector.values_capacity && !vector.ReadStorageBit(slot.index)) { + ++slot.index; + } + } + + SlotVector& vector; + SlotId slot; + }; + + using iterator = Iterator; + using const_iterator = Iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + SlotVector() { Reserve(InitialCapacity); } @@ -60,7 +120,7 @@ public: } template - [[nodiscard]] SlotId insert(Args&&... args) noexcept { + SlotId insert(Args&&... args) noexcept { const u32 index = FreeValueIndex(); new (&values[index].object) T(std::forward(args)...); SetStorageBit(index); @@ -78,6 +138,54 @@ public: return values_capacity - free_list.size(); } + iterator begin() noexcept { + return iterator(*this, 0); + } + + const_iterator begin() const noexcept { + return const_iterator(*this, 0); + } + + const_iterator cbegin() const noexcept { + return begin(); + } + + iterator end() noexcept { + return iterator(*this, values_capacity); + } + + const_iterator end() const noexcept { + return const_iterator(*this, values_capacity); + } + + const_iterator cend() const noexcept { + return end(); + } + + reverse_iterator rbegin() noexcept { + return reverse_iterator(end()); + } + + const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(end()); + } + + const_reverse_iterator crbegin() const noexcept { + return rbegin(); + } + + reverse_iterator rend() noexcept { + return reverse_iterator(begin()); + } + + const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(begin()); + } + + const_reverse_iterator crend() const noexcept { + return rend(); + } + private: struct NonTrivialDummy { NonTrivialDummy() noexcept {} diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index 9ebb842cc..f2e6279f4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -154,6 +154,7 @@ void Traverse(EmitContext& ctx, const IR::Program& program) { for (IR::Inst& inst : node.data.block->Instructions()) { EmitInst(ctx, &inst); } + ctx.first_to_last_label_map[label.value] = ctx.last_label; break; } case IR::AbstractSyntaxNode::Type::If: { @@ -298,6 +299,10 @@ void SetupCapabilities(const Info& info, const Profile& profile, EmitContext& ct if (stage == LogicalStage::TessellationControl || stage == LogicalStage::TessellationEval) { ctx.AddCapability(spv::Capability::Tessellation); } + if (info.dma_types != IR::Type::Void) { + ctx.AddCapability(spv::Capability::PhysicalStorageBufferAddresses); + ctx.AddExtension("SPV_KHR_physical_storage_buffer"); + } } void DefineEntryPoint(const Info& info, EmitContext& ctx, Id main) { @@ -387,7 +392,7 @@ void SetupFloatMode(EmitContext& ctx, const Profile& profile, const RuntimeInfo& void PatchPhiNodes(const IR::Program& program, EmitContext& ctx) { auto inst{program.blocks.front()->begin()}; size_t block_index{0}; - ctx.PatchDeferredPhi([&](size_t phi_arg) { + ctx.PatchDeferredPhi([&](u32 phi_arg, Id first_parent) { if (phi_arg == 0) { ++inst; if (inst == program.blocks[block_index]->end() || @@ -398,7 +403,9 @@ void PatchPhiNodes(const IR::Program& program, EmitContext& ctx) { } while (inst->GetOpcode() != IR::Opcode::Phi); } } - return ctx.Def(inst->Arg(phi_arg)); + const Id arg = ctx.Def(inst->Arg(phi_arg)); + const Id parent = ctx.first_to_last_label_map[first_parent.value]; + return std::make_pair(arg, parent); }); } } // Anonymous namespace diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index c3799fb4b..d7c73ca8f 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -60,7 +60,7 @@ Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id address = ctx.OpIAdd(ctx.U32[1], address, buffer.offset); } const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(2u)); - const auto [id, pointer_type] = buffer[EmitContext::BufferAlias::U32]; + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); const auto [scope, semantics]{AtomicArgs(ctx)}; return BufferAtomicU32BoundsCheck(ctx, index, buffer.size_dwords, [&] { @@ -257,7 +257,7 @@ Id EmitImageAtomicExchange32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id co Id EmitDataAppend(EmitContext& ctx, u32 gds_addr, u32 binding) { const auto& buffer = ctx.buffers[binding]; - const auto [id, pointer_type] = buffer[EmitContext::BufferAlias::U32]; + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, ctx.ConstU32(gds_addr)); const auto [scope, semantics]{AtomicArgs(ctx)}; return ctx.OpAtomicIIncrement(ctx.U32[1], ptr, scope, semantics); @@ -265,7 +265,7 @@ Id EmitDataAppend(EmitContext& ctx, u32 gds_addr, u32 binding) { Id EmitDataConsume(EmitContext& ctx, u32 gds_addr, u32 binding) { const auto& buffer = ctx.buffers[binding]; - const auto [id, pointer_type] = buffer[EmitContext::BufferAlias::U32]; + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, ctx.ConstU32(gds_addr)); const auto [scope, semantics]{AtomicArgs(ctx)}; return ctx.OpAtomicIDecrement(ctx.U32[1], ptr, scope, semantics); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index eff562955..658d4759f 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -161,26 +161,25 @@ void EmitGetGotoVariable(EmitContext&) { UNREACHABLE_MSG("Unreachable instruction"); } -using BufferAlias = EmitContext::BufferAlias; +using PointerType = EmitContext::PointerType; -Id EmitReadConst(EmitContext& ctx, IR::Inst* inst) { +Id EmitReadConst(EmitContext& ctx, IR::Inst* inst, Id addr, Id offset) { const u32 flatbuf_off_dw = inst->Flags(); - const auto& srt_flatbuf = ctx.buffers.back(); - ASSERT(srt_flatbuf.binding >= 0 && flatbuf_off_dw > 0 && - srt_flatbuf.buffer_type == BufferType::ReadConstUbo); - LOG_DEBUG(Render_Recompiler, "ReadConst from flatbuf dword {}", flatbuf_off_dw); - const auto [id, pointer_type] = srt_flatbuf[BufferAlias::U32]; - const Id ptr{ - ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, ctx.ConstU32(flatbuf_off_dw))}; - return ctx.OpLoad(ctx.U32[1], ptr); + // We can only provide a fallback for immediate offsets. + if (flatbuf_off_dw == 0) { + return ctx.OpFunctionCall(ctx.U32[1], ctx.read_const_dynamic, addr, offset); + } else { + return ctx.OpFunctionCall(ctx.U32[1], ctx.read_const, addr, offset, + ctx.ConstU32(flatbuf_off_dw)); + } } -template +template Id ReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { const auto& buffer = ctx.buffers[handle]; index = ctx.OpIAdd(ctx.U32[1], index, buffer.offset_dwords); - const auto [id, pointer_type] = buffer[alias]; - const auto value_type = alias == BufferAlias::U32 ? ctx.U32[1] : ctx.F32[1]; + const auto [id, pointer_type] = buffer[type]; + const auto value_type = type == PointerType::U32 ? ctx.U32[1] : ctx.F32[1]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; const Id result{ctx.OpLoad(value_type, ptr)}; @@ -192,7 +191,7 @@ Id ReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { } Id EmitReadConstBuffer(EmitContext& ctx, u32 handle, Id index) { - return ReadConstBuffer(ctx, handle, index); + return ReadConstBuffer(ctx, handle, index); } Id EmitReadStepRate(EmitContext& ctx, int rate_idx) { @@ -251,7 +250,7 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, Id index) { ctx.OpUDiv(ctx.U32[1], ctx.OpLoad(ctx.U32[1], ctx.instance_id), step_rate), ctx.ConstU32(param.num_components)), ctx.ConstU32(comp)); - return ReadConstBuffer(ctx, param.buffer_handle, offset); + return ReadConstBuffer(ctx, param.buffer_handle, offset); } Id result; @@ -437,7 +436,7 @@ static Id EmitLoadBufferBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, return result; } -template +template static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { const auto flags = inst->Flags(); const auto& spv_buffer = ctx.buffers[handle]; @@ -445,7 +444,7 @@ static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id a address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(2u)); - const auto& data_types = alias == BufferAlias::U32 ? ctx.U32 : ctx.F32; + const auto& data_types = alias == PointerType::U32 ? ctx.U32 : ctx.F32; const auto [id, pointer_type] = spv_buffer[alias]; boost::container::static_vector ids; @@ -456,7 +455,7 @@ static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id a if (!flags.typed) { // Untyped loads have bounds checking per-component. ids.push_back(EmitLoadBufferBoundsCheck<1>(ctx, index_i, spv_buffer.size_dwords, - result_i, alias == BufferAlias::F32)); + result_i, alias == PointerType::F32)); } else { ids.push_back(result_i); } @@ -466,7 +465,7 @@ static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id a if (flags.typed) { // Typed loads have single bounds check for the whole load. return EmitLoadBufferBoundsCheck(ctx, index, spv_buffer.size_dwords, result, - alias == BufferAlias::F32); + alias == PointerType::F32); } return result; } @@ -476,7 +475,7 @@ Id EmitLoadBufferU8(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { if (Sirit::ValidId(spv_buffer.offset)) { address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } - const auto [id, pointer_type] = spv_buffer[BufferAlias::U8]; + const auto [id, pointer_type] = spv_buffer[PointerType::U8]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, address)}; const Id result{ctx.OpUConvert(ctx.U32[1], ctx.OpLoad(ctx.U8, ptr))}; return EmitLoadBufferBoundsCheck<1>(ctx, address, spv_buffer.size, result, false); @@ -487,7 +486,7 @@ Id EmitLoadBufferU16(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { if (Sirit::ValidId(spv_buffer.offset)) { address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } - const auto [id, pointer_type] = spv_buffer[BufferAlias::U16]; + const auto [id, pointer_type] = spv_buffer[PointerType::U16]; const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(1u)); const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; const Id result{ctx.OpUConvert(ctx.U32[1], ctx.OpLoad(ctx.U16, ptr))}; @@ -495,35 +494,35 @@ Id EmitLoadBufferU16(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { } Id EmitLoadBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<1, BufferAlias::U32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<1, PointerType::U32>(ctx, inst, handle, address); } Id EmitLoadBufferU32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<2, BufferAlias::U32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<2, PointerType::U32>(ctx, inst, handle, address); } Id EmitLoadBufferU32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<3, BufferAlias::U32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<3, PointerType::U32>(ctx, inst, handle, address); } Id EmitLoadBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<4, BufferAlias::U32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<4, PointerType::U32>(ctx, inst, handle, address); } Id EmitLoadBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<1, BufferAlias::F32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<1, PointerType::F32>(ctx, inst, handle, address); } Id EmitLoadBufferF32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<2, BufferAlias::F32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<2, PointerType::F32>(ctx, inst, handle, address); } Id EmitLoadBufferF32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<3, BufferAlias::F32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<3, PointerType::F32>(ctx, inst, handle, address); } Id EmitLoadBufferF32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { - return EmitLoadBufferB32xN<4, BufferAlias::F32>(ctx, inst, handle, address); + return EmitLoadBufferB32xN<4, PointerType::F32>(ctx, inst, handle, address); } Id EmitLoadBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { @@ -553,7 +552,7 @@ void EmitStoreBufferBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func(); } -template +template static void EmitStoreBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { const auto flags = inst->Flags(); @@ -562,7 +561,7 @@ static void EmitStoreBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, I address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(2u)); - const auto& data_types = alias == BufferAlias::U32 ? ctx.U32 : ctx.F32; + const auto& data_types = alias == PointerType::U32 ? ctx.U32 : ctx.F32; const auto [id, pointer_type] = spv_buffer[alias]; auto store = [&] { @@ -593,7 +592,7 @@ void EmitStoreBufferU8(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id v if (Sirit::ValidId(spv_buffer.offset)) { address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } - const auto [id, pointer_type] = spv_buffer[BufferAlias::U8]; + const auto [id, pointer_type] = spv_buffer[PointerType::U8]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, address)}; const Id result{ctx.OpUConvert(ctx.U8, value)}; EmitStoreBufferBoundsCheck<1>(ctx, address, spv_buffer.size, [&] { ctx.OpStore(ptr, result); }); @@ -604,7 +603,7 @@ void EmitStoreBufferU16(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id if (Sirit::ValidId(spv_buffer.offset)) { address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); } - const auto [id, pointer_type] = spv_buffer[BufferAlias::U16]; + const auto [id, pointer_type] = spv_buffer[PointerType::U16]; const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(1u)); const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; const Id result{ctx.OpUConvert(ctx.U16, value)}; @@ -613,35 +612,35 @@ void EmitStoreBufferU16(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id } void EmitStoreBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<1, BufferAlias::U32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<1, PointerType::U32>(ctx, inst, handle, address, value); } void EmitStoreBufferU32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<2, BufferAlias::U32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<2, PointerType::U32>(ctx, inst, handle, address, value); } void EmitStoreBufferU32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<3, BufferAlias::U32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<3, PointerType::U32>(ctx, inst, handle, address, value); } void EmitStoreBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<4, BufferAlias::U32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<4, PointerType::U32>(ctx, inst, handle, address, value); } void EmitStoreBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<1, BufferAlias::F32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<1, PointerType::F32>(ctx, inst, handle, address, value); } void EmitStoreBufferF32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<2, BufferAlias::F32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<2, PointerType::F32>(ctx, inst, handle, address, value); } void EmitStoreBufferF32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<3, BufferAlias::F32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<3, PointerType::F32>(ctx, inst, handle, address, value); } void EmitStoreBufferF32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { - EmitStoreBufferB32xN<4, BufferAlias::F32>(ctx, inst, handle, address, value); + EmitStoreBufferB32xN<4, PointerType::F32>(ctx, inst, handle, address, value); } void EmitStoreBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 269f372d5..09f9732bf 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -61,7 +61,7 @@ void EmitSetVectorRegister(EmitContext& ctx); void EmitSetGotoVariable(EmitContext& ctx); void EmitGetGotoVariable(EmitContext& ctx); void EmitSetScc(EmitContext& ctx); -Id EmitReadConst(EmitContext& ctx, IR::Inst* inst); +Id EmitReadConst(EmitContext& ctx, IR::Inst* inst, Id addr, Id offset); Id EmitReadConstBuffer(EmitContext& ctx, u32 handle, Id index); Id EmitLoadBufferU8(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferU16(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 2640030df..68bfcc0d0 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -7,6 +7,7 @@ #include "shader_recompiler/frontend/fetch_shader.h" #include "shader_recompiler/runtime_info.h" #include "video_core/amdgpu/types.h" +#include "video_core/buffer_cache/buffer_cache.h" #include #include @@ -70,6 +71,12 @@ EmitContext::EmitContext(const Profile& profile_, const RuntimeInfo& runtime_inf Bindings& binding_) : Sirit::Module(profile_.supported_spirv), info{info_}, runtime_info{runtime_info_}, profile{profile_}, stage{info.stage}, l_stage{info.l_stage}, binding{binding_} { + if (info.dma_types != IR::Type::Void) { + SetMemoryModel(spv::AddressingModel::PhysicalStorageBuffer64, spv::MemoryModel::GLSL450); + } else { + SetMemoryModel(spv::AddressingModel::Logical, spv::MemoryModel::GLSL450); + } + AddCapability(spv::Capability::Shader); DefineArithmeticTypes(); DefineInterfaces(); @@ -137,9 +144,13 @@ void EmitContext::DefineArithmeticTypes() { true_value = ConstantTrue(U1[1]); false_value = ConstantFalse(U1[1]); + u8_one_value = Constant(U8, 1U); + u8_zero_value = Constant(U8, 0U); u32_one_value = ConstU32(1U); u32_zero_value = ConstU32(0U); f32_zero_value = ConstF32(0.0f); + u64_one_value = Constant(U64, 1ULL); + u64_zero_value = Constant(U64, 0ULL); pi_x2 = ConstF32(2.0f * float{std::numbers::pi}); @@ -157,6 +168,35 @@ void EmitContext::DefineArithmeticTypes() { if (info.uses_fp64) { frexp_result_f64 = Name(TypeStruct(F64[1], S32[1]), "frexp_result_f64"); } + + if (True(info.dma_types & IR::Type::F64)) { + physical_pointer_types[PointerType::F64] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, F64[1]); + } + if (True(info.dma_types & IR::Type::U64)) { + physical_pointer_types[PointerType::U64] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, U64); + } + if (True(info.dma_types & IR::Type::F32)) { + physical_pointer_types[PointerType::F32] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, F32[1]); + } + if (True(info.dma_types & IR::Type::U32)) { + physical_pointer_types[PointerType::U32] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, U32[1]); + } + if (True(info.dma_types & IR::Type::F16)) { + physical_pointer_types[PointerType::F16] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, F16[1]); + } + if (True(info.dma_types & IR::Type::U16)) { + physical_pointer_types[PointerType::U16] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, U16); + } + if (True(info.dma_types & IR::Type::U8)) { + physical_pointer_types[PointerType::U8] = + TypePointer(spv::StorageClass::PhysicalStorageBuffer, U8); + } } void EmitContext::DefineInterfaces() { @@ -195,9 +235,10 @@ EmitContext::SpirvAttribute EmitContext::GetAttributeInfo(AmdGpu::NumberFormat f } Id EmitContext::GetBufferSize(const u32 sharp_idx) { - const auto& srt_flatbuf = buffers.back(); - ASSERT(srt_flatbuf.buffer_type == BufferType::ReadConstUbo); - const auto [id, pointer_type] = srt_flatbuf[BufferAlias::U32]; + // Can this be done with memory access? Like we do now with ReadConst + const auto& srt_flatbuf = buffers[flatbuf_index]; + ASSERT(srt_flatbuf.buffer_type == BufferType::Flatbuf); + const auto [id, pointer_type] = srt_flatbuf[PointerType::U32]; const auto rsrc1{ OpLoad(U32[1], OpAccessChain(pointer_type, id, u32_zero_value, ConstU32(sharp_idx + 1)))}; @@ -690,8 +731,14 @@ EmitContext::BufferSpv EmitContext::DefineBuffer(bool is_storage, bool is_writte case Shader::BufferType::GdsBuffer: Name(id, "gds_buffer"); break; - case Shader::BufferType::ReadConstUbo: - Name(id, "srt_flatbuf_ubo"); + case Shader::BufferType::Flatbuf: + Name(id, "srt_flatbuf"); + break; + case Shader::BufferType::BdaPagetable: + Name(id, "bda_pagetable"); + break; + case Shader::BufferType::FaultBuffer: + Name(id, "fault_buffer"); break; case Shader::BufferType::SharedMemory: Name(id, "ssbo_shmem"); @@ -705,35 +752,53 @@ EmitContext::BufferSpv EmitContext::DefineBuffer(bool is_storage, bool is_writte }; void EmitContext::DefineBuffers() { - if (!profile.supports_robust_buffer_access && !info.has_readconst) { - // In case ReadConstUbo has not already been bound by IR and is needed + if (!profile.supports_robust_buffer_access && + info.readconst_types == Info::ReadConstType::None) { + // In case Flatbuf has not already been bound by IR and is needed // to query buffer sizes, bind it now. info.buffers.push_back({ .used_types = IR::Type::U32, - .inline_cbuf = AmdGpu::Buffer::Null(), - .buffer_type = BufferType::ReadConstUbo, + // We can't guarantee that flatbuf will not grow past UBO + // limit if there are a lot of ReadConsts. (We could specialize) + .inline_cbuf = AmdGpu::Buffer::Placeholder(std::numeric_limits::max()), + .buffer_type = BufferType::Flatbuf, }); + // In the future we may want to read buffer sizes from GPU memory if available. + // info.readconst_types |= Info::ReadConstType::Immediate; } for (const auto& desc : info.buffers) { const auto buf_sharp = desc.GetSharp(info); const bool is_storage = desc.IsStorage(buf_sharp, profile); + // Set indexes for special buffers. + if (desc.buffer_type == BufferType::Flatbuf) { + flatbuf_index = buffers.size(); + } else if (desc.buffer_type == BufferType::BdaPagetable) { + bda_pagetable_index = buffers.size(); + } else if (desc.buffer_type == BufferType::FaultBuffer) { + fault_buffer_index = buffers.size(); + } + // Define aliases depending on the shader usage. auto& spv_buffer = buffers.emplace_back(binding.buffer++, desc.buffer_type); + if (True(desc.used_types & IR::Type::U64)) { + spv_buffer[PointerType::U64] = + DefineBuffer(is_storage, desc.is_written, 3, desc.buffer_type, U64); + } if (True(desc.used_types & IR::Type::U32)) { - spv_buffer[BufferAlias::U32] = + spv_buffer[PointerType::U32] = DefineBuffer(is_storage, desc.is_written, 2, desc.buffer_type, U32[1]); } if (True(desc.used_types & IR::Type::F32)) { - spv_buffer[BufferAlias::F32] = + spv_buffer[PointerType::F32] = DefineBuffer(is_storage, desc.is_written, 2, desc.buffer_type, F32[1]); } if (True(desc.used_types & IR::Type::U16)) { - spv_buffer[BufferAlias::U16] = + spv_buffer[PointerType::U16] = DefineBuffer(is_storage, desc.is_written, 1, desc.buffer_type, U16); } if (True(desc.used_types & IR::Type::U8)) { - spv_buffer[BufferAlias::U8] = + spv_buffer[PointerType::U8] = DefineBuffer(is_storage, desc.is_written, 0, desc.buffer_type, U8); } ++binding.unified; @@ -1003,6 +1068,101 @@ Id EmitContext::DefineUfloatM5ToFloat32(u32 mantissa_bits, const std::string_vie return func; } +Id EmitContext::DefineGetBdaPointer() { + const auto caching_pagebits{ + Constant(U64, static_cast(VideoCore::BufferCache::CACHING_PAGEBITS))}; + const auto caching_pagemask{Constant(U64, VideoCore::BufferCache::CACHING_PAGESIZE - 1)}; + + const auto func_type{TypeFunction(U64, U64)}; + const auto func{OpFunction(U64, spv::FunctionControlMask::MaskNone, func_type)}; + const auto address{OpFunctionParameter(U64)}; + Name(func, "get_bda_pointer"); + AddLabel(); + + const auto fault_label{OpLabel()}; + const auto available_label{OpLabel()}; + const auto merge_label{OpLabel()}; + + // Get page BDA + const auto page{OpShiftRightLogical(U64, address, caching_pagebits)}; + const auto page32{OpUConvert(U32[1], page)}; + const auto& bda_buffer{buffers[bda_pagetable_index]}; + const auto [bda_buffer_id, bda_pointer_type] = bda_buffer[PointerType::U64]; + const auto bda_ptr{OpAccessChain(bda_pointer_type, bda_buffer_id, u32_zero_value, page32)}; + const auto bda{OpLoad(U64, bda_ptr)}; + + // Check if page is GPU cached + const auto is_fault{OpIEqual(U1[1], bda, u64_zero_value)}; + OpSelectionMerge(merge_label, spv::SelectionControlMask::MaskNone); + OpBranchConditional(is_fault, fault_label, available_label); + + // First time acces, mark as fault + AddLabel(fault_label); + const auto& fault_buffer{buffers[fault_buffer_index]}; + const auto [fault_buffer_id, fault_pointer_type] = fault_buffer[PointerType::U8]; + const auto page_div8{OpShiftRightLogical(U32[1], page32, ConstU32(3U))}; + const auto page_mod8{OpBitwiseAnd(U32[1], page32, ConstU32(7U))}; + const auto page_mask{OpShiftLeftLogical(U8, u8_one_value, page_mod8)}; + const auto fault_ptr{ + OpAccessChain(fault_pointer_type, fault_buffer_id, u32_zero_value, page_div8)}; + const auto fault_value{OpLoad(U8, fault_ptr)}; + const auto fault_value_masked{OpBitwiseOr(U8, fault_value, page_mask)}; + OpStore(fault_ptr, fault_value_masked); + + // Return null pointer + const auto fallback_result{u64_zero_value}; + OpBranch(merge_label); + + // Value is available, compute address + AddLabel(available_label); + const auto offset_in_bda{OpBitwiseAnd(U64, address, caching_pagemask)}; + const auto addr{OpIAdd(U64, bda, offset_in_bda)}; + OpBranch(merge_label); + + // Merge + AddLabel(merge_label); + const auto result{OpPhi(U64, addr, available_label, fallback_result, fault_label)}; + OpReturnValue(result); + OpFunctionEnd(); + return func; +} + +Id EmitContext::DefineReadConst(bool dynamic) { + const auto func_type{!dynamic ? TypeFunction(U32[1], U32[2], U32[1], U32[1]) + : TypeFunction(U32[1], U32[2], U32[1])}; + const auto func{OpFunction(U32[1], spv::FunctionControlMask::MaskNone, func_type)}; + const auto base{OpFunctionParameter(U32[2])}; + const auto offset{OpFunctionParameter(U32[1])}; + const auto flatbuf_offset{!dynamic ? OpFunctionParameter(U32[1]) : Id{}}; + Name(func, dynamic ? "read_const_dynamic" : "read_const"); + AddLabel(); + + const auto base_lo{OpUConvert(U64, OpCompositeExtract(U32[1], base, 0))}; + const auto base_hi{OpUConvert(U64, OpCompositeExtract(U32[1], base, 1))}; + const auto base_shift{OpShiftLeftLogical(U64, base_hi, ConstU32(32U))}; + const auto base_addr{OpBitwiseOr(U64, base_lo, base_shift)}; + const auto offset_bytes{OpShiftLeftLogical(U32[1], offset, ConstU32(2U))}; + const auto addr{OpIAdd(U64, base_addr, OpUConvert(U64, offset_bytes))}; + + const auto result = EmitMemoryRead(U32[1], addr, [&]() { + if (dynamic) { + return u32_zero_value; + } else { + const auto& flatbuf_buffer{buffers[flatbuf_index]}; + ASSERT(flatbuf_buffer.binding >= 0 && + flatbuf_buffer.buffer_type == BufferType::Flatbuf); + const auto [flatbuf_buffer_id, flatbuf_pointer_type] = flatbuf_buffer[PointerType::U32]; + const auto ptr{OpAccessChain(flatbuf_pointer_type, flatbuf_buffer_id, u32_zero_value, + flatbuf_offset)}; + return OpLoad(U32[1], ptr); + } + }); + + OpReturnValue(result); + OpFunctionEnd(); + return func; +} + void EmitContext::DefineFunctions() { if (info.uses_pack_10_11_11) { f32_to_uf11 = DefineFloat32ToUfloatM5(6, "f32_to_uf11"); @@ -1012,6 +1172,18 @@ void EmitContext::DefineFunctions() { uf11_to_f32 = DefineUfloatM5ToFloat32(6, "uf11_to_f32"); uf10_to_f32 = DefineUfloatM5ToFloat32(5, "uf10_to_f32"); } + if (info.dma_types != IR::Type::Void) { + get_bda_pointer = DefineGetBdaPointer(); + } + + if (True(info.readconst_types & Info::ReadConstType::Immediate)) { + LOG_DEBUG(Render_Recompiler, "Shader {:#x} uses immediate ReadConst", info.pgm_hash); + read_const = DefineReadConst(false); + } + if (True(info.readconst_types & Info::ReadConstType::Dynamic)) { + LOG_DEBUG(Render_Recompiler, "Shader {:#x} uses dynamic ReadConst", info.pgm_hash); + read_const_dynamic = DefineReadConst(true); + } } } // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 38d55e0e4..a2e0d2f47 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include "shader_recompiler/backend/bindings.h" @@ -41,6 +42,17 @@ public: Bindings& binding); ~EmitContext(); + enum class PointerType : u32 { + U8, + U16, + F16, + U32, + F32, + U64, + F64, + NumAlias, + }; + Id Def(const IR::Value& value); void DefineBufferProperties(); @@ -133,12 +145,72 @@ public: return ConstantComposite(type, constituents); } + inline Id AddLabel() { + last_label = Module::AddLabel(); + return last_label; + } + + inline Id AddLabel(Id label) { + last_label = Module::AddLabel(label); + return last_label; + } + + PointerType PointerTypeFromType(Id type) { + if (type.value == U8.value) + return PointerType::U8; + if (type.value == U16.value) + return PointerType::U16; + if (type.value == F16[1].value) + return PointerType::F16; + if (type.value == U32[1].value) + return PointerType::U32; + if (type.value == F32[1].value) + return PointerType::F32; + if (type.value == U64.value) + return PointerType::U64; + if (type.value == F64[1].value) + return PointerType::F64; + UNREACHABLE_MSG("Unknown type for pointer"); + } + + Id EmitMemoryRead(Id type, Id address, auto&& fallback) { + const Id available_label = OpLabel(); + const Id fallback_label = OpLabel(); + const Id merge_label = OpLabel(); + + const Id addr = OpFunctionCall(U64, get_bda_pointer, address); + const Id is_available = OpINotEqual(U1[1], addr, u64_zero_value); + OpSelectionMerge(merge_label, spv::SelectionControlMask::MaskNone); + OpBranchConditional(is_available, available_label, fallback_label); + + // Available + AddLabel(available_label); + const auto pointer_type = PointerTypeFromType(type); + const Id pointer_type_id = physical_pointer_types[pointer_type]; + const Id addr_ptr = OpConvertUToPtr(pointer_type_id, addr); + const Id result = OpLoad(type, addr_ptr, spv::MemoryAccessMask::Aligned, 4u); + OpBranch(merge_label); + + // Fallback + AddLabel(fallback_label); + const Id fallback_result = fallback(); + OpBranch(merge_label); + + // Merge + AddLabel(merge_label); + const Id final_result = + OpPhi(type, fallback_result, fallback_label, result, available_label); + return final_result; + } + Info& info; const RuntimeInfo& runtime_info; const Profile& profile; Stage stage; LogicalStage l_stage{}; + Id last_label{}; + Id void_id{}; Id U8{}; Id S8{}; @@ -161,9 +233,13 @@ public: Id true_value{}; Id false_value{}; + Id u8_one_value{}; + Id u8_zero_value{}; Id u32_one_value{}; Id u32_zero_value{}; Id f32_zero_value{}; + Id u64_one_value{}; + Id u64_zero_value{}; Id shared_u8{}; Id shared_u16{}; @@ -231,14 +307,6 @@ public: bool is_storage = false; }; - enum class BufferAlias : u32 { - U8, - U16, - U32, - F32, - NumAlias, - }; - struct BufferSpv { Id id; Id pointer_type; @@ -252,22 +320,40 @@ public: Id size; Id size_shorts; Id size_dwords; - std::array aliases; + std::array aliases; - const BufferSpv& operator[](BufferAlias alias) const { + const BufferSpv& operator[](PointerType alias) const { return aliases[u32(alias)]; } - BufferSpv& operator[](BufferAlias alias) { + BufferSpv& operator[](PointerType alias) { return aliases[u32(alias)]; } }; + struct PhysicalPointerTypes { + std::array types; + + const Id& operator[](PointerType type) const { + return types[u32(type)]; + } + + Id& operator[](PointerType type) { + return types[u32(type)]; + } + }; + Bindings& binding; boost::container::small_vector buf_type_ids; boost::container::small_vector buffers; boost::container::small_vector images; boost::container::small_vector samplers; + PhysicalPointerTypes physical_pointer_types; + std::unordered_map first_to_last_label_map; + + size_t flatbuf_index{}; + size_t bda_pagetable_index{}; + size_t fault_buffer_index{}; Id sampler_type{}; Id sampler_pointer_type{}; @@ -292,6 +378,11 @@ public: Id uf10_to_f32{}; Id f32_to_uf10{}; + Id get_bda_pointer{}; + + Id read_const{}; + Id read_const_dynamic{}; + private: void DefineArithmeticTypes(); void DefineInterfaces(); @@ -312,6 +403,10 @@ private: Id DefineFloat32ToUfloatM5(u32 mantissa_bits, std::string_view name); Id DefineUfloatM5ToFloat32(u32 mantissa_bits, std::string_view name); + Id DefineGetBdaPointer(); + + Id DefineReadConst(bool dynamic); + Id GetBufferSize(u32 sharp_idx); }; diff --git a/src/shader_recompiler/frontend/translate/scalar_memory.cpp b/src/shader_recompiler/frontend/translate/scalar_memory.cpp index 376cc304e..3c6fd3968 100644 --- a/src/shader_recompiler/frontend/translate/scalar_memory.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_memory.cpp @@ -39,21 +39,22 @@ void Translator::EmitScalarMemory(const GcnInst& inst) { void Translator::S_LOAD_DWORD(int num_dwords, const GcnInst& inst) { const auto& smrd = inst.control.smrd; - const u32 dword_offset = [&] -> u32 { + const IR::ScalarReg sbase{inst.src[0].code * 2}; + const IR::U32 dword_offset = [&] -> IR::U32 { if (smrd.imm) { - return smrd.offset; + return ir.Imm32(smrd.offset); } if (smrd.offset == SQ_SRC_LITERAL) { - return inst.src[1].code; + return ir.Imm32(inst.src[1].code); } - UNREACHABLE(); + return ir.ShiftRightLogical(ir.GetScalarReg(IR::ScalarReg(smrd.offset)), ir.Imm32(2)); }(); - const IR::ScalarReg sbase{inst.src[0].code * 2}; const IR::Value base = ir.CompositeConstruct(ir.GetScalarReg(sbase), ir.GetScalarReg(sbase + 1)); IR::ScalarReg dst_reg{inst.dst[0].code}; for (u32 i = 0; i < num_dwords; i++) { - ir.SetScalarReg(dst_reg + i, ir.ReadConst(base, ir.Imm32(dword_offset + i))); + IR::U32 index = ir.IAdd(dword_offset, ir.Imm32(i)); + ir.SetScalarReg(dst_reg + i, ir.ReadConst(base, index)); } } diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index ba28d7e43..d349d7827 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -41,7 +41,9 @@ constexpr u32 NUM_TEXTURE_TYPES = 7; enum class BufferType : u32 { Guest, - ReadConstUbo, + Flatbuf, + BdaPagetable, + FaultBuffer, GdsBuffer, SharedMemory, }; @@ -215,11 +217,18 @@ struct Info { bool stores_tess_level_outer{}; bool stores_tess_level_inner{}; bool translation_failed{}; - bool has_readconst{}; u8 mrt_mask{0u}; bool has_fetch_shader{false}; u32 fetch_shader_sgpr_base{0u}; + enum class ReadConstType { + None = 0, + Immediate = 1 << 0, + Dynamic = 1 << 1, + }; + ReadConstType readconst_types{}; + IR::Type dma_types{IR::Type::Void}; + explicit Info(Stage stage_, LogicalStage l_stage_, ShaderParams params) : stage{stage_}, l_stage{l_stage_}, pgm_hash{params.hash}, pgm_base{params.Base()}, user_data{params.user_data} {} @@ -277,6 +286,7 @@ struct Info { sizeof(tess_constants)); } }; +DECLARE_ENUM_FLAG_OPERATORS(Info::ReadConstType); constexpr AmdGpu::Buffer BufferResource::GetSharp(const Info& info) const noexcept { return inline_cbuf ? inline_cbuf : info.ReadUdSharp(sharp_idx); diff --git a/src/shader_recompiler/ir/abstract_syntax_list.cpp b/src/shader_recompiler/ir/abstract_syntax_list.cpp new file mode 100644 index 000000000..0d967ac11 --- /dev/null +++ b/src/shader_recompiler/ir/abstract_syntax_list.cpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "abstract_syntax_list.h" + +namespace Shader::IR { + +std::string DumpASLNode(const AbstractSyntaxNode& node, + const std::map& block_to_index, + const std::map& inst_to_index) { + switch (node.type) { + case AbstractSyntaxNode::Type::Block: + return fmt::format("Block: ${}", block_to_index.at(node.data.block)); + case AbstractSyntaxNode::Type::If: + return fmt::format("If: cond = %{}, body = ${}, merge = ${}", + inst_to_index.at(node.data.if_node.cond.Inst()), + block_to_index.at(node.data.if_node.body), + block_to_index.at(node.data.if_node.merge)); + case AbstractSyntaxNode::Type::EndIf: + return fmt::format("EndIf: merge = ${}", block_to_index.at(node.data.end_if.merge)); + case AbstractSyntaxNode::Type::Loop: + return fmt::format("Loop: body = ${}, continue = ${}, merge = ${}", + block_to_index.at(node.data.loop.body), + block_to_index.at(node.data.loop.continue_block), + block_to_index.at(node.data.loop.merge)); + case AbstractSyntaxNode::Type::Repeat: + return fmt::format("Repeat: cond = %{}, header = ${}, merge = ${}", + inst_to_index.at(node.data.repeat.cond.Inst()), + block_to_index.at(node.data.repeat.loop_header), + block_to_index.at(node.data.repeat.merge)); + case AbstractSyntaxNode::Type::Break: + return fmt::format("Break: cond = %{}, merge = ${}, skip = ${}", + inst_to_index.at(node.data.break_node.cond.Inst()), + block_to_index.at(node.data.break_node.merge), + block_to_index.at(node.data.break_node.skip)); + case AbstractSyntaxNode::Type::Return: + return "Return"; + case AbstractSyntaxNode::Type::Unreachable: + return "Unreachable"; + }; + UNREACHABLE(); +} + +} // namespace Shader::IR \ No newline at end of file diff --git a/src/shader_recompiler/ir/abstract_syntax_list.h b/src/shader_recompiler/ir/abstract_syntax_list.h index 313a23abc..a620baccb 100644 --- a/src/shader_recompiler/ir/abstract_syntax_list.h +++ b/src/shader_recompiler/ir/abstract_syntax_list.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "shader_recompiler/ir/value.h" @@ -53,4 +54,8 @@ struct AbstractSyntaxNode { }; using AbstractSyntaxList = std::vector; +std::string DumpASLNode(const AbstractSyntaxNode& node, + const std::map& block_to_index, + const std::map& inst_to_index); + } // namespace Shader::IR diff --git a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp index f53a0f4d4..d4759b32e 100644 --- a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp +++ b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "shader_recompiler/ir/program.h" +#include "video_core/buffer_cache/buffer_cache.h" namespace Shader::Optimization { @@ -79,14 +80,21 @@ void Visit(Info& info, const IR::Inst& inst) { info.uses_lane_id = true; break; case IR::Opcode::ReadConst: - if (!info.has_readconst) { + if (info.readconst_types == Info::ReadConstType::None) { info.buffers.push_back({ .used_types = IR::Type::U32, - .inline_cbuf = AmdGpu::Buffer::Null(), - .buffer_type = BufferType::ReadConstUbo, + // We can't guarantee that flatbuf will not grow past UBO + // limit if there are a lot of ReadConsts. (We could specialize) + .inline_cbuf = AmdGpu::Buffer::Placeholder(std::numeric_limits::max()), + .buffer_type = BufferType::Flatbuf, }); - info.has_readconst = true; } + if (inst.Flags() != 0) { + info.readconst_types |= Info::ReadConstType::Immediate; + } else { + info.readconst_types |= Info::ReadConstType::Dynamic; + } + info.dma_types |= IR::Type::U32; break; case IR::Opcode::PackUfloat10_11_11: info.uses_pack_10_11_11 = true; @@ -105,6 +113,21 @@ void CollectShaderInfoPass(IR::Program& program) { Visit(program.info, inst); } } + + if (program.info.dma_types != IR::Type::Void) { + program.info.buffers.push_back({ + .used_types = IR::Type::U64, + .inline_cbuf = AmdGpu::Buffer::Placeholder(VideoCore::BufferCache::BDA_PAGETABLE_SIZE), + .buffer_type = BufferType::BdaPagetable, + .is_written = true, + }); + program.info.buffers.push_back({ + .used_types = IR::Type::U8, + .inline_cbuf = AmdGpu::Buffer::Placeholder(VideoCore::BufferCache::FAULT_BUFFER_SIZE), + .buffer_type = BufferType::FaultBuffer, + .is_written = true, + }); + } } } // namespace Shader::Optimization diff --git a/src/shader_recompiler/ir/program.cpp b/src/shader_recompiler/ir/program.cpp index 7728a3ccb..f2f6e34fa 100644 --- a/src/shader_recompiler/ir/program.cpp +++ b/src/shader_recompiler/ir/program.cpp @@ -6,13 +6,30 @@ #include +#include "common/config.h" +#include "common/io_file.h" +#include "common/path_util.h" #include "shader_recompiler/ir/basic_block.h" #include "shader_recompiler/ir/program.h" #include "shader_recompiler/ir/value.h" namespace Shader::IR { -std::string DumpProgram(const Program& program) { +void DumpProgram(const Program& program, const Info& info, const std::string& type) { + using namespace Common::FS; + + if (!Config::dumpShaders()) { + return; + } + + const auto dump_dir = GetUserPath(PathType::ShaderDir) / "dumps"; + if (!std::filesystem::exists(dump_dir)) { + std::filesystem::create_directories(dump_dir); + } + const auto ir_filename = + fmt::format("{}_{:#018x}.{}irprogram.txt", info.stage, info.pgm_hash, type); + const auto ir_file = IOFile{dump_dir / ir_filename, FileAccessMode::Write, FileType::TextFile}; + size_t index{0}; std::map inst_to_index; std::map block_to_index; @@ -21,11 +38,20 @@ std::string DumpProgram(const Program& program) { block_to_index.emplace(block, index); ++index; } - std::string ret; + for (const auto& block : program.blocks) { - ret += IR::DumpBlock(*block, block_to_index, inst_to_index, index) + '\n'; + std::string s = IR::DumpBlock(*block, block_to_index, inst_to_index, index) + '\n'; + ir_file.WriteString(s); + } + + const auto asl_filename = fmt::format("{}_{:#018x}.{}asl.txt", info.stage, info.pgm_hash, type); + const auto asl_file = + IOFile{dump_dir / asl_filename, FileAccessMode::Write, FileType::TextFile}; + + for (const auto& node : program.syntax_list) { + std::string s = IR::DumpASLNode(node, block_to_index, inst_to_index) + '\n'; + asl_file.WriteString(s); } - return ret; } } // namespace Shader::IR diff --git a/src/shader_recompiler/ir/program.h b/src/shader_recompiler/ir/program.h index 84a1a2d40..3ffd4dc96 100644 --- a/src/shader_recompiler/ir/program.h +++ b/src/shader_recompiler/ir/program.h @@ -21,6 +21,6 @@ struct Program { Info& info; }; -[[nodiscard]] std::string DumpProgram(const Program& program); +void DumpProgram(const Program& program, const Info& info, const std::string& type = ""); } // namespace Shader::IR diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index 3e0bd98d2..9f92857d6 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -85,6 +85,8 @@ IR::Program TranslateProgram(std::span code, Pools& pools, Info& info Shader::Optimization::ConstantPropagationPass(program.post_order_blocks); Shader::Optimization::CollectShaderInfoPass(program); + Shader::IR::DumpProgram(program, info); + return program; } diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index d1cd98634..706e94c2b 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -133,6 +133,7 @@ void Liverpool::Process(std::stop_token stoken) { VideoCore::EndCapture(); if (rasterizer) { + rasterizer->ProcessFaults(); rasterizer->Flush(); } submit_done = false; diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index 9060074fb..89ac04f9a 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -37,6 +37,13 @@ struct Buffer { return buffer; } + static constexpr Buffer Placeholder(u32 size) { + Buffer buffer{}; + buffer.base_address = 1; + buffer.num_records = size; + return buffer; + } + bool Valid() const { return type == 0u; } diff --git a/src/video_core/buffer_cache/buffer.cpp b/src/video_core/buffer_cache/buffer.cpp index 15ef746cd..15bf0d81e 100644 --- a/src/video_core/buffer_cache/buffer.cpp +++ b/src/video_core/buffer_cache/buffer.cpp @@ -70,8 +70,11 @@ UniqueBuffer::~UniqueBuffer() { void UniqueBuffer::Create(const vk::BufferCreateInfo& buffer_ci, MemoryUsage usage, VmaAllocationInfo* out_alloc_info) { + const bool with_bda = bool(buffer_ci.usage & vk::BufferUsageFlagBits::eShaderDeviceAddress); + const VmaAllocationCreateFlags bda_flag = + with_bda ? VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT : 0; const VmaAllocationCreateInfo alloc_ci = { - .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | MemoryUsageVmaFlags(usage), + .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT | bda_flag | MemoryUsageVmaFlags(usage), .usage = MemoryUsageVma(usage), .requiredFlags = 0, .preferredFlags = MemoryUsagePreferredVmaFlags(usage), @@ -86,6 +89,15 @@ void UniqueBuffer::Create(const vk::BufferCreateInfo& buffer_ci, MemoryUsage usa ASSERT_MSG(result == VK_SUCCESS, "Failed allocating buffer with error {}", vk::to_string(vk::Result{result})); buffer = vk::Buffer{unsafe_buffer}; + + if (with_bda) { + vk::BufferDeviceAddressInfo bda_info{ + .buffer = buffer, + }; + auto bda_result = device.getBufferAddress(bda_info); + ASSERT_MSG(bda_result != 0, "Failed to get buffer device address"); + bda_addr = bda_result; + } } Buffer::Buffer(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, MemoryUsage usage_, diff --git a/src/video_core/buffer_cache/buffer.h b/src/video_core/buffer_cache/buffer.h index 188b4b2ca..530968787 100644 --- a/src/video_core/buffer_cache/buffer.h +++ b/src/video_core/buffer_cache/buffer.h @@ -68,6 +68,7 @@ struct UniqueBuffer { VmaAllocator allocator; VmaAllocation allocation; vk::Buffer buffer{}; + vk::DeviceAddress bda_addr = 0; }; class Buffer { @@ -115,6 +116,11 @@ public: return buffer; } + vk::DeviceAddress BufferDeviceAddress() const noexcept { + ASSERT_MSG(buffer.bda_addr != 0, "Can't get BDA from a non BDA buffer"); + return buffer.bda_addr; + } + std::optional GetBarrier( vk::Flags dst_acess_mask, vk::PipelineStageFlagBits2 dst_stage, u32 offset = 0) { diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index c993ef3e5..45863d8e8 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -3,13 +3,17 @@ #include #include "common/alignment.h" +#include "common/debug.h" #include "common/scope_exit.h" #include "common/types.h" #include "video_core/amdgpu/liverpool.h" #include "video_core/buffer_cache/buffer_cache.h" +#include "video_core/host_shaders/fault_buffer_process_comp.h" #include "video_core/renderer_vulkan/vk_graphics_pipeline.h" #include "video_core/renderer_vulkan/vk_instance.h" +#include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" #include "video_core/texture_cache/texture_cache.h" namespace VideoCore { @@ -17,17 +21,26 @@ namespace VideoCore { static constexpr size_t DataShareBufferSize = 64_KB; static constexpr size_t StagingBufferSize = 512_MB; static constexpr size_t UboStreamBufferSize = 128_MB; +static constexpr size_t DownloadBufferSize = 128_MB; +static constexpr size_t MaxPageFaults = 1024; BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, - AmdGpu::Liverpool* liverpool_, TextureCache& texture_cache_, - PageManager& tracker_) - : instance{instance_}, scheduler{scheduler_}, liverpool{liverpool_}, + Vulkan::Rasterizer& rasterizer_, AmdGpu::Liverpool* liverpool_, + TextureCache& texture_cache_, PageManager& tracker_) + : instance{instance_}, scheduler{scheduler_}, rasterizer{rasterizer_}, liverpool{liverpool_}, texture_cache{texture_cache_}, tracker{tracker_}, staging_buffer{instance, scheduler, MemoryUsage::Upload, StagingBufferSize}, stream_buffer{instance, scheduler, MemoryUsage::Stream, UboStreamBufferSize}, + download_buffer(instance, scheduler, MemoryUsage::Download, DownloadBufferSize), gds_buffer{instance, scheduler, MemoryUsage::Stream, 0, AllFlags, DataShareBufferSize}, - memory_tracker{&tracker} { + bda_pagetable_buffer{instance, scheduler, MemoryUsage::DeviceLocal, + 0, AllFlags, BDA_PAGETABLE_SIZE}, + fault_buffer(instance, scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, FAULT_BUFFER_SIZE), + memory_tracker{tracker} { Vulkan::SetObjectName(instance.GetDevice(), gds_buffer.Handle(), "GDS Buffer"); + Vulkan::SetObjectName(instance.GetDevice(), bda_pagetable_buffer.Handle(), + "BDA Page Table Buffer"); + Vulkan::SetObjectName(instance.GetDevice(), fault_buffer.Handle(), "Fault Buffer"); // Ensure the first slot is used for the null buffer const auto null_id = @@ -35,15 +48,93 @@ BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& s ASSERT(null_id.index == 0); const vk::Buffer& null_buffer = slot_buffers[null_id].buffer; Vulkan::SetObjectName(instance.GetDevice(), null_buffer, "Null Buffer"); + + // Prepare the fault buffer parsing pipeline + boost::container::static_vector bindings{ + { + .binding = 0, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + }, + { + .binding = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eCompute, + }, + }; + + const vk::DescriptorSetLayoutCreateInfo desc_layout_ci = { + .flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR, + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data(), + }; + auto [desc_layout_result, desc_layout] = + instance.GetDevice().createDescriptorSetLayoutUnique(desc_layout_ci); + ASSERT_MSG(desc_layout_result == vk::Result::eSuccess, + "Failed to create descriptor set layout: {}", vk::to_string(desc_layout_result)); + fault_process_desc_layout = std::move(desc_layout); + + const auto& module = Vulkan::Compile(HostShaders::FAULT_BUFFER_PROCESS_COMP, + vk::ShaderStageFlagBits::eCompute, instance.GetDevice()); + Vulkan::SetObjectName(instance.GetDevice(), module, "Fault Buffer Parser"); + + const vk::SpecializationMapEntry specialization_map_entry = { + .constantID = 0, + .offset = 0, + .size = sizeof(u32), + }; + + const vk::SpecializationInfo specialization_info = { + .mapEntryCount = 1, + .pMapEntries = &specialization_map_entry, + .dataSize = sizeof(u32), + .pData = &CACHING_PAGEBITS, + }; + + const vk::PipelineShaderStageCreateInfo shader_ci = { + .stage = vk::ShaderStageFlagBits::eCompute, + .module = module, + .pName = "main", + .pSpecializationInfo = &specialization_info, + }; + + const vk::PipelineLayoutCreateInfo layout_info = { + .setLayoutCount = 1U, + .pSetLayouts = &(*fault_process_desc_layout), + }; + auto [layout_result, layout] = instance.GetDevice().createPipelineLayoutUnique(layout_info); + ASSERT_MSG(layout_result == vk::Result::eSuccess, "Failed to create pipeline layout: {}", + vk::to_string(layout_result)); + fault_process_pipeline_layout = std::move(layout); + + const vk::ComputePipelineCreateInfo pipeline_info = { + .stage = shader_ci, + .layout = *fault_process_pipeline_layout, + }; + auto [pipeline_result, pipeline] = + instance.GetDevice().createComputePipelineUnique({}, pipeline_info); + ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create compute pipeline: {}", + vk::to_string(pipeline_result)); + fault_process_pipeline = std::move(pipeline); + Vulkan::SetObjectName(instance.GetDevice(), *fault_process_pipeline, + "Fault Buffer Parser Pipeline"); + + instance.GetDevice().destroyShaderModule(module); } BufferCache::~BufferCache() = default; -void BufferCache::InvalidateMemory(VAddr device_addr, u64 size) { +void BufferCache::InvalidateMemory(VAddr device_addr, u64 size, bool unmap) { const bool is_tracked = IsRegionRegistered(device_addr, size); if (is_tracked) { // Mark the page as CPU modified to stop tracking writes. memory_tracker.MarkRegionAsCpuModified(device_addr, size); + + if (unmap) { + return; + } } } @@ -69,20 +160,20 @@ void BufferCache::DownloadBufferMemory(Buffer& buffer, VAddr device_addr, u64 si if (total_size_bytes == 0) { return; } - const auto [staging, offset] = staging_buffer.Map(total_size_bytes); + const auto [download, offset] = download_buffer.Map(total_size_bytes); for (auto& copy : copies) { // Modify copies to have the staging offset in mind copy.dstOffset += offset; } - staging_buffer.Commit(); + download_buffer.Commit(); scheduler.EndRendering(); const auto cmdbuf = scheduler.CommandBuffer(); - cmdbuf.copyBuffer(buffer.buffer, staging_buffer.Handle(), copies); + cmdbuf.copyBuffer(buffer.buffer, download_buffer.Handle(), copies); scheduler.Finish(); for (const auto& copy : copies) { const VAddr copy_device_addr = buffer.CpuAddr() + copy.srcOffset; const u64 dst_offset = copy.dstOffset - offset; - std::memcpy(std::bit_cast(copy_device_addr), staging + dst_offset, copy.size); + std::memcpy(std::bit_cast(copy_device_addr), download + dst_offset, copy.size); } } @@ -206,58 +297,37 @@ void BufferCache::InlineData(VAddr address, const void* value, u32 num_bytes, bo memcpy(std::bit_cast(address), value, num_bytes); return; } - scheduler.EndRendering(); - const Buffer* buffer = [&] { + Buffer* buffer = [&] { if (is_gds) { return &gds_buffer; } const BufferId buffer_id = FindBuffer(address, num_bytes); return &slot_buffers[buffer_id]; }(); - const auto cmdbuf = scheduler.CommandBuffer(); - const vk::BufferMemoryBarrier2 pre_barrier = { - .srcStageMask = vk::PipelineStageFlagBits2::eAllCommands, - .srcAccessMask = vk::AccessFlagBits2::eMemoryRead, - .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, - .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, - .buffer = buffer->Handle(), - .offset = buffer->Offset(address), - .size = num_bytes, - }; - const vk::BufferMemoryBarrier2 post_barrier = { - .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, - .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, - .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, - .dstAccessMask = vk::AccessFlagBits2::eMemoryRead, - .buffer = buffer->Handle(), - .offset = buffer->Offset(address), - .size = num_bytes, - }; - cmdbuf.pipelineBarrier2(vk::DependencyInfo{ - .dependencyFlags = vk::DependencyFlagBits::eByRegion, - .bufferMemoryBarrierCount = 1, - .pBufferMemoryBarriers = &pre_barrier, - }); - // vkCmdUpdateBuffer can only copy up to 65536 bytes at a time. - static constexpr u32 UpdateBufferMaxSize = 65536; - const auto dst_offset = buffer->Offset(address); - for (u32 offset = 0; offset < num_bytes; offset += UpdateBufferMaxSize) { - const auto* update_src = static_cast(value) + offset; - const auto update_dst = dst_offset + offset; - const auto update_size = std::min(num_bytes - offset, UpdateBufferMaxSize); - cmdbuf.updateBuffer(buffer->Handle(), update_dst, update_size, update_src); + InlineDataBuffer(*buffer, address, value, num_bytes); +} + +void BufferCache::WriteData(VAddr address, const void* value, u32 num_bytes, bool is_gds) { + ASSERT_MSG(address % 4 == 0, "GDS offset must be dword aligned"); + if (!is_gds && !IsRegionRegistered(address, num_bytes)) { + memcpy(std::bit_cast(address), value, num_bytes); + return; } - cmdbuf.pipelineBarrier2(vk::DependencyInfo{ - .dependencyFlags = vk::DependencyFlagBits::eByRegion, - .bufferMemoryBarrierCount = 1, - .pBufferMemoryBarriers = &post_barrier, - }); + Buffer* buffer = [&] { + if (is_gds) { + return &gds_buffer; + } + const BufferId buffer_id = FindBuffer(address, num_bytes); + return &slot_buffers[buffer_id]; + }(); + WriteDataBuffer(*buffer, address, value, num_bytes); } std::pair BufferCache::ObtainBuffer(VAddr device_addr, u32 size, bool is_written, bool is_texel_buffer, BufferId buffer_id) { // For small uniform buffers that have not been modified by gpu // use device local stream buffer to reduce renderpass breaks. + // Maybe we want to modify the threshold now that the page size is 16KB? static constexpr u64 StreamThreshold = CACHING_PAGESIZE; const bool is_gpu_dirty = memory_tracker.IsRegionGpuModified(device_addr, size); if (!is_written && size <= StreamThreshold && !is_gpu_dirty) { @@ -280,7 +350,7 @@ std::pair BufferCache::ObtainBuffer(VAddr device_addr, u32 size, b std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, bool prefer_gpu) { // Check if any buffer contains the full requested range. const u64 page = gpu_addr >> CACHING_PAGEBITS; - const BufferId buffer_id = page_table[page]; + const BufferId buffer_id = page_table[page].buffer_id; if (buffer_id) { Buffer& buffer = slot_buffers[buffer_id]; if (buffer.IsInBounds(gpu_addr, size)) { @@ -300,24 +370,8 @@ std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, } bool BufferCache::IsRegionRegistered(VAddr addr, size_t size) { - const VAddr end_addr = addr + size; - const u64 page_end = Common::DivCeil(end_addr, CACHING_PAGESIZE); - for (u64 page = addr >> CACHING_PAGEBITS; page < page_end;) { - const BufferId buffer_id = page_table[page]; - if (!buffer_id) { - ++page; - continue; - } - std::shared_lock lk{mutex}; - Buffer& buffer = slot_buffers[buffer_id]; - const VAddr buf_start_addr = buffer.CpuAddr(); - const VAddr buf_end_addr = buf_start_addr + buffer.SizeBytes(); - if (buf_start_addr < end_addr && addr < buf_end_addr) { - return true; - } - page = Common::DivCeil(buf_end_addr, CACHING_PAGESIZE); - } - return false; + // Check if we are missing some edge case here + return buffer_ranges.Intersects(addr, size); } bool BufferCache::IsRegionCpuModified(VAddr addr, size_t size) { @@ -333,7 +387,7 @@ BufferId BufferCache::FindBuffer(VAddr device_addr, u32 size) { return NULL_BUFFER_ID; } const u64 page = device_addr >> CACHING_PAGEBITS; - const BufferId buffer_id = page_table[page]; + const BufferId buffer_id = page_table[page].buffer_id; if (!buffer_id) { return CreateBuffer(device_addr, size); } @@ -379,7 +433,7 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(VAddr device_addr, u32 w } for (; device_addr >> CACHING_PAGEBITS < Common::DivCeil(end, CACHING_PAGESIZE); device_addr += CACHING_PAGESIZE) { - const BufferId overlap_id = page_table[device_addr >> CACHING_PAGEBITS]; + const BufferId overlap_id = page_table[device_addr >> CACHING_PAGEBITS].buffer_id; if (!overlap_id) { continue; } @@ -480,11 +534,21 @@ BufferId BufferCache::CreateBuffer(VAddr device_addr, u32 wanted_size) { const OverlapResult overlap = ResolveOverlaps(device_addr, wanted_size); const u32 size = static_cast(overlap.end - overlap.begin); const BufferId new_buffer_id = [&] { - std::scoped_lock lk{mutex}; + std::scoped_lock lk{slot_buffers_mutex}; return slot_buffers.insert(instance, scheduler, MemoryUsage::DeviceLocal, overlap.begin, - AllFlags, size); + AllFlags | vk::BufferUsageFlagBits::eShaderDeviceAddress, size); }(); auto& new_buffer = slot_buffers[new_buffer_id]; + boost::container::small_vector bda_addrs; + const u64 start_page = overlap.begin >> CACHING_PAGEBITS; + const u64 size_pages = size >> CACHING_PAGEBITS; + bda_addrs.reserve(size_pages); + for (u64 i = 0; i < size_pages; ++i) { + vk::DeviceAddress addr = new_buffer.BufferDeviceAddress() + (i << CACHING_PAGEBITS); + bda_addrs.push_back(addr); + } + WriteDataBuffer(bda_pagetable_buffer, start_page * sizeof(vk::DeviceAddress), bda_addrs.data(), + bda_addrs.size() * sizeof(vk::DeviceAddress)); const size_t size_bytes = new_buffer.SizeBytes(); const auto cmdbuf = scheduler.CommandBuffer(); scheduler.EndRendering(); @@ -496,6 +560,129 @@ BufferId BufferCache::CreateBuffer(VAddr device_addr, u32 wanted_size) { return new_buffer_id; } +void BufferCache::ProcessFaultBuffer() { + // Run fault processing shader + const auto [mapped, offset] = download_buffer.Map(MaxPageFaults * sizeof(u64)); + vk::BufferMemoryBarrier2 fault_buffer_barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .srcAccessMask = vk::AccessFlagBits2::eShaderWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eComputeShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead, + .buffer = fault_buffer.Handle(), + .offset = 0, + .size = FAULT_BUFFER_SIZE, + }; + vk::BufferMemoryBarrier2 download_barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eComputeShader, + .dstAccessMask = vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite, + .buffer = download_buffer.Handle(), + .offset = offset, + .size = MaxPageFaults * sizeof(u64), + }; + std::array barriers{fault_buffer_barrier, download_barrier}; + vk::DescriptorBufferInfo fault_buffer_info{ + .buffer = fault_buffer.Handle(), + .offset = 0, + .range = FAULT_BUFFER_SIZE, + }; + vk::DescriptorBufferInfo download_info{ + .buffer = download_buffer.Handle(), + .offset = offset, + .range = MaxPageFaults * sizeof(u64), + }; + boost::container::small_vector writes{ + { + .dstSet = VK_NULL_HANDLE, + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &fault_buffer_info, + }, + { + .dstSet = VK_NULL_HANDLE, + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &download_info, + }, + }; + download_buffer.Commit(); + scheduler.EndRendering(); + const auto cmdbuf = scheduler.CommandBuffer(); + cmdbuf.fillBuffer(download_buffer.Handle(), offset, MaxPageFaults * sizeof(u64), 0); + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 2, + .pBufferMemoryBarriers = barriers.data(), + }); + cmdbuf.bindPipeline(vk::PipelineBindPoint::eCompute, *fault_process_pipeline); + cmdbuf.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *fault_process_pipeline_layout, 0, + writes); + constexpr u32 num_threads = CACHING_NUMPAGES / 32; // 1 bit per page, 32 pages per workgroup + constexpr u32 num_workgroups = Common::DivCeil(num_threads, 64u); + cmdbuf.dispatch(num_workgroups, 1, 1); + + // Reset fault buffer + const vk::BufferMemoryBarrier2 reset_pre_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eComputeShader, + .srcAccessMask = vk::AccessFlagBits2::eShaderRead, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .buffer = fault_buffer.Handle(), + .offset = 0, + .size = FAULT_BUFFER_SIZE, + }; + const vk::BufferMemoryBarrier2 reset_post_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite, + .buffer = fault_buffer.Handle(), + .offset = 0, + .size = FAULT_BUFFER_SIZE, + }; + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &reset_pre_barrier, + }); + cmdbuf.fillBuffer(fault_buffer.buffer, 0, FAULT_BUFFER_SIZE, 0); + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &reset_post_barrier, + }); + + // Defer creating buffers + scheduler.DeferOperation([this, mapped]() { + // Create the fault buffers batched + boost::icl::interval_set fault_ranges; + const u64* fault_ptr = std::bit_cast(mapped); + const u32 fault_count = static_cast(*(fault_ptr++)); + for (u32 i = 0; i < fault_count; ++i) { + const VAddr fault = *(fault_ptr++); + const VAddr fault_end = fault + CACHING_PAGESIZE; // This can be adjusted + fault_ranges += + boost::icl::interval_set::interval_type::right_open(fault, fault_end); + LOG_INFO(Render_Vulkan, "Accessed non-GPU mapped memory at {:#x}", fault); + } + for (const auto& range : fault_ranges) { + const VAddr start = range.lower(); + const VAddr end = range.upper(); + const u64 page_start = start >> CACHING_PAGEBITS; + const u64 page_end = Common::DivCeil(end, CACHING_PAGESIZE); + // Buffer size is in 32 bits + ASSERT_MSG((range.upper() - range.lower()) <= std::numeric_limits::max(), + "Buffer size is too large"); + CreateBuffer(start, static_cast(end - start)); + } + }); +} + void BufferCache::Register(BufferId buffer_id) { ChangeRegister(buffer_id); } @@ -514,11 +701,16 @@ void BufferCache::ChangeRegister(BufferId buffer_id) { const u64 page_end = Common::DivCeil(device_addr_end, CACHING_PAGESIZE); for (u64 page = page_begin; page != page_end; ++page) { if constexpr (insert) { - page_table[page] = buffer_id; + page_table[page].buffer_id = buffer_id; } else { - page_table[page] = BufferId{}; + page_table[page].buffer_id = BufferId{}; } } + if constexpr (insert) { + buffer_ranges.Add(buffer.CpuAddr(), buffer.SizeBytes(), buffer_id); + } else { + buffer_ranges.Subtract(buffer.CpuAddr(), buffer.SizeBytes()); + } } void BufferCache::SynchronizeBuffer(Buffer& buffer, VAddr device_addr, u32 size, @@ -697,6 +889,138 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, VAddr device_addr, return true; } +void BufferCache::SynchronizeBuffersInRange(VAddr device_addr, u64 size) { + if (device_addr == 0) { + return; + } + VAddr device_addr_end = device_addr + size; + ForEachBufferInRange(device_addr, size, [&](BufferId buffer_id, Buffer& buffer) { + RENDERER_TRACE; + VAddr start = std::max(buffer.CpuAddr(), device_addr); + VAddr end = std::min(buffer.CpuAddr() + buffer.SizeBytes(), device_addr_end); + u32 size = static_cast(end - start); + SynchronizeBuffer(buffer, start, size, false); + }); +} + +void BufferCache::MemoryBarrier() { + // Vulkan doesn't know which buffer we access in a shader if we use + // BufferDeviceAddress. We need a full memory barrier. + // For now, we only read memory using BDA. If we want to write to it, + // we might need to change this. + scheduler.EndRendering(); + const auto cmdbuf = scheduler.CommandBuffer(); + vk::MemoryBarrier2 barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eMemoryWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstAccessMask = vk::AccessFlagBits2::eMemoryRead, + }; + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .memoryBarrierCount = 1, + .pMemoryBarriers = &barrier, + }); +} + +void BufferCache::InlineDataBuffer(Buffer& buffer, VAddr address, const void* value, + u32 num_bytes) { + scheduler.EndRendering(); + const auto cmdbuf = scheduler.CommandBuffer(); + const vk::BufferMemoryBarrier2 pre_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .srcAccessMask = vk::AccessFlagBits2::eMemoryRead, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .buffer = buffer.Handle(), + .offset = buffer.Offset(address), + .size = num_bytes, + }; + const vk::BufferMemoryBarrier2 post_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstAccessMask = vk::AccessFlagBits2::eMemoryRead, + .buffer = buffer.Handle(), + .offset = buffer.Offset(address), + .size = num_bytes, + }; + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &pre_barrier, + }); + // vkCmdUpdateBuffer can only copy up to 65536 bytes at a time. + static constexpr u32 UpdateBufferMaxSize = 65536; + const auto dst_offset = buffer.Offset(address); + for (u32 offset = 0; offset < num_bytes; offset += UpdateBufferMaxSize) { + const auto* update_src = static_cast(value) + offset; + const auto update_dst = dst_offset + offset; + const auto update_size = std::min(num_bytes - offset, UpdateBufferMaxSize); + cmdbuf.updateBuffer(buffer.Handle(), update_dst, update_size, update_src); + } + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &post_barrier, + }); +} + +void BufferCache::WriteDataBuffer(Buffer& buffer, VAddr address, const void* value, u32 num_bytes) { + vk::BufferCopy copy = { + .srcOffset = 0, + .dstOffset = buffer.Offset(address), + .size = num_bytes, + }; + vk::Buffer src_buffer = staging_buffer.Handle(); + if (num_bytes < StagingBufferSize) { + const auto [staging, offset] = staging_buffer.Map(num_bytes); + std::memcpy(staging, value, num_bytes); + copy.srcOffset = offset; + staging_buffer.Commit(); + } else { + // For large one time transfers use a temporary host buffer. + // RenderDoc can lag quite a bit if the stream buffer is too large. + Buffer temp_buffer{ + instance, scheduler, MemoryUsage::Upload, 0, vk::BufferUsageFlagBits::eTransferSrc, + num_bytes}; + src_buffer = temp_buffer.Handle(); + u8* const staging = temp_buffer.mapped_data.data(); + std::memcpy(staging, value, num_bytes); + scheduler.DeferOperation([buffer = std::move(temp_buffer)]() mutable {}); + } + scheduler.EndRendering(); + const auto cmdbuf = scheduler.CommandBuffer(); + const vk::BufferMemoryBarrier2 pre_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .srcAccessMask = vk::AccessFlagBits2::eMemoryRead, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .buffer = buffer.Handle(), + .offset = buffer.Offset(address), + .size = num_bytes, + }; + const vk::BufferMemoryBarrier2 post_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eAllCommands, + .dstAccessMask = vk::AccessFlagBits2::eMemoryRead | vk::AccessFlagBits2::eMemoryWrite, + .buffer = buffer.Handle(), + .offset = buffer.Offset(address), + .size = num_bytes, + }; + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &pre_barrier, + }); + cmdbuf.copyBuffer(src_buffer, buffer.Handle(), copy); + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &post_barrier, + }); +} + void BufferCache::DeleteBuffer(BufferId buffer_id) { Buffer& buffer = slot_buffers[buffer_id]; Unregister(buffer_id); diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 71a6bed2a..2d6551a7f 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -38,14 +38,22 @@ class TextureCache; class BufferCache { public: - static constexpr u32 CACHING_PAGEBITS = 12; + static constexpr u32 CACHING_PAGEBITS = 14; static constexpr u64 CACHING_PAGESIZE = u64{1} << CACHING_PAGEBITS; - static constexpr u64 DEVICE_PAGESIZE = 4_KB; + static constexpr u64 DEVICE_PAGESIZE = 16_KB; + static constexpr u64 CACHING_NUMPAGES = u64{1} << (40 - CACHING_PAGEBITS); + + static constexpr u64 BDA_PAGETABLE_SIZE = CACHING_NUMPAGES * sizeof(vk::DeviceAddress); + static constexpr u64 FAULT_BUFFER_SIZE = CACHING_NUMPAGES / 8; // Bit per page + + struct PageData { + BufferId buffer_id{}; + }; struct Traits { - using Entry = BufferId; + using Entry = PageData; static constexpr size_t AddressSpaceBits = 40; - static constexpr size_t FirstLevelBits = 14; + static constexpr size_t FirstLevelBits = 16; static constexpr size_t PageBits = CACHING_PAGEBITS; }; using PageTable = MultiLevelPageTable; @@ -59,8 +67,8 @@ public: public: explicit BufferCache(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler, - AmdGpu::Liverpool* liverpool, TextureCache& texture_cache, - PageManager& tracker); + Vulkan::Rasterizer& rasterizer_, AmdGpu::Liverpool* liverpool, + TextureCache& texture_cache, PageManager& tracker); ~BufferCache(); /// Returns a pointer to GDS device local buffer. @@ -73,13 +81,23 @@ public: return stream_buffer; } + /// Retrieves the device local DBA page table buffer. + [[nodiscard]] Buffer* GetBdaPageTableBuffer() noexcept { + return &bda_pagetable_buffer; + } + + /// Retrieves the fault buffer. + [[nodiscard]] Buffer* GetFaultBuffer() noexcept { + return &fault_buffer; + } + /// Retrieves the buffer with the specified id. [[nodiscard]] Buffer& GetBuffer(BufferId id) { return slot_buffers[id]; } /// Invalidates any buffer in the logical page range. - void InvalidateMemory(VAddr device_addr, u64 size); + void InvalidateMemory(VAddr device_addr, u64 size, bool unmap); /// Binds host vertex buffers for the current draw. void BindVertexBuffers(const Vulkan::GraphicsPipeline& pipeline); @@ -87,9 +105,12 @@ public: /// Bind host index buffer for the current draw. void BindIndexBuffer(u32 index_offset); - /// Writes a value to GPU buffer. + /// Writes a value to GPU buffer. (uses command buffer to temporarily store the data) void InlineData(VAddr address, const void* value, u32 num_bytes, bool is_gds); + /// Writes a value to GPU buffer. (uses staging buffer to temporarily store the data) + void WriteData(VAddr address, const void* value, u32 num_bytes, bool is_gds); + /// Obtains a buffer for the specified region. [[nodiscard]] std::pair ObtainBuffer(VAddr gpu_addr, u32 size, bool is_written, bool is_texel_buffer = false, @@ -108,24 +129,29 @@ public: /// Return true when a CPU region is modified from the GPU [[nodiscard]] bool IsRegionGpuModified(VAddr addr, size_t size); - [[nodiscard]] BufferId FindBuffer(VAddr device_addr, u32 size); + /// Return buffer id for the specified region + BufferId FindBuffer(VAddr device_addr, u32 size); + + /// Processes the fault buffer. + void ProcessFaultBuffer(); + + /// Synchronizes all buffers in the specified range. + void SynchronizeBuffersInRange(VAddr device_addr, u64 size); + + /// Synchronizes all buffers neede for DMA. + void SynchronizeDmaBuffers(); + + /// Record memory barrier. Used for buffers when accessed via BDA. + void MemoryBarrier(); private: template void ForEachBufferInRange(VAddr device_addr, u64 size, Func&& func) { - const u64 page_end = Common::DivCeil(device_addr + size, CACHING_PAGESIZE); - for (u64 page = device_addr >> CACHING_PAGEBITS; page < page_end;) { - const BufferId buffer_id = page_table[page]; - if (!buffer_id) { - ++page; - continue; - } - Buffer& buffer = slot_buffers[buffer_id]; - func(buffer_id, buffer); - - const VAddr end_addr = buffer.CpuAddr() + buffer.SizeBytes(); - page = Common::DivCeil(end_addr, CACHING_PAGESIZE); - } + buffer_ranges.ForEachInRange(device_addr, size, + [&](u64 page_start, u64 page_end, BufferId id) { + Buffer& buffer = slot_buffers[id]; + func(id, buffer); + }); } void DownloadBufferMemory(Buffer& buffer, VAddr device_addr, u64 size); @@ -134,7 +160,7 @@ private: void JoinOverlap(BufferId new_buffer_id, BufferId overlap_id, bool accumulate_stream_score); - [[nodiscard]] BufferId CreateBuffer(VAddr device_addr, u32 wanted_size); + BufferId CreateBuffer(VAddr device_addr, u32 wanted_size); void Register(BufferId buffer_id); @@ -147,21 +173,33 @@ private: bool SynchronizeBufferFromImage(Buffer& buffer, VAddr device_addr, u32 size); + void InlineDataBuffer(Buffer& buffer, VAddr address, const void* value, u32 num_bytes); + + void WriteDataBuffer(Buffer& buffer, VAddr address, const void* value, u32 num_bytes); + void DeleteBuffer(BufferId buffer_id); const Vulkan::Instance& instance; Vulkan::Scheduler& scheduler; + Vulkan::Rasterizer& rasterizer; AmdGpu::Liverpool* liverpool; TextureCache& texture_cache; PageManager& tracker; StreamBuffer staging_buffer; StreamBuffer stream_buffer; + StreamBuffer download_buffer; Buffer gds_buffer; - std::shared_mutex mutex; + Buffer bda_pagetable_buffer; + Buffer fault_buffer; + std::shared_mutex slot_buffers_mutex; Common::SlotVector slot_buffers; RangeSet gpu_modified_ranges; + SplitRangeMap buffer_ranges; MemoryTracker memory_tracker; PageTable page_table; + vk::UniqueDescriptorSetLayout fault_process_desc_layout; + vk::UniquePipeline fault_process_pipeline; + vk::UniquePipelineLayout fault_process_pipeline_layout; }; } // namespace VideoCore diff --git a/src/video_core/buffer_cache/memory_tracker_base.h b/src/video_core/buffer_cache/memory_tracker_base.h index d9166b11c..c60aa9c80 100644 --- a/src/video_core/buffer_cache/memory_tracker_base.h +++ b/src/video_core/buffer_cache/memory_tracker_base.h @@ -7,6 +7,7 @@ #include #include #include +#include "common/debug.h" #include "common/types.h" #include "video_core/buffer_cache/word_manager.h" @@ -19,11 +20,11 @@ public: static constexpr size_t MANAGER_POOL_SIZE = 32; public: - explicit MemoryTracker(PageManager* tracker_) : tracker{tracker_} {} + explicit MemoryTracker(PageManager& tracker_) : tracker{&tracker_} {} ~MemoryTracker() = default; /// Returns true if a region has been modified from the CPU - [[nodiscard]] bool IsRegionCpuModified(VAddr query_cpu_addr, u64 query_size) noexcept { + bool IsRegionCpuModified(VAddr query_cpu_addr, u64 query_size) noexcept { return IteratePages( query_cpu_addr, query_size, [](RegionManager* manager, u64 offset, size_t size) { return manager->template IsRegionModified(offset, size); @@ -31,7 +32,7 @@ public: } /// Returns true if a region has been modified from the GPU - [[nodiscard]] bool IsRegionGpuModified(VAddr query_cpu_addr, u64 query_size) noexcept { + bool IsRegionGpuModified(VAddr query_cpu_addr, u64 query_size) noexcept { return IteratePages( query_cpu_addr, query_size, [](RegionManager* manager, u64 offset, size_t size) { return manager->template IsRegionModified(offset, size); @@ -57,8 +58,7 @@ public: } /// Call 'func' for each CPU modified range and unmark those pages as CPU modified - template - void ForEachUploadRange(VAddr query_cpu_range, u64 query_size, Func&& func) { + void ForEachUploadRange(VAddr query_cpu_range, u64 query_size, auto&& func) { IteratePages(query_cpu_range, query_size, [&func](RegionManager* manager, u64 offset, size_t size) { manager->template ForEachModifiedRange( @@ -67,17 +67,12 @@ public: } /// Call 'func' for each GPU modified range and unmark those pages as GPU modified - template - void ForEachDownloadRange(VAddr query_cpu_range, u64 query_size, Func&& func) { + template + void ForEachDownloadRange(VAddr query_cpu_range, u64 query_size, auto&& func) { IteratePages(query_cpu_range, query_size, [&func](RegionManager* manager, u64 offset, size_t size) { - if constexpr (clear) { - manager->template ForEachModifiedRange( - manager->GetCpuAddr() + offset, size, func); - } else { - manager->template ForEachModifiedRange( - manager->GetCpuAddr() + offset, size, func); - } + manager->template ForEachModifiedRange( + manager->GetCpuAddr() + offset, size, func); }); } @@ -91,6 +86,7 @@ private: */ template bool IteratePages(VAddr cpu_address, size_t size, Func&& func) { + RENDERER_TRACE; using FuncReturn = typename std::invoke_result::type; static constexpr bool BOOL_BREAK = std::is_same_v; std::size_t remaining_size{size}; diff --git a/src/video_core/buffer_cache/range_set.h b/src/video_core/buffer_cache/range_set.h index 2abf6e524..5c8e78c7c 100644 --- a/src/video_core/buffer_cache/range_set.h +++ b/src/video_core/buffer_cache/range_set.h @@ -3,7 +3,10 @@ #pragma once +#include #include +#include +#include #include #include #include @@ -38,6 +41,22 @@ struct RangeSet { m_ranges_set.subtract(interval); } + void Clear() { + m_ranges_set.clear(); + } + + bool Contains(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::contains(m_ranges_set, interval); + } + + bool Intersects(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::intersects(m_ranges_set, interval); + } + template void ForEach(Func&& func) const { if (m_ranges_set.empty()) { @@ -77,14 +96,29 @@ struct RangeSet { } } + template + void ForEachNotInRange(VAddr base_addr, size_t size, Func&& func) const { + const VAddr end_addr = base_addr + size; + ForEachInRange(base_addr, size, [&](VAddr range_addr, VAddr range_end) { + if (size_t gap_size = range_addr - base_addr; gap_size != 0) { + func(base_addr, gap_size); + } + base_addr = range_end; + }); + if (base_addr != end_addr) { + func(base_addr, end_addr - base_addr); + } + } + IntervalSet m_ranges_set; }; +template class RangeMap { public: using IntervalMap = - boost::icl::interval_map; using IntervalType = typename IntervalMap::interval_type; @@ -99,7 +133,7 @@ public: RangeMap(RangeMap&& other); RangeMap& operator=(RangeMap&& other); - void Add(VAddr base_address, size_t size, u64 value) { + void Add(VAddr base_address, size_t size, const T& value) { const VAddr end_address = base_address + size; IntervalType interval{base_address, end_address}; m_ranges_map.add({interval, value}); @@ -111,6 +145,35 @@ public: m_ranges_map -= interval; } + void Clear() { + m_ranges_map.clear(); + } + + bool Contains(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::contains(m_ranges_map, interval); + } + + bool Intersects(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::intersects(m_ranges_map, interval); + } + + template + void ForEach(Func&& func) const { + if (m_ranges_map.empty()) { + return; + } + + for (const auto& [interval, value] : m_ranges_map) { + const VAddr inter_addr_end = interval.upper(); + const VAddr inter_addr = interval.lower(); + func(inter_addr, inter_addr_end, value); + } + } + template void ForEachInRange(VAddr base_addr, size_t size, Func&& func) const { if (m_ranges_map.empty()) { @@ -140,7 +203,111 @@ public: template void ForEachNotInRange(VAddr base_addr, size_t size, Func&& func) const { const VAddr end_addr = base_addr + size; - ForEachInRange(base_addr, size, [&](VAddr range_addr, VAddr range_end, u64) { + ForEachInRange(base_addr, size, [&](VAddr range_addr, VAddr range_end, const T&) { + if (size_t gap_size = range_addr - base_addr; gap_size != 0) { + func(base_addr, gap_size); + } + base_addr = range_end; + }); + if (base_addr != end_addr) { + func(base_addr, end_addr - base_addr); + } + } + +private: + IntervalMap m_ranges_map; +}; + +template +class SplitRangeMap { +public: + using IntervalMap = boost::icl::split_interval_map< + VAddr, T, boost::icl::total_absorber, std::less, boost::icl::inplace_identity, + boost::icl::inter_section, ICL_INTERVAL_INSTANCE(ICL_INTERVAL_DEFAULT, VAddr, std::less), + RangeSetsAllocator>; + using IntervalType = typename IntervalMap::interval_type; + +public: + SplitRangeMap() = default; + ~SplitRangeMap() = default; + + SplitRangeMap(SplitRangeMap const&) = delete; + SplitRangeMap& operator=(SplitRangeMap const&) = delete; + + SplitRangeMap(SplitRangeMap&& other); + SplitRangeMap& operator=(SplitRangeMap&& other); + + void Add(VAddr base_address, size_t size, const T& value) { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + m_ranges_map.add({interval, value}); + } + + void Subtract(VAddr base_address, size_t size) { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + m_ranges_map -= interval; + } + + void Clear() { + m_ranges_map.clear(); + } + + bool Contains(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::contains(m_ranges_map, interval); + } + + bool Intersects(VAddr base_address, size_t size) const { + const VAddr end_address = base_address + size; + IntervalType interval{base_address, end_address}; + return boost::icl::intersects(m_ranges_map, interval); + } + + template + void ForEach(Func&& func) const { + if (m_ranges_map.empty()) { + return; + } + + for (const auto& [interval, value] : m_ranges_map) { + const VAddr inter_addr_end = interval.upper(); + const VAddr inter_addr = interval.lower(); + func(inter_addr, inter_addr_end, value); + } + } + + template + void ForEachInRange(VAddr base_addr, size_t size, Func&& func) const { + if (m_ranges_map.empty()) { + return; + } + const VAddr start_address = base_addr; + const VAddr end_address = start_address + size; + const IntervalType search_interval{start_address, end_address}; + auto it = m_ranges_map.lower_bound(search_interval); + if (it == m_ranges_map.end()) { + return; + } + auto end_it = m_ranges_map.upper_bound(search_interval); + for (; it != end_it; it++) { + VAddr inter_addr_end = it->first.upper(); + VAddr inter_addr = it->first.lower(); + if (inter_addr_end > end_address) { + inter_addr_end = end_address; + } + if (inter_addr < start_address) { + inter_addr = start_address; + } + func(inter_addr, inter_addr_end, it->second); + } + } + + template + void ForEachNotInRange(VAddr base_addr, size_t size, Func&& func) const { + const VAddr end_addr = base_addr + size; + ForEachInRange(base_addr, size, [&](VAddr range_addr, VAddr range_end, const T&) { if (size_t gap_size = range_addr - base_addr; gap_size != 0) { func(base_addr, gap_size); } diff --git a/src/video_core/buffer_cache/word_manager.h b/src/video_core/buffer_cache/word_manager.h index 5ad724f96..51a912c62 100644 --- a/src/video_core/buffer_cache/word_manager.h +++ b/src/video_core/buffer_cache/word_manager.h @@ -10,8 +10,10 @@ #ifdef __linux__ #include "common/adaptive_mutex.h" -#endif +#else #include "common/spin_lock.h" +#endif +#include "common/debug.h" #include "common/types.h" #include "video_core/page_manager.h" @@ -56,7 +58,7 @@ public: return cpu_addr; } - static u64 ExtractBits(u64 word, size_t page_start, size_t page_end) { + static constexpr u64 ExtractBits(u64 word, size_t page_start, size_t page_end) { constexpr size_t number_bits = sizeof(u64) * 8; const size_t limit_page_end = number_bits - std::min(page_end, number_bits); u64 bits = (word >> page_start) << page_start; @@ -64,7 +66,7 @@ public: return bits; } - static std::pair GetWordPage(VAddr address) { + static constexpr std::pair GetWordPage(VAddr address) { const size_t converted_address = static_cast(address); const size_t word_number = converted_address / BYTES_PER_WORD; const size_t amount_pages = converted_address % BYTES_PER_WORD; @@ -73,6 +75,7 @@ public: template void IterateWords(size_t offset, size_t size, Func&& func) const { + RENDERER_TRACE; using FuncReturn = std::invoke_result_t; static constexpr bool BOOL_BREAK = std::is_same_v; const size_t start = static_cast(std::max(static_cast(offset), 0LL)); @@ -104,13 +107,13 @@ public: } } - template - void IteratePages(u64 mask, Func&& func) const { + void IteratePages(u64 mask, auto&& func) const { + RENDERER_TRACE; size_t offset = 0; while (mask != 0) { const size_t empty_bits = std::countr_zero(mask); offset += empty_bits; - mask = mask >> empty_bits; + mask >>= empty_bits; const size_t continuous_bits = std::countr_one(mask); func(offset, continuous_bits); @@ -155,8 +158,9 @@ public: * @param size Size in bytes of the CPU range to loop over * @param func Function to call for each turned off region */ - template - void ForEachModifiedRange(VAddr query_cpu_range, s64 size, Func&& func) { + template + void ForEachModifiedRange(VAddr query_cpu_range, s64 size, auto&& func) { + RENDERER_TRACE; std::scoped_lock lk{lock}; static_assert(type != Type::Untracked); @@ -170,6 +174,7 @@ public: (pending_pointer - pending_offset) * BYTES_PER_PAGE); }; IterateWords(offset, size, [&](size_t index, u64 mask) { + RENDERER_TRACE; if constexpr (type == Type::GPU) { mask &= ~untracked[index]; } @@ -177,14 +182,13 @@ public: if constexpr (clear) { if constexpr (type == Type::CPU) { UpdateProtection(index, untracked[index], mask); - } - state_words[index] &= ~mask; - if constexpr (type == Type::CPU) { untracked[index] &= ~mask; } + state_words[index] &= ~mask; } const size_t base_offset = index * PAGES_PER_WORD; IteratePages(word, [&](size_t pages_offset, size_t pages_size) { + RENDERER_TRACE; const auto reset = [&]() { pending_offset = base_offset + pages_offset; pending_pointer = base_offset + pages_offset + pages_size; @@ -245,11 +249,13 @@ private: */ template void UpdateProtection(u64 word_index, u64 current_bits, u64 new_bits) const { + RENDERER_TRACE; + constexpr s32 delta = add_to_tracker ? 1 : -1; u64 changed_bits = (add_to_tracker ? current_bits : ~current_bits) & new_bits; VAddr addr = cpu_addr + word_index * BYTES_PER_WORD; IteratePages(changed_bits, [&](size_t offset, size_t size) { - tracker->UpdatePagesCachedCount(addr + offset * BYTES_PER_PAGE, size * BYTES_PER_PAGE, - add_to_tracker ? 1 : -1); + tracker->UpdatePageWatchers(addr + offset * BYTES_PER_PAGE, + size * BYTES_PER_PAGE); }); } diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 3001bf773..d52afe738 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -11,6 +11,7 @@ set(SHADER_FILES detilers/micro_32bpp.comp detilers/micro_64bpp.comp detilers/micro_8bpp.comp + fault_buffer_process.comp fs_tri.vert fsr.comp post_process.frag diff --git a/src/video_core/host_shaders/fault_buffer_process.comp b/src/video_core/host_shaders/fault_buffer_process.comp new file mode 100644 index 000000000..a712cf441 --- /dev/null +++ b/src/video_core/host_shaders/fault_buffer_process.comp @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#version 450 +#extension GL_ARB_gpu_shader_int64 : enable + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(std430, binding = 0) buffer input_buf { + uint fault_buffer[]; +}; + +layout(std430, binding = 1) buffer output_buf { + uint64_t download_buffer[]; +}; + +// Overlap for 32 bit atomics +layout(std430, binding = 1) buffer output_buf32 { + uint download_buffer32[]; +}; + +layout(constant_id = 0) const uint CACHING_PAGEBITS = 0; + +void main() { + uint id = gl_GlobalInvocationID.x; + uint word = fault_buffer[id]; + if (word == 0u) { + return; + } + // 1 page per bit + uint base_bit = id * 32u; + while (word != 0u) { + uint bit = findLSB(word); + word &= word - 1; + uint page = base_bit + bit; + uint store_index = atomicAdd(download_buffer32[0], 1u) + 1u; + // It is very unlikely, but should we check for overflow? + if (store_index < 1024u) { // only support 1024 page faults + download_buffer[store_index] = uint64_t(page) << CACHING_PAGEBITS; + } + } +} diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index 47ed9e543..36145d0c5 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -1,11 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include "common/alignment.h" +#include #include "common/assert.h" -#include "common/error.h" +#include "common/debug.h" #include "common/signal_context.h" #include "core/memory.h" #include "core/signals.h" @@ -15,23 +13,60 @@ #ifndef _WIN64 #include #ifdef ENABLE_USERFAULTFD +#include #include #include #include #include +#include "common/error.h" #endif #else #include #endif +#ifdef __linux__ +#include "common/adaptive_mutex.h" +#else +#include "common/spin_lock.h" +#endif + namespace VideoCore { -constexpr size_t PAGESIZE = 4_KB; -constexpr size_t PAGEBITS = 12; +constexpr size_t PAGE_SIZE = 4_KB; +constexpr size_t PAGE_BITS = 12; -#ifdef ENABLE_USERFAULTFD struct PageManager::Impl { - Impl(Vulkan::Rasterizer* rasterizer_) : rasterizer{rasterizer_} { + struct PageState { + u8 num_watchers{}; + + Core::MemoryPermission Perm() const noexcept { + return num_watchers == 0 ? Core::MemoryPermission::ReadWrite + : Core::MemoryPermission::Read; + } + + template + u8 AddDelta() { + if constexpr (delta == 1) { + return ++num_watchers; + } else { + ASSERT_MSG(num_watchers > 0, "Not enough watchers"); + return --num_watchers; + } + } + }; + + struct UpdateProtectRange { + VAddr addr; + u64 size; + Core::MemoryPermission perms; + }; + + static constexpr size_t ADDRESS_BITS = 40; + static constexpr size_t NUM_ADDRESS_PAGES = 1ULL << (40 - PAGE_BITS); + inline static Vulkan::Rasterizer* rasterizer; +#ifdef ENABLE_USERFAULTFD + Impl(Vulkan::Rasterizer* rasterizer_) { + rasterizer = rasterizer_; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY); ASSERT_MSG(uffd != -1, "{}", Common::GetLastErrorMsg()); @@ -63,7 +98,8 @@ struct PageManager::Impl { ASSERT_MSG(ret != -1, "Uffdio unregister failed"); } - void Protect(VAddr address, size_t size, bool allow_write) { + void Protect(VAddr address, size_t size, Core::MemoryPermission perms) { + bool allow_write = True(perms & Core::MemoryPermission::Write); uffdio_writeprotect wp; wp.range.start = address; wp.range.len = size; @@ -118,12 +154,9 @@ struct PageManager::Impl { } } - Vulkan::Rasterizer* rasterizer; std::jthread ufd_thread; int uffd; -}; #else -struct PageManager::Impl { Impl(Vulkan::Rasterizer* rasterizer_) { rasterizer = rasterizer_; @@ -141,12 +174,11 @@ struct PageManager::Impl { // No-op } - void Protect(VAddr address, size_t size, bool allow_write) { + void Protect(VAddr address, size_t size, Core::MemoryPermission perms) { + RENDERER_TRACE; auto* memory = Core::Memory::Instance(); auto& impl = memory->GetAddressSpace(); - impl.Protect(address, size, - allow_write ? Core::MemoryPermission::ReadWrite - : Core::MemoryPermission::Read); + impl.Protect(address, size, perms); } static bool GuestFaultSignalHandler(void* context, void* fault_address) { @@ -157,23 +189,76 @@ struct PageManager::Impl { return false; } - inline static Vulkan::Rasterizer* rasterizer; -}; #endif + template + void UpdatePageWatchers(VAddr addr, u64 size) { + RENDERER_TRACE; + boost::container::small_vector update_ranges; + { + std::scoped_lock lk(lock); + + size_t page = addr >> PAGE_BITS; + auto perms = cached_pages[page].Perm(); + u64 range_begin = 0; + u64 range_bytes = 0; + + const auto release_pending = [&] { + if (range_bytes > 0) { + RENDERER_TRACE; + // Add pending (un)protect action + update_ranges.push_back({range_begin << PAGE_BITS, range_bytes, perms}); + range_bytes = 0; + } + }; + + // Iterate requested pages + const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE); + for (; page != page_end; ++page) { + PageState& state = cached_pages[page]; + + // Apply the change to the page state + const u8 new_count = state.AddDelta(); + + // If the protection changed add pending (un)protect action + if (auto new_perms = state.Perm(); new_perms != perms) [[unlikely]] { + release_pending(); + perms = new_perms; + } + + // If the page must be (un)protected, add it to the pending range + if ((new_count == 0 && delta < 0) || (new_count == 1 && delta > 0)) { + if (range_bytes == 0) { + range_begin = page; + } + range_bytes += PAGE_SIZE; + } else { + release_pending(); + } + } + + // Add pending (un)protect action + release_pending(); + } + + // Flush deferred protects + for (const auto& range : update_ranges) { + Protect(range.addr, range.size, range.perms); + } + } + + std::array cached_pages{}; +#ifdef __linux__ + Common::AdaptiveMutex lock; +#else + Common::SpinLock lock; +#endif +}; PageManager::PageManager(Vulkan::Rasterizer* rasterizer_) - : impl{std::make_unique(rasterizer_)}, rasterizer{rasterizer_} {} + : impl{std::make_unique(rasterizer_)} {} PageManager::~PageManager() = default; -VAddr PageManager::GetPageAddr(VAddr addr) { - return Common::AlignDown(addr, PAGESIZE); -} - -VAddr PageManager::GetNextPageAddr(VAddr addr) { - return Common::AlignUp(addr + 1, PAGESIZE); -} - void PageManager::OnGpuMap(VAddr address, size_t size) { impl->OnMap(address, size); } @@ -182,41 +267,12 @@ void PageManager::OnGpuUnmap(VAddr address, size_t size) { impl->OnUnmap(address, size); } -void PageManager::UpdatePagesCachedCount(VAddr addr, u64 size, s32 delta) { - static constexpr u64 PageShift = 12; - - std::scoped_lock lk{lock}; - const u64 num_pages = ((addr + size - 1) >> PageShift) - (addr >> PageShift) + 1; - const u64 page_start = addr >> PageShift; - const u64 page_end = page_start + num_pages; - - const auto pages_interval = - decltype(cached_pages)::interval_type::right_open(page_start, page_end); - if (delta > 0) { - cached_pages.add({pages_interval, delta}); - } - - const auto& range = cached_pages.equal_range(pages_interval); - for (const auto& [range, count] : boost::make_iterator_range(range)) { - const auto interval = range & pages_interval; - const VAddr interval_start_addr = boost::icl::first(interval) << PageShift; - const VAddr interval_end_addr = boost::icl::last_next(interval) << PageShift; - const u32 interval_size = interval_end_addr - interval_start_addr; - ASSERT_MSG(rasterizer->IsMapped(interval_start_addr, interval_size), - "Attempted to track non-GPU memory at address {:#x}, size {:#x}.", - interval_start_addr, interval_size); - if (delta > 0 && count == delta) { - impl->Protect(interval_start_addr, interval_size, false); - } else if (delta < 0 && count == -delta) { - impl->Protect(interval_start_addr, interval_size, true); - } else { - ASSERT(count >= 0); - } - } - - if (delta < 0) { - cached_pages.add({pages_interval, delta}); - } +template +void PageManager::UpdatePageWatchers(VAddr addr, u64 size) const { + impl->UpdatePageWatchers(addr, size); } +template void PageManager::UpdatePageWatchers<1>(VAddr addr, u64 size) const; +template void PageManager::UpdatePageWatchers<-1>(VAddr addr, u64 size) const; + } // namespace VideoCore diff --git a/src/video_core/page_manager.h b/src/video_core/page_manager.h index f6bae9641..98dd099af 100644 --- a/src/video_core/page_manager.h +++ b/src/video_core/page_manager.h @@ -4,11 +4,7 @@ #pragma once #include -#include -#ifdef __linux__ -#include "common/adaptive_mutex.h" -#endif -#include "common/spin_lock.h" +#include "common/alignment.h" #include "common/types.h" namespace Vulkan { @@ -18,6 +14,9 @@ class Rasterizer; namespace VideoCore { class PageManager { + static constexpr size_t PAGE_BITS = 12; + static constexpr size_t PAGE_SIZE = 1ULL << PAGE_BITS; + public: explicit PageManager(Vulkan::Rasterizer* rasterizer); ~PageManager(); @@ -28,22 +27,23 @@ public: /// Unregister a range of gpu memory that was unmapped. void OnGpuUnmap(VAddr address, size_t size); - /// Increase/decrease the number of surface in pages touching the specified region - void UpdatePagesCachedCount(VAddr addr, u64 size, s32 delta); + /// Updates watches in the pages touching the specified region. + template + void UpdatePageWatchers(VAddr addr, u64 size) const; - static VAddr GetPageAddr(VAddr addr); - static VAddr GetNextPageAddr(VAddr addr); + /// Returns page aligned address. + static constexpr VAddr GetPageAddr(VAddr addr) { + return Common::AlignDown(addr, PAGE_SIZE); + } + + /// Returns address of the next page. + static constexpr VAddr GetNextPageAddr(VAddr addr) { + return Common::AlignUp(addr + 1, PAGE_SIZE); + } private: struct Impl; std::unique_ptr impl; - Vulkan::Rasterizer* rasterizer; - boost::icl::interval_map cached_pages; -#ifdef PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP - Common::AdaptiveMutex lock; -#else - Common::SpinLock lock; -#endif }; } // namespace VideoCore diff --git a/src/video_core/renderdoc.cpp b/src/video_core/renderdoc.cpp index b082fd1ca..4cf2ddd53 100644 --- a/src/video_core/renderdoc.cpp +++ b/src/video_core/renderdoc.cpp @@ -121,6 +121,7 @@ void SetOutputDir(const std::filesystem::path& path, const std::string& prefix) if (!rdoc_api) { return; } + LOG_WARNING(Common, "RenderDoc capture path: {}", (path / prefix).string()); rdoc_api->SetCaptureFilePathTemplate(fmt::UTF((path / prefix).u8string()).data.data()); } diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index e31b95844..9584329f0 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -147,6 +147,7 @@ Instance::Instance(Frontend::WindowSDL& window, s32 physical_device_index, available_extensions = GetSupportedExtensions(physical_device); format_properties = GetFormatProperties(physical_device); properties = physical_device.getProperties(); + memory_properties = physical_device.getMemoryProperties(); CollectDeviceParameters(); ASSERT_MSG(properties.apiVersion >= TargetVulkanApiVersion, "Vulkan {}.{} is required, but only {}.{} is supported by device!", @@ -375,6 +376,7 @@ bool Instance::CreateDevice() { .separateDepthStencilLayouts = vk12_features.separateDepthStencilLayouts, .hostQueryReset = vk12_features.hostQueryReset, .timelineSemaphore = vk12_features.timelineSemaphore, + .bufferDeviceAddress = vk12_features.bufferDeviceAddress, }, vk::PhysicalDeviceVulkan13Features{ .robustImageAccess = vk13_features.robustImageAccess, @@ -505,6 +507,7 @@ void Instance::CreateAllocator() { }; const VmaAllocatorCreateInfo allocator_info = { + .flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, .physicalDevice = physical_device, .device = *device, .pVulkanFunctions = &functions, diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 573473869..30848e8b7 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -286,6 +286,11 @@ public: return vk12_props; } + /// Returns the memory properties of the physical device. + const vk::PhysicalDeviceMemoryProperties& GetMemoryProperties() const noexcept { + return memory_properties; + } + /// Returns true if shaders can declare the ClipDistance attribute bool IsShaderClipDistanceSupported() const { return features.shaderClipDistance; @@ -335,6 +340,7 @@ private: vk::PhysicalDevice physical_device; vk::UniqueDevice device; vk::PhysicalDeviceProperties properties; + vk::PhysicalDeviceMemoryProperties memory_properties; vk::PhysicalDeviceVulkan11Properties vk11_props; vk::PhysicalDeviceVulkan12Properties vk12_props; vk::PhysicalDevicePushDescriptorPropertiesKHR push_descriptor_props; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index e7b42a34b..4bdb08bf2 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -36,7 +36,7 @@ static Shader::PushData MakeUserData(const AmdGpu::Liverpool::Regs& regs) { Rasterizer::Rasterizer(const Instance& instance_, Scheduler& scheduler_, AmdGpu::Liverpool* liverpool_) : instance{instance_}, scheduler{scheduler_}, page_manager{this}, - buffer_cache{instance, scheduler, liverpool_, texture_cache, page_manager}, + buffer_cache{instance, scheduler, *this, liverpool_, texture_cache, page_manager}, texture_cache{instance, scheduler, buffer_cache, page_manager}, liverpool{liverpool_}, memory{Core::Memory::Instance()}, pipeline_cache{instance, scheduler, liverpool} { if (!Config::nullGpu()) { @@ -439,6 +439,13 @@ void Rasterizer::Finish() { scheduler.Finish(); } +void Rasterizer::ProcessFaults() { + if (fault_process_pending) { + fault_process_pending = false; + buffer_cache.ProcessFaultBuffer(); + } +} + bool Rasterizer::BindResources(const Pipeline* pipeline) { if (IsComputeMetaClear(pipeline)) { return false; @@ -449,6 +456,8 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) { buffer_infos.clear(); image_infos.clear(); + bool uses_dma = false; + // Bind resource buffers and textures. Shader::Backend::Bindings binding{}; Shader::PushData push_data = MakeUserData(liverpool->regs); @@ -459,9 +468,28 @@ bool Rasterizer::BindResources(const Pipeline* pipeline) { stage->PushUd(binding, push_data); BindBuffers(*stage, binding, push_data); BindTextures(*stage, binding); + + uses_dma |= stage->dma_types != Shader::IR::Type::Void; } pipeline->BindResources(set_writes, buffer_barriers, push_data); + + if (uses_dma && !fault_process_pending) { + // We only use fault buffer for DMA right now. + { + // TODO: GPU might have written to memory (for example with EVENT_WRITE_EOP) + // we need to account for that and synchronize. + Common::RecursiveSharedLock lock{mapped_ranges_mutex}; + for (auto& range : mapped_ranges) { + buffer_cache.SynchronizeBuffersInRange(range.lower(), + range.upper() - range.lower()); + } + } + buffer_cache.MemoryBarrier(); + } + + fault_process_pending |= uses_dma; + return true; } @@ -520,12 +548,18 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding if (desc.buffer_type == Shader::BufferType::GdsBuffer) { const auto* gds_buf = buffer_cache.GetGdsBuffer(); buffer_infos.emplace_back(gds_buf->Handle(), 0, gds_buf->SizeBytes()); - } else if (desc.buffer_type == Shader::BufferType::ReadConstUbo) { + } else if (desc.buffer_type == Shader::BufferType::Flatbuf) { auto& vk_buffer = buffer_cache.GetStreamBuffer(); const u32 ubo_size = stage.flattened_ud_buf.size() * sizeof(u32); const u64 offset = vk_buffer.Copy(stage.flattened_ud_buf.data(), ubo_size, instance.UniformMinAlignment()); buffer_infos.emplace_back(vk_buffer.Handle(), offset, ubo_size); + } else if (desc.buffer_type == Shader::BufferType::BdaPagetable) { + const auto* bda_buffer = buffer_cache.GetBdaPageTableBuffer(); + buffer_infos.emplace_back(bda_buffer->Handle(), 0, bda_buffer->SizeBytes()); + } else if (desc.buffer_type == Shader::BufferType::FaultBuffer) { + const auto* fault_buffer = buffer_cache.GetFaultBuffer(); + buffer_infos.emplace_back(fault_buffer->Handle(), 0, fault_buffer->SizeBytes()); } else if (desc.buffer_type == Shader::BufferType::SharedMemory) { auto& lds_buffer = buffer_cache.GetStreamBuffer(); const auto& cs_program = liverpool->GetCsRegs(); @@ -925,7 +959,7 @@ bool Rasterizer::InvalidateMemory(VAddr addr, u64 size) { // Not GPU mapped memory, can skip invalidation logic entirely. return false; } - buffer_cache.InvalidateMemory(addr, size); + buffer_cache.InvalidateMemory(addr, size, false); texture_cache.InvalidateMemory(addr, size); return true; } @@ -937,24 +971,24 @@ bool Rasterizer::IsMapped(VAddr addr, u64 size) { } const auto range = decltype(mapped_ranges)::interval_type::right_open(addr, addr + size); - std::shared_lock lock{mapped_ranges_mutex}; + Common::RecursiveSharedLock lock{mapped_ranges_mutex}; return boost::icl::contains(mapped_ranges, range); } void Rasterizer::MapMemory(VAddr addr, u64 size) { { - std::unique_lock lock{mapped_ranges_mutex}; + std::scoped_lock lock{mapped_ranges_mutex}; mapped_ranges += decltype(mapped_ranges)::interval_type::right_open(addr, addr + size); } page_manager.OnGpuMap(addr, size); } void Rasterizer::UnmapMemory(VAddr addr, u64 size) { - buffer_cache.InvalidateMemory(addr, size); + buffer_cache.InvalidateMemory(addr, size, true); texture_cache.UnmapMemory(addr, size); page_manager.OnGpuUnmap(addr, size); { - std::unique_lock lock{mapped_ranges_mutex}; + std::scoped_lock lock{mapped_ranges_mutex}; mapped_ranges -= decltype(mapped_ranges)::interval_type::right_open(addr, addr + size); } } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 4e0ed0996..fb9ca4bbe 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -4,7 +4,7 @@ #pragma once #include - +#include "common/recursive_lock.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/page_manager.h" #include "video_core/renderer_vulkan/vk_pipeline_cache.h" @@ -65,11 +65,21 @@ public: void CpSync(); u64 Flush(); void Finish(); + void ProcessFaults(); PipelineCache& GetPipelineCache() { return pipeline_cache; } + template + void ForEachMappedRangeInRange(VAddr addr, u64 size, Func&& func) { + const auto range = decltype(mapped_ranges)::interval_type::right_open(addr, addr + size); + Common::RecursiveSharedLock lock{mapped_ranges_mutex}; + for (const auto& mapped_range : (mapped_ranges & range)) { + func(mapped_range); + } + } + private: RenderState PrepareRenderState(u32 mrt_mask); void BeginRendering(const GraphicsPipeline& pipeline, RenderState& state); @@ -100,6 +110,8 @@ private: bool IsComputeMetaClear(const Pipeline* pipeline); private: + friend class VideoCore::BufferCache; + const Instance& instance; Scheduler& scheduler; VideoCore::PageManager page_manager; @@ -126,6 +138,7 @@ private: boost::container::static_vector buffer_bindings; using ImageBindingInfo = std::pair; boost::container::static_vector image_bindings; + bool fault_process_pending{false}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 8d4188a22..e75a69924 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -70,6 +70,11 @@ void Scheduler::Flush(SubmitInfo& info) { SubmitExecution(info); } +void Scheduler::Flush() { + SubmitInfo info{}; + Flush(info); +} + void Scheduler::Finish() { // When finishing, we need to wait for the submission to have executed on the device. const u64 presubmit_tick = CurrentTick(); @@ -85,6 +90,15 @@ void Scheduler::Wait(u64 tick) { Flush(info); } master_semaphore.Wait(tick); + + // CAUTION: This can introduce unexpected variation in the wait time. + // We don't currently sync the GPU, and some games are very sensitive to this. + // If this becomes a problem, it can be commented out. + // Idealy we would implement proper gpu sync. + while (!pending_ops.empty() && pending_ops.front().gpu_tick <= tick) { + pending_ops.front().callback(); + pending_ops.pop(); + } } void Scheduler::AllocateWorkerCommandBuffers() { diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 7709e1d41..c30fc6e0e 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -307,6 +307,10 @@ public: /// and increments the scheduler timeline semaphore. void Flush(SubmitInfo& info); + /// Sends the current execution context to the GPU + /// and increments the scheduler timeline semaphore. + void Flush(); + /// Sends the current execution context to the GPU and waits for it to complete. void Finish(); diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 82f4d6413..409085511 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -672,7 +672,7 @@ void TextureCache::TrackImage(ImageId image_id) { // Re-track the whole image image.track_addr = image_begin; image.track_addr_end = image_end; - tracker.UpdatePagesCachedCount(image_begin, image.info.guest_size, 1); + tracker.UpdatePageWatchers<1>(image_begin, image.info.guest_size); } else { if (image_begin < image.track_addr) { TrackImageHead(image_id); @@ -695,7 +695,7 @@ void TextureCache::TrackImageHead(ImageId image_id) { ASSERT(image.track_addr != 0 && image_begin < image.track_addr); const auto size = image.track_addr - image_begin; image.track_addr = image_begin; - tracker.UpdatePagesCachedCount(image_begin, size, 1); + tracker.UpdatePageWatchers<1>(image_begin, size); } void TextureCache::TrackImageTail(ImageId image_id) { @@ -711,7 +711,7 @@ void TextureCache::TrackImageTail(ImageId image_id) { const auto addr = image.track_addr_end; const auto size = image_end - image.track_addr_end; image.track_addr_end = image_end; - tracker.UpdatePagesCachedCount(addr, size, 1); + tracker.UpdatePageWatchers<1>(addr, size); } void TextureCache::UntrackImage(ImageId image_id) { @@ -724,7 +724,7 @@ void TextureCache::UntrackImage(ImageId image_id) { image.track_addr = 0; image.track_addr_end = 0; if (size != 0) { - tracker.UpdatePagesCachedCount(addr, size, -1); + tracker.UpdatePageWatchers<-1>(addr, size); } } @@ -743,7 +743,7 @@ void TextureCache::UntrackImageHead(ImageId image_id) { // Cehck its hash later. MarkAsMaybeDirty(image_id, image); } - tracker.UpdatePagesCachedCount(image_begin, size, -1); + tracker.UpdatePageWatchers<-1>(image_begin, size); } void TextureCache::UntrackImageTail(ImageId image_id) { @@ -762,7 +762,7 @@ void TextureCache::UntrackImageTail(ImageId image_id) { // Cehck its hash later. MarkAsMaybeDirty(image_id, image); } - tracker.UpdatePagesCachedCount(addr, size, -1); + tracker.UpdatePageWatchers<-1>(addr, size); } void TextureCache::DeleteImage(ImageId image_id) { From e518a7062c6b26c9fad8966f229f9a597d52bb44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 22 May 2025 21:17:34 +0200 Subject: [PATCH 061/141] Fix image extent in buffer copy to image (#2961) --- src/video_core/texture_cache/texture_cache.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 409085511..edf5b6e17 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -538,10 +538,16 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule image.mip_hashes[m] = hash; } + auto mip_pitch = static_cast(mip.pitch); + auto mip_height = static_cast(mip.height); + + auto image_extent_width = mip_pitch ? std::min(mip_pitch, width) : width; + auto image_extent_height = mip_height ? std::min(mip_height, height) : height; + image_copy.push_back({ .bufferOffset = mip.offset, - .bufferRowLength = static_cast(mip.pitch), - .bufferImageHeight = static_cast(mip.height), + .bufferRowLength = mip_pitch, + .bufferImageHeight = mip_height, .imageSubresource{ .aspectMask = image.aspect_mask & ~vk::ImageAspectFlagBits::eStencil, .mipLevel = m, @@ -549,7 +555,7 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule .layerCount = num_layers, }, .imageOffset = {0, 0, 0}, - .imageExtent = {width, height, depth}, + .imageExtent = {image_extent_width, image_extent_height, depth}, }); } From d124f40503eaaeee132be51a413444e3c13e83d3 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Thu, 22 May 2025 18:22:05 -0700 Subject: [PATCH 062/141] externals: Update MoltenVK (#2979) --- CMakeLists.txt | 2 +- externals/MoltenVK/MoltenVK | 2 +- externals/MoltenVK/SPIRV-Cross | 2 +- externals/vulkan-headers | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 53a2281ec..eb7a4f427 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,7 +227,7 @@ find_package(SDL3 3.1.2 CONFIG) find_package(stb MODULE) find_package(toml11 4.2.0 CONFIG) find_package(tsl-robin-map 1.3.0 CONFIG) -find_package(VulkanHeaders 1.4.309 CONFIG) +find_package(VulkanHeaders 1.4.314 CONFIG) find_package(VulkanMemoryAllocator 3.1.0 CONFIG) find_package(xbyak 7.07 CONFIG) find_package(xxHash 0.8.2 MODULE) diff --git a/externals/MoltenVK/MoltenVK b/externals/MoltenVK/MoltenVK index 87a8e8b13..3a0b07a24 160000 --- a/externals/MoltenVK/MoltenVK +++ b/externals/MoltenVK/MoltenVK @@ -1 +1 @@ -Subproject commit 87a8e8b13d4ad8835367fea1ebad1896d0460946 +Subproject commit 3a0b07a24a4a681ffe70b461b1f4333b2729e2ef diff --git a/externals/MoltenVK/SPIRV-Cross b/externals/MoltenVK/SPIRV-Cross index 791877574..969e75f7c 160000 --- a/externals/MoltenVK/SPIRV-Cross +++ b/externals/MoltenVK/SPIRV-Cross @@ -1 +1 @@ -Subproject commit 7918775748c5e2f5c40d9918ce68825035b5a1e1 +Subproject commit 969e75f7cc0718774231d029f9d52fa87d4ae1b2 diff --git a/externals/vulkan-headers b/externals/vulkan-headers index 5ceb9ed48..9c77de5c3 160000 --- a/externals/vulkan-headers +++ b/externals/vulkan-headers @@ -1 +1 @@ -Subproject commit 5ceb9ed481e58e705d0d9b5326537daedd06b97d +Subproject commit 9c77de5c3dd216f28e407eec65ed9c0a296c1f74 From e5c6c88835a7ab6c33316f5b62bb9f97c21f73dc Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Fri, 23 May 2025 10:56:46 -0700 Subject: [PATCH 063/141] texture_cache: Handle overlap with equal address and different tiling mode (#2981) --- .../texture_cache/texture_cache.cpp | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index edf5b6e17..ddb4ea799 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -222,14 +222,23 @@ std::tuple TextureCache::ResolveOverlap(const ImageInfo& imag -1, -1}; } - ImageId new_image_id{}; - if (image_info.type == tex_cache_image.info.type) { - ASSERT(image_info.resources > tex_cache_image.info.resources); - new_image_id = ExpandImage(image_info, cache_image_id); - } else { - UNREACHABLE(); + if (image_info.type == tex_cache_image.info.type && + image_info.resources > tex_cache_image.info.resources) { + // Size and resources are greater, expand the image. + return {ExpandImage(image_info, cache_image_id), -1, -1}; } - return {new_image_id, -1, -1}; + + if (image_info.tiling_mode != tex_cache_image.info.tiling_mode) { + // Size is greater but resources are not, because the tiling mode is different. + // Likely this memory address is being reused for a different image with a different + // tiling mode. + if (safe_to_delete) { + FreeImage(cache_image_id); + } + return {merged_image_id, -1, -1}; + } + + UNREACHABLE_MSG("Encountered unresolvable image overlap with equal memory address."); } // Right overlap, the image requested is a possible subresource of the image from cache. From 10d09ac97735076b4068079ca2d1aedd119e837d Mon Sep 17 00:00:00 2001 From: Lander Gallastegi Date: Sat, 24 May 2025 00:48:40 +0200 Subject: [PATCH 064/141] Added back the "Attempted to track non-GPU memory" assert. (#2980) * Fix log message * Add non-GPU memory assert back --- src/video_core/buffer_cache/buffer_cache.cpp | 2 +- src/video_core/page_manager.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 45863d8e8..4717a5ff8 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -668,7 +668,7 @@ void BufferCache::ProcessFaultBuffer() { const VAddr fault_end = fault + CACHING_PAGESIZE; // This can be adjusted fault_ranges += boost::icl::interval_set::interval_type::right_open(fault, fault_end); - LOG_INFO(Render_Vulkan, "Accessed non-GPU mapped memory at {:#x}", fault); + LOG_INFO(Render_Vulkan, "Accessed non-GPU cached memory at {:#x}", fault); } for (const auto& range : fault_ranges) { const VAddr start = range.lower(); diff --git a/src/video_core/page_manager.cpp b/src/video_core/page_manager.cpp index 36145d0c5..39c03e7da 100644 --- a/src/video_core/page_manager.cpp +++ b/src/video_core/page_manager.cpp @@ -213,6 +213,12 @@ struct PageManager::Impl { // Iterate requested pages const u64 page_end = Common::DivCeil(addr + size, PAGE_SIZE); + const u64 aligned_addr = page << PAGE_BITS; + const u64 aligned_end = page_end << PAGE_BITS; + ASSERT_MSG(rasterizer->IsMapped(aligned_addr, aligned_end - aligned_addr), + "Attempted to track non-GPU memory at address {:#x}, size {:#x}.", + aligned_addr, aligned_end - aligned_addr); + for (; page != page_end; ++page) { PageState& state = cached_pages[page]; From 149898193f00a174e250ea9b3812703cb13b2991 Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Sat, 24 May 2025 14:15:10 +0200 Subject: [PATCH 065/141] Devtools: Add Module Viewer (#2976) * impl * add User or Sysmodule detection * fix compiler warning * clang * fix string * add HLE * prevent crash * fix mutex * remove ref in arg * cleanup * move gamefolder to elfinfo --- CMakeLists.txt | 2 + src/common/elf_info.h | 5 ++ src/core/devtools/layer.cpp | 8 +++ src/core/devtools/widget/module_list.cpp | 55 ++++++++++++++++ src/core/devtools/widget/module_list.h | 82 ++++++++++++++++++++++++ src/core/linker.cpp | 7 ++ src/emulator.cpp | 3 + 7 files changed, 162 insertions(+) create mode 100644 src/core/devtools/widget/module_list.cpp create mode 100644 src/core/devtools/widget/module_list.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eb7a4f427..7962488c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -622,6 +622,8 @@ set(DEV_TOOLS src/core/devtools/layer.cpp src/core/devtools/widget/imgui_memory_editor.h src/core/devtools/widget/memory_map.cpp src/core/devtools/widget/memory_map.h + src/core/devtools/widget/module_list.cpp + src/core/devtools/widget/module_list.h src/core/devtools/widget/reg_popup.cpp src/core/devtools/widget/reg_popup.h src/core/devtools/widget/reg_view.cpp diff --git a/src/common/elf_info.h b/src/common/elf_info.h index 062cee012..02b845cb5 100644 --- a/src/common/elf_info.h +++ b/src/common/elf_info.h @@ -71,6 +71,7 @@ class ElfInfo { PSFAttributes psf_attributes{}; std::filesystem::path splash_path{}; + std::filesystem::path game_folder{}; public: static constexpr u32 FW_15 = 0x1500000; @@ -123,6 +124,10 @@ public: [[nodiscard]] const std::filesystem::path& GetSplashPath() const { return splash_path; } + + [[nodiscard]] const std::filesystem::path& GetGameFolder() const { + return game_folder; + } }; } // namespace Common diff --git a/src/core/devtools/layer.cpp b/src/core/devtools/layer.cpp index a93178de5..5380d3be9 100644 --- a/src/core/devtools/layer.cpp +++ b/src/core/devtools/layer.cpp @@ -17,6 +17,7 @@ #include "widget/frame_dump.h" #include "widget/frame_graph.h" #include "widget/memory_map.h" +#include "widget/module_list.h" #include "widget/shader_list.h" extern std::unique_ptr presenter; @@ -40,6 +41,7 @@ static bool just_opened_options = false; static Widget::MemoryMapViewer memory_map; static Widget::ShaderList shader_list; +static Widget::ModuleList module_list; // clang-format off static std::string help_text = @@ -108,6 +110,9 @@ void L::DrawMenuBar() { if (MenuItem("Memory map")) { memory_map.open = true; } + if (MenuItem("Module list")) { + module_list.open = true; + } ImGui::EndMenu(); } @@ -256,6 +261,9 @@ void L::DrawAdvanced() { if (shader_list.open) { shader_list.Draw(); } + if (module_list.open) { + module_list.Draw(); + } } void L::DrawSimple() { diff --git a/src/core/devtools/widget/module_list.cpp b/src/core/devtools/widget/module_list.cpp new file mode 100644 index 000000000..73afe3462 --- /dev/null +++ b/src/core/devtools/widget/module_list.cpp @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "module_list.h" + +#include + +#include "common.h" +#include "core/debug_state.h" +#include "imgui/imgui_std.h" + +using namespace ImGui; + +namespace Core::Devtools::Widget { +void ModuleList::Draw() { + SetNextWindowSize({550.0f, 600.0f}, ImGuiCond_FirstUseEver); + if (!Begin("Module List", &open)) { + End(); + return; + } + + if (BeginTable("ModuleTable", 3, + ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | + ImGuiTableFlags_RowBg)) { + TableSetupColumn("Modulname", ImGuiTableColumnFlags_WidthStretch); + TableHeadersRow(); + + std::scoped_lock lock(modules_mutex); + for (const auto& module : modules) { + TableNextRow(); + + TableSetColumnIndex(0); + TextUnformatted(module.name.c_str()); + + TableSetColumnIndex(1); + if (module.is_sys_module) { + TextColored({0.2f, 0.6f, 0.8f, 1.0f}, "System Module"); + } else { + TextColored({0.8f, 0.4f, 0.2f, 1.0f}, "Game Module"); + } + + TableSetColumnIndex(2); + if (module.is_lle) { + TextColored({0.4f, 0.7f, 0.4f, 1.0f}, "LLE"); + } else { + TextColored({0.7f, 0.4f, 0.5f, 1.0f}, "HLE"); + } + } + EndTable(); + } + + End(); +} + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/devtools/widget/module_list.h b/src/core/devtools/widget/module_list.h new file mode 100644 index 000000000..4c961919e --- /dev/null +++ b/src/core/devtools/widget/module_list.h @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include "common/elf_info.h" +#include "common/path_util.h" + +namespace Core::Devtools::Widget { + +class ModuleList { +public: + ModuleList() = default; + ~ModuleList() = default; + + void Draw(); + bool open = false; + + static bool IsSystemModule(const std::filesystem::path& path) { + const auto sys_modules_path = Common::FS::GetUserPath(Common::FS::PathType::SysModuleDir); + + const auto abs_path = std::filesystem::absolute(path).lexically_normal(); + const auto abs_sys_path = std::filesystem::absolute(sys_modules_path).lexically_normal(); + + const auto path_str = abs_path.string(); + const auto sys_path_str = abs_sys_path.string(); + + return path_str.starts_with(sys_path_str); + } + + static bool IsSystemModule(const std::string& name) { + const auto game_modules_path = Common::ElfInfo::Instance().GetGameFolder() / "sce_module"; + const auto prx_path = game_modules_path / name; + + if (!std::filesystem::exists(prx_path)) { + return true; + } + return false; + } + + static void AddModule(const std::string& name, std::filesystem::path path) { + if (name == "eboot.bin") { + return; + } + std::scoped_lock lock(modules_mutex); + modules.push_back({name, IsSystemModule(path), true}); + } + + static void AddModule(std::string name) { + name = name + ".prx"; + std::scoped_lock lock(modules_mutex); + + bool is_sys_module = IsSystemModule(name); + bool is_lle = false; + auto it = std::find_if(modules.begin(), modules.end(), + [&name, is_sys_module, is_lle](const ModuleInfo& entry) { + return entry.name == name && !entry.is_lle; + }); + + if (it == modules.end()) { + modules.push_back({name, is_sys_module, is_lle}); + } + } + +private: + struct ModuleInfo { + std::string name; + bool is_sys_module; + bool is_lle; + }; + + static inline std::mutex modules_mutex; + + static inline std::vector modules; +}; + +} // namespace Core::Devtools::Widget \ No newline at end of file diff --git a/src/core/linker.cpp b/src/core/linker.cpp index eced87968..3e6d8c22e 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -12,6 +12,7 @@ #include "common/thread.h" #include "core/aerolib/aerolib.h" #include "core/aerolib/stubs.h" +#include "core/devtools/widget/module_list.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/threads.h" #include "core/linker.h" @@ -147,6 +148,9 @@ s32 Linker::LoadModule(const std::filesystem::path& elf_name, bool is_dynamic) { num_static_modules += !is_dynamic; m_modules.emplace_back(std::move(module)); + + Core::Devtools::Widget::ModuleList::AddModule(elf_name.filename().string(), elf_name); + return m_modules.size() - 1; } @@ -325,6 +329,9 @@ bool Linker::Resolve(const std::string& name, Loader::SymbolType sym_type, Modul } if (record) { *return_info = *record; + + Core::Devtools::Widget::ModuleList::AddModule(sr.library); + return true; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 9a0429d5d..2ad8446ab 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -25,6 +25,7 @@ #include "common/polyfill_thread.h" #include "common/scm_rev.h" #include "common/singleton.h" +#include "core/devtools/widget/module_list.h" #include "core/file_format/psf.h" #include "core/file_format/trp.h" #include "core/file_sys/fs.h" @@ -188,6 +189,8 @@ void Emulator::Run(const std::filesystem::path& file, const std::vector", id, title, app_version); std::string window_title = ""; std::string remote_url(Common::g_scm_remote_url); From 99ccf569389240b0261c716ae389dedc43f6bf95 Mon Sep 17 00:00:00 2001 From: Lander Gallastegi Date: Sun, 25 May 2025 12:49:39 +0200 Subject: [PATCH 066/141] Fix float on f64 (#2985) --- .../backend/spirv/emit_spirv_floating_point.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp index 347c4cb0a..01c51e399 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_floating_point.cpp @@ -154,7 +154,7 @@ Id EmitFPRecip32(EmitContext& ctx, Id value) { } Id EmitFPRecip64(EmitContext& ctx, Id value) { - return ctx.OpFDiv(ctx.F64[1], ctx.Constant(ctx.F64[1], 1.0f), value); + return ctx.OpFDiv(ctx.F64[1], ctx.Constant(ctx.F64[1], f64{1.0}), value); } Id EmitFPRecipSqrt32(EmitContext& ctx, Id value) { From 139a253edc56420cb262c815e6ada7dfb84c79be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 27 May 2025 21:09:03 +0200 Subject: [PATCH 067/141] Stub PM4 0x8E opcode (#2998) --- src/video_core/amdgpu/liverpool.cpp | 4 ++++ src/video_core/amdgpu/pm4_opcodes.h | 1 + 2 files changed, 5 insertions(+) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 706e94c2b..4db7648c6 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -752,6 +752,10 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spanbuffer_select.Value()); break; } + case PM4ItOpcode::GetLodStats: { + LOG_WARNING(Render_Vulkan, "Unimplemented IT_GET_LOD_STATS"); + break; + } default: UNREACHABLE_MSG("Unknown PM4 type 3 opcode {:#x} with count {}", static_cast(opcode), count); diff --git a/src/video_core/amdgpu/pm4_opcodes.h b/src/video_core/amdgpu/pm4_opcodes.h index ce388d1ba..4a5f2be4e 100644 --- a/src/video_core/amdgpu/pm4_opcodes.h +++ b/src/video_core/amdgpu/pm4_opcodes.h @@ -71,6 +71,7 @@ enum class PM4ItOpcode : u32 { IncrementDeCounter = 0x85, WaitOnCeCounter = 0x86, WaitOnDeCounterDiff = 0x88, + GetLodStats = 0x8E, DrawIndexIndirectCountMulti = 0x9d, }; From e0309a4b01493cf5e543da856319f56c6fedd94a Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Tue, 27 May 2025 23:47:54 +0200 Subject: [PATCH 068/141] Equeue: fix WaitEqueue assert on nullptr (#2994) * fix * fix infinite call of waitforsmalltimer * fix wrong nullptr check * add comment back * fix discrepancy * remove assert --------- Co-authored-by: georgemoralis --- src/core/libraries/kernel/equeue.cpp | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 958019cd3..c33a0b09e 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -187,7 +187,8 @@ int EqueueInternal::WaitForSmallTimer(SceKernelEvent* ev, int num, u32 micros) { ASSERT(num == 1); auto curr_clock = std::chrono::steady_clock::now(); - const auto wait_end_us = curr_clock + std::chrono::microseconds{micros}; + const auto wait_end_us = (micros == 0) ? std::chrono::steady_clock::time_point::max() + : curr_clock + std::chrono::microseconds{micros}; do { curr_clock = std::chrono::steady_clock::now(); @@ -266,23 +267,26 @@ int PS4_SYSV_ABI sceKernelWaitEqueue(SceKernelEqueue eq, SceKernelEvent* ev, int return ORBIS_KERNEL_ERROR_EINVAL; } + // When the timeout is nullptr, we wait indefinitely if (eq->HasSmallTimer()) { - ASSERT(timo && *timo); - *out = eq->WaitForSmallTimer(ev, num, *timo); - } else { - if (timo == nullptr) { // wait until an event arrives without timing out - *out = eq->WaitForEvents(ev, num, 0); - } - - if (timo != nullptr) { + if (timo == nullptr) { + *out = eq->WaitForSmallTimer(ev, num, 0); + } else if (*timo == 0) { // Only events that have already arrived at the time of this function call can be // received - if (*timo == 0) { - *out = eq->GetTriggeredEvents(ev, num); - } else { - // Wait until an event arrives with timing out - *out = eq->WaitForEvents(ev, num, *timo); - } + *out = eq->GetTriggeredEvents(ev, num); + } else { + *out = eq->WaitForSmallTimer(ev, num, *timo); + } + } else { + if (timo == nullptr) { + *out = eq->WaitForEvents(ev, num, 0); + } else if (*timo == 0) { + // Only events that have already arrived at the time of this function call can be + // received + *out = eq->GetTriggeredEvents(ev, num); + } else { + *out = eq->WaitForEvents(ev, num, *timo); } } From 95f04b746d5396383bc246251fdbbb210143191d Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 28 May 2025 09:54:47 -0700 Subject: [PATCH 069/141] equeue: Move small timer check to WaitForEvents. (#3000) --- src/core/libraries/kernel/equeue.cpp | 33 +++++++++++----------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index c33a0b09e..263cf24b8 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -98,6 +98,11 @@ bool EqueueInternal::RemoveEvent(u64 id, s16 filter) { } int EqueueInternal::WaitForEvents(SceKernelEvent* ev, int num, u32 micros) { + if (HasSmallTimer()) { + // If a small timer is set, just wait for it to expire. + return WaitForSmallTimer(ev, num, micros); + } + int count = 0; const auto predicate = [&] { @@ -267,27 +272,15 @@ int PS4_SYSV_ABI sceKernelWaitEqueue(SceKernelEqueue eq, SceKernelEvent* ev, int return ORBIS_KERNEL_ERROR_EINVAL; } - // When the timeout is nullptr, we wait indefinitely - if (eq->HasSmallTimer()) { - if (timo == nullptr) { - *out = eq->WaitForSmallTimer(ev, num, 0); - } else if (*timo == 0) { - // Only events that have already arrived at the time of this function call can be - // received - *out = eq->GetTriggeredEvents(ev, num); - } else { - *out = eq->WaitForSmallTimer(ev, num, *timo); - } + if (timo == nullptr) { + // When the timeout is nullptr, we wait indefinitely + *out = eq->WaitForEvents(ev, num, 0); + } else if (*timo == 0) { + // Only events that have already arrived at the time of this function call can be received + *out = eq->GetTriggeredEvents(ev, num); } else { - if (timo == nullptr) { - *out = eq->WaitForEvents(ev, num, 0); - } else if (*timo == 0) { - // Only events that have already arrived at the time of this function call can be - // received - *out = eq->GetTriggeredEvents(ev, num); - } else { - *out = eq->WaitForEvents(ev, num, *timo); - } + // Wait for up to the specified timeout value + *out = eq->WaitForEvents(ev, num, *timo); } if (*out == 0) { From 7f0503bf8b274d31ddaf13fdb65780e4798b41f7 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 29 May 2025 11:59:34 +0300 Subject: [PATCH 070/141] Implemented sceNetInetNtop in RE (#3003) * implemented sceNetInetNtop in RE * some logging * added freebsd_inet_ntop4 * freebsd_inet_ntop6 * fixups based on reviews * review fixes --- src/core/libraries/network/net.cpp | 148 +++++++++++++++++++++++++++-- src/core/libraries/network/net.h | 4 + 2 files changed, 144 insertions(+), 8 deletions(-) diff --git a/src/core/libraries/network/net.cpp b/src/core/libraries/network/net.cpp index 0ef4a84f5..9607f0d78 100644 --- a/src/core/libraries/network/net.cpp +++ b/src/core/libraries/network/net.cpp @@ -955,16 +955,148 @@ u16 PS4_SYSV_ABI sceNetHtons(u16 host16) { return htons(host16); } -const char* PS4_SYSV_ABI sceNetInetNtop(int af, const void* src, char* dst, u32 size) { #ifdef WIN32 - const char* res = InetNtopA(af, src, dst, size); -#else - const char* res = inet_ntop(af, src, dst, size); -#endif - if (res == nullptr) { - UNREACHABLE(); +// there isn't a strlcpy function in windows so implement one +u64 strlcpy(char* dst, const char* src, u64 size) { + u64 src_len = strlen(src); + + if (size > 0) { + u64 copy_len = (src_len >= size) ? (size - 1) : src_len; + memcpy(dst, src, copy_len); + dst[copy_len] = '\0'; } - return dst; + + return src_len; +} + +#endif + +const char* freebsd_inet_ntop4(const char* src, char* dst, u64 size) { + static const char fmt[] = "%u.%u.%u.%u"; + char tmp[sizeof "255.255.255.255"]; + int l; + + l = snprintf(tmp, sizeof(tmp), fmt, src[0], src[1], src[2], src[3]); + if (l <= 0 || (socklen_t)l >= size) { + return nullptr; + } + strlcpy(dst, tmp, size); + return (dst); +} + +const char* freebsd_inet_ntop6(const char* src, char* dst, u64 size) { + /* + * Note that int32_t and int16_t need only be "at least" large enough + * to contain a value of the specified size. On some systems, like + * Crays, there is no such thing as an integer variable with 16 bits. + * Keep this in mind if you think this function should have been coded + * to use pointer overlays. All the world's not a VAX. + */ + char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"], *tp; + struct { + int base, len; + } best, cur; +#define NS_IN6ADDRSZ 16 +#define NS_INT16SZ 2 + u_int words[NS_IN6ADDRSZ / NS_INT16SZ]; + int i; + + /* + * Preprocess: + * Copy the input (bytewise) array into a wordwise array. + * Find the longest run of 0x00's in src[] for :: shorthanding. + */ + memset(words, '\0', sizeof words); + for (i = 0; i < NS_IN6ADDRSZ; i++) + words[i / 2] |= (src[i] << ((1 - (i % 2)) << 3)); + best.base = -1; + best.len = 0; + cur.base = -1; + cur.len = 0; + for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) { + if (words[i] == 0) { + if (cur.base == -1) + cur.base = i, cur.len = 1; + else + cur.len++; + } else { + if (cur.base != -1) { + if (best.base == -1 || cur.len > best.len) + best = cur; + cur.base = -1; + } + } + } + if (cur.base != -1) { + if (best.base == -1 || cur.len > best.len) + best = cur; + } + if (best.base != -1 && best.len < 2) + best.base = -1; + + /* + * Format the result. + */ + tp = tmp; + for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) { + /* Are we inside the best run of 0x00's? */ + if (best.base != -1 && i >= best.base && i < (best.base + best.len)) { + if (i == best.base) + *tp++ = ':'; + continue; + } + /* Are we following an initial run of 0x00s or any real hex? */ + if (i != 0) + *tp++ = ':'; + /* Is this address an encapsulated IPv4? */ + if (i == 6 && best.base == 0 && + (best.len == 6 || (best.len == 7 && words[7] != 0x0001) || + (best.len == 5 && words[5] == 0xffff))) { + if (!freebsd_inet_ntop4(src + 12, tp, sizeof tmp - (tp - tmp))) + return nullptr; + tp += strlen(tp); + break; + } + tp += sprintf(tp, "%x", words[i]); + } + /* Was it a trailing run of 0x00's? */ + if (best.base != -1 && (best.base + best.len) == (NS_IN6ADDRSZ / NS_INT16SZ)) + *tp++ = ':'; + *tp++ = '\0'; + + /* + * Check for overflow, copy, and we're done. + */ + if ((u64)(tp - tmp) > size) { + return nullptr; + } + strcpy(dst, tmp); + return (dst); +} +const char* PS4_SYSV_ABI sceNetInetNtop(int af, const void* src, char* dst, u32 size) { + if (!(src && dst)) { + *sceNetErrnoLoc() = ORBIS_NET_ENOSPC; + LOG_ERROR(Lib_Net, "returned ORBIS_NET_ENOSPC"); + return nullptr; + } + const char* returnvalue = nullptr; + switch (af) { + case ORBIS_NET_AF_INET: + returnvalue = freebsd_inet_ntop4((const char*)src, dst, size); + break; + case ORBIS_NET_AF_INET6: + returnvalue = freebsd_inet_ntop6((const char*)src, dst, size); + break; + default: + *sceNetErrnoLoc() = ORBIS_NET_EAFNOSUPPORT; + LOG_ERROR(Lib_Net, "returned ORBIS_NET_EAFNOSUPPORT"); + return nullptr; + } + if (returnvalue == nullptr) { + *sceNetErrnoLoc() = ORBIS_NET_ENOSPC; + LOG_ERROR(Lib_Net, "returned ORBIS_NET_ENOSPC"); + } + return returnvalue; } int PS4_SYSV_ABI sceNetInetNtopWithScopeId() { diff --git a/src/core/libraries/network/net.h b/src/core/libraries/network/net.h index 812ee6bd7..1393ecb1d 100644 --- a/src/core/libraries/network/net.h +++ b/src/core/libraries/network/net.h @@ -20,6 +20,10 @@ class SymbolsResolver; namespace Libraries::Net { +enum OrbisNetFamily : u32 { + ORBIS_NET_AF_INET = 2, + ORBIS_NET_AF_INET6 = 28, +}; enum OrbisNetSocketType : u32 { ORBIS_NET_SOCK_STREAM = 1, ORBIS_NET_SOCK_DGRAM = 2, From eb9f66c349ae7c8fef780f4f55e98b8065947e14 Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Thu, 29 May 2025 11:33:56 +0200 Subject: [PATCH 071/141] Libs: CompanionUtil (#2963) * impl * more * move log * cleanup type definitions * error code cleanup and clang * cleanup ugly RE code * shame * clang --------- Co-authored-by: georgemoralis --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + .../libraries/companion/companion_error.h | 7 ++ .../libraries/companion/companion_util.cpp | 72 +++++++++++++++++++ src/core/libraries/companion/companion_util.h | 33 +++++++++ src/core/libraries/libs.cpp | 2 + 7 files changed, 118 insertions(+) create mode 100644 src/core/libraries/companion/companion_util.cpp create mode 100644 src/core/libraries/companion/companion_util.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7962488c8..f85d22b6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -603,6 +603,8 @@ set(CAMERA_LIBS src/core/libraries/camera/camera.cpp set(COMPANION_LIBS src/core/libraries/companion/companion_httpd.cpp src/core/libraries/companion/companion_httpd.h + src/core/libraries/companion/companion_util.cpp + src/core/libraries/companion/companion_util.h src/core/libraries/companion/companion_error.h ) set(DEV_TOOLS src/core/devtools/layer.cpp diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 231cbf849..fd0614e1b 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -140,6 +140,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, SigninDialog) \ SUB(Lib, Camera) \ SUB(Lib, CompanionHttpd) \ + SUB(Lib, CompanionUtil) \ CLS(Frontend) \ CLS(Render) \ SUB(Render, Vulkan) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index e4eae59af..bbfd7455b 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -107,6 +107,7 @@ enum class Class : u8 { Lib_SigninDialog, ///< The LibSigninDialog implementation. Lib_Camera, ///< The LibCamera implementation. Lib_CompanionHttpd, ///< The LibCompanionHttpd implementation. + Lib_CompanionUtil, ///< The LibCompanionUtil implementation. Frontend, ///< Emulator UI Render, ///< Video Core Render_Vulkan, ///< Vulkan backend diff --git a/src/core/libraries/companion/companion_error.h b/src/core/libraries/companion/companion_error.h index 2d1a3833c..0459c33f8 100644 --- a/src/core/libraries/companion/companion_error.h +++ b/src/core/libraries/companion/companion_error.h @@ -3,6 +3,8 @@ #pragma once +#include "common/types.h" + // companion_httpd error codes constexpr int ORBIS_COMPANION_HTTPD_ERROR_UNKNOWN = 0x80E40001; constexpr int ORBIS_COMPANION_HTTPD_ERROR_FATAL = 0x80E40002; @@ -18,3 +20,8 @@ constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_STARTED = 0x80E4000B; constexpr int ORBIS_COMPANION_HTTPD_ERROR_ALREADY_REGISTERED = 0x80E4000; constexpr int ORBIS_COMPANION_HTTPD_ERROR_NOT_CONNECTED = 0x80E4000D; constexpr int ORBIS_COMPANION_HTTPD_ERROR_USER_NOT_FOUND = 0x80E4000E; + +// companion_util error codes +constexpr u32 ORBIS_COMPANION_UTIL_INVALID_ARGUMENT = 0x80AD0004; +constexpr u32 ORBIS_COMPANION_UTIL_INVALID_POINTER = 0x80AD0006; +constexpr u32 ORBIS_COMPANION_UTIL_NO_EVENT = 0x80AD0008; \ No newline at end of file diff --git a/src/core/libraries/companion/companion_util.cpp b/src/core/libraries/companion/companion_util.cpp new file mode 100644 index 000000000..c144ebdcc --- /dev/null +++ b/src/core/libraries/companion/companion_util.cpp @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "companion_error.h" +#include "core/libraries/companion/companion_util.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" + +namespace Libraries::CompanionUtil { + +u32 PS4_SYSV_ABI getEvent(sceCompanionUtilContext* ctx, sceCompanionUtilEvent* outEvent, + s32 param_3) { + if (outEvent == 0) { + return ORBIS_COMPANION_UTIL_INVALID_ARGUMENT; + } + + if (ctx == nullptr) { + return ORBIS_COMPANION_UTIL_INVALID_POINTER; + } + + uint8_t* base = ctx->blob; + int flag = *reinterpret_cast(base + 0x178); + if (flag == 0) { + return ORBIS_COMPANION_UTIL_NO_EVENT; + } + + return ORBIS_COMPANION_UTIL_OK; +} + +s32 PS4_SYSV_ABI sceCompanionUtilGetEvent(sceCompanionUtilEvent* outEvent) { + sceCompanionUtilContext* ctx = nullptr; + u32 ret = getEvent(ctx, outEvent, 1); + + LOG_DEBUG(Lib_CompanionUtil, "(STUBBED) called ret: {}", ret); + return ret; +} + +s32 PS4_SYSV_ABI sceCompanionUtilGetRemoteOskEvent() { + LOG_ERROR(Lib_CompanionUtil, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionUtilInitialize() { + LOG_ERROR(Lib_CompanionUtil, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionUtilOptParamInitialize() { + LOG_ERROR(Lib_CompanionUtil, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceCompanionUtilTerminate() { + LOG_ERROR(Lib_CompanionUtil, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterlibSceCompanionUtil(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("cE5Msy11WhU", "libSceCompanionUtil", 1, "libSceCompanionUtil", 1, 1, + sceCompanionUtilGetEvent); + LIB_FUNCTION("MaVrz79mT5o", "libSceCompanionUtil", 1, "libSceCompanionUtil", 1, 1, + sceCompanionUtilGetRemoteOskEvent); + LIB_FUNCTION("xb1xlIhf0QY", "libSceCompanionUtil", 1, "libSceCompanionUtil", 1, 1, + sceCompanionUtilInitialize); + LIB_FUNCTION("IPN-FRSrafk", "libSceCompanionUtil", 1, "libSceCompanionUtil", 1, 1, + sceCompanionUtilOptParamInitialize); + LIB_FUNCTION("H1fYQd5lFAI", "libSceCompanionUtil", 1, "libSceCompanionUtil", 1, 1, + sceCompanionUtilTerminate); +}; + +} // namespace Libraries::CompanionUtil \ No newline at end of file diff --git a/src/core/libraries/companion/companion_util.h b/src/core/libraries/companion/companion_util.h new file mode 100644 index 000000000..921b5b21e --- /dev/null +++ b/src/core/libraries/companion/companion_util.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::CompanionUtil { + +constexpr u32 ORBIS_COMPANION_UTIL_OK = 0; + +struct sceCompanionUtilEvent { + std::uint8_t blob[0x104]{}; /// 0x104 bytes of data, dont know what it is exactly +}; + +struct sceCompanionUtilContext { + std::uint8_t blob[0x27B]{}; /// 0x27B bytes of data, dont know what it is exactly +}; + +u32 PS4_SYSV_ABI getEvent(sceCompanionUtilContext* ctx, sceCompanionUtilEvent* outEvent, + s32 param_3); +s32 PS4_SYSV_ABI sceCompanionUtilGetEvent(sceCompanionUtilEvent* outEvent); +s32 PS4_SYSV_ABI sceCompanionUtilGetRemoteOskEvent(); +s32 PS4_SYSV_ABI sceCompanionUtilInitialize(); +s32 PS4_SYSV_ABI sceCompanionUtilOptParamInitialize(); +s32 PS4_SYSV_ABI sceCompanionUtilTerminate(); + +void RegisterlibSceCompanionUtil(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::CompanionUtil \ No newline at end of file diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 2ab46d3a0..45b32846f 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -10,6 +10,7 @@ #include "core/libraries/avplayer/avplayer.h" #include "core/libraries/camera/camera.h" #include "core/libraries/companion/companion_httpd.h" +#include "core/libraries/companion/companion_util.h" #include "core/libraries/disc_map/disc_map.h" #include "core/libraries/game_live_streaming/gamelivestreaming.h" #include "core/libraries/gnmdriver/gnmdriver.h" @@ -126,6 +127,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::SigninDialog::RegisterlibSceSigninDialog(sym); Libraries::Camera::RegisterlibSceCamera(sym); Libraries::CompanionHttpd::RegisterlibSceCompanionHttpd(sym); + Libraries::CompanionUtil::RegisterlibSceCompanionUtil(sym); } } // namespace Libraries From aca8e7e9eb778c2d9d78a62af969cf5d56b777d6 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 29 May 2025 12:42:52 +0300 Subject: [PATCH 072/141] New Crowdin updates (#2982) * New translations en_us.ts (Spanish) * New translations en_us.ts (Spanish) * New translations en_us.ts (Turkish) --- src/qt_gui/translations/es_ES.ts | 4 ++-- src/qt_gui/translations/tr_TR.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qt_gui/translations/es_ES.ts b/src/qt_gui/translations/es_ES.ts index e73386c96..9568388cc 100644 --- a/src/qt_gui/translations/es_ES.ts +++ b/src/qt_gui/translations/es_ES.ts @@ -26,7 +26,7 @@ Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n - Los cheats/patches son experimentales.\nÚselos con precaución.\n\nDescargue los cheats individualmente seleccionando el repositorio y haciendo clic en el botón de descarga.\nEn la pestaña Patches, puede descargar todos los patches a la vez, elegir cuáles desea usar y guardar la selección.\n\nComo no desarrollamos los Cheats/Patches,\npor favor informe los problemas al autor del cheat.\n\n¿Creaste un nuevo cheat? Visita:\n + Los trucos/parches son experimentales.\nÚselos con precaución.\n\nPuede descargar cada truco seleccionando el repositorio y haciendo clic en el botón de descarga.\nEn la pestaña Parches podrá descargar todos los parches a la vez, elegir cuáles desea usar y guardar la selección.\n\nComo no desarrollamos los trucos/parches,\ndebe informar de cualquier problema a sus autores correspondientes.\n\n¿Creaste un truco nuevo? Visita:\n No Image Available @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Versión de Vulkan no soportada diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index 9539ca139..e61985e90 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -2048,7 +2048,7 @@ * Unsupported Vulkan Version - * Unsupported Vulkan Version + * Desteklenmeyen Vulkan Sürümü From 6cdc52cdde23a7bf35c6f316217155c04e99418f Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 29 May 2025 10:56:03 -0500 Subject: [PATCH 073/141] Core: More Memory Cleanup & Fixes (#2997) * Only perform GPU memory mapping when GPU can access it This better aligns with hardware observations, and should also speed up unmaps and decommits, since they don't need to be compared with the GPU max address anymore. * Reserve fixes ReserveVirtualRange seems to follow the 0x200000000 base address like MemoryPoolReserve does. Both also need checks in their flags Fixed path to ensure we're mapping in-bounds. If we're not in mapping to our address space, we'll end up reserving and returning the wrong address, which could lead to weird memory issues in games. I'll need to test on real hardware to verify if such changes are appropriate. * Better sceKernelMmap Handles errors where we would previously throw exceptions. Also moves the file logic to MapFile, since that way all the possible errors are in one place. Also fixes some function parameters to align with our current standards. * Major refactor MapDirectMemory, MapFlexibleMemory, ReserveVirtualRange, and MemoryPoolReserve all internally use mmap to perform their mappings. Naturally, this means that all functions have similar behaviors, and a lot of duplicate code. This add necessary conditional behavior to MapMemory so MemoryPoolReserve and ReserveVirtualRange can use it, without disrupting the behavior of MapDirectMemory or MapFlexibleMemory calls. * Accurate phys_addr for non-direct mappings * Properly handle GPU access rights Since my first commit restricts GPU mappings to memory areas with GPU access permissions, we also need to be updating the GPU mappings appropriately during Protect calls too. * Update memory.cpp * Update memory.h * Update memory.cpp * Update memory.cpp * Update memory.cpp * Revert "Update memory.cpp" This reverts commit 2c55d014c0efbdfadee4121b01e1dcf5af60e63d. * Coalesce dmem map Aligns with hardware observations, hopefully shouldn't break anything since nothing should change hardware-wise when release dmem calls and unmap calls are performed? Either that or Windows breaks because Windows, will need to test. * Implement posix_mprotect Unity calls this Also fixes the names of sceKernelMprotect and sceKernelMtypeprotect, though that's more of a style change and can be reverted if requested. * Fix sceKernelSetVirtualRangeName Partially addresses a "regression" introduced when I fixed up some asserts. As noted in the code, this implementation is still slightly inaccurate, as handling this properly could cause regressions on Windows. * Unconditional assert in MapFile * Remove protect warning This is expected behavior, shouldn't need any logging. * Respect alignment Forgot to properly do this when updating ReserveVirtualRange and MemoryPoolReserve * Fix Mprotect on free memory On real hardware, this just does nothing. If something did get protected, there's no way to query that information. Therefore, it seems pretty safe to just behave like munmap and return size here. * Minor tidy-up No functional difference, but looks better. --- src/core/libraries/kernel/memory.cpp | 106 +++++++----- src/core/libraries/kernel/memory.h | 18 +- src/core/memory.cpp | 236 +++++++++++++-------------- src/core/memory.h | 14 +- 4 files changed, 193 insertions(+), 181 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index cb41a664a..ce694dc1e 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -8,7 +8,6 @@ #include "common/logging/log.h" #include "common/scope_exit.h" #include "common/singleton.h" -#include "core/file_sys/fs.h" #include "core/libraries/kernel/kernel.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" @@ -152,7 +151,8 @@ s32 PS4_SYSV_ABI sceKernelReserveVirtualRange(void** addr, u64 len, int flags, u const VAddr in_addr = reinterpret_cast(*addr); const auto map_flags = static_cast(flags); - s32 result = memory->Reserve(addr, in_addr, len, map_flags, alignment); + s32 result = memory->MapMemory(addr, in_addr, len, Core::MemoryProt::NoAccess, map_flags, + Core::VMAType::Reserved, "anon", false, -1, alignment); if (result == 0) { LOG_INFO(Kernel_Vmm, "out_addr = {}", fmt::ptr(*addr)); } @@ -263,13 +263,22 @@ int PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** return memory->QueryProtection(std::bit_cast(addr), start, end, prot); } -int PS4_SYSV_ABI sceKernelMProtect(const void* addr, size_t size, int prot) { +s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot) { Core::MemoryManager* memory_manager = Core::Memory::Instance(); Core::MemoryProt protection_flags = static_cast(prot); return memory_manager->Protect(std::bit_cast(addr), size, protection_flags); } -int PS4_SYSV_ABI sceKernelMTypeProtect(const void* addr, size_t size, int mtype, int prot) { +s32 PS4_SYSV_ABI posix_mprotect(const void* addr, u64 size, s32 prot) { + s32 result = sceKernelMprotect(addr, size, prot); + if (result < 0) { + ErrSceToPosix(result); + return -1; + } + return result; +} + +s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s32 prot) { Core::MemoryManager* memory_manager = Core::Memory::Instance(); Core::MemoryProt protection_flags = static_cast(prot); return memory_manager->Protect(std::bit_cast(addr), size, protection_flags); @@ -344,7 +353,7 @@ s32 PS4_SYSV_ABI sceKernelBatchMap2(OrbisKernelBatchMapEntry* entries, int numEn break; } case MemoryOpTypes::ORBIS_KERNEL_MAP_OP_PROTECT: { - result = sceKernelMProtect(entries[i].start, entries[i].length, entries[i].protection); + result = sceKernelMprotect(entries[i].start, entries[i].length, entries[i].protection); LOG_INFO(Kernel_Vmm, "entry = {}, operation = {}, len = {:#x}, result = {}", i, entries[i].operation, entries[i].length, result); break; @@ -359,7 +368,7 @@ s32 PS4_SYSV_ABI sceKernelBatchMap2(OrbisKernelBatchMapEntry* entries, int numEn break; } case MemoryOpTypes::ORBIS_KERNEL_MAP_OP_TYPE_PROTECT: { - result = sceKernelMTypeProtect(entries[i].start, entries[i].length, entries[i].type, + result = sceKernelMtypeprotect(entries[i].start, entries[i].length, entries[i].type, entries[i].protection); LOG_INFO(Kernel_Vmm, "entry = {}, operation = {}, len = {:#x}, result = {}", i, entries[i].operation, entries[i].length, result); @@ -380,7 +389,7 @@ s32 PS4_SYSV_ABI sceKernelBatchMap2(OrbisKernelBatchMapEntry* entries, int numEn return result; } -s32 PS4_SYSV_ABI sceKernelSetVirtualRangeName(const void* addr, size_t len, const char* name) { +s32 PS4_SYSV_ABI sceKernelSetVirtualRangeName(const void* addr, u64 len, const char* name) { if (name == nullptr) { LOG_ERROR(Kernel_Vmm, "name is invalid!"); return ORBIS_KERNEL_ERROR_EFAULT; @@ -396,8 +405,8 @@ s32 PS4_SYSV_ABI sceKernelSetVirtualRangeName(const void* addr, size_t len, cons return ORBIS_OK; } -s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, size_t len, - size_t alignment, u64* physAddrOut) { +s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, u64 len, u64 alignment, + u64* physAddrOut) { if (searchStart < 0 || searchEnd <= searchStart) { LOG_ERROR(Kernel_Vmm, "Provided address range is invalid!"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -439,10 +448,10 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, size_ return ORBIS_OK; } -s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addrIn, size_t len, size_t alignment, int flags, - void** addrOut) { - LOG_INFO(Kernel_Vmm, "addrIn = {}, len = {:#x}, alignment = {:#x}, flags = {:#x}", - fmt::ptr(addrIn), len, alignment, flags); +s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addr_in, u64 len, u64 alignment, s32 flags, + void** addr_out) { + LOG_INFO(Kernel_Vmm, "addr_in = {}, len = {:#x}, alignment = {:#x}, flags = {:#x}", + fmt::ptr(addr_in), len, alignment, flags); if (len == 0 || !Common::Is2MBAligned(len)) { LOG_ERROR(Kernel_Vmm, "Map size is either zero or not 2MB aligned!"); @@ -456,14 +465,16 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addrIn, size_t len, size_t ali } auto* memory = Core::Memory::Instance(); - const VAddr in_addr = reinterpret_cast(addrIn); + const VAddr in_addr = reinterpret_cast(addr_in); const auto map_flags = static_cast(flags); - memory->PoolReserve(addrOut, in_addr, len, map_flags, alignment); + u64 map_alignment = alignment == 0 ? 2_MB : alignment; - return ORBIS_OK; + return memory->MapMemory(addr_out, std::bit_cast(addr_in), len, + Core::MemoryProt::NoAccess, map_flags, Core::VMAType::PoolReserved, + "anon", false, -1, map_alignment); } -s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags) { +s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, u64 len, s32 type, s32 prot, s32 flags) { if (addr == nullptr) { LOG_ERROR(Kernel_Vmm, "Address is invalid!"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -482,7 +493,7 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int return memory->PoolCommit(in_addr, len, mem_prot); } -s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags) { +s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, u64 len, s32 flags) { if (addr == nullptr) { LOG_ERROR(Kernel_Vmm, "Address is invalid!"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -523,12 +534,12 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolBatch(const OrbisKernelMemoryPoolBatchEntry* break; } case OrbisKernelMemoryPoolOpcode::Protect: { - result = sceKernelMProtect(entry.protect_params.addr, entry.protect_params.len, + result = sceKernelMprotect(entry.protect_params.addr, entry.protect_params.len, entry.protect_params.prot); break; } case OrbisKernelMemoryPoolOpcode::TypeProtect: { - result = sceKernelMTypeProtect( + result = sceKernelMtypeprotect( entry.type_protect_params.addr, entry.type_protect_params.len, entry.type_protect_params.type, entry.type_protect_params.prot); break; @@ -553,30 +564,48 @@ s32 PS4_SYSV_ABI sceKernelMemoryPoolBatch(const OrbisKernelMemoryPoolBatchEntry* return result; } -int PS4_SYSV_ABI sceKernelMmap(void* addr, u64 len, int prot, int flags, int fd, size_t offset, - void** res) { - LOG_INFO(Kernel_Vmm, "called addr = {}, len = {}, prot = {}, flags = {}, fd = {}, offset = {}", - fmt::ptr(addr), len, prot, flags, fd, offset); - auto* h = Common::Singleton::Instance(); +void* PS4_SYSV_ABI posix_mmap(void* addr, u64 len, s32 prot, s32 flags, s32 fd, s64 phys_addr) { + LOG_INFO(Kernel_Vmm, + "called addr = {}, len = {}, prot = {}, flags = {}, fd = {}, phys_addr = {}", + fmt::ptr(addr), len, prot, flags, fd, phys_addr); + + void* addr_out; auto* memory = Core::Memory::Instance(); const auto mem_prot = static_cast(prot); const auto mem_flags = static_cast(flags); + + s32 result = ORBIS_OK; if (fd == -1) { - return memory->MapMemory(res, std::bit_cast(addr), len, mem_prot, mem_flags, - Core::VMAType::Flexible); + result = memory->MapMemory(&addr_out, std::bit_cast(addr), len, mem_prot, mem_flags, + Core::VMAType::Flexible); } else { - const uintptr_t handle = h->GetFile(fd)->f.GetFileMapping(); - return memory->MapFile(res, std::bit_cast(addr), len, mem_prot, mem_flags, handle, - offset); + result = memory->MapFile(&addr_out, std::bit_cast(addr), len, mem_prot, mem_flags, + fd, phys_addr); } + + if (result != ORBIS_OK) { + // If the memory mappings fail, mmap sets errno to the appropriate error code, + // then returns (void*)-1; + ErrSceToPosix(result); + return reinterpret_cast(-1); + } + + return addr_out; } -void* PS4_SYSV_ABI posix_mmap(void* addr, u64 len, int prot, int flags, int fd, u64 offset) { - void* ptr; - LOG_INFO(Kernel_Vmm, "posix mmap redirect to sceKernelMmap"); - int result = sceKernelMmap(addr, len, prot, flags, fd, offset, &ptr); - ASSERT(result == 0); - return ptr; +s32 PS4_SYSV_ABI sceKernelMmap(void* addr, u64 len, s32 prot, s32 flags, s32 fd, s64 phys_addr, + void** res) { + void* addr_out = posix_mmap(addr, len, prot, flags, fd, phys_addr); + + if (addr_out == reinterpret_cast(-1)) { + // posix_mmap failed, calculate and return the appropriate kernel error code using errno. + LOG_ERROR(Kernel_Fs, "error = {}", *__Error()); + return ErrnoToSceKernelError(*__Error()); + } + + // Set the outputted address + *res = addr_out; + return ORBIS_OK; } s32 PS4_SYSV_ABI sceKernelConfiguredFlexibleMemorySize(u64* sizeOut) { @@ -678,8 +707,9 @@ void RegisterMemory(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("n1-v6FgU7MQ", "libkernel", 1, "libkernel", 1, 1, sceKernelConfiguredFlexibleMemorySize); - LIB_FUNCTION("9bfdLIyuwCY", "libkernel", 1, "libkernel", 1, 1, sceKernelMTypeProtect); - LIB_FUNCTION("vSMAm3cxYTY", "libkernel", 1, "libkernel", 1, 1, sceKernelMProtect); + LIB_FUNCTION("vSMAm3cxYTY", "libkernel", 1, "libkernel", 1, 1, sceKernelMprotect); + LIB_FUNCTION("YQOfxL4QfeU", "libScePosix", 1, "libkernel", 1, 1, posix_mprotect); + LIB_FUNCTION("9bfdLIyuwCY", "libkernel", 1, "libkernel", 1, 1, sceKernelMtypeprotect); // Memory pool LIB_FUNCTION("qCSfqDILlns", "libkernel", 1, "libkernel", 1, 1, sceKernelMemoryPoolExpand); diff --git a/src/core/libraries/kernel/memory.h b/src/core/libraries/kernel/memory.h index 92e158a00..6cefe0d07 100644 --- a/src/core/libraries/kernel/memory.h +++ b/src/core/libraries/kernel/memory.h @@ -147,9 +147,9 @@ s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, std::size_t len, int flags); int PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, u32* prot); -int PS4_SYSV_ABI sceKernelMProtect(const void* addr, size_t size, int prot); +s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot); -int PS4_SYSV_ABI sceKernelMTypeProtect(const void* addr, size_t size, int mtype, int prot); +s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s32 prot); int PS4_SYSV_ABI sceKernelDirectMemoryQuery(u64 offset, int flags, OrbisQueryInfo* query_info, size_t infoSize); @@ -165,14 +165,14 @@ s32 PS4_SYSV_ABI sceKernelBatchMap(OrbisKernelBatchMapEntry* entries, int numEnt s32 PS4_SYSV_ABI sceKernelBatchMap2(OrbisKernelBatchMapEntry* entries, int numEntries, int* numEntriesOut, int flags); -s32 PS4_SYSV_ABI sceKernelSetVirtualRangeName(const void* addr, size_t len, const char* name); +s32 PS4_SYSV_ABI sceKernelSetVirtualRangeName(const void* addr, u64 len, const char* name); -s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, size_t len, - size_t alignment, u64* physAddrOut); -s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addrIn, size_t len, size_t alignment, int flags, - void** addrOut); -s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, size_t len, int type, int prot, int flags); -s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, size_t len, int flags); +s32 PS4_SYSV_ABI sceKernelMemoryPoolExpand(u64 searchStart, u64 searchEnd, u64 len, u64 alignment, + u64* physAddrOut); +s32 PS4_SYSV_ABI sceKernelMemoryPoolReserve(void* addr_in, u64 len, u64 alignment, s32 flags, + void** addr_out); +s32 PS4_SYSV_ABI sceKernelMemoryPoolCommit(void* addr, u64 len, s32 type, s32 prot, s32 flags); +s32 PS4_SYSV_ABI sceKernelMemoryPoolDecommit(void* addr, u64 len, s32 flags); s32 PS4_SYSV_ABI sceKernelMemoryPoolBatch(const OrbisKernelMemoryPoolBatchEntry* entries, s32 count, s32* num_processed, s32 flags); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index ca6a0d6cd..ab59219b2 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -5,6 +5,7 @@ #include "common/assert.h" #include "common/config.h" #include "common/debug.h" +#include "core/file_sys/fs.h" #include "core/libraries/kernel/memory.h" #include "core/libraries/kernel/orbis_error.h" #include "core/libraries/kernel/process.h" @@ -181,6 +182,7 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, auto& area = CarveDmemArea(mapping_start, size)->second; area.memory_type = memory_type; area.is_free = false; + MergeAdjacent(dmem_map, dmem_area); return mapping_start; } @@ -214,90 +216,6 @@ void MemoryManager::Free(PAddr phys_addr, size_t size) { MergeAdjacent(dmem_map, dmem_area); } -int MemoryManager::PoolReserve(void** out_addr, VAddr virtual_addr, size_t size, - MemoryMapFlags flags, u64 alignment) { - std::scoped_lock lk{mutex}; - alignment = alignment > 0 ? alignment : 2_MB; - VAddr min_address = Common::AlignUp(impl.SystemManagedVirtualBase(), alignment); - VAddr mapped_addr = Common::AlignUp(virtual_addr, alignment); - - // Fixed mapping means the virtual address must exactly match the provided one. - if (True(flags & MemoryMapFlags::Fixed)) { - // Make sure we're mapping to a valid address - mapped_addr = mapped_addr > min_address ? mapped_addr : min_address; - auto vma = FindVMA(mapped_addr)->second; - size_t remaining_size = vma.base + vma.size - mapped_addr; - // If the VMA is mapped or there's not enough space, unmap the region first. - if (vma.IsMapped() || remaining_size < size) { - UnmapMemoryImpl(mapped_addr, size); - vma = FindVMA(mapped_addr)->second; - } - } - - if (False(flags & MemoryMapFlags::Fixed)) { - // When MemoryMapFlags::Fixed is not specified, and mapped_addr is 0, - // search from address 0x200000000 instead. - mapped_addr = mapped_addr == 0 ? 0x200000000 : mapped_addr; - mapped_addr = SearchFree(mapped_addr, size, alignment); - if (mapped_addr == -1) { - // No suitable memory areas to map to - return ORBIS_KERNEL_ERROR_ENOMEM; - } - } - - // Add virtual memory area - const auto new_vma_handle = CarveVMA(mapped_addr, size); - auto& new_vma = new_vma_handle->second; - new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); - new_vma.prot = MemoryProt::NoAccess; - new_vma.name = "anon"; - new_vma.type = VMAType::PoolReserved; - - *out_addr = std::bit_cast(mapped_addr); - return ORBIS_OK; -} - -int MemoryManager::Reserve(void** out_addr, VAddr virtual_addr, size_t size, MemoryMapFlags flags, - u64 alignment) { - std::scoped_lock lk{mutex}; - - virtual_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; - alignment = alignment > 0 ? alignment : 16_KB; - VAddr mapped_addr = alignment > 0 ? Common::AlignUp(virtual_addr, alignment) : virtual_addr; - - // Fixed mapping means the virtual address must exactly match the provided one. - if (True(flags & MemoryMapFlags::Fixed)) { - auto vma = FindVMA(mapped_addr)->second; - size_t remaining_size = vma.base + vma.size - mapped_addr; - // If the VMA is mapped or there's not enough space, unmap the region first. - if (vma.IsMapped() || remaining_size < size) { - UnmapMemoryImpl(mapped_addr, size); - vma = FindVMA(mapped_addr)->second; - } - } - - // Find the first free area starting with provided virtual address. - if (False(flags & MemoryMapFlags::Fixed)) { - mapped_addr = SearchFree(mapped_addr, size, alignment); - if (mapped_addr == -1) { - // No suitable memory areas to map to - return ORBIS_KERNEL_ERROR_ENOMEM; - } - } - - // Add virtual memory area - const auto new_vma_handle = CarveVMA(mapped_addr, size); - auto& new_vma = new_vma_handle->second; - new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); - new_vma.prot = MemoryProt::NoAccess; - new_vma.name = "anon"; - new_vma.type = VMAType::Reserved; - MergeAdjacent(vma_map, new_vma_handle); - - *out_addr = std::bit_cast(mapped_addr); - return ORBIS_OK; -} - int MemoryManager::PoolCommit(VAddr virtual_addr, size_t size, MemoryProt prot) { std::scoped_lock lk{mutex}; @@ -344,14 +262,17 @@ int MemoryManager::PoolCommit(VAddr virtual_addr, size_t size, MemoryProt prot) void* out_addr = impl.Map(mapped_addr, size, alignment, -1, false); TRACK_ALLOC(out_addr, size, "VMEM"); - if (IsValidGpuMapping(mapped_addr, size)) { + if (prot >= MemoryProt::GpuRead) { + // PS4s only map to GPU memory when the protection includes GPU access. + // If the address to map to is too high, PS4s throw a page fault and crash. + ASSERT_MSG(IsValidGpuMapping(mapped_addr, size), "Invalid address for GPU mapping"); rasterizer->MapMemory(mapped_addr, size); } return ORBIS_OK; } -int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, MemoryProt prot, +s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, VMAType type, std::string_view name, bool is_exec, PAddr phys_addr, u64 alignment) { std::scoped_lock lk{mutex}; @@ -366,17 +287,18 @@ int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, M VAddr mapped_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; // Fixed mapping means the virtual address must exactly match the provided one. - if (True(flags & MemoryMapFlags::Fixed)) { + // On a PS4, the Fixed flag is ignored if address 0 is provided. + if (True(flags & MemoryMapFlags::Fixed) && virtual_addr != 0) { auto vma = FindVMA(mapped_addr)->second; - size_t remaining_size = vma.base + vma.size - mapped_addr; // There's a possible edge case where we're mapping to a partially reserved range. // To account for this, unmap any reserved areas within this mapping range first. auto unmap_addr = mapped_addr; auto unmap_size = size; + // If flag NoOverwrite is provided, don't overwrite mapped VMAs. // When it isn't provided, VMAs can be overwritten regardless of if they're mapped. while ((False(flags & MemoryMapFlags::NoOverwrite) || !vma.IsMapped()) && - unmap_addr < mapped_addr + size && remaining_size < size) { + unmap_addr < mapped_addr + size) { auto unmapped = UnmapBytesFromEntry(unmap_addr, vma, unmap_size); unmap_addr += unmapped; unmap_size -= unmapped; @@ -384,51 +306,69 @@ int MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, size_t size, M } vma = FindVMA(mapped_addr)->second; - remaining_size = vma.base + vma.size - mapped_addr; + auto remaining_size = vma.base + vma.size - mapped_addr; if (vma.IsMapped() || remaining_size < size) { LOG_ERROR(Kernel_Vmm, "Unable to map {:#x} bytes at address {:#x}", size, mapped_addr); return ORBIS_KERNEL_ERROR_ENOMEM; } - } - - // Find the first free area starting with provided virtual address. - if (False(flags & MemoryMapFlags::Fixed)) { - // Provided address needs to be aligned before we can map. + } else { + // When MemoryMapFlags::Fixed is not specified, and mapped_addr is 0, + // search from address 0x200000000 instead. alignment = alignment > 0 ? alignment : 16_KB; - mapped_addr = SearchFree(Common::AlignUp(mapped_addr, alignment), size, alignment); + mapped_addr = virtual_addr == 0 ? 0x200000000 : mapped_addr; + mapped_addr = SearchFree(mapped_addr, size, alignment); if (mapped_addr == -1) { // No suitable memory areas to map to return ORBIS_KERNEL_ERROR_ENOMEM; } } - // Perform the mapping. - *out_addr = impl.Map(mapped_addr, size, alignment, phys_addr, is_exec); - TRACK_ALLOC(*out_addr, size, "VMEM"); + // Create a memory area representing this mapping. + const auto new_vma_handle = CarveVMA(mapped_addr, size); + auto& new_vma = new_vma_handle->second; - auto& new_vma = CarveVMA(mapped_addr, size)->second; - new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); - new_vma.prot = prot; - new_vma.name = name; - new_vma.type = type; - new_vma.is_exec = is_exec; - - if (type == VMAType::Direct) { - new_vma.phys_base = phys_addr; - } + // If type is Flexible, we need to track how much flexible memory is used here. if (type == VMAType::Flexible) { flexible_usage += size; } - if (IsValidGpuMapping(mapped_addr, size)) { + new_vma.disallow_merge = True(flags & MemoryMapFlags::NoCoalesce); + new_vma.prot = prot; + new_vma.name = name; + new_vma.type = type; + new_vma.phys_base = phys_addr == -1 ? 0 : phys_addr; + new_vma.is_exec = is_exec; + + if (type == VMAType::Reserved) { + // Technically this should be done for direct and flexible mappings too, + // But some Windows-specific limitations make that hard to accomplish. + MergeAdjacent(vma_map, new_vma_handle); + } + + if (prot >= MemoryProt::GpuRead) { + // PS4s only map to GPU memory when the protection includes GPU access. + // If the address to map to is too high, PS4s throw a page fault and crash. + ASSERT_MSG(IsValidGpuMapping(mapped_addr, size), "Invalid address for GPU mapping"); rasterizer->MapMemory(mapped_addr, size); } + if (type == VMAType::Reserved || type == VMAType::PoolReserved) { + // For Reserved/PoolReserved mappings, we don't perform any address space allocations. + // Just set out_addr to mapped_addr instead. + *out_addr = std::bit_cast(mapped_addr); + } else { + // Type is either Direct, Flexible, or Code, these need to be mapped in our address space. + *out_addr = impl.Map(mapped_addr, size, alignment, phys_addr, is_exec); + } + + TRACK_ALLOC(*out_addr, size, "VMEM"); return ORBIS_OK; } -int MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, size_t size, MemoryProt prot, - MemoryMapFlags flags, uintptr_t fd, size_t offset) { +s32 MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, + MemoryMapFlags flags, s32 fd, s64 phys_addr) { + auto* h = Common::Singleton::Instance(); + VAddr mapped_addr = (virtual_addr == 0) ? impl.SystemManagedVirtualBase() : virtual_addr; const size_t size_aligned = Common::AlignUp(size, 16_KB); @@ -449,8 +389,19 @@ int MemoryManager::MapFile(void** out_addr, VAddr virtual_addr, size_t size, Mem vma.base, vma.base + vma.size, virtual_addr, virtual_addr + size); } - // Map the file. - impl.MapFile(mapped_addr, size_aligned, offset, std::bit_cast(prot), fd); + // Get the file to map + auto file = h->GetFile(fd); + if (file == nullptr) { + return ORBIS_KERNEL_ERROR_EBADF; + } + + const auto handle = file->f.GetFileMapping(); + + impl.MapFile(mapped_addr, size_aligned, phys_addr, std::bit_cast(prot), handle); + + if (prot >= MemoryProt::GpuRead) { + ASSERT_MSG(false, "Files cannot be mapped to GPU memory"); + } // Add virtual memory area auto& new_vma = CarveVMA(mapped_addr, size_aligned)->second; @@ -478,6 +429,7 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { const bool is_exec = vma_base.is_exec; const auto start_in_vma = virtual_addr - vma_base_addr; const auto type = vma_base.type; + const auto prot = vma_base.prot; if (type != VMAType::PoolReserved && type != VMAType::Pooled) { LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); @@ -489,7 +441,8 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { pool_budget += size; } - if (IsValidGpuMapping(virtual_addr, size)) { + if (prot >= MemoryProt::GpuRead) { + // If this mapping has GPU access, unmap from GPU. rasterizer->UnmapMemory(virtual_addr, size); } @@ -528,6 +481,7 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma const auto adjusted_size = vma_base_size - start_in_vma < size ? vma_base_size - start_in_vma : size; const bool has_backing = type == VMAType::Direct || type == VMAType::File; + const auto prot = vma_base.prot; if (type == VMAType::Free) { return adjusted_size; @@ -536,8 +490,9 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma flexible_usage -= adjusted_size; } - if (IsValidGpuMapping(virtual_addr, adjusted_size)) { - rasterizer->UnmapMemory(virtual_addr, adjusted_size); + if (prot >= MemoryProt::GpuRead) { + // If this mapping has GPU access, unmap from GPU. + rasterizer->UnmapMemory(virtual_addr, size); } // Mark region as free and attempt to coalesce it with neighbours. @@ -605,8 +560,8 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s vma_base.size - start_in_vma < size ? vma_base.size - start_in_vma : size; if (vma_base.type == VMAType::Free) { - LOG_ERROR(Kernel_Vmm, "Cannot change protection on free memory region"); - return ORBIS_KERNEL_ERROR_EINVAL; + // On PS4, protecting freed memory does nothing. + return adjusted_size; } // Validate protection flags @@ -621,6 +576,18 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s return ORBIS_KERNEL_ERROR_EINVAL; } + if (vma_base.prot < MemoryProt::GpuRead && prot >= MemoryProt::GpuRead) { + // New protection will give the GPU access to this VMA, perform a rasterizer map + ASSERT_MSG(IsValidGpuMapping(addr, size), "Invalid address for GPU mapping"); + rasterizer->MapMemory(addr, size); + } + + if (vma_base.prot >= MemoryProt::GpuRead && prot < MemoryProt::GpuRead) { + // New protection will remove the GPU's access to this VMA, perform a rasterizer unmap + ASSERT_MSG(IsValidGpuMapping(addr, size), "Invalid address for GPU unmap"); + rasterizer->UnmapMemory(addr, size); + } + // Change protection vma_base.prot = prot; @@ -798,12 +765,31 @@ s32 MemoryManager::SetDirectMemoryType(s64 phys_addr, s32 memory_type) { return ORBIS_OK; } -void MemoryManager::NameVirtualRange(VAddr virtual_addr, size_t size, std::string_view name) { - auto it = FindVMA(virtual_addr); +void MemoryManager::NameVirtualRange(VAddr virtual_addr, u64 size, std::string_view name) { + // Sizes are aligned up to the nearest 16_KB + auto aligned_size = Common::AlignUp(size, 16_KB); + // Addresses are aligned down to the nearest 16_KB + auto aligned_addr = Common::AlignDown(virtual_addr, 16_KB); - ASSERT_MSG(it->second.Contains(virtual_addr, size), - "Range provided is not fully contained in vma"); - it->second.name = name; + auto it = FindVMA(aligned_addr); + s64 remaining_size = aligned_size; + auto current_addr = aligned_addr; + while (remaining_size > 0) { + // Nothing needs to be done to free VMAs + if (!it->second.IsFree()) { + if (remaining_size < it->second.size) { + // We should split VMAs here, but this could cause trouble for Windows. + // Instead log a warning and name the whole VMA. + // it = CarveVMA(current_addr, remaining_size); + LOG_WARNING(Kernel_Vmm, "Trying to partially name a range"); + } + auto& vma = it->second; + vma.name = name; + } + remaining_size -= it->second.size; + current_addr += it->second.size; + it = FindVMA(current_addr); + } } void MemoryManager::InvalidateMemory(const VAddr addr, const u64 size) const { @@ -824,6 +810,8 @@ VAddr MemoryManager::SearchFree(VAddr virtual_addr, size_t size, u32 alignment) ASSERT_MSG(virtual_addr <= max_search_address, "Input address {:#x} is out of bounds", virtual_addr); + // Align up the virtual_addr first. + virtual_addr = Common::AlignUp(virtual_addr, alignment); auto it = FindVMA(virtual_addr); // If the VMA is free and contains the requested mapping we are done. diff --git a/src/core/memory.h b/src/core/memory.h index 883b48854..b3ebe3c27 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -183,20 +183,14 @@ public: void Free(PAddr phys_addr, size_t size); - int PoolReserve(void** out_addr, VAddr virtual_addr, size_t size, MemoryMapFlags flags, - u64 alignment = 0); - - int Reserve(void** out_addr, VAddr virtual_addr, size_t size, MemoryMapFlags flags, - u64 alignment = 0); - int PoolCommit(VAddr virtual_addr, size_t size, MemoryProt prot); - int MapMemory(void** out_addr, VAddr virtual_addr, size_t size, MemoryProt prot, + s32 MapMemory(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, MemoryMapFlags flags, VMAType type, std::string_view name = "anon", bool is_exec = false, PAddr phys_addr = -1, u64 alignment = 0); - int MapFile(void** out_addr, VAddr virtual_addr, size_t size, MemoryProt prot, - MemoryMapFlags flags, uintptr_t fd, size_t offset); + s32 MapFile(void** out_addr, VAddr virtual_addr, u64 size, MemoryProt prot, + MemoryMapFlags flags, s32 fd, s64 phys_addr); s32 PoolDecommit(VAddr virtual_addr, size_t size); @@ -221,7 +215,7 @@ public: s32 SetDirectMemoryType(s64 phys_addr, s32 memory_type); - void NameVirtualRange(VAddr virtual_addr, size_t size, std::string_view name); + void NameVirtualRange(VAddr virtual_addr, u64 size, std::string_view name); void InvalidateMemory(VAddr addr, u64 size) const; From 8fffdc39186f1748460c637fab251271fbc257d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 29 May 2025 21:20:16 +0200 Subject: [PATCH 074/141] Handle V_CVT_F64_U32 (#3008) --- src/shader_recompiler/frontend/translate/translate.h | 1 + src/shader_recompiler/frontend/translate/vector_alu.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 68d5e8dc8..7b4b03f27 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -183,6 +183,7 @@ public: void V_READFIRSTLANE_B32(const GcnInst& inst); void V_CVT_I32_F64(const GcnInst& inst); void V_CVT_F64_I32(const GcnInst& inst); + void V_CVT_F64_U32(const GcnInst& inst); void V_CVT_F32_I32(const GcnInst& inst); void V_CVT_F32_U32(const GcnInst& inst); void V_CVT_U32_F32(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index 6171cca07..fb3f52c7f 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -110,6 +110,8 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { return V_CVT_I32_F64(inst); case Opcode::V_CVT_F64_I32: return V_CVT_F64_I32(inst); + case Opcode::V_CVT_F64_U32: + return V_CVT_F64_U32(inst); case Opcode::V_CVT_F32_I32: return V_CVT_F32_I32(inst); case Opcode::V_CVT_F32_U32: @@ -684,6 +686,11 @@ void Translator::V_CVT_F64_I32(const GcnInst& inst) { SetDst64(inst.dst[0], ir.ConvertSToF(64, 32, src0)); } +void Translator::V_CVT_F64_U32(const GcnInst& inst) { + const IR::U32 src0{GetSrc(inst.src[0])}; + SetDst64(inst.dst[0], ir.ConvertUToF(64, 32, src0)); +} + void Translator::V_CVT_F32_I32(const GcnInst& inst) { const IR::U32 src0{GetSrc(inst.src[0])}; SetDst(inst.dst[0], ir.ConvertSToF(32, 32, src0)); From 2091bc56513fd072ae8477b51c9a20be7817e618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Fri, 30 May 2025 01:56:24 +0200 Subject: [PATCH 075/141] Handle R128 bit in MIMG instructions (#3010) --- src/shader_recompiler/frontend/decode.cpp | 1 - .../frontend/translate/vector_memory.cpp | 5 +++++ src/shader_recompiler/info.h | 9 ++++++++- .../ir/passes/resource_tracking_pass.cpp | 1 + src/shader_recompiler/ir/reg.h | 1 + 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/frontend/decode.cpp b/src/shader_recompiler/frontend/decode.cpp index 20b78e869..37e8a0973 100644 --- a/src/shader_recompiler/frontend/decode.cpp +++ b/src/shader_recompiler/frontend/decode.cpp @@ -1032,7 +1032,6 @@ void GcnDecodeContext::decodeInstructionMIMG(uint64_t hexInstruction) { m_instruction.control.mimg = *reinterpret_cast(&hexInstruction); m_instruction.control.mimg.mod = getMimgModifier(m_instruction.opcode); - ASSERT(m_instruction.control.mimg.r128 == 0); } void GcnDecodeContext::decodeInstructionDS(uint64_t hexInstruction) { diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 5639bc56a..8c1946390 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -377,6 +377,7 @@ void Translator::IMAGE_LOAD(bool has_mip, const GcnInst& inst) { IR::TextureInstInfo info{}; info.has_lod.Assign(has_mip); info.is_array.Assign(mimg.da); + info.is_r128.Assign(mimg.r128); const IR::Value texel = ir.ImageRead(handle, body, {}, {}, info); for (u32 i = 0; i < 4; i++) { @@ -426,6 +427,7 @@ void Translator::IMAGE_GET_RESINFO(const GcnInst& inst) { IR::TextureInstInfo info{}; info.is_array.Assign(mimg.da); + info.is_r128.Assign(mimg.r128); const IR::Value size = ir.ImageQueryDimension(tsharp, lod, ir.Imm1(has_mips), info); @@ -451,6 +453,7 @@ void Translator::IMAGE_ATOMIC(AtomicOp op, const GcnInst& inst) { IR::TextureInstInfo info{}; info.is_array.Assign(mimg.da); + info.is_r128.Assign(mimg.r128); const IR::Value value = ir.GetVectorReg(val_reg); const IR::Value handle = ir.GetScalarReg(tsharp_reg); @@ -509,6 +512,7 @@ IR::Value EmitImageSample(IR::IREmitter& ir, const GcnInst& inst, const IR::Scal info.has_lod.Assign(flags.any(MimgModifier::Lod)); info.is_array.Assign(mimg.da); info.is_unnormalized.Assign(mimg.unrm); + info.is_r128.Assign(mimg.r128); if (gather) { info.gather_comp.Assign(std::bit_width(mimg.dmask) - 1); @@ -617,6 +621,7 @@ void Translator::IMAGE_GET_LOD(const GcnInst& inst) { IR::TextureInstInfo info{}; info.is_array.Assign(mimg.da); + info.is_r128.Assign(mimg.r128); const IR::Value handle = ir.GetScalarReg(tsharp_reg); const IR::Value body = ir.CompositeConstruct( diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index d349d7827..24e0741c1 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -84,6 +84,7 @@ struct ImageResource { bool is_atomic{}; bool is_array{}; bool is_written{}; + bool is_r128{}; [[nodiscard]] constexpr AmdGpu::Image GetSharp(const Info& info) const noexcept; }; @@ -293,7 +294,13 @@ constexpr AmdGpu::Buffer BufferResource::GetSharp(const Info& info) const noexce } constexpr AmdGpu::Image ImageResource::GetSharp(const Info& info) const noexcept { - const auto image = info.ReadUdSharp(sharp_idx); + AmdGpu::Image image{0}; + if (!is_r128) { + image = info.ReadUdSharp(sharp_idx); + } else { + AmdGpu::Buffer buf = info.ReadUdSharp(sharp_idx); + memcpy(&image, &buf, sizeof(buf)); + } if (!image.Valid()) { // Fall back to null image if unbound. return AmdGpu::Image::Null(); diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index cc0bf83d3..18c77e600 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -411,6 +411,7 @@ void PatchImageSharp(IR::Block& block, IR::Inst& inst, Info& info, Descriptors& .is_atomic = IsImageAtomicInstruction(inst), .is_array = bool(inst_info.is_array), .is_written = is_written, + .is_r128 = bool(inst_info.is_r128), }); IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)}; diff --git a/src/shader_recompiler/ir/reg.h b/src/shader_recompiler/ir/reg.h index 622190cf0..82aa436a7 100644 --- a/src/shader_recompiler/ir/reg.h +++ b/src/shader_recompiler/ir/reg.h @@ -44,6 +44,7 @@ union TextureInstInfo { BitField<9, 1, u32> is_array; BitField<10, 1, u32> is_unnormalized; BitField<11, 1, u32> is_gather; + BitField<12, 1, u32> is_r128; }; union BufferInstInfo { From 790b54bf2953fd03b933c2e5d5247db41b76444a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Fri, 30 May 2025 03:51:36 +0200 Subject: [PATCH 076/141] Misc opcodes fixes (#3009) --- src/shader_recompiler/frontend/translate/translate.cpp | 2 +- src/shader_recompiler/frontend/translate/vector_memory.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index e49f95d9a..5675adf3c 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -380,7 +380,7 @@ T Translator::GetSrc64(const InstOperand& operand) { break; case OperandField::VccLo: if constexpr (is_float) { - UNREACHABLE(); + value = ir.PackDouble2x32(ir.CompositeConstruct(ir.GetVccLo(), ir.GetVccHi())); } else { value = ir.PackUint2x32(ir.CompositeConstruct(ir.GetVccLo(), ir.GetVccHi())); } diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 8c1946390..5c972c607 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -152,6 +152,7 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { // Image gather operations case Opcode::IMAGE_GATHER4: + case Opcode::IMAGE_GATHER4_L: case Opcode::IMAGE_GATHER4_LZ: case Opcode::IMAGE_GATHER4_C: case Opcode::IMAGE_GATHER4_O: From 4019319d92b289e40fae8257e94562b949fca3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Fri, 30 May 2025 21:04:31 +0200 Subject: [PATCH 077/141] Provide custom border color to samplers (#3014) --- src/video_core/amdgpu/liverpool.h | 15 ++++++++- .../renderer_vulkan/vk_rasterizer.cpp | 2 +- src/video_core/texture_cache/sampler.cpp | 31 +++++++++++++++++-- src/video_core/texture_cache/sampler.h | 3 +- .../texture_cache/texture_cache.cpp | 6 ++-- src/video_core/texture_cache/texture_cache.h | 4 ++- 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index a62141099..245e34d35 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -608,6 +608,16 @@ struct Liverpool { } }; + struct BorderColorBufferBase { + u32 base_addr_lo; + BitField<0, 8, u32> base_addr_hi; + + template + T Address() const { + return std::bit_cast(u64(base_addr_hi) << 40 | u64(base_addr_lo) << 8); + } + }; + struct IndexBufferBase { BitField<0, 8, u32> base_addr_hi; u32 base_addr_lo; @@ -1299,7 +1309,9 @@ struct Liverpool { Scissor screen_scissor; INSERT_PADDING_WORDS(0xA010 - 0xA00C - 2); DepthBuffer depth_buffer; - INSERT_PADDING_WORDS(0xA080 - 0xA018); + INSERT_PADDING_WORDS(8); + BorderColorBufferBase ta_bc_base; + INSERT_PADDING_WORDS(0xA080 - 0xA020 - 2); WindowOffset window_offset; ViewportScissor window_scissor; INSERT_PADDING_WORDS(0xA08E - 0xA081 - 2); @@ -1626,6 +1638,7 @@ static_assert(GFX6_3D_REG_INDEX(depth_htile_data_base) == 0xA005); static_assert(GFX6_3D_REG_INDEX(screen_scissor) == 0xA00C); static_assert(GFX6_3D_REG_INDEX(depth_buffer.z_info) == 0xA010); static_assert(GFX6_3D_REG_INDEX(depth_buffer.depth_slice) == 0xA017); +static_assert(GFX6_3D_REG_INDEX(ta_bc_base) == 0xA020); static_assert(GFX6_3D_REG_INDEX(window_offset) == 0xA080); static_assert(GFX6_3D_REG_INDEX(window_scissor) == 0xA081); static_assert(GFX6_3D_REG_INDEX(color_target_mask) == 0xA08E); diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 4bdb08bf2..dff4e5a5f 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -716,7 +716,7 @@ void Rasterizer::BindTextures(const Shader::Info& stage, Shader::Backend::Bindin ssharp.max_aniso.Assign(AmdGpu::AnisoRatio::One); } } - const auto vk_sampler = texture_cache.GetSampler(ssharp); + const auto vk_sampler = texture_cache.GetSampler(ssharp, liverpool->regs.ta_bc_base); image_infos.emplace_back(vk_sampler, VK_NULL_HANDLE, vk::ImageLayout::eGeneral); set_writes.push_back({ .dstSet = VK_NULL_HANDLE, diff --git a/src/video_core/texture_cache/sampler.cpp b/src/video_core/texture_cache/sampler.cpp index 8dbdd2912..e18c79a59 100644 --- a/src/video_core/texture_cache/sampler.cpp +++ b/src/video_core/texture_cache/sampler.cpp @@ -9,7 +9,8 @@ namespace VideoCore { -Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler) { +Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler, + const AmdGpu::Liverpool::BorderColorBufferBase& border_color_base) { if (sampler.force_degamma) { LOG_WARNING(Render_Vulkan, "Texture requires gamma correction"); } @@ -20,7 +21,33 @@ Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sample const float maxAnisotropy = anisotropyEnable ? std::clamp(sampler.MaxAniso(), 1.0f, instance.MaxSamplerAnisotropy()) : 1.0f; + auto borderColor = LiverpoolToVK::BorderColor(sampler.border_color_type); + if (!instance.IsCustomBorderColorSupported()) { + LOG_WARNING(Render_Vulkan, "Custom border color is not supported, falling back to black"); + borderColor = vk::BorderColor::eFloatOpaqueBlack; + } + + const auto customColor = [&]() -> std::optional { + if (borderColor == vk::BorderColor::eFloatCustomEXT) { + const auto borderColorIndex = sampler.border_color_ptr.Value(); + const auto borderColorBuffer = border_color_base.Address*>(); + const auto customBorderColorArray = borderColorBuffer[borderColorIndex]; + + const vk::SamplerCustomBorderColorCreateInfoEXT ret{ + .customBorderColor = + vk::ClearColorValue{ + .float32 = customBorderColorArray, + }, + .format = vk::Format::eR32G32B32A32Sfloat, + }; + return ret; + } else { + return std::nullopt; + } + }(); + const vk::SamplerCreateInfo sampler_ci = { + .pNext = customColor ? &*customColor : nullptr, .magFilter = LiverpoolToVK::Filter(sampler.xy_mag_filter), .minFilter = LiverpoolToVK::Filter(sampler.xy_min_filter), .mipmapMode = LiverpoolToVK::MipFilter(sampler.mip_filter), @@ -34,7 +61,7 @@ Sampler::Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sample .compareOp = LiverpoolToVK::DepthCompare(sampler.depth_compare_func), .minLod = sampler.MinLod(), .maxLod = sampler.MaxLod(), - .borderColor = LiverpoolToVK::BorderColor(sampler.border_color_type), + .borderColor = borderColor, .unnormalizedCoordinates = false, // Handled in shader due to Vulkan limitations. }; auto [sampler_result, smplr] = instance.GetDevice().createSamplerUnique(sampler_ci); diff --git a/src/video_core/texture_cache/sampler.h b/src/video_core/texture_cache/sampler.h index 856d39763..28ba0f67b 100644 --- a/src/video_core/texture_cache/sampler.h +++ b/src/video_core/texture_cache/sampler.h @@ -14,7 +14,8 @@ namespace VideoCore { class Sampler { public: - explicit Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler); + explicit Sampler(const Vulkan::Instance& instance, const AmdGpu::Sampler& sampler, + const AmdGpu::Liverpool::BorderColorBufferBase& border_color_base); ~Sampler(); Sampler(const Sampler&) = delete; diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index ddb4ea799..63cfc4431 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -636,9 +636,11 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule image.flags &= ~ImageFlagBits::Dirty; } -vk::Sampler TextureCache::GetSampler(const AmdGpu::Sampler& sampler) { +vk::Sampler TextureCache::GetSampler( + const AmdGpu::Sampler& sampler, + const AmdGpu::Liverpool::BorderColorBufferBase& border_color_base) { const u64 hash = XXH3_64bits(&sampler, sizeof(sampler)); - const auto [it, new_sampler] = samplers.try_emplace(hash, instance, sampler); + const auto [it, new_sampler] = samplers.try_emplace(hash, instance, sampler, border_color_base); return it->second.Handle(); } diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index b6bf88958..ccfeb36b2 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -139,7 +139,9 @@ public: void RefreshImage(Image& image, Vulkan::Scheduler* custom_scheduler = nullptr); /// Retrieves the sampler that matches the provided S# descriptor. - [[nodiscard]] vk::Sampler GetSampler(const AmdGpu::Sampler& sampler); + [[nodiscard]] vk::Sampler GetSampler( + const AmdGpu::Sampler& sampler, + const AmdGpu::Liverpool::BorderColorBufferBase& border_color_base); /// Retrieves the image with the specified id. [[nodiscard]] Image& GetImage(ImageId id) { From c09e463b8ee86233ee883c1357bc883ce97bccaa Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 31 May 2025 09:35:52 -0500 Subject: [PATCH 078/141] Revert new GPU map logic (#3019) Something's wrong somewhere, and there's just too many places that somewhere could be for me to debug it right now. --- src/core/memory.cpp | 51 ++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index ab59219b2..bf9d1cabd 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -262,10 +262,7 @@ int MemoryManager::PoolCommit(VAddr virtual_addr, size_t size, MemoryProt prot) void* out_addr = impl.Map(mapped_addr, size, alignment, -1, false); TRACK_ALLOC(out_addr, size, "VMEM"); - if (prot >= MemoryProt::GpuRead) { - // PS4s only map to GPU memory when the protection includes GPU access. - // If the address to map to is too high, PS4s throw a page fault and crash. - ASSERT_MSG(IsValidGpuMapping(mapped_addr, size), "Invalid address for GPU mapping"); + if (IsValidGpuMapping(mapped_addr, size)) { rasterizer->MapMemory(mapped_addr, size); } @@ -345,19 +342,15 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo MergeAdjacent(vma_map, new_vma_handle); } - if (prot >= MemoryProt::GpuRead) { - // PS4s only map to GPU memory when the protection includes GPU access. - // If the address to map to is too high, PS4s throw a page fault and crash. - ASSERT_MSG(IsValidGpuMapping(mapped_addr, size), "Invalid address for GPU mapping"); - rasterizer->MapMemory(mapped_addr, size); - } - if (type == VMAType::Reserved || type == VMAType::PoolReserved) { // For Reserved/PoolReserved mappings, we don't perform any address space allocations. // Just set out_addr to mapped_addr instead. *out_addr = std::bit_cast(mapped_addr); } else { - // Type is either Direct, Flexible, or Code, these need to be mapped in our address space. + // If this is not a reservation, then map to GPU and address space + if (IsValidGpuMapping(mapped_addr, size)) { + rasterizer->MapMemory(mapped_addr, size); + } *out_addr = impl.Map(mapped_addr, size, alignment, phys_addr, is_exec); } @@ -429,7 +422,6 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { const bool is_exec = vma_base.is_exec; const auto start_in_vma = virtual_addr - vma_base_addr; const auto type = vma_base.type; - const auto prot = vma_base.prot; if (type != VMAType::PoolReserved && type != VMAType::Pooled) { LOG_ERROR(Kernel_Vmm, "Attempting to decommit non-pooled memory!"); @@ -437,15 +429,15 @@ s32 MemoryManager::PoolDecommit(VAddr virtual_addr, size_t size) { } if (type == VMAType::Pooled) { + // We always map PoolCommitted memory to GPU, so unmap when decomitting. + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + // Track how much pooled memory is decommitted pool_budget += size; } - if (prot >= MemoryProt::GpuRead) { - // If this mapping has GPU access, unmap from GPU. - rasterizer->UnmapMemory(virtual_addr, size); - } - // Mark region as free and attempt to coalesce it with neighbours. const auto new_it = CarveVMA(virtual_addr, size); auto& vma = new_it->second; @@ -486,15 +478,11 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma if (type == VMAType::Free) { return adjusted_size; } + if (type == VMAType::Flexible) { flexible_usage -= adjusted_size; } - if (prot >= MemoryProt::GpuRead) { - // If this mapping has GPU access, unmap from GPU. - rasterizer->UnmapMemory(virtual_addr, size); - } - // Mark region as free and attempt to coalesce it with neighbours. const auto new_it = CarveVMA(virtual_addr, adjusted_size); auto& vma = new_it->second; @@ -507,6 +495,11 @@ u64 MemoryManager::UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma auto& post_merge_vma = post_merge_it->second; bool readonly_file = post_merge_vma.prot == MemoryProt::CpuRead && type == VMAType::File; if (type != VMAType::Reserved && type != VMAType::PoolReserved) { + // If this mapping has GPU access, unmap from GPU. + if (IsValidGpuMapping(virtual_addr, size)) { + rasterizer->UnmapMemory(virtual_addr, size); + } + // Unmap the memory region. impl.Unmap(vma_base_addr, vma_base_size, start_in_vma, start_in_vma + adjusted_size, phys_base, is_exec, has_backing, readonly_file); @@ -576,18 +569,6 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s return ORBIS_KERNEL_ERROR_EINVAL; } - if (vma_base.prot < MemoryProt::GpuRead && prot >= MemoryProt::GpuRead) { - // New protection will give the GPU access to this VMA, perform a rasterizer map - ASSERT_MSG(IsValidGpuMapping(addr, size), "Invalid address for GPU mapping"); - rasterizer->MapMemory(addr, size); - } - - if (vma_base.prot >= MemoryProt::GpuRead && prot < MemoryProt::GpuRead) { - // New protection will remove the GPU's access to this VMA, perform a rasterizer unmap - ASSERT_MSG(IsValidGpuMapping(addr, size), "Invalid address for GPU unmap"); - rasterizer->UnmapMemory(addr, size); - } - // Change protection vma_base.prot = prot; From bb199865cf2f09695535d98db38dc7c78fd2d054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Sun, 1 Jun 2025 13:52:09 +0200 Subject: [PATCH 079/141] Qt: Set Minimum Icon Size List to 48 (#3018) --- src/qt_gui/main_window.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 1966aa52b..77b088b78 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -387,9 +387,9 @@ void MainWindow::CreateConnects() { connect(ui->sizeSlider, &QSlider::valueChanged, this, [this](int value) { if (isTableList) { m_game_list_frame->icon_size = - 36 + value; // 36 is the minimum icon size to use due to text disappearing. - m_game_list_frame->ResizeIcons(36 + value); - Config::setIconSize(36 + value); + 48 + value; // 48 is the minimum icon size to use due to text disappearing. + m_game_list_frame->ResizeIcons(48 + value); + Config::setIconSize(48 + value); Config::setSliderPosition(value); } else { m_game_grid_frame->icon_size = 69 + value; From b1af1334c91382d0e28b9cec06f4604d93357bea Mon Sep 17 00:00:00 2001 From: WujekFoliarz <72314465+WujekFoliarz@users.noreply.github.com> Date: Sun, 1 Jun 2025 18:13:02 +0200 Subject: [PATCH 080/141] Fix touchpad handling and change gyro calculation (#3006) * Change touchpad handling and orientation calculation * remove unnecessary includes in pad.cpp * remove the cmake command arguments * remove the weird file * try to fix formatting * limit new gyro and touchpad logic to controller 1 * remove cout * fix formatting and add the handle check to scePadRead * swap y and z back --- src/core/libraries/pad/pad.cpp | 137 +++++++++++++++++++++++++++++--- src/input/controller.cpp | 141 ++++++++++++++++++++------------- src/input/controller.h | 22 +++++ 3 files changed, 232 insertions(+), 68 deletions(-) diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index 5dfc68e90..42582783b 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -316,22 +316,79 @@ int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num) { pData[i].angularVelocity.y = states[i].angularVelocity.y; pData[i].angularVelocity.z = states[i].angularVelocity.z; pData[i].orientation = {0.0f, 0.0f, 0.0f, 1.0f}; - if (engine) { + pData[i].acceleration.x = states[i].acceleration.x * 0.098; + pData[i].acceleration.y = states[i].acceleration.y * 0.098; + pData[i].acceleration.z = states[i].acceleration.z * 0.098; + pData[i].angularVelocity.x = states[i].angularVelocity.x; + pData[i].angularVelocity.y = states[i].angularVelocity.y; + pData[i].angularVelocity.z = states[i].angularVelocity.z; + + if (engine && handle == 1) { const auto gyro_poll_rate = engine->GetAccelPollRate(); if (gyro_poll_rate != 0.0f) { - GameController::CalculateOrientation(pData[i].acceleration, - pData[i].angularVelocity, - 1.0f / gyro_poll_rate, pData[i].orientation); + auto now = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration_cast( + now - controller->GetLastUpdate()) + .count() / + 1000000.0f; + controller->SetLastUpdate(now); + Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation(); + Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; + GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity, + deltaTime, lastOrientation, outputOrientation); + pData[i].orientation = outputOrientation; + controller->SetLastOrientation(outputOrientation); } } + pData[i].touchData.touchNum = (states[i].touchpad[0].state ? 1 : 0) + (states[i].touchpad[1].state ? 1 : 0); + + if (handle == 1) { + if (controller->GetTouchCount() >= 127) { + controller->SetTouchCount(0); + } + + if (controller->GetSecondaryTouchCount() >= 127) { + controller->SetSecondaryTouchCount(0); + } + + if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) { + controller->SetTouchCount(controller->GetTouchCount() + 1); + controller->SetSecondaryTouchCount(controller->GetTouchCount()); + } else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) { + controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1); + } else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) { + if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) { + controller->SetTouchCount(controller->GetSecondaryTouchCount()); + } else { + if (controller->WasSecondaryTouchReset()) { + controller->SetTouchCount(controller->GetSecondaryTouchCount()); + controller->UnsetSecondaryTouchResetBool(); + } + } + } + + controller->SetPreviousTouchNum(pData->touchData.touchNum); + + if (pData->touchData.touchNum == 1) { + states[i].touchpad[0].ID = controller->GetTouchCount(); + states[i].touchpad[1].ID = 0; + } else if (pData->touchData.touchNum == 2) { + states[i].touchpad[0].ID = controller->GetTouchCount(); + states[i].touchpad[1].ID = controller->GetSecondaryTouchCount(); + } + } else { + states[i].touchpad[0].ID = 1; + states[i].touchpad[1].ID = 2; + } + pData[i].touchData.touch[0].x = states[i].touchpad[0].x; pData[i].touchData.touch[0].y = states[i].touchpad[0].y; - pData[i].touchData.touch[0].id = 1; + pData[i].touchData.touch[0].id = states[i].touchpad[0].ID; pData[i].touchData.touch[1].x = states[i].touchpad[1].x; pData[i].touchData.touch[1].y = states[i].touchpad[1].y; - pData[i].touchData.touch[1].id = 2; + pData[i].touchData.touch[1].id = states[i].touchpad[1].ID; pData[i].connected = connected; pData[i].timestamp = states[i].time; pData[i].connectedCount = connected_count; @@ -376,31 +433,85 @@ int PS4_SYSV_ABI scePadReadState(s32 handle, OrbisPadData* pData) { pData->leftStick.x = state.axes[static_cast(Input::Axis::LeftX)]; pData->leftStick.y = state.axes[static_cast(Input::Axis::LeftY)]; pData->rightStick.x = state.axes[static_cast(Input::Axis::RightX)]; + pData->rightStick.x = state.axes[static_cast(Input::Axis::RightX)]; pData->rightStick.y = state.axes[static_cast(Input::Axis::RightY)]; pData->analogButtons.l2 = state.axes[static_cast(Input::Axis::TriggerLeft)]; pData->analogButtons.r2 = state.axes[static_cast(Input::Axis::TriggerRight)]; - pData->acceleration.x = state.acceleration.x; - pData->acceleration.y = state.acceleration.y; - pData->acceleration.z = state.acceleration.z; + pData->acceleration.x = state.acceleration.x * 0.098; + pData->acceleration.y = state.acceleration.y * 0.098; + pData->acceleration.z = state.acceleration.z * 0.098; pData->angularVelocity.x = state.angularVelocity.x; pData->angularVelocity.y = state.angularVelocity.y; pData->angularVelocity.z = state.angularVelocity.z; pData->orientation = {0.0f, 0.0f, 0.0f, 1.0f}; - if (engine) { + + // Only do this on handle 1 for now + if (engine && handle == 1) { const auto gyro_poll_rate = engine->GetAccelPollRate(); if (gyro_poll_rate != 0.0f) { + auto now = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration_cast( + now - controller->GetLastUpdate()) + .count() / + 1000000.0f; + controller->SetLastUpdate(now); + Libraries::Pad::OrbisFQuaternion lastOrientation = controller->GetLastOrientation(); + Libraries::Pad::OrbisFQuaternion outputOrientation = {0.0f, 0.0f, 0.0f, 1.0f}; GameController::CalculateOrientation(pData->acceleration, pData->angularVelocity, - 1.0f / gyro_poll_rate, pData->orientation); + deltaTime, lastOrientation, outputOrientation); + pData->orientation = outputOrientation; + controller->SetLastOrientation(outputOrientation); } } pData->touchData.touchNum = (state.touchpad[0].state ? 1 : 0) + (state.touchpad[1].state ? 1 : 0); + + // Only do this on handle 1 for now + if (handle == 1) { + if (controller->GetTouchCount() >= 127) { + controller->SetTouchCount(0); + } + + if (controller->GetSecondaryTouchCount() >= 127) { + controller->SetSecondaryTouchCount(0); + } + + if (pData->touchData.touchNum == 1 && controller->GetPreviousTouchNum() == 0) { + controller->SetTouchCount(controller->GetTouchCount() + 1); + controller->SetSecondaryTouchCount(controller->GetTouchCount()); + } else if (pData->touchData.touchNum == 2 && controller->GetPreviousTouchNum() == 1) { + controller->SetSecondaryTouchCount(controller->GetSecondaryTouchCount() + 1); + } else if (pData->touchData.touchNum == 0 && controller->GetPreviousTouchNum() > 0) { + if (controller->GetTouchCount() < controller->GetSecondaryTouchCount()) { + controller->SetTouchCount(controller->GetSecondaryTouchCount()); + } else { + if (controller->WasSecondaryTouchReset()) { + controller->SetTouchCount(controller->GetSecondaryTouchCount()); + controller->UnsetSecondaryTouchResetBool(); + } + } + } + + controller->SetPreviousTouchNum(pData->touchData.touchNum); + + if (pData->touchData.touchNum == 1) { + state.touchpad[0].ID = controller->GetTouchCount(); + state.touchpad[1].ID = 0; + } else if (pData->touchData.touchNum == 2) { + state.touchpad[0].ID = controller->GetTouchCount(); + state.touchpad[1].ID = controller->GetSecondaryTouchCount(); + } + } else { + state.touchpad[0].ID = 1; + state.touchpad[1].ID = 2; + } + pData->touchData.touch[0].x = state.touchpad[0].x; pData->touchData.touch[0].y = state.touchpad[0].y; - pData->touchData.touch[0].id = 1; + pData->touchData.touch[0].id = state.touchpad[0].ID; pData->touchData.touch[1].x = state.touchpad[1].x; pData->touchData.touch[1].y = state.touchpad[1].y; - pData->touchData.touch[1].id = 2; + pData->touchData.touch[1].id = state.touchpad[1].ID; pData->timestamp = state.time; pData->connected = true; // isConnected; //TODO fix me proper pData->connectedCount = 1; // connectedCount; diff --git a/src/input/controller.cpp b/src/input/controller.cpp index bb8db9a7c..42cabb837 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include @@ -165,69 +165,37 @@ void GameController::Acceleration(int id, const float acceleration[3]) { AddState(state); } -// Stolen from -// https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs -float eInt[3] = {0.0f, 0.0f, 0.0f}; // Integral error terms -const float Kp = 50.0f; // Proportional gain -const float Ki = 1.0f; // Integral gain -Libraries::Pad::OrbisFQuaternion o = {1, 0, 0, 0}; void GameController::CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, Libraries::Pad::OrbisFVector3& angularVelocity, float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, Libraries::Pad::OrbisFQuaternion& orientation) { - float ax = acceleration.x, ay = acceleration.y, az = acceleration.z; - float gx = angularVelocity.x, gy = angularVelocity.y, gz = angularVelocity.z; + Libraries::Pad::OrbisFQuaternion q = lastOrientation; + Libraries::Pad::OrbisFQuaternion ω = {angularVelocity.x, angularVelocity.y, angularVelocity.z, + 0.0f}; - float q1 = o.w, q2 = o.x, q3 = o.y, q4 = o.z; + Libraries::Pad::OrbisFQuaternion qω = {q.w * ω.x + q.x * ω.w + q.y * ω.z - q.z * ω.y, + q.w * ω.y + q.y * ω.w + q.z * ω.x - q.x * ω.z, + q.w * ω.z + q.z * ω.w + q.x * ω.y - q.y * ω.x, + q.w * ω.w - q.x * ω.x - q.y * ω.y - q.z * ω.z}; - // Normalize accelerometer measurement - float norm = std::sqrt(ax * ax + ay * ay + az * az); - if (norm == 0.0f || deltaTime == 0.0f) - return; // Handle NaN - norm = 1.0f / norm; - ax *= norm; - ay *= norm; - az *= norm; + Libraries::Pad::OrbisFQuaternion qDot = {0.5f * qω.x, 0.5f * qω.y, 0.5f * qω.z, 0.5f * qω.w}; - // Estimated direction of gravity - float vx = 2.0f * (q2 * q4 - q1 * q3); - float vy = 2.0f * (q1 * q2 + q3 * q4); - float vz = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4; + q.x += qDot.x * deltaTime; + q.y += qDot.y * deltaTime; + q.z += qDot.z * deltaTime; + q.w += qDot.w * deltaTime; - // Error is cross product between estimated direction and measured direction of gravity - float ex = (ay * vz - az * vy); - float ey = (az * vx - ax * vz); - float ez = (ax * vy - ay * vx); - if (Ki > 0.0f) { - eInt[0] += ex * deltaTime; // Accumulate integral error - eInt[1] += ey * deltaTime; - eInt[2] += ez * deltaTime; - } else { - eInt[0] = eInt[1] = eInt[2] = 0.0f; // Prevent integral wind-up - } + float norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); + q.x /= norm; + q.y /= norm; + q.z /= norm; + q.w /= norm; - // Apply feedback terms - gx += Kp * ex + Ki * eInt[0]; - gy += Kp * ey + Ki * eInt[1]; - gz += Kp * ez + Ki * eInt[2]; - - //// Integrate rate of change of quaternion - q1 += (-q2 * gx - q3 * gy - q4 * gz) * (0.5f * deltaTime); - q2 += (q1 * gx + q3 * gz - q4 * gy) * (0.5f * deltaTime); - q3 += (q1 * gy - q2 * gz + q4 * gx) * (0.5f * deltaTime); - q4 += (q1 * gz + q2 * gy - q3 * gx) * (0.5f * deltaTime); - - // Normalize quaternion - norm = std::sqrt(q1 * q1 + q2 * q2 + q3 * q3 + q4 * q4); - norm = 1.0f / norm; - orientation.w = q1 * norm; - orientation.x = q2 * norm; - orientation.y = q3 * norm; - orientation.z = q4 * norm; - o.w = q1 * norm; - o.x = q2 * norm; - o.y = q3 * norm; - o.z = q4 * norm; + orientation.x = q.x; + orientation.y = q.y; + orientation.z = q.z; + orientation.w = q.w; LOG_DEBUG(Lib_Pad, "Calculated orientation: {:.2f} {:.2f} {:.2f} {:.2f}", orientation.x, orientation.y, orientation.z, orientation.w); } @@ -260,6 +228,69 @@ void GameController::SetTouchpadState(int touchIndex, bool touchDown, float x, f } } +u8 GameController::GetTouchCount() { + std::scoped_lock lock{m_mutex}; + return m_touch_count; +} + +void GameController::SetTouchCount(u8 touchCount) { + std::scoped_lock lock{m_mutex}; + m_touch_count = touchCount; +} + +u8 GameController::GetSecondaryTouchCount() { + std::scoped_lock lock{m_mutex}; + return m_secondary_touch_count; +} + +void GameController::SetSecondaryTouchCount(u8 touchCount) { + std::scoped_lock lock{m_mutex}; + m_secondary_touch_count = touchCount; + if (touchCount == 0) { + m_was_secondary_reset = true; + } +} + +u8 GameController::GetPreviousTouchNum() { + std::scoped_lock lock{m_mutex}; + return m_previous_touchnum; +} + +void GameController::SetPreviousTouchNum(u8 touchNum) { + std::scoped_lock lock{m_mutex}; + m_previous_touchnum = touchNum; +} + +bool GameController::WasSecondaryTouchReset() { + std::scoped_lock lock{m_mutex}; + return m_was_secondary_reset; +} + +void GameController::UnsetSecondaryTouchResetBool() { + std::scoped_lock lock{m_mutex}; + m_was_secondary_reset = false; +} + +void GameController::SetLastOrientation(Libraries::Pad::OrbisFQuaternion& orientation) { + std::scoped_lock lock{m_mutex}; + m_orientation = orientation; +} + +Libraries::Pad::OrbisFQuaternion GameController::GetLastOrientation() { + std::scoped_lock lock{m_mutex}; + return m_orientation; +} + +std::chrono::steady_clock::time_point GameController::GetLastUpdate() { + std::scoped_lock lock{m_mutex}; + return m_last_update; +} + +void GameController::SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate) { + std::scoped_lock lock{m_mutex}; + m_last_update = lastUpdate; +} + void GameController::SetEngine(std::unique_ptr engine) { std::scoped_lock _{m_mutex}; m_engine = std::move(engine); diff --git a/src/input/controller.h b/src/input/controller.h index bbaed75ea..f427a55ec 100644 --- a/src/input/controller.h +++ b/src/input/controller.h @@ -23,6 +23,7 @@ enum class Axis { }; struct TouchpadEntry { + u8 ID = 0; bool state{}; u16 x{}; u16 y{}; @@ -82,9 +83,23 @@ public: Engine* GetEngine(); u32 Poll(); + u8 GetTouchCount(); + void SetTouchCount(u8 touchCount); + u8 GetSecondaryTouchCount(); + void SetSecondaryTouchCount(u8 touchCount); + u8 GetPreviousTouchNum(); + void SetPreviousTouchNum(u8 touchNum); + bool WasSecondaryTouchReset(); + void UnsetSecondaryTouchResetBool(); + + void SetLastOrientation(Libraries::Pad::OrbisFQuaternion& orientation); + Libraries::Pad::OrbisFQuaternion GetLastOrientation(); + std::chrono::steady_clock::time_point GetLastUpdate(); + void SetLastUpdate(std::chrono::steady_clock::time_point lastUpdate); static void CalculateOrientation(Libraries::Pad::OrbisFVector3& acceleration, Libraries::Pad::OrbisFVector3& angularVelocity, float deltaTime, + Libraries::Pad::OrbisFQuaternion& lastOrientation, Libraries::Pad::OrbisFQuaternion& orientation); private: @@ -98,8 +113,15 @@ private: int m_connected_count = 0; u32 m_states_num = 0; u32 m_first_state = 0; + u8 m_touch_count = 0; + u8 m_secondary_touch_count = 0; + u8 m_previous_touch_count = 0; + u8 m_previous_touchnum = 0; + bool m_was_secondary_reset = false; std::array m_states; std::array m_private; + std::chrono::steady_clock::time_point m_last_update = {}; + Libraries::Pad::OrbisFQuaternion m_orientation = {0.0f, 0.0f, 0.0f, 1.0f}; std::unique_ptr m_engine = nullptr; }; From 6bdd83684bc8f3a9bd7e960ae55eea669fc152dd Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 1 Jun 2025 11:30:03 -0500 Subject: [PATCH 081/141] Libs: libSceVoice stubs (#3022) * voice lib stubs Primarily for GTA V * Clang --- CMakeLists.txt | 2 + src/common/logging/filter.cpp | 1 + src/common/logging/types.h | 1 + src/core/libraries/libs.cpp | 2 + src/core/libraries/voice/voice.cpp | 203 +++++++++++++++++++++++++++++ src/core/libraries/voice/voice.h | 56 ++++++++ 6 files changed, 265 insertions(+) create mode 100644 src/core/libraries/voice/voice.cpp create mode 100644 src/core/libraries/voice/voice.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f85d22b6c..20d33ac95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,6 +296,8 @@ set(AJM_LIB src/core/libraries/ajm/ajm.cpp set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioin.h + src/core/libraries/voice/voice.cpp + src/core/libraries/voice/voice.h src/core/libraries/audio/audioout.cpp src/core/libraries/audio/audioout.h src/core/libraries/audio/audioout_backend.h diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index fd0614e1b..05935fbdc 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -141,6 +141,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { SUB(Lib, Camera) \ SUB(Lib, CompanionHttpd) \ SUB(Lib, CompanionUtil) \ + SUB(Lib, Voice) \ CLS(Frontend) \ CLS(Render) \ SUB(Render, Vulkan) \ diff --git a/src/common/logging/types.h b/src/common/logging/types.h index bbfd7455b..1da84b219 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -98,6 +98,7 @@ enum class Class : u8 { Lib_Fiber, ///< The LibSceFiber implementation. Lib_Vdec2, ///< The LibSceVideodec2 implementation. Lib_Videodec, ///< The LibSceVideodec implementation. + Lib_Voice, ///< The LibSceVoice implementation. Lib_RazorCpu, ///< The LibRazorCpu implementation. Lib_Mouse, ///< The LibSceMouse implementation Lib_WebBrowserDialog, ///< The LibSceWebBrowserDialog implementation diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 45b32846f..762c1e762 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -60,6 +60,7 @@ #include "core/libraries/videodec/videodec.h" #include "core/libraries/videodec/videodec2.h" #include "core/libraries/videoout/video_out.h" +#include "core/libraries/voice/voice.h" #include "core/libraries/web_browser_dialog/webbrowserdialog.h" #include "core/libraries/zlib/zlib_sce.h" #include "fiber/fiber.h" @@ -128,6 +129,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) { Libraries::Camera::RegisterlibSceCamera(sym); Libraries::CompanionHttpd::RegisterlibSceCompanionHttpd(sym); Libraries::CompanionUtil::RegisterlibSceCompanionUtil(sym); + Libraries::Voice::RegisterlibSceVoice(sym); } } // namespace Libraries diff --git a/src/core/libraries/voice/voice.cpp b/src/core/libraries/voice/voice.cpp new file mode 100644 index 000000000..caa16431a --- /dev/null +++ b/src/core/libraries/voice/voice.cpp @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/voice/voice.h" + +namespace Libraries::Voice { + +s32 PS4_SYSV_ABI sceVoiceConnectIPortToOPort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceCreatePort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceDeletePort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceDisconnectIPortFromOPort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceEnd() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetBitRate(u32 port_id, u32* bitrate) { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + *bitrate = 48000; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetMuteFlag() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetPortAttr() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetPortInfo(u32 port_id, OrbisVoicePortInfo* info) { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + info->port_type = 0; + info->state = 3; + info->byte_count = 0; + info->frame_size = 1; + info->edge_count = 0; + info->reserved = 0; + + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetResourceInfo() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceGetVolume() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceInit() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceInitHQ() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoicePausePort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoicePausePortAll() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceReadFromOPort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceResetPort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceResumePort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceResumePortAll() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceSetBitRate() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceSetMuteFlag() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceSetMuteFlagAll() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceSetThreadsParams() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceSetVolume() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceStart() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceStop() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceUpdatePort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceVADAdjustment() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceVADSetVersion() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceVoiceWriteToIPort() { + LOG_ERROR(Lib_Voice, "(STUBBED) called"); + return ORBIS_OK; +} + +void RegisterlibSceVoice(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("oV9GAdJ23Gw", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceConnectIPortToOPort); + LIB_FUNCTION("nXpje5yNpaE", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceCreatePort); + LIB_FUNCTION("b7kJI+nx2hg", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceDeletePort); + LIB_FUNCTION("ajVj3QG2um4", "libSceVoice", 1, "libSceVoice", 0, 0, + sceVoiceDisconnectIPortFromOPort); + LIB_FUNCTION("Oo0S5PH7FIQ", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceEnd); + LIB_FUNCTION("cJLufzou6bc", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetBitRate); + LIB_FUNCTION("Pc4z1QjForU", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetMuteFlag); + LIB_FUNCTION("elcxZTEfHZM", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetPortAttr); + LIB_FUNCTION("CrLqDwWLoXM", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetPortInfo); + LIB_FUNCTION("Z6QV6j7igvE", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetResourceInfo); + LIB_FUNCTION("jjkCjneOYSs", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceGetVolume); + LIB_FUNCTION("9TrhuGzberQ", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceInit); + LIB_FUNCTION("IPHvnM5+g04", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceInitHQ); + LIB_FUNCTION("x0slGBQW+wY", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoicePausePort); + LIB_FUNCTION("Dinob0yMRl8", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoicePausePortAll); + LIB_FUNCTION("cQ6DGsQEjV4", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceReadFromOPort); + LIB_FUNCTION("udAxvCePkUs", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceResetPort); + LIB_FUNCTION("gAgN+HkiEzY", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceResumePort); + LIB_FUNCTION("jbkJFmOZ9U0", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceResumePortAll); + LIB_FUNCTION("TexwmOHQsDg", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceSetBitRate); + LIB_FUNCTION("gwUynkEgNFY", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceSetMuteFlag); + LIB_FUNCTION("oUha0S-Ij9Q", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceSetMuteFlagAll); + LIB_FUNCTION("clyKUyi3RYU", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceSetThreadsParams); + LIB_FUNCTION("QBFoAIjJoXQ", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceSetVolume); + LIB_FUNCTION("54phPH2LZls", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceStart); + LIB_FUNCTION("Ao2YNSA7-Qo", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceStop); + LIB_FUNCTION("jSZNP7xJrcw", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceUpdatePort); + LIB_FUNCTION("hg9T73LlRiU", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceVADAdjustment); + LIB_FUNCTION("wFeAxEeEi-8", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceVADSetVersion); + LIB_FUNCTION("YeJl6yDlhW0", "libSceVoice", 1, "libSceVoice", 0, 0, sceVoiceWriteToIPort); +}; + +} // namespace Libraries::Voice \ No newline at end of file diff --git a/src/core/libraries/voice/voice.h b/src/core/libraries/voice/voice.h new file mode 100644 index 000000000..8f008f2cc --- /dev/null +++ b/src/core/libraries/voice/voice.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Voice { + +struct OrbisVoicePortInfo { + s32 port_type; + s32 state; + u32* edge; + u32 byte_count; + u32 frame_size; + u16 edge_count; + u16 reserved; +}; + +s32 PS4_SYSV_ABI sceVoiceConnectIPortToOPort(); +s32 PS4_SYSV_ABI sceVoiceCreatePort(); +s32 PS4_SYSV_ABI sceVoiceDeletePort(); +s32 PS4_SYSV_ABI sceVoiceDisconnectIPortFromOPort(); +s32 PS4_SYSV_ABI sceVoiceEnd(); +s32 PS4_SYSV_ABI sceVoiceGetBitRate(u32 port_id, u32* bitrate); +s32 PS4_SYSV_ABI sceVoiceGetMuteFlag(); +s32 PS4_SYSV_ABI sceVoiceGetPortAttr(); +s32 PS4_SYSV_ABI sceVoiceGetPortInfo(u32 port_id, OrbisVoicePortInfo* info); +s32 PS4_SYSV_ABI sceVoiceGetResourceInfo(); +s32 PS4_SYSV_ABI sceVoiceGetVolume(); +s32 PS4_SYSV_ABI sceVoiceInit(); +s32 PS4_SYSV_ABI sceVoiceInitHQ(); +s32 PS4_SYSV_ABI sceVoicePausePort(); +s32 PS4_SYSV_ABI sceVoicePausePortAll(); +s32 PS4_SYSV_ABI sceVoiceReadFromOPort(); +s32 PS4_SYSV_ABI sceVoiceResetPort(); +s32 PS4_SYSV_ABI sceVoiceResumePort(); +s32 PS4_SYSV_ABI sceVoiceResumePortAll(); +s32 PS4_SYSV_ABI sceVoiceSetBitRate(); +s32 PS4_SYSV_ABI sceVoiceSetMuteFlag(); +s32 PS4_SYSV_ABI sceVoiceSetMuteFlagAll(); +s32 PS4_SYSV_ABI sceVoiceSetThreadsParams(); +s32 PS4_SYSV_ABI sceVoiceSetVolume(); +s32 PS4_SYSV_ABI sceVoiceStart(); +s32 PS4_SYSV_ABI sceVoiceStop(); +s32 PS4_SYSV_ABI sceVoiceUpdatePort(); +s32 PS4_SYSV_ABI sceVoiceVADAdjustment(); +s32 PS4_SYSV_ABI sceVoiceVADSetVersion(); +s32 PS4_SYSV_ABI sceVoiceWriteToIPort(); + +void RegisterlibSceVoice(Core::Loader::SymbolsResolver* sym); +} // namespace Libraries::Voice \ No newline at end of file From 1930a2132c8c5b61eb72b0646f6712716c900b3f Mon Sep 17 00:00:00 2001 From: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> Date: Mon, 2 Jun 2025 18:02:37 +0200 Subject: [PATCH 082/141] Fix SSH remote links (#3025) --- src/common/scm_rev.cpp.in | 23 +++++++++++++++++++++++ src/common/scm_rev.h | 4 ++++ src/emulator.cpp | 10 +--------- src/qt_gui/main_window.cpp | 10 +--------- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index 71c4c2d0a..0b113eb31 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "common/scm_rev.h" namespace Common { @@ -15,5 +17,26 @@ constexpr char g_scm_remote_name[] = "@GIT_REMOTE_NAME@"; constexpr char g_scm_remote_url[] = "@GIT_REMOTE_URL@"; constexpr char g_scm_date[] = "@BUILD_DATE@"; +const std::string GetRemoteNameFromLink() { + std::string remote_url(Common::g_scm_remote_url); + std::string remote_host; + try { + if (remote_url.starts_with("http")) { + if (*remote_url.rbegin() == '/') { + remote_url.pop_back(); + } + remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); + } else if (remote_url.starts_with("git@")) { + auto after_comma_pos = remote_url.find(':') + 1, slash_pos = remote_url.find('/'); + remote_host = remote_url.substr(after_comma_pos, slash_pos - after_comma_pos); + } else { + remote_host = "unknown"; + } + } catch (...) { + remote_host = "unknown"; + } + return remote_host; +} + } // namespace diff --git a/src/common/scm_rev.h b/src/common/scm_rev.h index 36b844e94..2f6d770bb 100644 --- a/src/common/scm_rev.h +++ b/src/common/scm_rev.h @@ -3,6 +3,8 @@ #pragma once +#include + namespace Common { extern const char g_version[]; @@ -15,4 +17,6 @@ extern const char g_scm_remote_name[]; extern const char g_scm_remote_url[]; extern const char g_scm_date[]; +const std::string GetRemoteNameFromLink(); + } // namespace Common diff --git a/src/emulator.cpp b/src/emulator.cpp index 2ad8446ab..4fcb0cfc2 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -194,15 +194,7 @@ void Emulator::Run(const std::filesystem::path& file, const std::vector", id, title, app_version); std::string window_title = ""; std::string remote_url(Common::g_scm_remote_url); - std::string remote_host; - try { - if (*remote_url.rbegin() == '/') { - remote_url.pop_back(); - } - remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); - } catch (...) { - remote_host = "unknown"; - } + std::string remote_host = Common::GetRemoteNameFromLink(); if (Common::g_is_release) { if (remote_host == "shadps4-emu" || remote_url.length() == 0) { window_title = fmt::format("shadPS4 v{} | {}", Common::g_version, game_title); diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 77b088b78..0f6773751 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -56,15 +56,7 @@ bool MainWindow::Init() { setMinimumSize(720, 405); std::string window_title = ""; std::string remote_url(Common::g_scm_remote_url); - std::string remote_host; - try { - if (*remote_url.rbegin() == '/') { - remote_url.pop_back(); - } - remote_host = remote_url.substr(19, remote_url.rfind('/') - 19); - } catch (...) { - remote_host = "unknown"; - } + std::string remote_host = Common::GetRemoteNameFromLink(); if (Common::g_is_release) { if (remote_host == "shadps4-emu" || remote_url.length() == 0) { window_title = fmt::format("shadPS4 v{}", Common::g_version); From 2c782721851ba4bb66ad5964944c4f6ee6f990f7 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:02:51 -0500 Subject: [PATCH 083/141] Emulate libSceGnmDriver's init behavior (#3024) This time, perform it after the LoadSharedLibraries call, which places it after the various init memory mappings GFD engine titles perform. --- src/core/linker.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/core/linker.cpp b/src/core/linker.cpp index 3e6d8c22e..c50b03a8f 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -117,6 +117,18 @@ void Linker::Execute(const std::vector args) { Common::SetCurrentThreadName("GAME_MainThread"); LoadSharedLibraries(); + // Simulate libSceGnmDriver initialization, which maps a chunk of direct memory. + // Some games fail without accurately emulating this behavior. + s64 phys_addr{}; + s32 result = Libraries::Kernel::sceKernelAllocateDirectMemory( + 0, Libraries::Kernel::sceKernelGetDirectMemorySize(), 0x10000, 0x10000, 3, &phys_addr); + if (result == 0) { + void* addr{reinterpret_cast(0xfe0000000)}; + result = Libraries::Kernel::sceKernelMapNamedDirectMemory( + &addr, 0x10000, 0x13, 0, phys_addr, 0x10000, "SceGnmDriver"); + } + ASSERT_MSG(result == 0, "Unable to emulate libSceGnmDriver initialization"); + // Start main module. EntryParams params{}; params.argc = 1; From eed99141b3170b6417c9403df74bbb3e0920d915 Mon Sep 17 00:00:00 2001 From: mailwl Date: Tue, 3 Jun 2025 09:11:18 +0300 Subject: [PATCH 084/141] Network Play: set user signed in (#2944) * Network Play: set user signed in * get signedIn status from config --------- Co-authored-by: georgemoralis --- src/common/config.cpp | 14 +++++++- src/common/config.h | 4 ++- src/core/libraries/np_manager/np_manager.cpp | 36 ++++++++++++++------ src/core/libraries/np_manager/np_manager.h | 9 ++++- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/common/config.cpp b/src/common/config.cpp index 6bccd0f37..6565ab82a 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -69,7 +69,7 @@ static bool vkGuestMarkers = false; static bool rdocEnable = false; static bool isFpsColor = true; static bool isSeparateLogFilesEnabled = false; -static s16 cursorState = HideCursorState::Idle; +static int cursorState = HideCursorState::Idle; static int cursorHideTimeout = 5; // 5 seconds (default) static double trophyNotificationDuration = 6.0; static bool useUnifiedInputConfig = true; @@ -78,6 +78,7 @@ static int controllerCustomColorRGB[3] = {0, 0, 255}; static bool compatibilityData = false; static bool checkCompatibilityOnStartup = false; static std::string trophyKey; +static bool isPSNSignedIn = false; // Gui static bool load_game_size = true; @@ -730,6 +731,14 @@ void setShowBackgroundImage(bool show) { showBackgroundImage = show; } +bool getPSNSignedIn() { + return isPSNSignedIn; +} + +void setPSNSignedIn(bool sign) { + isPSNSignedIn = sign; +} + void load(const std::filesystem::path& path) { // If the configuration file does not exist, create it and return std::error_code error; @@ -754,6 +763,7 @@ void load(const std::filesystem::path& path) { isNeo = toml::find_or(general, "isPS4Pro", false); isDevKit = toml::find_or(general, "isDevKit", false); + isPSNSignedIn = toml::find_or(general, "isPSNSignedIn", false); playBGM = toml::find_or(general, "playBGM", false); isTrophyPopupDisabled = toml::find_or(general, "isTrophyPopupDisabled", false); trophyNotificationDuration = @@ -953,6 +963,7 @@ void save(const std::filesystem::path& path) { data["General"]["isPS4Pro"] = isNeo; data["General"]["isDevKit"] = isDevKit; + data["General"]["isPSNSignedIn"] = isPSNSignedIn; data["General"]["isTrophyPopupDisabled"] = isTrophyPopupDisabled; data["General"]["trophyNotificationDuration"] = trophyNotificationDuration; data["General"]["playBGM"] = playBGM; @@ -1098,6 +1109,7 @@ void setDefaultValues() { isHDRAllowed = false; isNeo = false; isDevKit = false; + isPSNSignedIn = false; isFullscreen = false; isTrophyPopupDisabled = false; playBGM = false; diff --git a/src/common/config.h b/src/common/config.h index aba23621c..404854ae2 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -14,7 +14,7 @@ struct GameInstallDir { bool enabled; }; -enum HideCursorState : s16 { Never, Idle, Always }; +enum HideCursorState : int { Never, Idle, Always }; void load(const std::filesystem::path& path); void save(const std::filesystem::path& path); @@ -39,6 +39,7 @@ bool getCompatibilityEnabled(); bool getCheckCompatibilityOnStartup(); int getBackgroundImageOpacity(); bool getShowBackgroundImage(); +bool getPSNSignedIn(); std::string getLogFilter(); std::string getLogType(); @@ -111,6 +112,7 @@ void setCompatibilityEnabled(bool use); void setCheckCompatibilityOnStartup(bool use); void setBackgroundImageOpacity(int opacity); void setShowBackgroundImage(bool show); +void setPSNSignedIn(bool sign); void setCursorState(s16 cursorState); void setCursorHideTimeout(int newcursorHideTimeout); diff --git a/src/core/libraries/np_manager/np_manager.cpp b/src/core/libraries/np_manager/np_manager.cpp index a60dcd86f..bc920b5a9 100644 --- a/src/core/libraries/np_manager/np_manager.cpp +++ b/src/core/libraries/np_manager/np_manager.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/config.h" #include "common/logging/log.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" @@ -10,6 +11,8 @@ namespace Libraries::NpManager { +#define SIGNEDIN_STATUS (Config::getPSNSignedIn() ? ORBIS_OK : ORBIS_NP_ERROR_SIGNED_OUT) + int PS4_SYSV_ABI Func_EF4378573542A508() { LOG_ERROR(Lib_NpManager, "(STUBBED) called"); return ORBIS_OK; @@ -921,9 +924,16 @@ int PS4_SYSV_ABI sceNpGetAccountCountry() { return ORBIS_OK; } -int PS4_SYSV_ABI sceNpGetAccountCountryA() { - LOG_ERROR(Lib_NpManager, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceNpGetAccountCountryA(OrbisUserServiceUserId user_id, + OrbisNpCountryCode* country_code) { + LOG_INFO(Lib_NpManager, "(STUBBED) called, user_id = {}", user_id); + if (country_code == nullptr) { + return ORBIS_NP_ERROR_INVALID_ARGUMENT; + } + ::memset(country_code, 0, sizeof(OrbisNpCountryCode)); + // TODO: get NP country code from config + ::memcpy(country_code->country_code, "us", 2); + return SIGNEDIN_STATUS; } int PS4_SYSV_ABI sceNpGetAccountDateOfBirth() { @@ -941,8 +951,8 @@ int PS4_SYSV_ABI sceNpGetAccountId(OrbisNpOnlineId* online_id, u64* account_id) if (online_id == nullptr || account_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - *account_id = 0; - return ORBIS_NP_ERROR_SIGNED_OUT; + *account_id = 0xFEEDFACE; + return SIGNEDIN_STATUS; } int PS4_SYSV_ABI sceNpGetAccountIdA(OrbisUserServiceUserId user_id, u64* account_id) { @@ -950,8 +960,8 @@ int PS4_SYSV_ABI sceNpGetAccountIdA(OrbisUserServiceUserId user_id, u64* account if (account_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - *account_id = 0; - return ORBIS_NP_ERROR_SIGNED_OUT; + *account_id = 0xFEEDFACE; + return SIGNEDIN_STATUS; } int PS4_SYSV_ABI sceNpGetAccountLanguage() { @@ -984,7 +994,9 @@ int PS4_SYSV_ABI sceNpGetNpId(OrbisUserServiceUserId user_id, OrbisNpId* np_id) if (np_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - return ORBIS_NP_ERROR_SIGNED_OUT; + memset(np_id, 0, sizeof(OrbisNpId)); + strncpy(np_id->handle.data, Config::getUserName().c_str(), sizeof(np_id->handle.data)); + return SIGNEDIN_STATUS; } int PS4_SYSV_ABI sceNpGetNpReachabilityState() { @@ -997,7 +1009,9 @@ int PS4_SYSV_ABI sceNpGetOnlineId(OrbisUserServiceUserId user_id, OrbisNpOnlineI if (online_id == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - return ORBIS_NP_ERROR_SIGNED_OUT; + memset(online_id, 0, sizeof(OrbisNpOnlineId)); + strncpy(online_id->data, Config::getUserName().c_str(), sizeof(online_id->data)); + return SIGNEDIN_STATUS; } int PS4_SYSV_ABI sceNpGetParentalControlInfo() { @@ -1014,8 +1028,8 @@ int PS4_SYSV_ABI sceNpGetState(OrbisUserServiceUserId user_id, OrbisNpState* sta if (state == nullptr) { return ORBIS_NP_ERROR_INVALID_ARGUMENT; } - *state = OrbisNpState::SignedOut; - LOG_DEBUG(Lib_NpManager, "Signed out"); + *state = Config::getPSNSignedIn() ? OrbisNpState::SignedIn : OrbisNpState::SignedOut; + LOG_DEBUG(Lib_NpManager, "Signed {}", Config::getPSNSignedIn() ? "in" : "out"); return ORBIS_OK; } diff --git a/src/core/libraries/np_manager/np_manager.h b/src/core/libraries/np_manager/np_manager.h index 02a1a32f6..1078a9f3e 100644 --- a/src/core/libraries/np_manager/np_manager.h +++ b/src/core/libraries/np_manager/np_manager.h @@ -32,6 +32,12 @@ struct OrbisNpId { u8 reserved[8]; }; +struct OrbisNpCountryCode { + char country_code[2]; + char end; + char pad; +}; + int PS4_SYSV_ABI Func_EF4378573542A508(); int PS4_SYSV_ABI _sceNpIpcCreateMemoryFromKernel(); int PS4_SYSV_ABI _sceNpIpcCreateMemoryFromPool(); @@ -215,7 +221,8 @@ int PS4_SYSV_ABI sceNpCreateRequest(); int PS4_SYSV_ABI sceNpDeleteRequest(int reqId); int PS4_SYSV_ABI sceNpGetAccountAge(); int PS4_SYSV_ABI sceNpGetAccountCountry(); -int PS4_SYSV_ABI sceNpGetAccountCountryA(); +int PS4_SYSV_ABI sceNpGetAccountCountryA(OrbisUserServiceUserId user_id, + OrbisNpCountryCode* country_code); int PS4_SYSV_ABI sceNpGetAccountDateOfBirth(); int PS4_SYSV_ABI sceNpGetAccountDateOfBirthA(); int PS4_SYSV_ABI sceNpGetAccountId(OrbisNpOnlineId* online_id, u64* account_id); From bb3f8af81ad6b4c60cd8d1d33c18e7472759fe57 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 3 Jun 2025 01:29:25 -0500 Subject: [PATCH 085/141] Core: Protect fixes (#3029) * Swap do-while to while If we use a do-while loop, we waste time if `aligned_size = 0`. This is also still accurate to FreeBSD behavior, where it returns success if `start == end` during mprotect. This also effectively prevents the memory assert seen in updated versions of RESIDENT EVIL 2 (CUSA09193) * Move prot validation outside loop The prot variable shouldn't change during a mprotect call, so we can check the flags before protecting instead. Also cleans up the code for prot validation. This should improve performance, and is more accurate to FreeBSD code. * Add logging for protect calls This will help in debugging future problems --- src/core/libraries/kernel/memory.cpp | 4 ++++ src/core/memory.cpp | 32 +++++++++++++++------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index ce694dc1e..18676cbdf 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -264,6 +264,8 @@ int PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** } s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot) { + LOG_INFO(Kernel_Vmm, "called addr = {}, size = {:#x}, prot = {:#x}", fmt::ptr(addr), size, + prot); Core::MemoryManager* memory_manager = Core::Memory::Instance(); Core::MemoryProt protection_flags = static_cast(prot); return memory_manager->Protect(std::bit_cast(addr), size, protection_flags); @@ -279,6 +281,8 @@ s32 PS4_SYSV_ABI posix_mprotect(const void* addr, u64 size, s32 prot) { } s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s32 prot) { + LOG_INFO(Kernel_Vmm, "called addr = {}, size = {:#x}, prot = {:#x}", fmt::ptr(addr), size, + prot); Core::MemoryManager* memory_manager = Core::Memory::Instance(); Core::MemoryProt protection_flags = static_cast(prot); return memory_manager->Protect(std::bit_cast(addr), size, protection_flags); diff --git a/src/core/memory.cpp b/src/core/memory.cpp index bf9d1cabd..8550ece17 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -557,18 +557,6 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s return adjusted_size; } - // Validate protection flags - constexpr static MemoryProt valid_flags = MemoryProt::NoAccess | MemoryProt::CpuRead | - MemoryProt::CpuReadWrite | MemoryProt::GpuRead | - MemoryProt::GpuWrite | MemoryProt::GpuReadWrite; - - MemoryProt invalid_flags = prot & ~valid_flags; - if (u32(invalid_flags) != 0 && u32(invalid_flags) != u32(MemoryProt::NoAccess)) { - LOG_ERROR(Kernel_Vmm, "Invalid protection flags: prot = {:#x}, invalid flags = {:#x}", - u32(prot), u32(invalid_flags)); - return ORBIS_KERNEL_ERROR_EINVAL; - } - // Change protection vma_base.prot = prot; @@ -598,11 +586,25 @@ s64 MemoryManager::ProtectBytes(VAddr addr, VirtualMemoryArea vma_base, size_t s s32 MemoryManager::Protect(VAddr addr, size_t size, MemoryProt prot) { std::scoped_lock lk{mutex}; - s64 protected_bytes = 0; + // Validate protection flags + constexpr static MemoryProt valid_flags = MemoryProt::NoAccess | MemoryProt::CpuRead | + MemoryProt::CpuReadWrite | MemoryProt::GpuRead | + MemoryProt::GpuWrite | MemoryProt::GpuReadWrite; + + MemoryProt invalid_flags = prot & ~valid_flags; + if (invalid_flags != MemoryProt::NoAccess) { + LOG_ERROR(Kernel_Vmm, "Invalid protection flags"); + return ORBIS_KERNEL_ERROR_EINVAL; + } + + // Align addr and size to the nearest page boundary. auto aligned_addr = Common::AlignDown(addr, 16_KB); auto aligned_size = Common::AlignUp(size + addr - aligned_addr, 16_KB); - do { + + // Protect all VMAs between aligned_addr and aligned_addr + aligned_size. + s64 protected_bytes = 0; + while (protected_bytes < aligned_size) { auto it = FindVMA(aligned_addr + protected_bytes); auto& vma_base = it->second; ASSERT_MSG(vma_base.Contains(addr + protected_bytes, 0), "Address {:#x} is out of bounds", @@ -615,7 +617,7 @@ s32 MemoryManager::Protect(VAddr addr, size_t size, MemoryProt prot) { return result; } protected_bytes += result; - } while (protected_bytes < aligned_size); + } return ORBIS_OK; } From ca0f458505e42bcab4b2c277b8abd7d302f760be Mon Sep 17 00:00:00 2001 From: Schweeeeeeeeeeeeeeee <88291316+Schweeeeeeeeeeeeeeee@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:40:31 +0200 Subject: [PATCH 086/141] Add missing dependency for Fedora (#3005) Propablly missing because of fedora 42? --- documents/building-linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documents/building-linux.md b/documents/building-linux.md index cdc8ba12f..bd07b2eff 100644 --- a/documents/building-linux.md +++ b/documents/building-linux.md @@ -29,7 +29,7 @@ sudo dnf install clang git cmake libatomic alsa-lib-devel \ openssl-devel libevdev-devel libudev-devel libXext-devel \ qt6-qtbase-devel qt6-qtbase-private-devel \ qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel \ - vulkan-devel vulkan-validation-layers libpng-devel + vulkan-devel vulkan-validation-layers libpng-devel libuuid-devel ``` #### Arch Linux From c09d1c3cffa8591db056347c05063a9cd2b6c84e Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Tue, 3 Jun 2025 12:34:29 +0200 Subject: [PATCH 087/141] Add support for game folder and fail early if eboot.bin is missing or corrupt (#3027) --- src/emulator.cpp | 19 +++++++++++++++++-- src/emulator.h | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/emulator.cpp b/src/emulator.cpp index 4fcb0cfc2..bb50b8686 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include @@ -62,8 +63,13 @@ Emulator::~Emulator() { Config::saveMainWindow(config_dir / "config.toml"); } -void Emulator::Run(const std::filesystem::path& file, const std::vector args) { +void Emulator::Run(std::filesystem::path file, const std::vector args) { + if (std::filesystem::is_directory(file)) { + file /= "eboot.bin"; + } + const auto eboot_name = file.filename().string(); + auto game_folder = file.parent_path(); if (const auto game_folder_name = game_folder.filename().string(); game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch")) { @@ -114,6 +120,11 @@ void Emulator::Run(const std::filesystem::path& file, const std::vectorGetHostPath("/app0/" + eboot_name); - linker->LoadModule(eboot_path); + if (linker->LoadModule(eboot_path) == -1) { + LOG_CRITICAL(Loader, "Failed to load game's eboot.bin: {}", + std::filesystem::absolute(eboot_path).string()); + std::quick_exit(0); + } // check if we have system modules to load LoadSystemModules(game_info.game_serial); diff --git a/src/emulator.h b/src/emulator.h index 08c2807a1..257ccd694 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -25,7 +25,7 @@ public: Emulator(); ~Emulator(); - void Run(const std::filesystem::path& file, const std::vector args = {}); + void Run(std::filesystem::path file, const std::vector args = {}); void UpdatePlayTime(const std::string& serial); private: From 8cdd8dd725ae5c5b383ead35ad3eff59b3b55d09 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 3 Jun 2025 05:43:56 -0500 Subject: [PATCH 088/141] Core: Pthread affinity fixups (#3021) * posix_pthread_attr_getschedparam * Fixes for scePthreadGetAffinity and scePthreadSetAffinity Looking at FreeBSD source, and our other pthread functions, we should be using our FindThread function to get the appropriate thread if thread != g_curthread. --- src/core/libraries/kernel/threads/pthread.cpp | 31 ++++++++++++++++--- .../libraries/kernel/threads/pthread_attr.cpp | 2 ++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/kernel/threads/pthread.cpp b/src/core/libraries/kernel/threads/pthread.cpp index 61310bfb5..59b427d22 100644 --- a/src/core/libraries/kernel/threads/pthread.cpp +++ b/src/core/libraries/kernel/threads/pthread.cpp @@ -576,8 +576,19 @@ int PS4_SYSV_ABI posix_pthread_getaffinity_np(PthreadT thread, size_t cpusetsize if (thread == nullptr || cpusetp == nullptr) { return POSIX_EINVAL; } + + auto* thread_state = ThrState::Instance(); + if (thread == g_curthread) { + g_curthread->lock.lock(); + } else if (auto ret = thread_state->FindThread(thread, /*include dead*/ 0); ret != 0) { + return ret; + } + auto* attr_ptr = &thread->attr; - return posix_pthread_attr_getaffinity_np(&attr_ptr, cpusetsize, cpusetp); + auto ret = posix_pthread_attr_getaffinity_np(&attr_ptr, cpusetsize, cpusetp); + + thread->lock.unlock(); + return ret; } int PS4_SYSV_ABI posix_pthread_setaffinity_np(PthreadT thread, size_t cpusetsize, @@ -585,11 +596,23 @@ int PS4_SYSV_ABI posix_pthread_setaffinity_np(PthreadT thread, size_t cpusetsize if (thread == nullptr || cpusetp == nullptr) { return POSIX_EINVAL; } - auto* attr_ptr = &thread->attr; - if (const auto ret = posix_pthread_attr_setaffinity_np(&attr_ptr, cpusetsize, cpusetp)) { + + auto* thread_state = ThrState::Instance(); + if (thread == g_curthread) { + g_curthread->lock.lock(); + } else if (auto ret = thread_state->FindThread(thread, /*include dead*/ 0); ret != 0) { return ret; } - return thread->SetAffinity(thread->attr.cpuset); + + auto* attr_ptr = &thread->attr; + auto ret = posix_pthread_attr_setaffinity_np(&attr_ptr, cpusetsize, cpusetp); + + if (ret == ORBIS_OK) { + ret = thread->SetAffinity(thread->attr.cpuset); + } + + thread->lock.unlock(); + return ret; } int PS4_SYSV_ABI scePthreadGetaffinity(PthreadT thread, u64* mask) { diff --git a/src/core/libraries/kernel/threads/pthread_attr.cpp b/src/core/libraries/kernel/threads/pthread_attr.cpp index 71f6438a6..e098b00a4 100644 --- a/src/core/libraries/kernel/threads/pthread_attr.cpp +++ b/src/core/libraries/kernel/threads/pthread_attr.cpp @@ -306,6 +306,8 @@ void RegisterThreadAttr(Core::Loader::SymbolsResolver* sym) { posix_pthread_attr_getdetachstate); LIB_FUNCTION("JKyG3SWyA10", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_attr_setguardsize); + LIB_FUNCTION("qlk9pSLsUmM", "libScePosix", 1, "libkernel", 1, 1, + posix_pthread_attr_getschedparam); // Orbis LIB_FUNCTION("4+h9EzwKF4I", "libkernel", 1, "libkernel", 1, 1, From 5b6fc788b31bfa3ed6e3f81770b2751f87d31fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Tue, 3 Jun 2025 20:42:20 +0200 Subject: [PATCH 089/141] Fix passing user data in user-triggered equeue events (#2948) --- src/core/libraries/kernel/equeue.cpp | 2 ++ src/core/libraries/kernel/equeue.h | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 263cf24b8..911ae4cd5 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -145,6 +145,8 @@ bool EqueueInternal::TriggerEvent(u64 ident, s16 filter, void* trigger_data) { if (event.event.ident == ident && event.event.filter == filter) { if (filter == SceKernelEvent::Filter::VideoOut) { event.TriggerDisplay(trigger_data); + } else if (filter == SceKernelEvent::Filter::User) { + event.TriggerUser(trigger_data); } else { event.Trigger(trigger_data); } diff --git a/src/core/libraries/kernel/equeue.h b/src/core/libraries/kernel/equeue.h index a0367c66a..e6e3c0c53 100644 --- a/src/core/libraries/kernel/equeue.h +++ b/src/core/libraries/kernel/equeue.h @@ -98,6 +98,12 @@ struct EqueueEvent { event.data = reinterpret_cast(data); } + void TriggerUser(void* data) { + is_triggered = true; + event.fflags++; + event.udata = data; + } + void TriggerDisplay(void* data) { is_triggered = true; if (data != nullptr) { From 88f6cb4d4197748c2dde637e3c9340823193228c Mon Sep 17 00:00:00 2001 From: Dmugetsu <168934208+diegolix29@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:26:04 -0600 Subject: [PATCH 090/141] clean up main window from extraction remains (#3035) --- src/qt_gui/main_window.cpp | 1 - src/qt_gui/main_window.h | 1 - 2 files changed, 2 deletions(-) diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 0f6773751..906a3066e 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -372,7 +372,6 @@ void MainWindow::CreateConnects() { connect(ui->refreshGameListAct, &QAction::triggered, this, &MainWindow::RefreshGameTable); connect(ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshGameTable); connect(ui->showGameListAct, &QAction::triggered, this, &MainWindow::ShowGameList); - connect(this, &MainWindow::ExtractionFinished, this, &MainWindow::RefreshGameTable); connect(ui->toggleLabelsAct, &QAction::toggled, this, &MainWindow::toggleLabelsUnderIcons); connect(ui->fullscreenButton, &QPushButton::clicked, this, &MainWindow::toggleFullscreen); diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index a5ec08d36..97c56433d 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -29,7 +29,6 @@ class MainWindow : public QMainWindow { Q_OBJECT signals: void WindowResized(QResizeEvent* event); - void ExtractionFinished(); public: explicit MainWindow(QWidget* parent = nullptr); From 23710f397eae17724777cc3dfa5333163c552f89 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 4 Jun 2025 00:17:35 -0500 Subject: [PATCH 091/141] Reduce clamp threshold to 2MB (#3034) A proposed solution to the non-GPU memory asserts seen in RESIDENT EVIL 2 (CUSA09193) --- src/core/memory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 8550ece17..ba3640877 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -68,7 +68,7 @@ void MemoryManager::SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1 } u64 MemoryManager::ClampRangeSize(VAddr virtual_addr, u64 size) { - static constexpr u64 MinSizeToClamp = 512_MB; + static constexpr u64 MinSizeToClamp = 2_MB; // Dont bother with clamping if the size is small so we dont pay a map lookup on every buffer. if (size < MinSizeToClamp) { return size; From 4d1a1ce9c2aae933a3d4e9a0a07e55eb5e5875e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 5 Jun 2025 01:55:47 +0200 Subject: [PATCH 092/141] v_rcp_legacy_f32 (#3040) --- .../frontend/translate/translate.h | 1 + .../frontend/translate/vector_alu.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 7b4b03f27..2584d5c5e 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -204,6 +204,7 @@ public: void V_EXP_F32(const GcnInst& inst); void V_LOG_F32(const GcnInst& inst); void V_RCP_F32(const GcnInst& inst); + void V_RCP_LEGACY_F32(const GcnInst& inst); void V_RCP_F64(const GcnInst& inst); void V_RSQ_F32(const GcnInst& inst); void V_SQRT_F32(const GcnInst& inst); diff --git a/src/shader_recompiler/frontend/translate/vector_alu.cpp b/src/shader_recompiler/frontend/translate/vector_alu.cpp index fb3f52c7f..3b88e4dec 100644 --- a/src/shader_recompiler/frontend/translate/vector_alu.cpp +++ b/src/shader_recompiler/frontend/translate/vector_alu.cpp @@ -158,6 +158,8 @@ void Translator::EmitVectorAlu(const GcnInst& inst) { return V_LOG_F32(inst); case Opcode::V_RCP_F32: return V_RCP_F32(inst); + case Opcode::V_RCP_LEGACY_F32: + return V_RCP_LEGACY_F32(inst); case Opcode::V_RCP_F64: return V_RCP_F64(inst); case Opcode::V_RCP_IFLAG_F32: @@ -798,6 +800,20 @@ void Translator::V_RCP_F32(const GcnInst& inst) { SetDst(inst.dst[0], ir.FPRecip(src0)); } +void Translator::V_RCP_LEGACY_F32(const GcnInst& inst) { + const IR::F32 src0{GetSrc(inst.src[0])}; + const auto result = ir.FPRecip(src0); + const auto inf = ir.FPIsInf(result); + + const auto raw_result = ir.ConvertFToU(32, result); + const auto sign_bit = ir.ShiftRightLogical(raw_result, ir.Imm32(31u)); + const auto sign_bit_set = ir.INotEqual(sign_bit, ir.Imm32(0u)); + const IR::F32 inf_result{ir.Select(sign_bit_set, ir.Imm32(-0.0f), ir.Imm32(0.0f))}; + const IR::F32 val{ir.Select(inf, inf_result, result)}; + + SetDst(inst.dst[0], val); +} + void Translator::V_RCP_F64(const GcnInst& inst) { const IR::F64 src0{GetSrc64(inst.src[0])}; SetDst64(inst.dst[0], ir.FPRecip(src0)); From d4fbeea085d704a94151e44a5dcf686e2d33a7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 5 Jun 2025 02:00:11 +0200 Subject: [PATCH 093/141] Stub PM4 COPY_DATA opcode (#3032) --- src/video_core/amdgpu/liverpool.cpp | 15 ++++++-- src/video_core/amdgpu/pm4_cmds.h | 55 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 4db7648c6..118c43cef 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -394,7 +394,7 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span dcb, std::span(header); - LOG_DEBUG(Render_Vulkan, - "Encountered EventWrite: event_type = {}, event_index = {}", + LOG_DEBUG(Render, "Encountered EventWrite: event_type = {}, event_index = {}", magic_enum::enum_name(event->event_type.Value()), magic_enum::enum_name(event->event_index.Value())); if (event->event_type.Value() == EventType::SoVgtStreamoutFlush) { @@ -673,6 +672,16 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); + LOG_WARNING(Render, + "unhandled IT_COPY_DATA src_sel = {}, dst_sel = {}, " + "count_sel = {}, wr_confirm = {}, engine_sel = {}", + u32(copy_data->src_sel.Value()), u32(copy_data->dst_sel.Value()), + copy_data->count_sel.Value(), copy_data->wr_confirm.Value(), + u32(copy_data->engine_sel.Value())); + break; + } case PM4ItOpcode::MemSemaphore: { const auto* mem_semaphore = reinterpret_cast(header); if (mem_semaphore->IsSignaling()) { diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 58ecda93e..011e47bf0 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -554,6 +554,61 @@ struct PM4DmaData { } }; +enum class CopyDataSrc : u32 { + MappedRegister = 0, + Memory = 1, + TCL2 = 2, + Gds = 3, + // Reserved = 4, + Immediate = 5, + Atomic = 6, + GdsAtomic0 = 7, + GdsAtomic1 = 8, + GpuClock = 9, +}; + +enum class CopyDataDst : u32 { + MappedRegister = 0, + MemorySync = 1, + TCL2 = 2, + Gds = 3, + // Reserved = 4, + MemoryAsync = 5, +}; + +enum class CopyDataEngine : u32 { + Me = 0, + Pfp = 1, + Ce = 2, + // Reserved = 3 +}; + +struct PM4CmdCopyData { + PM4Type3Header header; + union { + BitField<0, 4, CopyDataSrc> src_sel; + BitField<8, 4, CopyDataDst> dst_sel; + BitField<16, 1, u32> count_sel; + BitField<20, 1, u32> wr_confirm; + BitField<30, 2, CopyDataEngine> engine_sel; + u32 control; + }; + u32 src_addr_lo; + u32 src_addr_hi; + u32 dst_addr_lo; + u32 dst_addr_hi; + + template + T SrcAddress() const { + return std::bit_cast(src_addr_lo | u64(src_addr_hi) << 32); + } + + template + T DstAddress() const { + return std::bit_cast(dst_addr_lo | u64(dst_addr_hi) << 32); + } +}; + struct PM4CmdRewind { PM4Type3Header header; union { From 285df1b5befcedb1287007ed992e1805b148025f Mon Sep 17 00:00:00 2001 From: DanielSvoboda Date: Thu, 5 Jun 2025 02:48:47 -0300 Subject: [PATCH 094/141] QT: AutoUpdate - Fix Changelog Error (#3042) --- .github/workflows/build.yml | 6 +++--- src/qt_gui/check_update.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ceb915f6a..bb3d157b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -494,7 +494,7 @@ jobs: with: token: ${{ secrets.SHADPS4_TOKEN_REPO }} name: "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}" - tag: "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}" + tag: "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.fullhash }}" draft: false prerelease: true body: "Full Changelog: [${{ env.last_release_tag }}...${{ needs.get-info.outputs.shorthash }}](https://github.com/shadps4-emu/shadPS4/compare/${{ env.last_release_tag }}...${{ needs.get-info.outputs.fullhash }})" @@ -530,14 +530,14 @@ jobs: # Check if release already exists and get ID release_id=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ - "https://api.github.com/repos/$REPO/releases/tags/Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}" | jq -r '.id') + "https://api.github.com/repos/$REPO/releases/tags/Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.fullhash }}" | jq -r '.id') if [[ "$release_id" == "null" ]]; then echo "Creating release in $REPO for $filename" release_id=$(curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ -d '{ - "tag_name": "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}", + "tag_name": "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.fullhash }}", "name": "Pre-release-shadPS4-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}", "draft": false, "prerelease": true, diff --git a/src/qt_gui/check_update.cpp b/src/qt_gui/check_update.cpp index 550fdddb5..b0858840a 100644 --- a/src/qt_gui/check_update.cpp +++ b/src/qt_gui/check_update.cpp @@ -137,7 +137,7 @@ tr("The Auto Updater allows up to 60 update checks per hour.\\nYou have reached } } - latestRev = latestVersion.right(7); + latestRev = latestVersion.right(40); latestDate = jsonObj["published_at"].toString(); QJsonArray assets = jsonObj["assets"].toArray(); @@ -167,7 +167,7 @@ tr("The Auto Updater allows up to 60 update checks per hour.\\nYou have reached QDateTime dateTime = QDateTime::fromString(latestDate, Qt::ISODate); latestDate = dateTime.isValid() ? dateTime.toString("yyyy-MM-dd HH:mm:ss") : "Unknown date"; - if (latestRev == currentRev.left(7)) { + if (latestRev == currentRev) { if (showMessage) { QMessageBox::information(this, tr("Auto Updater"), tr("Your version is already up to date!")); @@ -215,7 +215,7 @@ void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate, "%3" "(%4)" "

") - .arg(currentRev.left(7), currentDate, latestRev, latestDate); + .arg(currentRev.left(7), currentDate, latestRev.left(7), latestDate); QLabel* updateLabel = new QLabel(updateText, this); layout->addWidget(updateLabel); From 93222c6f9f01c15855b3cee23c7856b963b6b1e2 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 5 Jun 2025 08:49:32 +0300 Subject: [PATCH 095/141] New Crowdin updates (#3038) * New translations en_us.ts (Portuguese, Brazilian) * New translations en_us.ts (Turkish) --- src/qt_gui/translations/pt_BR.ts | 2 +- src/qt_gui/translations/tr_TR.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qt_gui/translations/pt_BR.ts b/src/qt_gui/translations/pt_BR.ts index 34d31f240..9f254e272 100644 --- a/src/qt_gui/translations/pt_BR.ts +++ b/src/qt_gui/translations/pt_BR.ts @@ -2048,7 +2048,7 @@
* Unsupported Vulkan Version - * Unsupported Vulkan Version + * Versão do Vulkan não suportada
diff --git a/src/qt_gui/translations/tr_TR.ts b/src/qt_gui/translations/tr_TR.ts index e61985e90..c6d641470 100644 --- a/src/qt_gui/translations/tr_TR.ts +++ b/src/qt_gui/translations/tr_TR.ts @@ -138,7 +138,7 @@ File Exists - Dosya mevcut + Dosya Mevcut File already exists. Do you want to replace it? @@ -1221,7 +1221,7 @@ Exit shadPS4 - shadPS4'ten Çık + shadPS4 Çıkış Exit the application. @@ -1381,7 +1381,7 @@ Game Boot - Oyun Başlatma + Oyun Başlat Only one file can be selected! From 0e9420a7b228f3e560ba154ad33e037358679638 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 5 Jun 2025 08:43:39 -0500 Subject: [PATCH 096/141] Fix request queues in libSceZlib (#3041) Queues are a FIFO data structure, so pop() removes the front, not the end. --- src/core/libraries/zlib/zlib.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/zlib/zlib.cpp b/src/core/libraries/zlib/zlib.cpp index 899cb5bf6..b304992ad 100644 --- a/src/core/libraries/zlib/zlib.cpp +++ b/src/core/libraries/zlib/zlib.cpp @@ -51,7 +51,7 @@ void ZlibTaskThread(const std::stop_token& stop) { if (!task_queue_cv.wait(lock, stop, [&] { return !task_queue.empty(); })) { break; } - task = task_queue.back(); + task = task_queue.front(); task_queue.pop(); } @@ -136,7 +136,7 @@ s32 PS4_SYSV_ABI sceZlibWaitForDone(u64* request_id, const u32* timeout) { } else { done_queue_cv.wait(lock, pred); } - *request_id = done_queue.back(); + *request_id = done_queue.front(); done_queue.pop(); } return ORBIS_OK; From 3b3026ff1c98137e4f3051ec44c0eb4e1fa2f8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A5IGA?= <164882787+Xphalnos@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:44:02 +0200 Subject: [PATCH 097/141] [CI] Update Qt to 6.9.1 (#3037) --- .github/workflows/build.yml | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb3d157b7..588236b14 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,18 +76,13 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: append-timestamp: false key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - - name: Setup VS Environment - uses: ilammy/msvc-dev-cmd@v1.13.0 - with: - arch: amd64 - - name: Configure CMake run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache @@ -111,7 +106,7 @@ jobs: - name: Setup Qt uses: jurplel/install-qt-action@v4 with: - version: 6.9.0 + version: 6.9.1 host: windows target: desktop arch: win64_msvc2022_64 @@ -130,18 +125,13 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-qt-cache-cmake-build with: append-timestamp: false key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} - - name: Setup VS Environment - uses: ilammy/msvc-dev-cmd@v1.13.0 - with: - arch: amd64 - - name: Configure CMake run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache @@ -186,7 +176,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{runner.os}}-sdl-cache-cmake-build with: @@ -228,7 +218,7 @@ jobs: - name: Setup Qt uses: jurplel/install-qt-action@v4 with: - version: 6.9.0 + version: 6.9.1 host: mac target: desktop arch: clang_64 @@ -247,7 +237,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{runner.os}}-qt-cache-cmake-build with: @@ -301,7 +291,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-sdl-cache-cmake-build with: @@ -362,7 +352,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-qt-cache-cmake-build with: @@ -409,7 +399,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-sdl-gcc-cache-cmake-build with: @@ -445,7 +435,7 @@ jobs: ${{ env.cache-name }}- - name: Cache CMake Build - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 env: cache-name: ${{ runner.os }}-qt-gcc-cache-cmake-build with: From 43bf4ed1bca1cd6442e97b04d9afb8383219bb86 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:14:34 -0500 Subject: [PATCH 098/141] sceVideoOutGetResolutionStatus error behavior (#3044) --- src/core/libraries/videoout/video_out.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/libraries/videoout/video_out.cpp b/src/core/libraries/videoout/video_out.cpp index c5208b6dd..da715b3bf 100644 --- a/src/core/libraries/videoout/video_out.cpp +++ b/src/core/libraries/videoout/video_out.cpp @@ -282,7 +282,12 @@ s32 PS4_SYSV_ABI sceVideoOutGetVblankStatus(int handle, SceVideoOutVblankStatus* s32 PS4_SYSV_ABI sceVideoOutGetResolutionStatus(s32 handle, SceVideoOutResolutionStatus* status) { LOG_INFO(Lib_VideoOut, "called"); - *status = driver->GetPort(handle)->resolution; + auto* port = driver->GetPort(handle); + if (!port || !port->is_open) { + return ORBIS_VIDEO_OUT_ERROR_INVALID_HANDLE; + } + + *status = port->resolution; return ORBIS_OK; } From fff3bf9917faef7a185cae896efce87bea7b5b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Thu, 5 Jun 2025 23:33:25 +0200 Subject: [PATCH 099/141] s_flbit_i32_b64 (#3033) * s_flbit_i32_b64 * Split FindUMsb64 into two 32bit ops --- .../backend/spirv/emit_spirv_instructions.h | 1 + .../backend/spirv/emit_spirv_integer.cpp | 14 ++++++++++++++ .../frontend/translate/scalar_alu.cpp | 13 +++++++++++++ .../frontend/translate/translate.h | 1 + src/shader_recompiler/ir/ir_emitter.cpp | 11 +++++++++-- src/shader_recompiler/ir/ir_emitter.h | 2 +- src/shader_recompiler/ir/opcodes.inc | 1 + 7 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 09f9732bf..172358866 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -372,6 +372,7 @@ Id EmitBitCount64(EmitContext& ctx, Id value); Id EmitBitwiseNot32(EmitContext& ctx, Id value); Id EmitFindSMsb32(EmitContext& ctx, Id value); Id EmitFindUMsb32(EmitContext& ctx, Id value); +Id EmitFindUMsb64(EmitContext& ctx, Id value); Id EmitFindILsb32(EmitContext& ctx, Id value); Id EmitFindILsb64(EmitContext& ctx, Id value); Id EmitSMin32(EmitContext& ctx, Id a, Id b); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_integer.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_integer.cpp index 10bfbb2ab..1a995354d 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_integer.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_integer.cpp @@ -229,6 +229,20 @@ Id EmitFindUMsb32(EmitContext& ctx, Id value) { return ctx.OpFindUMsb(ctx.U32[1], value); } +Id EmitFindUMsb64(EmitContext& ctx, Id value) { + // Vulkan restricts some bitwise operations to 32-bit only, so decompose into + // two 32-bit values and select the correct result. + const Id unpacked{ctx.OpBitcast(ctx.U32[2], value)}; + const Id hi{ctx.OpCompositeExtract(ctx.U32[1], unpacked, 1U)}; + const Id lo{ctx.OpCompositeExtract(ctx.U32[1], unpacked, 0U)}; + const Id hi_msb{ctx.OpFindUMsb(ctx.U32[1], hi)}; + const Id lo_msb{ctx.OpFindUMsb(ctx.U32[1], lo)}; + const Id found_hi{ctx.OpINotEqual(ctx.U1[1], hi_msb, ctx.ConstU32(u32(-1)))}; + const Id shifted_hi{ctx.OpIAdd(ctx.U32[1], hi_msb, ctx.ConstU32(32u))}; + // value == 0 case is checked in IREmitter + return ctx.OpSelect(ctx.U32[1], found_hi, shifted_hi, lo_msb); +} + Id EmitFindILsb32(EmitContext& ctx, Id value) { return ctx.OpFindILsb(ctx.U32[1], value); } diff --git a/src/shader_recompiler/frontend/translate/scalar_alu.cpp b/src/shader_recompiler/frontend/translate/scalar_alu.cpp index 3a8e894ae..7beb594c3 100644 --- a/src/shader_recompiler/frontend/translate/scalar_alu.cpp +++ b/src/shader_recompiler/frontend/translate/scalar_alu.cpp @@ -114,6 +114,8 @@ void Translator::EmitScalarAlu(const GcnInst& inst) { return S_FF1_I32_B64(inst); case Opcode::S_FLBIT_I32_B32: return S_FLBIT_I32_B32(inst); + case Opcode::S_FLBIT_I32_B64: + return S_FLBIT_I32_B64(inst); case Opcode::S_BITSET0_B32: return S_BITSET_B32(inst, 0); case Opcode::S_BITSET1_B32: @@ -686,6 +688,17 @@ void Translator::S_FLBIT_I32_B32(const GcnInst& inst) { SetDst(inst.dst[0], IR::U32{ir.Select(cond, pos_from_left, ir.Imm32(~0U))}); } +void Translator::S_FLBIT_I32_B64(const GcnInst& inst) { + const IR::U64 src0{GetSrc64(inst.src[0])}; + // Gcn wants the MSB position counting from the left, but SPIR-V counts from the rightmost (LSB) + // position + const IR::U32 msb_pos = ir.FindUMsb(src0); + const IR::U32 pos_from_left = ir.ISub(ir.Imm32(63), msb_pos); + // Select 0xFFFFFFFF if src0 was 0 + const IR::U1 cond = ir.INotEqual(src0, ir.Imm64(u64(0u))); + SetDst(inst.dst[0], IR::U32{ir.Select(cond, pos_from_left, ir.Imm32(~0U))}); +} + void Translator::S_BITSET_B32(const GcnInst& inst, u32 bit_value) { const IR::U32 old_value{GetSrc(inst.dst[0])}; const IR::U32 offset{ir.BitFieldExtract(GetSrc(inst.src[0]), ir.Imm32(0U), ir.Imm32(5U))}; diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 2584d5c5e..15ba8c8d7 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -121,6 +121,7 @@ public: void S_FF1_I32_B32(const GcnInst& inst); void S_FF1_I32_B64(const GcnInst& inst); void S_FLBIT_I32_B32(const GcnInst& inst); + void S_FLBIT_I32_B64(const GcnInst& inst); void S_BITSET_B32(const GcnInst& inst, u32 bit_value); void S_GETPC_B64(u32 pc, const GcnInst& inst); void S_SAVEEXEC_B64(NegateMode negate, bool is_or, const GcnInst& inst); diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 01d945178..dcb734d01 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -1546,8 +1546,15 @@ U32 IREmitter::FindSMsb(const U32& value) { return Inst(Opcode::FindSMsb32, value); } -U32 IREmitter::FindUMsb(const U32& value) { - return Inst(Opcode::FindUMsb32, value); +U32 IREmitter::FindUMsb(const U32U64& value) { + switch (value.Type()) { + case Type::U32: + return Inst(Opcode::FindUMsb32, value); + case Type::U64: + return Inst(Opcode::FindUMsb64, value); + default: + ThrowInvalidType(value.Type()); + } } U32 IREmitter::FindILsb(const U32U64& value) { diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index 8f8a12736..da7adf42b 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -266,7 +266,7 @@ public: [[nodiscard]] U32 BitwiseNot(const U32& value); [[nodiscard]] U32 FindSMsb(const U32& value); - [[nodiscard]] U32 FindUMsb(const U32& value); + [[nodiscard]] U32 FindUMsb(const U32U64& value); [[nodiscard]] U32 FindILsb(const U32U64& value); [[nodiscard]] U32 SMin(const U32& a, const U32& b); [[nodiscard]] U32 UMin(const U32& a, const U32& b); diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index ab6dbfde9..647432bcf 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -349,6 +349,7 @@ OPCODE(BitwiseNot32, U32, U32, OPCODE(FindSMsb32, U32, U32, ) OPCODE(FindUMsb32, U32, U32, ) +OPCODE(FindUMsb64, U32, U64, ) OPCODE(FindILsb32, U32, U32, ) OPCODE(FindILsb64, U32, U64, ) OPCODE(SMin32, U32, U32, U32, ) From 91d29459fb55cb0d28006639e7a38134c5a368ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Fri, 6 Jun 2025 05:19:05 +0200 Subject: [PATCH 100/141] Implement PM4CondExec (#3046) --- src/video_core/amdgpu/liverpool.cpp | 13 +++++++++++++ src/video_core/amdgpu/pm4_cmds.h | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index 118c43cef..e031d0ebc 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -765,6 +765,19 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::span(header); + if (cond_exec->command.Value() != 0) { + LOG_WARNING(Render, "IT_COND_EXEC used a reserved command"); + } + const auto skip = *cond_exec->Address() == false; + if (skip) { + dcb = NextPacket(dcb, + header->type3.NumWords() + 1 + cond_exec->exec_count.Value()); + continue; + } + break; + } default: UNREACHABLE_MSG("Unknown PM4 type 3 opcode {:#x} with count {}", static_cast(opcode), count); diff --git a/src/video_core/amdgpu/pm4_cmds.h b/src/video_core/amdgpu/pm4_cmds.h index 011e47bf0..23c1b8f21 100644 --- a/src/video_core/amdgpu/pm4_cmds.h +++ b/src/video_core/amdgpu/pm4_cmds.h @@ -1159,4 +1159,25 @@ struct PM4CmdMemSemaphore { } }; +struct PM4CmdCondExec { + PM4Type3Header header; + union { + BitField<2, 30, u32> bool_addr_lo; ///< low 32 address bits for the block in memory from + ///< where the CP will fetch the condition + }; + union { + BitField<0, 16, u32> bool_addr_hi; ///< high address bits for the condition + BitField<28, 4, u32> command; + }; + union { + BitField<0, 14, u32> exec_count; ///< Number of DWords that the CP will skip + ///< if bool pointed to is zero + }; + + bool* Address() const { + return std::bit_cast(u64(bool_addr_hi.Value()) << 32 | u64(bool_addr_lo.Value()) + << 2); + } +}; + } // namespace AmdGpu From 5edd9ff54b6baca5025dff0f5491ddf0d0746a19 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:17:45 -0500 Subject: [PATCH 101/141] Improved sceKernelMapNamedFlexibleMemory logging (#3050) * More descriptive sceKernelMapNamedFlexibleMemory logging * Misc exports These functions are used by Overwatch: Origins Edition * Clang * Function parameter cleanup Changes the parameters on our sceKernelMapNamedFlexibleMemory and sceKernelMapFlexibleMemory functions to better align with our current standards. --- src/core/libraries/kernel/kernel.cpp | 4 ++++ src/core/libraries/kernel/memory.cpp | 21 +++++++++------------ src/core/libraries/kernel/memory.h | 7 +++---- src/core/libraries/kernel/threads/mutex.cpp | 1 + 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/core/libraries/kernel/kernel.cpp b/src/core/libraries/kernel/kernel.cpp index 180850217..930640d0e 100644 --- a/src/core/libraries/kernel/kernel.cpp +++ b/src/core/libraries/kernel/kernel.cpp @@ -273,6 +273,10 @@ void RegisterKernel(Core::Loader::SymbolsResolver* sym) { Libraries::Net::sceNetInetNtop); // TODO fix it to sys_ ... LIB_FUNCTION("4n51s0zEf0c", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sceNetInetPton); // TODO fix it to sys_ ... + LIB_FUNCTION("XVL8So3QJUk", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_connect); + LIB_FUNCTION("3e+4Iv7IJ8U", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_accept); + LIB_FUNCTION("aNeavPDNKzA", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_sendmsg); + LIB_FUNCTION("pxnCmagrtao", "libScePosix", 1, "libkernel", 1, 1, Libraries::Net::sys_listen); } } // namespace Libraries::Kernel diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 18676cbdf..5e94199e1 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -222,9 +222,10 @@ s32 PS4_SYSV_ABI sceKernelMapDirectMemory2(void** addr, u64 len, s32 type, s32 p return ret; } -s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, std::size_t len, int prot, - int flags, const char* name) { - +s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, u64 len, s32 prot, s32 flags, + const char* name) { + LOG_INFO(Kernel_Vmm, "in_addr = {}, len = {:#x}, prot = {:#x}, flags = {:#x}, name = '{}'", + fmt::ptr(*addr_in_out), len, prot, flags, name); if (len == 0 || !Common::Is16KBAligned(len)) { LOG_ERROR(Kernel_Vmm, "len is 0 or not 16kb multiple"); return ORBIS_KERNEL_ERROR_EINVAL; @@ -243,18 +244,14 @@ s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, std::size_t const VAddr in_addr = reinterpret_cast(*addr_in_out); const auto mem_prot = static_cast(prot); const auto map_flags = static_cast(flags); - SCOPE_EXIT { - LOG_INFO(Kernel_Vmm, - "in_addr = {:#x}, out_addr = {}, len = {:#x}, prot = {:#x}, flags = {:#x}", - in_addr, fmt::ptr(*addr_in_out), len, prot, flags); - }; auto* memory = Core::Memory::Instance(); - return memory->MapMemory(addr_in_out, in_addr, len, mem_prot, map_flags, - Core::VMAType::Flexible, name); + const auto ret = memory->MapMemory(addr_in_out, in_addr, len, mem_prot, map_flags, + Core::VMAType::Flexible, name); + LOG_INFO(Kernel_Vmm, "out_addr = {}", fmt::ptr(*addr_in_out)); + return ret; } -s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, std::size_t len, int prot, - int flags) { +s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, u64 len, s32 prot, s32 flags) { return sceKernelMapNamedFlexibleMemory(addr_in_out, len, prot, flags, "anon"); } diff --git a/src/core/libraries/kernel/memory.h b/src/core/libraries/kernel/memory.h index 6cefe0d07..ea42e7546 100644 --- a/src/core/libraries/kernel/memory.h +++ b/src/core/libraries/kernel/memory.h @@ -141,10 +141,9 @@ s32 PS4_SYSV_ABI sceKernelAvailableDirectMemorySize(u64 searchStart, u64 searchE s32 PS4_SYSV_ABI sceKernelVirtualQuery(const void* addr, int flags, OrbisVirtualQueryInfo* info, size_t infoSize); s32 PS4_SYSV_ABI sceKernelReserveVirtualRange(void** addr, u64 len, int flags, u64 alignment); -s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addrInOut, std::size_t len, int prot, - int flags, const char* name); -s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, std::size_t len, int prot, - int flags); +s32 PS4_SYSV_ABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, u64 len, s32 prot, s32 flags, + const char* name); +s32 PS4_SYSV_ABI sceKernelMapFlexibleMemory(void** addr_in_out, u64 len, s32 prot, s32 flags); int PS4_SYSV_ABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, u32* prot); s32 PS4_SYSV_ABI sceKernelMprotect(const void* addr, u64 size, s32 prot); diff --git a/src/core/libraries/kernel/threads/mutex.cpp b/src/core/libraries/kernel/threads/mutex.cpp index 956e5ef65..3dbade96a 100644 --- a/src/core/libraries/kernel/threads/mutex.cpp +++ b/src/core/libraries/kernel/threads/mutex.cpp @@ -426,6 +426,7 @@ void RegisterMutex(Core::Loader::SymbolsResolver* sym) { // Posix LIB_FUNCTION("ttHNfU+qDBU", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutex_init); LIB_FUNCTION("7H0iTOciTLo", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutex_lock); + LIB_FUNCTION("Io9+nTKXZtA", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutex_timedlock); LIB_FUNCTION("2Z+PpY6CaJg", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutex_unlock); LIB_FUNCTION("ltCfaGr2JGE", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutex_destroy); LIB_FUNCTION("dQHWEsJtoE4", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_mutexattr_init); From 2857ef34f037ac8e96730faabdf7dd3511c7d6af Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 8 Jun 2025 13:04:43 -0500 Subject: [PATCH 102/141] Don't coalesce dmem pages (#3059) Looks like this change is what broke P.T. I'll need to look closer at this when I have a chance, clearly we're doing something wrong here. --- src/core/memory.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/memory.cpp b/src/core/memory.cpp index ba3640877..54cae910b 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -182,7 +182,6 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, auto& area = CarveDmemArea(mapping_start, size)->second; area.memory_type = memory_type; area.is_free = false; - MergeAdjacent(dmem_map, dmem_area); return mapping_start; } From 5d064dd89f17b73be3cae84bd0bf98a7b02d6997 Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Sun, 8 Jun 2025 20:04:55 +0200 Subject: [PATCH 103/141] Dev Tools: Fix Module Viewer HLE detection (#3058) * fix * clang --- src/core/linker.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/core/linker.cpp b/src/core/linker.cpp index c50b03a8f..1f45caf12 100644 --- a/src/core/linker.cpp +++ b/src/core/linker.cpp @@ -332,21 +332,22 @@ bool Linker::Resolve(const std::string& name, Loader::SymbolType sym_type, Modul sr.type = sym_type; const auto* record = m_hle_symbols.FindSymbol(sr); - if (!record) { - // Check if it an export function - const auto* p = FindExportedModule(*module, *library); - if (p && p->export_sym.GetSize() > 0) { - record = p->export_sym.FindSymbol(sr); - } - } if (record) { *return_info = *record; - Core::Devtools::Widget::ModuleList::AddModule(sr.library); - return true; } + // Check if it an export function + const auto* p = FindExportedModule(*module, *library); + if (p && p->export_sym.GetSize() > 0) { + record = p->export_sym.FindSymbol(sr); + if (record) { + *return_info = *record; + return true; + } + } + const auto aeronid = AeroLib::FindByNid(sr.name.c_str()); if (aeronid) { return_info->name = aeronid->name; From 2bc199a41be994eaad6abd0db978808aac3cb2c0 Mon Sep 17 00:00:00 2001 From: Mahmoud Adel <94652220+AboMedoz@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:33:08 +0300 Subject: [PATCH 104/141] black image error fix (#3051) --- src/video_core/texture_cache/texture_cache.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 63cfc4431..4b173c313 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -299,6 +299,7 @@ ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId image_id) { auto& new_image = slot_images[new_image_id]; src_image.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}); + RefreshImage(new_image); new_image.CopyImage(src_image); if (src_image.binding.is_bound || src_image.binding.is_target) { From 952cef5a154c231d0b8879a6caca519cfaebb670 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 8 Jun 2025 21:38:58 +0300 Subject: [PATCH 105/141] shader_recompiler: Implement dual source blending (#3054) --- .../backend/spirv/spirv_emit_context.cpp | 15 +++++++++++++-- .../frontend/translate/export.cpp | 10 ++++++++-- src/shader_recompiler/runtime_info.h | 2 ++ .../renderer_vulkan/liverpool_to_vk.cpp | 13 +++++++++++++ src/video_core/renderer_vulkan/liverpool_to_vk.h | 2 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 9 +++++++++ 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 68bfcc0d0..bd10fd3df 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -634,7 +634,8 @@ void EmitContext::DefineOutputs() { } break; } - case LogicalStage::Fragment: + case LogicalStage::Fragment: { + u32 num_render_targets = 0; for (u32 i = 0; i < IR::NumRenderTargets; i++) { const IR::Attribute mrt{IR::Attribute::RenderTarget0 + i}; if (!info.stores.GetAny(mrt)) { @@ -643,11 +644,21 @@ void EmitContext::DefineOutputs() { const u32 num_components = info.stores.NumComponents(mrt); const AmdGpu::NumberFormat num_format{runtime_info.fs_info.color_buffers[i].num_format}; const Id type{GetAttributeType(*this, num_format)[num_components]}; - const Id id{DefineOutput(type, i)}; + Id id; + if (runtime_info.fs_info.dual_source_blending) { + id = DefineOutput(type, 0); + Decorate(id, spv::Decoration::Index, i); + } else { + id = DefineOutput(type, i); + } Name(id, fmt::format("frag_color{}", i)); frag_outputs[i] = GetAttributeInfo(num_format, id, num_components, true); + ++num_render_targets; } + ASSERT_MSG(!runtime_info.fs_info.dual_source_blending || num_render_targets == 2, + "Dual source blending enabled, there must be exactly two MRT exports"); break; + } case LogicalStage::Geometry: { output_position = DefineVariable(F32[4], spv::BuiltIn::Position, spv::StorageClass::Output); diff --git a/src/shader_recompiler/frontend/translate/export.cpp b/src/shader_recompiler/frontend/translate/export.cpp index 0abef2e81..8a99f38a9 100644 --- a/src/shader_recompiler/frontend/translate/export.cpp +++ b/src/shader_recompiler/frontend/translate/export.cpp @@ -26,8 +26,11 @@ void Translator::ExportMrtValue(IR::Attribute attribute, u32 comp, const IR::F32 } void Translator::ExportMrtCompressed(IR::Attribute attribute, u32 idx, const IR::U32& value) { - const u32 color_buffer_idx = + u32 color_buffer_idx = static_cast(attribute) - static_cast(IR::Attribute::RenderTarget0); + if (runtime_info.fs_info.dual_source_blending && attribute == IR::Attribute::RenderTarget1) { + color_buffer_idx = 0; + } const auto color_buffer = runtime_info.fs_info.color_buffers[color_buffer_idx]; AmdGpu::NumberFormat num_format; @@ -68,8 +71,11 @@ void Translator::ExportMrtCompressed(IR::Attribute attribute, u32 idx, const IR: } void Translator::ExportMrtUncompressed(IR::Attribute attribute, u32 comp, const IR::F32& value) { - const u32 color_buffer_idx = + u32 color_buffer_idx = static_cast(attribute) - static_cast(IR::Attribute::RenderTarget0); + if (runtime_info.fs_info.dual_source_blending && attribute == IR::Attribute::RenderTarget1) { + color_buffer_idx = 0; + } const auto color_buffer = runtime_info.fs_info.color_buffers[color_buffer_idx]; const auto swizzled_comp = SwizzleMrtComponent(color_buffer, comp); diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index b8ed42f5b..53d2d5303 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -196,11 +196,13 @@ struct FragmentRuntimeInfo { u32 num_inputs; std::array inputs; std::array color_buffers; + bool dual_source_blending; bool operator==(const FragmentRuntimeInfo& other) const noexcept { return std::ranges::equal(color_buffers, other.color_buffers) && en_flags.raw == other.en_flags.raw && addr_flags.raw == other.addr_flags.raw && num_inputs == other.num_inputs && + dual_source_blending == other.dual_source_blending && std::ranges::equal(inputs.begin(), inputs.begin() + num_inputs, other.inputs.begin(), other.inputs.begin() + num_inputs); } diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index a6ae0c304..5972296c0 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -214,6 +214,19 @@ vk::BlendFactor BlendFactor(Liverpool::BlendControl::BlendFactor factor) { } } +bool IsDualSourceBlendFactor(Liverpool::BlendControl::BlendFactor factor) { + using BlendFactor = Liverpool::BlendControl::BlendFactor; + switch (factor) { + case BlendFactor::Src1Color: + case BlendFactor::Src1Alpha: + case BlendFactor::InvSrc1Color: + case BlendFactor::InvSrc1Alpha: + return true; + default: + return false; + } +} + vk::BlendOp BlendOp(Liverpool::BlendControl::BlendFunc func) { using BlendFunc = Liverpool::BlendControl::BlendFunc; switch (func) { diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.h b/src/video_core/renderer_vulkan/liverpool_to_vk.h index fca0a8378..61fd4a8c1 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.h +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.h @@ -30,6 +30,8 @@ vk::FrontFace FrontFace(Liverpool::FrontFace mode); vk::BlendFactor BlendFactor(Liverpool::BlendControl::BlendFactor factor); +bool IsDualSourceBlendFactor(Liverpool::BlendControl::BlendFactor factor); + vk::BlendOp BlendOp(Liverpool::BlendControl::BlendFunc func); vk::SamplerAddressMode ClampMode(AmdGpu::ClampMode mode); diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index d7ad47a3c..b72f77e55 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -158,6 +158,15 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS info.fs_info.addr_flags = regs.ps_input_addr; const auto& ps_inputs = regs.ps_inputs; info.fs_info.num_inputs = regs.num_interp; + const auto& cb0_blend = regs.blend_control[0]; + info.fs_info.dual_source_blending = + LiverpoolToVK::IsDualSourceBlendFactor(cb0_blend.color_dst_factor) || + LiverpoolToVK::IsDualSourceBlendFactor(cb0_blend.color_src_factor); + if (cb0_blend.separate_alpha_blend) { + info.fs_info.dual_source_blending |= + LiverpoolToVK::IsDualSourceBlendFactor(cb0_blend.alpha_dst_factor) || + LiverpoolToVK::IsDualSourceBlendFactor(cb0_blend.alpha_src_factor); + } for (u32 i = 0; i < regs.num_interp; i++) { info.fs_info.inputs[i] = { .param_index = u8(ps_inputs[i].input_offset.Value()), From ce84e80f65745c9f00981e4dbcfe79ef1a11cfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Sun, 8 Jun 2025 20:43:58 +0200 Subject: [PATCH 106/141] BUFFER_ATOMIC_CMPSWAP (#3045) --- .../backend/spirv/emit_spirv_atomic.cpp | 22 +++++++++++++++++++ .../backend/spirv/emit_spirv_instructions.h | 2 ++ .../frontend/translate/vector_memory.cpp | 4 ++++ src/shader_recompiler/ir/ir_emitter.cpp | 5 +++++ src/shader_recompiler/ir/ir_emitter.h | 3 +++ src/shader_recompiler/ir/opcodes.inc | 1 + 6 files changed, 37 insertions(+) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index d7c73ca8f..a342b47b6 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -68,6 +68,22 @@ Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id }); } +Id BufferAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id, Id, Id)) { + const auto& buffer = ctx.buffers[handle]; + if (Sirit::ValidId(buffer.offset)) { + address = ctx.OpIAdd(ctx.U32[1], address, buffer.offset); + } + const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(2u)); + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; + const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); + const auto [scope, semantics]{AtomicArgs(ctx)}; + return BufferAtomicU32BoundsCheck(ctx, index, buffer.size_dwords, [&] { + return (ctx.*atomic_func)(ctx.U32[1], ptr, scope, semantics, semantics, value, cmp_value); + }); +} + Id ImageAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { const auto& texture = ctx.images[handle & 0xFFFF]; @@ -175,6 +191,12 @@ Id EmitBufferAtomicSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicExchange); } +Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value) { + return BufferAtomicU32CmpSwap(ctx, inst, handle, address, value, cmp_value, + &Sirit::Module::OpAtomicCompareExchange); +} + Id EmitImageAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value) { return ImageAtomicU32(ctx, inst, handle, coords, value, &Sirit::Module::OpAtomicIAdd); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 172358866..b9707224c 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -96,6 +96,8 @@ Id EmitBufferAtomicAnd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addres Id EmitBufferAtomicOr32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); +Id EmitBufferAtomicCmpSwap32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id cmp_value); Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, Id index); Id EmitGetAttributeU32(EmitContext& ctx, IR::Attribute attr, u32 comp); void EmitSetAttribute(EmitContext& ctx, IR::Attribute attr, Id value, u32 comp); diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 5c972c607..8c035f26c 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -331,6 +331,10 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) { switch (op) { case AtomicOp::Swap: return ir.BufferAtomicSwap(handle, address, vdata_val, buffer_info); + case AtomicOp::CmpSwap: { + IR::Value cmp_val = ir.GetVectorReg(vdata + 1); + return ir.BufferAtomicCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); + } case AtomicOp::Add: return ir.BufferAtomicIAdd(handle, address, vdata_val, buffer_info); case AtomicOp::Smin: diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index dcb734d01..07249edfe 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -513,6 +513,11 @@ Value IREmitter::BufferAtomicSwap(const Value& handle, const Value& address, con return Inst(Opcode::BufferAtomicSwap32, Flags{info}, handle, address, value); } +Value IREmitter::BufferAtomicCmpSwap(const Value& handle, const Value& address, const Value& vdata, + const Value& cmp_value, BufferInstInfo info) { + return Inst(Opcode::BufferAtomicCmpSwap32, Flags{info}, handle, address, vdata, cmp_value); +} + U32 IREmitter::DataAppend(const U32& counter) { return Inst(Opcode::DataAppend, counter, Imm32(0)); } diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index da7adf42b..7b9b81093 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -150,6 +150,9 @@ public: const Value& value, BufferInstInfo info); [[nodiscard]] Value BufferAtomicSwap(const Value& handle, const Value& address, const Value& value, BufferInstInfo info); + [[nodiscard]] Value BufferAtomicCmpSwap(const Value& handle, const Value& address, + const Value& value, const Value& cmp_value, + BufferInstInfo info); [[nodiscard]] U32 DataAppend(const U32& counter); [[nodiscard]] U32 DataConsume(const U32& counter); diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 647432bcf..5b3216be6 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -126,6 +126,7 @@ OPCODE(BufferAtomicAnd32, U32, Opaq OPCODE(BufferAtomicOr32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicXor32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicSwap32, U32, Opaque, Opaque, U32, ) +OPCODE(BufferAtomicCmpSwap32, U32, Opaque, Opaque, U32, U32, ) // Vector utility OPCODE(CompositeConstructU32x2, U32x2, U32, U32, ) From f2bbb6847dc22037c50a5723acd70195d30245c9 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 8 Jun 2025 11:53:11 -0700 Subject: [PATCH 107/141] fix: Missing switch case for BUFFER_ATOMIC_CMPSWAP --- src/shader_recompiler/frontend/translate/vector_memory.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 8c035f26c..5eb2079a4 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -70,6 +70,8 @@ void Translator::EmitVectorMemory(const GcnInst& inst) { return BUFFER_ATOMIC(AtomicOp::Add, inst); case Opcode::BUFFER_ATOMIC_SWAP: return BUFFER_ATOMIC(AtomicOp::Swap, inst); + case Opcode::BUFFER_ATOMIC_CMPSWAP: + return BUFFER_ATOMIC(AtomicOp::CmpSwap, inst); case Opcode::BUFFER_ATOMIC_SMIN: return BUFFER_ATOMIC(AtomicOp::Smin, inst); case Opcode::BUFFER_ATOMIC_UMIN: @@ -332,7 +334,7 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) { case AtomicOp::Swap: return ir.BufferAtomicSwap(handle, address, vdata_val, buffer_info); case AtomicOp::CmpSwap: { - IR::Value cmp_val = ir.GetVectorReg(vdata + 1); + const IR::Value cmp_val = ir.GetVectorReg(vdata + 1); return ir.BufferAtomicCmpSwap(handle, address, vdata_val, cmp_val, buffer_info); } case AtomicOp::Add: From a07a6bb9d3519fe128538a0c5d537e1e1e5bfbe1 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 8 Jun 2025 22:14:09 +0300 Subject: [PATCH 108/141] buffer_cache: Better image search for buffer validation (#3057) --- src/video_core/buffer_cache/buffer_cache.cpp | 51 ++++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 4717a5ff8..8a5283d83 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -798,24 +798,45 @@ void BufferCache::SynchronizeBuffer(Buffer& buffer, VAddr device_addr, u32 size, } bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, VAddr device_addr, u32 size) { - static constexpr FindFlags find_flags = - FindFlags::NoCreate | FindFlags::RelaxDim | FindFlags::RelaxFmt | FindFlags::RelaxSize; - TextureCache::BaseDesc desc{}; - desc.info.guest_address = device_addr; - desc.info.guest_size = size; - const ImageId image_id = texture_cache.FindImage(desc, find_flags); - if (!image_id) { + boost::container::small_vector image_ids; + texture_cache.ForEachImageInRegion(device_addr, size, [&](ImageId image_id, Image& image) { + if (image.info.guest_address != device_addr) { + return; + } + // Only perform sync if image is: + // - GPU modified; otherwise there are no changes to synchronize. + // - Not CPU dirty; otherwise we could overwrite CPU changes with stale GPU changes. + // - Not GPU dirty; otherwise we could overwrite GPU changes with stale image data. + if (False(image.flags & ImageFlagBits::GpuModified) || + True(image.flags & ImageFlagBits::Dirty)) { + return; + } + image_ids.push_back(image_id); + }); + if (image_ids.empty()) { return false; } + ImageId image_id{}; + if (image_ids.size() == 1) { + // Sometimes image size might not exactly match with requested buffer size + // If we only found 1 candidate image use it without too many questions. + image_id = image_ids[0]; + } else { + for (s32 i = 0; i < image_ids.size(); ++i) { + Image& image = texture_cache.GetImage(image_ids[i]); + if (image.info.guest_size == size) { + image_id = image_ids[i]; + break; + } + } + if (!image_id) { + LOG_WARNING(Render_Vulkan, + "Failed to find exact image match for copy addr={:#x}, size={:#x}", + device_addr, size); + return false; + } + } Image& image = texture_cache.GetImage(image_id); - // Only perform sync if image is: - // - GPU modified; otherwise there are no changes to synchronize. - // - Not CPU dirty; otherwise we could overwrite CPU changes with stale GPU changes. - // - Not GPU dirty; otherwise we could overwrite GPU changes with stale image data. - if (False(image.flags & ImageFlagBits::GpuModified) || - True(image.flags & ImageFlagBits::Dirty)) { - return false; - } ASSERT_MSG(device_addr == image.info.guest_address, "Texel buffer aliases image subresources {:x} : {:x}", device_addr, image.info.guest_address); From 5004e41100d6749dec1775ce335d7de3d93b46a8 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Sun, 8 Jun 2025 22:29:33 +0300 Subject: [PATCH 109/141] Patch movntss and movntsd (#3049) * Patch movntss and movntsd * clang-format * Deduplication * Allow rep to be in other places --- src/core/cpu_patches.cpp | 53 +++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/src/core/cpu_patches.cpp b/src/core/cpu_patches.cpp index 8937ef04b..8512858e9 100644 --- a/src/core/cpu_patches.cpp +++ b/src/core/cpu_patches.cpp @@ -88,7 +88,8 @@ static bool FilterTcbAccess(const ZydisDecodedOperand* operands) { dst_op.reg.value <= ZYDIS_REGISTER_R15; } -static void GenerateTcbAccess(const ZydisDecodedOperand* operands, Xbyak::CodeGenerator& c) { +static void GenerateTcbAccess(void* /* address */, const ZydisDecodedOperand* operands, + Xbyak::CodeGenerator& c) { const auto dst = ZydisToXbyakRegisterOperand(operands[0]); #if defined(_WIN32) @@ -126,7 +127,8 @@ static bool FilterNoSSE4a(const ZydisDecodedOperand*) { return !cpu.has(Cpu::tSSE4a); } -static void GenerateEXTRQ(const ZydisDecodedOperand* operands, Xbyak::CodeGenerator& c) { +static void GenerateEXTRQ(void* /* address */, const ZydisDecodedOperand* operands, + Xbyak::CodeGenerator& c) { bool immediateForm = operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && operands[2].type == ZYDIS_OPERAND_TYPE_IMMEDIATE; @@ -245,7 +247,8 @@ static void GenerateEXTRQ(const ZydisDecodedOperand* operands, Xbyak::CodeGenera } } -static void GenerateINSERTQ(const ZydisDecodedOperand* operands, Xbyak::CodeGenerator& c) { +static void GenerateINSERTQ(void* /* address */, const ZydisDecodedOperand* operands, + Xbyak::CodeGenerator& c) { bool immediateForm = operands[2].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && operands[3].type == ZYDIS_OPERAND_TYPE_IMMEDIATE; @@ -383,8 +386,44 @@ static void GenerateINSERTQ(const ZydisDecodedOperand* operands, Xbyak::CodeGene } } +static void ReplaceMOVNT(void* address, u8 rep_prefix) { + // Find the opcode byte + // There can be any amount of prefixes but the instruction can't be more than 15 bytes + // And we know for sure this is a MOVNTSS/MOVNTSD + bool found = false; + bool rep_prefix_found = false; + int index = 0; + u8* ptr = reinterpret_cast(address); + for (int i = 0; i < 15; i++) { + if (ptr[i] == rep_prefix) { + rep_prefix_found = true; + } else if (ptr[i] == 0x2B) { + index = i; + found = true; + break; + } + } + + // Some sanity checks + ASSERT(found); + ASSERT(index >= 2); + ASSERT(ptr[index - 1] == 0x0F); + ASSERT(rep_prefix_found); + + // This turns the MOVNTSS/MOVNTSD to a MOVSS/MOVSD m, xmm + ptr[index] = 0x11; +} + +static void ReplaceMOVNTSS(void* address, const ZydisDecodedOperand*, Xbyak::CodeGenerator&) { + ReplaceMOVNT(address, 0xF3); +} + +static void ReplaceMOVNTSD(void* address, const ZydisDecodedOperand*, Xbyak::CodeGenerator&) { + ReplaceMOVNT(address, 0xF2); +} + using PatchFilter = bool (*)(const ZydisDecodedOperand*); -using InstructionGenerator = void (*)(const ZydisDecodedOperand*, Xbyak::CodeGenerator&); +using InstructionGenerator = void (*)(void*, const ZydisDecodedOperand*, Xbyak::CodeGenerator&); struct PatchInfo { /// Filter for more granular patch conditions past just the instruction mnemonic. PatchFilter filter; @@ -400,6 +439,8 @@ static const std::unordered_map Patches = { // SSE4a {ZYDIS_MNEMONIC_EXTRQ, {FilterNoSSE4a, GenerateEXTRQ, true}}, {ZYDIS_MNEMONIC_INSERTQ, {FilterNoSSE4a, GenerateINSERTQ, true}}, + {ZYDIS_MNEMONIC_MOVNTSS, {FilterNoSSE4a, ReplaceMOVNTSS, false}}, + {ZYDIS_MNEMONIC_MOVNTSD, {FilterNoSSE4a, ReplaceMOVNTSD, false}}, #if defined(_WIN32) // Windows needs a trampoline. @@ -477,7 +518,7 @@ static std::pair TryPatch(u8* code, PatchModule* module) { auto& trampoline_gen = module->trampoline_gen; const auto trampoline_ptr = trampoline_gen.getCurr(); - patch_info.generator(operands, trampoline_gen); + patch_info.generator(code, operands, trampoline_gen); // Return to the following instruction at the end of the trampoline. trampoline_gen.jmp(code + instruction.length); @@ -485,7 +526,7 @@ static std::pair TryPatch(u8* code, PatchModule* module) { // Replace instruction with near jump to the trampoline. patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR); } else { - patch_info.generator(operands, patch_gen); + patch_info.generator(code, operands, patch_gen); } const auto patch_size = patch_gen.getCurr() - code; From 8ffcfc87bd9bcd8396cde82eec9daf2a250fd018 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 8 Jun 2025 22:46:34 +0300 Subject: [PATCH 110/141] shader_recompiler: Implement linear interpolation support (#3055) --- .../backend/spirv/spirv_emit_context.cpp | 23 +- .../backend/spirv/spirv_emit_context.h | 4 +- .../frontend/structured_control_flow.cpp | 12 +- .../frontend/translate/translate.cpp | 135 ++++---- .../frontend/translate/translate.h | 16 +- .../translate/vector_interpolation.cpp | 5 +- src/shader_recompiler/info.h | 4 + src/shader_recompiler/ir/attribute.h | 19 ++ src/shader_recompiler/ir/ir_emitter.cpp | 1 - src/shader_recompiler/ir/ir_emitter.h | 2 +- src/shader_recompiler/ir/reg.h | 2 +- src/video_core/amdgpu/pixel_format.h | 300 +++++++++++++++++- src/video_core/amdgpu/resource.h | 1 - src/video_core/amdgpu/types.h | 276 ---------------- 14 files changed, 425 insertions(+), 375 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index bd10fd3df..9e51f8e60 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -307,7 +307,9 @@ void EmitContext::DefineInterpolatedAttribs() { const Id p2{OpCompositeExtract(F32[4], p_array, 2U)}; const Id p10{OpFSub(F32[4], p1, p0)}; const Id p20{OpFSub(F32[4], p2, p0)}; - const Id bary_coord{OpLoad(F32[3], gl_bary_coord_id)}; + const Id bary_coord{OpLoad(F32[3], IsLinear(info.interp_qualifiers[i]) + ? bary_coord_linear_id + : bary_coord_persp_id)}; const Id bary_coord_y{OpCompositeExtract(F32[1], bary_coord, 1)}; const Id bary_coord_z{OpCompositeExtract(F32[1], bary_coord, 2)}; const Id p10_y{OpVectorTimesScalar(F32[4], p10, bary_coord_y)}; @@ -411,8 +413,14 @@ void EmitContext::DefineInputs() { DefineVariable(U1[1], spv::BuiltIn::FrontFacing, spv::StorageClass::Input); } if (profile.needs_manual_interpolation) { - gl_bary_coord_id = - DefineVariable(F32[3], spv::BuiltIn::BaryCoordKHR, spv::StorageClass::Input); + if (info.has_perspective_interp) { + bary_coord_persp_id = + DefineVariable(F32[3], spv::BuiltIn::BaryCoordKHR, spv::StorageClass::Input); + } + if (info.has_linear_interp) { + bary_coord_linear_id = DefineVariable(F32[3], spv::BuiltIn::BaryCoordNoPerspKHR, + spv::StorageClass::Input); + } } for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { const auto& input = runtime_info.fs_info.inputs[i]; @@ -435,9 +443,12 @@ void EmitContext::DefineInputs() { } else { attr_id = DefineInput(type, semantic); Name(attr_id, fmt::format("fs_in_attr{}", semantic)); - } - if (input.is_flat) { - Decorate(attr_id, spv::Decoration::Flat); + + if (input.is_flat) { + Decorate(attr_id, spv::Decoration::Flat); + } else if (IsLinear(info.interp_qualifiers[i])) { + Decorate(attr_id, spv::Decoration::NoPerspective); + } } input_params[semantic] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, num_components, false); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index a2e0d2f47..20d936cf0 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -293,8 +293,8 @@ public: Id shared_memory_u32_type{}; - Id interpolate_func{}; - Id gl_bary_coord_id{}; + Id bary_coord_persp_id{}; + Id bary_coord_linear_id{}; struct TextureDefinition { const VectorIds* data_types; diff --git a/src/shader_recompiler/frontend/structured_control_flow.cpp b/src/shader_recompiler/frontend/structured_control_flow.cpp index 11b40d07c..1a7a43f4d 100644 --- a/src/shader_recompiler/frontend/structured_control_flow.cpp +++ b/src/shader_recompiler/frontend/structured_control_flow.cpp @@ -605,11 +605,12 @@ public: Info& info_, const RuntimeInfo& runtime_info_, const Profile& profile_) : stmt_pool{stmt_pool_}, inst_pool{inst_pool_}, block_pool{block_pool_}, syntax_list{syntax_list_}, inst_list{inst_list_}, info{info_}, - runtime_info{runtime_info_}, profile{profile_} { + runtime_info{runtime_info_}, profile{profile_}, + translator{info_, runtime_info_, profile_} { Visit(root_stmt, nullptr, nullptr); - IR::Block& first_block{*syntax_list.front().data.block}; - Translator{&first_block, info, runtime_info, profile}.EmitPrologue(); + IR::Block* first_block = syntax_list.front().data.block; + translator.EmitPrologue(first_block); } private: @@ -637,8 +638,8 @@ private: current_block->has_multiple_predecessors = stmt.block->num_predecessors > 1; const u32 start = stmt.block->begin_index; const u32 size = stmt.block->end_index - start + 1; - Translate(current_block, stmt.block->begin, inst_list.subspan(start, size), - info, runtime_info, profile); + translator.Translate(current_block, stmt.block->begin, + inst_list.subspan(start, size)); } break; } @@ -820,6 +821,7 @@ private: Info& info; const RuntimeInfo& runtime_info; const Profile& profile; + Translator translator; }; } // Anonymous namespace diff --git a/src/shader_recompiler/frontend/translate/translate.cpp b/src/shader_recompiler/frontend/translate/translate.cpp index 5675adf3c..5853f3e72 100644 --- a/src/shader_recompiler/frontend/translate/translate.cpp +++ b/src/shader_recompiler/frontend/translate/translate.cpp @@ -21,16 +21,60 @@ namespace Shader::Gcn { -static u32 next_vgpr_num; -static std::unordered_map vgpr_map; - -Translator::Translator(IR::Block* block_, Info& info_, const RuntimeInfo& runtime_info_, - const Profile& profile_) - : ir{*block_, block_->begin()}, info{info_}, runtime_info{runtime_info_}, profile{profile_} { - next_vgpr_num = vgpr_map.empty() ? runtime_info.num_allocated_vgprs : next_vgpr_num; +Translator::Translator(Info& info_, const RuntimeInfo& runtime_info_, const Profile& profile_) + : info{info_}, runtime_info{runtime_info_}, profile{profile_}, + next_vgpr_num{runtime_info.num_allocated_vgprs} { + if (info.l_stage == LogicalStage::Fragment) { + dst_frag_vreg = GatherInterpQualifiers(); + } } -void Translator::EmitPrologue() { +IR::VectorReg Translator::GatherInterpQualifiers() { + u32 dst_vreg{}; + if (runtime_info.fs_info.addr_flags.persp_sample_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveSample; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveSample; // J + info.has_perspective_interp = true; + } + if (runtime_info.fs_info.addr_flags.persp_center_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveCenter; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveCenter; // J + info.has_perspective_interp = true; + } + if (runtime_info.fs_info.addr_flags.persp_centroid_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveCentroid; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::PerspectiveCentroid; // J + info.has_perspective_interp = true; + } + if (runtime_info.fs_info.addr_flags.persp_pull_model_ena) { + ++dst_vreg; // I/W + ++dst_vreg; // J/W + ++dst_vreg; // 1/W + } + if (runtime_info.fs_info.addr_flags.linear_sample_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearSample; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearSample; // J + info.has_linear_interp = true; + } + if (runtime_info.fs_info.addr_flags.linear_center_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearCenter; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearCenter; // J + info.has_linear_interp = true; + } + if (runtime_info.fs_info.addr_flags.linear_centroid_ena) { + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearCentroid; // I + vgpr_to_interp[dst_vreg++] = IR::Interpolation::LinearCentroid; // J + info.has_linear_interp = true; + } + if (runtime_info.fs_info.addr_flags.line_stipple_tex_ena) { + ++dst_vreg; + } + return IR::VectorReg(dst_vreg); +} + +void Translator::EmitPrologue(IR::Block* first_block) { + ir = IR::IREmitter(*first_block, first_block->begin()); + ir.Prologue(); ir.SetExec(ir.Imm1(true)); @@ -60,39 +104,7 @@ void Translator::EmitPrologue() { } break; case LogicalStage::Fragment: - dst_vreg = IR::VectorReg::V0; - if (runtime_info.fs_info.addr_flags.persp_sample_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.persp_center_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.persp_centroid_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.persp_pull_model_ena) { - ++dst_vreg; // I/W - ++dst_vreg; // J/W - ++dst_vreg; // 1/W - } - if (runtime_info.fs_info.addr_flags.linear_sample_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.linear_center_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.linear_centroid_ena) { - ++dst_vreg; // I - ++dst_vreg; // J - } - if (runtime_info.fs_info.addr_flags.line_stipple_tex_ena) { - ++dst_vreg; - } + dst_vreg = dst_frag_vreg; if (runtime_info.fs_info.addr_flags.pos_x_float_ena) { if (runtime_info.fs_info.en_flags.pos_x_float_ena) { ir.SetVectorReg(dst_vreg++, ir.GetAttribute(IR::Attribute::FragCoord, 0)); @@ -543,6 +555,26 @@ void Translator::LogMissingOpcode(const GcnInst& inst) { info.translation_failed = true; } +void Translator::Translate(IR::Block* block, u32 pc, std::span inst_list) { + if (inst_list.empty()) { + return; + } + ir = IR::IREmitter{*block, block->begin()}; + for (const auto& inst : inst_list) { + pc += inst.length; + + // Special case for emitting fetch shader. + if (inst.opcode == Opcode::S_SWAPPC_B64) { + ASSERT(info.stage == Stage::Vertex || info.stage == Stage::Export || + info.stage == Stage::Local); + EmitFetch(inst); + continue; + } + + TranslateInstruction(inst, pc); + } +} + void Translator::TranslateInstruction(const GcnInst& inst, const u32 pc) { // Emit instructions for each category. switch (inst.category) { @@ -577,25 +609,4 @@ void Translator::TranslateInstruction(const GcnInst& inst, const u32 pc) { } } -void Translate(IR::Block* block, u32 pc, std::span inst_list, Info& info, - const RuntimeInfo& runtime_info, const Profile& profile) { - if (inst_list.empty()) { - return; - } - Translator translator{block, info, runtime_info, profile}; - for (const auto& inst : inst_list) { - pc += inst.length; - - // Special case for emitting fetch shader. - if (inst.opcode == Opcode::S_SWAPPC_B64) { - ASSERT(info.stage == Stage::Vertex || info.stage == Stage::Export || - info.stage == Stage::Local); - translator.EmitFetch(inst); - continue; - } - - translator.TranslateInstruction(inst, pc); - } -} - } // namespace Shader::Gcn diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 15ba8c8d7..f8ffb9638 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -53,15 +53,17 @@ enum class NegateMode : u32 { Result, }; +static constexpr size_t MaxInterpVgpr = 16; + class Translator { public: - explicit Translator(IR::Block* block_, Info& info, const RuntimeInfo& runtime_info, - const Profile& profile); + explicit Translator(Info& info, const RuntimeInfo& runtime_info, const Profile& profile); + void Translate(IR::Block* block, u32 pc, std::span inst_list); void TranslateInstruction(const GcnInst& inst, u32 pc); // Instruction categories - void EmitPrologue(); + void EmitPrologue(IR::Block* first_block); void EmitFetch(const GcnInst& inst); void EmitExport(const GcnInst& inst); void EmitFlowControl(u32 pc, const GcnInst& inst); @@ -326,16 +328,18 @@ private: void LogMissingOpcode(const GcnInst& inst); IR::VectorReg GetScratchVgpr(u32 offset); + IR::VectorReg GatherInterpQualifiers(); private: IR::IREmitter ir; Info& info; const RuntimeInfo& runtime_info; const Profile& profile; + u32 next_vgpr_num; + std::unordered_map vgpr_map; + std::array vgpr_to_interp{}; + IR::VectorReg dst_frag_vreg{}; bool opcode_missing = false; }; -void Translate(IR::Block* block, u32 block_base, std::span inst_list, Info& info, - const RuntimeInfo& runtime_info, const Profile& profile); - } // namespace Shader::Gcn diff --git a/src/shader_recompiler/frontend/translate/vector_interpolation.cpp b/src/shader_recompiler/frontend/translate/vector_interpolation.cpp index 431cb2f04..2d7297c12 100644 --- a/src/shader_recompiler/frontend/translate/vector_interpolation.cpp +++ b/src/shader_recompiler/frontend/translate/vector_interpolation.cpp @@ -22,13 +22,14 @@ void Translator::EmitVectorInterpolation(const GcnInst& inst) { // VINTRP void Translator::V_INTERP_P2_F32(const GcnInst& inst) { - auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); + const auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); + info.interp_qualifiers[attr.param_index] = vgpr_to_interp[inst.src[0].code]; const IR::Attribute attrib{IR::Attribute::Param0 + attr.param_index}; SetDst(inst.dst[0], ir.GetAttribute(attrib, inst.control.vintrp.chan)); } void Translator::V_INTERP_MOV_F32(const GcnInst& inst) { - auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); + const auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); const IR::Attribute attrib{IR::Attribute::Param0 + attr.param_index}; SetDst(inst.dst[0], ir.GetAttribute(attrib, inst.control.vintrp.chan)); } diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index 24e0741c1..e14c7988d 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -193,6 +193,8 @@ struct Info { PersistentSrtInfo srt_info; std::vector flattened_ud_buf; + std::array interp_qualifiers{}; + IR::ScalarReg tess_consts_ptr_base = IR::ScalarReg::Max; s32 tess_consts_dword_offset = -1; @@ -206,6 +208,8 @@ struct Info { bool has_discard{}; bool has_image_gather{}; bool has_image_query{}; + bool has_perspective_interp{}; + bool has_linear_interp{}; bool uses_atomic_float_min_max{}; bool uses_lane_id{}; bool uses_group_quad{}; diff --git a/src/shader_recompiler/ir/attribute.h b/src/shader_recompiler/ir/attribute.h index 5117f5650..68472f052 100644 --- a/src/shader_recompiler/ir/attribute.h +++ b/src/shader_recompiler/ir/attribute.h @@ -83,6 +83,16 @@ enum class Attribute : u64 { Max, }; +enum class Interpolation { + Invalid = 0, + PerspectiveSample = 1, + PerspectiveCenter = 2, + PerspectiveCentroid = 3, + LinearSample = 4, + LinearCenter = 5, + LinearCentroid = 6, +}; + constexpr size_t NumAttributes = static_cast(Attribute::Max); constexpr size_t NumRenderTargets = 8; constexpr size_t NumParams = 32; @@ -104,6 +114,15 @@ constexpr bool IsMrt(Attribute attribute) noexcept { return attribute >= Attribute::RenderTarget0 && attribute <= Attribute::RenderTarget7; } +constexpr bool IsLinear(Interpolation interp) noexcept { + return interp >= Interpolation::LinearSample && interp <= Interpolation::LinearCentroid; +} + +constexpr bool IsPerspective(Interpolation interp) noexcept { + return interp >= Interpolation::PerspectiveSample && + interp <= Interpolation::PerspectiveCentroid; +} + [[nodiscard]] std::string NameOf(Attribute attribute); [[nodiscard]] constexpr Attribute operator+(Attribute attr, int num) { diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 07249edfe..e6cc32829 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include -#include #include #include #include "common/assert.h" diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index 7b9b81093..0e41f4b2d 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -6,7 +6,6 @@ #include #include -#include "shader_recompiler/info.h" #include "shader_recompiler/ir/attribute.h" #include "shader_recompiler/ir/basic_block.h" #include "shader_recompiler/ir/condition.h" @@ -17,6 +16,7 @@ namespace Shader::IR { class IREmitter { public: + explicit IREmitter() = default; explicit IREmitter(Block& block_) : block{&block_}, insertion_point{block->end()} {} explicit IREmitter(Block& block_, Block::iterator insertion_point_) : block{&block_}, insertion_point{insertion_point_} {} diff --git a/src/shader_recompiler/ir/reg.h b/src/shader_recompiler/ir/reg.h index 82aa436a7..c534eecd8 100644 --- a/src/shader_recompiler/ir/reg.h +++ b/src/shader_recompiler/ir/reg.h @@ -7,7 +7,7 @@ #include "common/bit_field.h" #include "common/enum.h" #include "common/types.h" -#include "video_core/amdgpu/types.h" +#include "video_core/amdgpu/pixel_format.h" namespace Shader::IR { diff --git a/src/video_core/amdgpu/pixel_format.h b/src/video_core/amdgpu/pixel_format.h index 38c81ba5f..faba8e285 100644 --- a/src/video_core/amdgpu/pixel_format.h +++ b/src/video_core/amdgpu/pixel_format.h @@ -5,34 +5,310 @@ #include #include +#include "common/assert.h" #include "common/types.h" -#include "video_core/amdgpu/types.h" namespace AmdGpu { -enum NumberClass { +// Table 8.13 Data and Image Formats [Sea Islands Series Instruction Set Architecture] +enum class DataFormat : u32 { + FormatInvalid = 0, + Format8 = 1, + Format16 = 2, + Format8_8 = 3, + Format32 = 4, + Format16_16 = 5, + Format10_11_11 = 6, + Format11_11_10 = 7, + Format10_10_10_2 = 8, + Format2_10_10_10 = 9, + Format8_8_8_8 = 10, + Format32_32 = 11, + Format16_16_16_16 = 12, + Format32_32_32 = 13, + Format32_32_32_32 = 14, + Format5_6_5 = 16, + Format1_5_5_5 = 17, + Format5_5_5_1 = 18, + Format4_4_4_4 = 19, + Format8_24 = 20, + Format24_8 = 21, + FormatX24_8_32 = 22, + FormatGB_GR = 32, + FormatBG_RG = 33, + Format5_9_9_9 = 34, + FormatBc1 = 35, + FormatBc2 = 36, + FormatBc3 = 37, + FormatBc4 = 38, + FormatBc5 = 39, + FormatBc6 = 40, + FormatBc7 = 41, + FormatFmask8_1 = 47, + FormatFmask8_2 = 48, + FormatFmask8_4 = 49, + FormatFmask16_1 = 50, + FormatFmask16_2 = 51, + FormatFmask32_2 = 52, + FormatFmask32_4 = 53, + FormatFmask32_8 = 54, + FormatFmask64_4 = 55, + FormatFmask64_8 = 56, + Format4_4 = 57, + Format6_5_5 = 58, + Format1 = 59, + Format1_Reversed = 60, + Format32_As_8 = 61, + Format32_As_8_8 = 62, + Format32_As_32_32_32_32 = 63, +}; + +enum class NumberFormat : u32 { + Unorm = 0, + Snorm = 1, + Uscaled = 2, + Sscaled = 3, + Uint = 4, + Sint = 5, + SnormNz = 6, + Float = 7, + Srgb = 9, + Ubnorm = 10, + UbnormNz = 11, + Ubint = 12, + Ubscaled = 13, +}; + +enum class NumberClass { Float, Sint, Uint, }; -[[nodiscard]] constexpr NumberClass GetNumberClass(const NumberFormat nfmt) { - switch (nfmt) { - case NumberFormat::Sint: - return Sint; - case NumberFormat::Uint: - return Uint; +enum class CompSwizzle : u8 { + Zero = 0, + One = 1, + Red = 4, + Green = 5, + Blue = 6, + Alpha = 7, +}; + +enum class NumberConversion : u32 { + None = 0, + UintToUscaled = 1, + SintToSscaled = 2, + UnormToUbnorm = 3, + Sint8ToSnormNz = 4, + Sint16ToSnormNz = 5, + Uint32ToUnorm = 6, +}; + +struct CompMapping { + CompSwizzle r; + CompSwizzle g; + CompSwizzle b; + CompSwizzle a; + + auto operator<=>(const CompMapping& other) const = default; + + template + [[nodiscard]] std::array Apply(const std::array& data) const { + return { + ApplySingle(data, r), + ApplySingle(data, g), + ApplySingle(data, b), + ApplySingle(data, a), + }; + } + + [[nodiscard]] CompMapping Inverse() const { + CompMapping result{}; + InverseSingle(result.r, CompSwizzle::Red); + InverseSingle(result.g, CompSwizzle::Green); + InverseSingle(result.b, CompSwizzle::Blue); + InverseSingle(result.a, CompSwizzle::Alpha); + return result; + } + +private: + template + T ApplySingle(const std::array& data, const CompSwizzle swizzle) const { + switch (swizzle) { + case CompSwizzle::Zero: + return T(0); + case CompSwizzle::One: + return T(1); + case CompSwizzle::Red: + return data[0]; + case CompSwizzle::Green: + return data[1]; + case CompSwizzle::Blue: + return data[2]; + case CompSwizzle::Alpha: + return data[3]; + default: + UNREACHABLE(); + } + } + + void InverseSingle(CompSwizzle& dst, const CompSwizzle target) const { + if (r == target) { + dst = CompSwizzle::Red; + } else if (g == target) { + dst = CompSwizzle::Green; + } else if (b == target) { + dst = CompSwizzle::Blue; + } else if (a == target) { + dst = CompSwizzle::Alpha; + } else { + dst = CompSwizzle::Zero; + } + } +}; + +static constexpr CompMapping IdentityMapping = { + .r = CompSwizzle::Red, + .g = CompSwizzle::Green, + .b = CompSwizzle::Blue, + .a = CompSwizzle::Alpha, +}; + +constexpr DataFormat RemapDataFormat(const DataFormat format) { + switch (format) { + case DataFormat::Format11_11_10: + return DataFormat::Format10_11_11; + case DataFormat::Format10_10_10_2: + return DataFormat::Format2_10_10_10; + case DataFormat::Format5_5_5_1: + return DataFormat::Format1_5_5_5; default: - return Float; + return format; } } -[[nodiscard]] constexpr bool IsInteger(const NumberFormat nfmt) { +constexpr NumberFormat RemapNumberFormat(const NumberFormat format, const DataFormat data_format) { + switch (format) { + case NumberFormat::Unorm: { + switch (data_format) { + case DataFormat::Format32: + case DataFormat::Format32_32: + case DataFormat::Format32_32_32: + case DataFormat::Format32_32_32_32: + return NumberFormat::Uint; + default: + return format; + } + } + case NumberFormat::Uscaled: + return NumberFormat::Uint; + case NumberFormat::Sscaled: + case NumberFormat::SnormNz: + return NumberFormat::Sint; + case NumberFormat::Ubnorm: + return NumberFormat::Unorm; + case NumberFormat::Float: + if (data_format == DataFormat::Format8) { + // Games may ask for 8-bit float when they want to access the stencil component + // of a depth-stencil image. Change to unsigned int to match the stencil format. + // This is also the closest approximation to pass the bits through unconverted. + return NumberFormat::Uint; + } + [[fallthrough]]; + default: + return format; + } +} + +constexpr CompMapping RemapSwizzle(const DataFormat format, const CompMapping swizzle) { + switch (format) { + case DataFormat::Format1_5_5_5: + case DataFormat::Format11_11_10: { + CompMapping result; + result.r = swizzle.b; + result.g = swizzle.g; + result.b = swizzle.r; + result.a = swizzle.a; + return result; + } + case DataFormat::Format10_10_10_2: { + CompMapping result; + result.r = swizzle.a; + result.g = swizzle.b; + result.b = swizzle.g; + result.a = swizzle.r; + return result; + } + case DataFormat::Format4_4_4_4: { + // Remap to a more supported component order. + CompMapping result; + result.r = swizzle.g; + result.g = swizzle.b; + result.b = swizzle.a; + result.a = swizzle.r; + return result; + } + default: + return swizzle; + } +} + +constexpr NumberConversion MapNumberConversion(const NumberFormat num_fmt, + const DataFormat data_fmt) { + switch (num_fmt) { + case NumberFormat::Unorm: { + switch (data_fmt) { + case DataFormat::Format32: + case DataFormat::Format32_32: + case DataFormat::Format32_32_32: + case DataFormat::Format32_32_32_32: + return NumberConversion::Uint32ToUnorm; + default: + return NumberConversion::None; + } + } + case NumberFormat::Uscaled: + return NumberConversion::UintToUscaled; + case NumberFormat::Sscaled: + return NumberConversion::SintToSscaled; + case NumberFormat::Ubnorm: + return NumberConversion::UnormToUbnorm; + case NumberFormat::SnormNz: { + switch (data_fmt) { + case DataFormat::Format8: + case DataFormat::Format8_8: + case DataFormat::Format8_8_8_8: + return NumberConversion::Sint8ToSnormNz; + case DataFormat::Format16: + case DataFormat::Format16_16: + case DataFormat::Format16_16_16_16: + return NumberConversion::Sint16ToSnormNz; + default: + UNREACHABLE_MSG("data_fmt = {}", u32(data_fmt)); + } + } + default: + return NumberConversion::None; + } +} + +constexpr NumberClass GetNumberClass(const NumberFormat nfmt) { + switch (nfmt) { + case NumberFormat::Sint: + return NumberClass::Sint; + case NumberFormat::Uint: + return NumberClass::Uint; + default: + return NumberClass::Float; + } +} + +constexpr bool IsInteger(const NumberFormat nfmt) { return nfmt == AmdGpu::NumberFormat::Sint || nfmt == AmdGpu::NumberFormat::Uint; } -[[nodiscard]] std::string_view NameOf(DataFormat fmt); -[[nodiscard]] std::string_view NameOf(NumberFormat fmt); +std::string_view NameOf(DataFormat fmt); +std::string_view NameOf(NumberFormat fmt); int NumComponents(DataFormat format); int NumBits(DataFormat format); diff --git a/src/video_core/amdgpu/resource.h b/src/video_core/amdgpu/resource.h index 89ac04f9a..5ede90200 100644 --- a/src/video_core/amdgpu/resource.h +++ b/src/video_core/amdgpu/resource.h @@ -6,7 +6,6 @@ #include "common/alignment.h" #include "common/assert.h" #include "common/bit_field.h" -#include "common/types.h" #include "video_core/amdgpu/pixel_format.h" namespace AmdGpu { diff --git a/src/video_core/amdgpu/types.h b/src/video_core/amdgpu/types.h index f7536f7e2..009fbbbb2 100644 --- a/src/video_core/amdgpu/types.h +++ b/src/video_core/amdgpu/types.h @@ -5,7 +5,6 @@ #include #include -#include "common/assert.h" #include "common/types.h" namespace AmdGpu { @@ -114,281 +113,6 @@ enum class GsOutputPrimitiveType : u32 { TriangleStrip = 2, }; -// Table 8.13 Data and Image Formats [Sea Islands Series Instruction Set Architecture] -enum class DataFormat : u32 { - FormatInvalid = 0, - Format8 = 1, - Format16 = 2, - Format8_8 = 3, - Format32 = 4, - Format16_16 = 5, - Format10_11_11 = 6, - Format11_11_10 = 7, - Format10_10_10_2 = 8, - Format2_10_10_10 = 9, - Format8_8_8_8 = 10, - Format32_32 = 11, - Format16_16_16_16 = 12, - Format32_32_32 = 13, - Format32_32_32_32 = 14, - Format5_6_5 = 16, - Format1_5_5_5 = 17, - Format5_5_5_1 = 18, - Format4_4_4_4 = 19, - Format8_24 = 20, - Format24_8 = 21, - FormatX24_8_32 = 22, - FormatGB_GR = 32, - FormatBG_RG = 33, - Format5_9_9_9 = 34, - FormatBc1 = 35, - FormatBc2 = 36, - FormatBc3 = 37, - FormatBc4 = 38, - FormatBc5 = 39, - FormatBc6 = 40, - FormatBc7 = 41, - FormatFmask8_1 = 47, - FormatFmask8_2 = 48, - FormatFmask8_4 = 49, - FormatFmask16_1 = 50, - FormatFmask16_2 = 51, - FormatFmask32_2 = 52, - FormatFmask32_4 = 53, - FormatFmask32_8 = 54, - FormatFmask64_4 = 55, - FormatFmask64_8 = 56, - Format4_4 = 57, - Format6_5_5 = 58, - Format1 = 59, - Format1_Reversed = 60, - Format32_As_8 = 61, - Format32_As_8_8 = 62, - Format32_As_32_32_32_32 = 63, -}; - -enum class NumberFormat : u32 { - Unorm = 0, - Snorm = 1, - Uscaled = 2, - Sscaled = 3, - Uint = 4, - Sint = 5, - SnormNz = 6, - Float = 7, - Srgb = 9, - Ubnorm = 10, - UbnormNz = 11, - Ubint = 12, - Ubscaled = 13, -}; - -enum class CompSwizzle : u8 { - Zero = 0, - One = 1, - Red = 4, - Green = 5, - Blue = 6, - Alpha = 7, -}; - -enum class NumberConversion : u32 { - None = 0, - UintToUscaled = 1, - SintToSscaled = 2, - UnormToUbnorm = 3, - Sint8ToSnormNz = 4, - Sint16ToSnormNz = 5, - Uint32ToUnorm = 6, -}; - -struct CompMapping { - CompSwizzle r; - CompSwizzle g; - CompSwizzle b; - CompSwizzle a; - - auto operator<=>(const CompMapping& other) const = default; - - template - [[nodiscard]] std::array Apply(const std::array& data) const { - return { - ApplySingle(data, r), - ApplySingle(data, g), - ApplySingle(data, b), - ApplySingle(data, a), - }; - } - - [[nodiscard]] CompMapping Inverse() const { - CompMapping result{}; - InverseSingle(result.r, CompSwizzle::Red); - InverseSingle(result.g, CompSwizzle::Green); - InverseSingle(result.b, CompSwizzle::Blue); - InverseSingle(result.a, CompSwizzle::Alpha); - return result; - } - -private: - template - T ApplySingle(const std::array& data, const CompSwizzle swizzle) const { - switch (swizzle) { - case CompSwizzle::Zero: - return T(0); - case CompSwizzle::One: - return T(1); - case CompSwizzle::Red: - return data[0]; - case CompSwizzle::Green: - return data[1]; - case CompSwizzle::Blue: - return data[2]; - case CompSwizzle::Alpha: - return data[3]; - default: - UNREACHABLE(); - } - } - - void InverseSingle(CompSwizzle& dst, const CompSwizzle target) const { - if (r == target) { - dst = CompSwizzle::Red; - } else if (g == target) { - dst = CompSwizzle::Green; - } else if (b == target) { - dst = CompSwizzle::Blue; - } else if (a == target) { - dst = CompSwizzle::Alpha; - } else { - dst = CompSwizzle::Zero; - } - } -}; - -static constexpr CompMapping IdentityMapping = { - .r = CompSwizzle::Red, - .g = CompSwizzle::Green, - .b = CompSwizzle::Blue, - .a = CompSwizzle::Alpha, -}; - -inline DataFormat RemapDataFormat(const DataFormat format) { - switch (format) { - case DataFormat::Format11_11_10: - return DataFormat::Format10_11_11; - case DataFormat::Format10_10_10_2: - return DataFormat::Format2_10_10_10; - case DataFormat::Format5_5_5_1: - return DataFormat::Format1_5_5_5; - default: - return format; - } -} - -inline NumberFormat RemapNumberFormat(const NumberFormat format, const DataFormat data_format) { - switch (format) { - case NumberFormat::Unorm: { - switch (data_format) { - case DataFormat::Format32: - case DataFormat::Format32_32: - case DataFormat::Format32_32_32: - case DataFormat::Format32_32_32_32: - return NumberFormat::Uint; - default: - return format; - } - } - case NumberFormat::Uscaled: - return NumberFormat::Uint; - case NumberFormat::Sscaled: - case NumberFormat::SnormNz: - return NumberFormat::Sint; - case NumberFormat::Ubnorm: - return NumberFormat::Unorm; - case NumberFormat::Float: - if (data_format == DataFormat::Format8) { - // Games may ask for 8-bit float when they want to access the stencil component - // of a depth-stencil image. Change to unsigned int to match the stencil format. - // This is also the closest approximation to pass the bits through unconverted. - return NumberFormat::Uint; - } - [[fallthrough]]; - default: - return format; - } -} - -inline CompMapping RemapSwizzle(const DataFormat format, const CompMapping swizzle) { - switch (format) { - case DataFormat::Format1_5_5_5: - case DataFormat::Format11_11_10: { - CompMapping result; - result.r = swizzle.b; - result.g = swizzle.g; - result.b = swizzle.r; - result.a = swizzle.a; - return result; - } - case DataFormat::Format10_10_10_2: { - CompMapping result; - result.r = swizzle.a; - result.g = swizzle.b; - result.b = swizzle.g; - result.a = swizzle.r; - return result; - } - case DataFormat::Format4_4_4_4: { - // Remap to a more supported component order. - CompMapping result; - result.r = swizzle.g; - result.g = swizzle.b; - result.b = swizzle.a; - result.a = swizzle.r; - return result; - } - default: - return swizzle; - } -} - -inline NumberConversion MapNumberConversion(const NumberFormat num_fmt, const DataFormat data_fmt) { - switch (num_fmt) { - case NumberFormat::Unorm: { - switch (data_fmt) { - case DataFormat::Format32: - case DataFormat::Format32_32: - case DataFormat::Format32_32_32: - case DataFormat::Format32_32_32_32: - return NumberConversion::Uint32ToUnorm; - default: - return NumberConversion::None; - } - } - case NumberFormat::Uscaled: - return NumberConversion::UintToUscaled; - case NumberFormat::Sscaled: - return NumberConversion::SintToSscaled; - case NumberFormat::Ubnorm: - return NumberConversion::UnormToUbnorm; - case NumberFormat::SnormNz: { - switch (data_fmt) { - case DataFormat::Format8: - case DataFormat::Format8_8: - case DataFormat::Format8_8_8_8: - return NumberConversion::Sint8ToSnormNz; - case DataFormat::Format16: - case DataFormat::Format16_16: - case DataFormat::Format16_16_16_16: - return NumberConversion::Sint16ToSnormNz; - default: - UNREACHABLE_MSG("data_fmt = {}", u32(data_fmt)); - } - } - default: - return NumberConversion::None; - } -} - } // namespace AmdGpu template <> From ce42eccc9d9b629b2e25296d42f7d686d53cfb25 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Sun, 8 Jun 2025 23:09:08 +0300 Subject: [PATCH 111/141] texture_cache: Handle compressed views of uncompressed images (#3056) * pixel_format: Remove unused tables, refactor * host_compatibilty: Cleanup and support uncompressed views of compressed formats * texture_cache: Handle compressed views of uncompressed images * tile_manager: Bump max supported mips to 16 Fixes a crash during start * oops * texture_cache: Fix order of format compat check --- CMakeLists.txt | 1 + .../ir/passes/lower_buffer_format_to_raw.cpp | 2 +- src/video_core/amdgpu/liverpool.h | 2 +- src/video_core/amdgpu/pixel_format.cpp | 214 +++++----- src/video_core/amdgpu/pixel_format.h | 6 +- .../host_shaders/detilers/micro_128bpp.comp | 2 +- .../host_shaders/detilers/micro_16bpp.comp | 2 +- .../host_shaders/detilers/micro_32bpp.comp | 2 +- .../host_shaders/detilers/micro_64bpp.comp | 2 +- .../host_shaders/detilers/micro_8bpp.comp | 2 +- .../texture_cache/host_compatibility.cpp | 220 ++++++++++ .../texture_cache/host_compatibility.h | 380 +----------------- src/video_core/texture_cache/image.cpp | 65 +-- src/video_core/texture_cache/image_info.cpp | 79 +++- src/video_core/texture_cache/image_info.h | 25 +- .../texture_cache/texture_cache.cpp | 25 +- src/video_core/texture_cache/tile_manager.cpp | 14 +- 17 files changed, 434 insertions(+), 609 deletions(-) create mode 100644 src/video_core/texture_cache/host_compatibility.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 20d33ac95..6dfe9348a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -964,6 +964,7 @@ set(VIDEO_CORE src/video_core/amdgpu/liverpool.cpp src/video_core/texture_cache/tile_manager.cpp src/video_core/texture_cache/tile_manager.h src/video_core/texture_cache/types.h + src/video_core/texture_cache/host_compatibility.cpp src/video_core/texture_cache/host_compatibility.h src/video_core/page_manager.cpp src/video_core/page_manager.h diff --git a/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp b/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp index 65be02541..fcb86e3fb 100644 --- a/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp +++ b/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp @@ -15,7 +15,7 @@ struct FormatInfo { AmdGpu::NumberFormat num_format; AmdGpu::CompMapping swizzle; AmdGpu::NumberConversion num_conversion; - int num_components; + u32 num_components; }; static bool IsBufferFormatLoad(const IR::Inst& inst) { diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index 245e34d35..2f33c7302 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -914,7 +914,7 @@ struct Liverpool { } size_t GetColorSliceSize() const { - const auto num_bytes_per_element = NumBits(info.format) / 8u; + const auto num_bytes_per_element = NumBitsPerBlock(info.format) / 8u; const auto slice_size = num_bytes_per_element * (slice.tile_max + 1) * 64u * NumSamples(); return slice_size; diff --git a/src/video_core/amdgpu/pixel_format.cpp b/src/video_core/amdgpu/pixel_format.cpp index 881c33e44..682cdf357 100644 --- a/src/video_core/amdgpu/pixel_format.cpp +++ b/src/video_core/amdgpu/pixel_format.cpp @@ -111,136 +111,106 @@ std::string_view NameOf(NumberFormat fmt) { } } -int NumComponents(DataFormat format) { - constexpr std::array num_components_per_element = { - 0, 1, 1, 2, 1, 2, 3, 3, 4, 4, 4, 2, 4, 3, 4, -1, 3, 4, 4, 4, 2, - 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, 3, 3, 3, 4, 4, 4, 1, 2, 3, 4, - -1, -1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 1, 1}; - - const u32 index = static_cast(format); - if (index >= num_components_per_element.size()) { - return 0; - } - return num_components_per_element[index]; -} - -int NumBits(DataFormat format) { - const std::array num_bits_per_element = { - 0, 8, 16, 16, 32, 32, 32, 32, 32, 32, 32, 64, 64, 96, 128, -1, 16, 16, 16, 16, 32, - 32, 64, -1, -1, -1, -1, -1, -1, -1, -1, -1, 16, 16, 32, 4, 8, 8, 4, 8, 8, 8, - -1, -1, 8, 8, 8, 8, 8, 8, 16, 16, 32, 32, 32, 64, 64, 8, 16, 1, 1}; - - const u32 index = static_cast(format); - if (index >= num_bits_per_element.size()) { - return 0; - } - return num_bits_per_element[index]; -} - -static constexpr std::array component_bits = { - std::array{0, 0, 0, 0}, // 0 FormatInvalid - std::array{8, 0, 0, 0}, // 1 Format8 - std::array{16, 0, 0, 0}, // 2 Format16 - std::array{8, 8, 0, 0}, // 3 Format8_8 - std::array{32, 0, 0, 0}, // 4 Format32 - std::array{16, 16, 0, 0}, // 5 Format16_16 - std::array{11, 11, 10, 0}, // 6 Format10_11_11 - std::array{10, 11, 11, 0}, // 7 Format11_11_10 - std::array{2, 10, 10, 10}, // 8 Format10_10_10_2 - std::array{10, 10, 10, 2}, // 9 Format2_10_10_10 - std::array{8, 8, 8, 8}, // 10 Format8_8_8_8 - std::array{32, 32, 0, 0}, // 11 Format32_32 - std::array{16, 16, 16, 16}, // 12 Format16_16_16_16 - std::array{32, 32, 32, 0}, // 13 Format32_32_32 - std::array{32, 32, 32, 32}, // 14 Format32_32_32_32 - std::array{0, 0, 0, 0}, // 15 - std::array{5, 6, 5, 0}, // 16 Format5_6_5 - std::array{5, 5, 5, 1}, // 17 Format1_5_5_5 - std::array{1, 5, 5, 5}, // 18 Format5_5_5_1 - std::array{4, 4, 4, 4}, // 19 Format4_4_4_4 - std::array{24, 8, 0, 0}, // 20 Format8_24 - std::array{8, 24, 0, 0}, // 21 Format24_8 - std::array{8, 24, 0, 0}, // 22 FormatX24_8_32 - std::array{0, 0, 0, 0}, // 23 - std::array{0, 0, 0, 0}, // 24 - std::array{0, 0, 0, 0}, // 25 - std::array{0, 0, 0, 0}, // 26 - std::array{0, 0, 0, 0}, // 27 - std::array{0, 0, 0, 0}, // 28 - std::array{0, 0, 0, 0}, // 29 - std::array{0, 0, 0, 0}, // 30 - std::array{0, 0, 0, 0}, // 31 - std::array{0, 0, 0, 0}, // 32 FormatGB_GR - std::array{0, 0, 0, 0}, // 33 FormatBG_RG - std::array{0, 0, 0, 0}, // 34 Format5_9_9_9 - std::array{0, 0, 0, 0}, // 35 FormatBc1 - std::array{0, 0, 0, 0}, // 36 FormatBc2 - std::array{0, 0, 0, 0}, // 37 FormatBc3 - std::array{0, 0, 0, 0}, // 38 FormatBc4 - std::array{0, 0, 0, 0}, // 39 FormatBc5 - std::array{0, 0, 0, 0}, // 40 FormatBc6 - std::array{0, 0, 0, 0}, // 41 FormatBc7 +static constexpr std::array NUM_COMPONENTS = { + 0, // 0 FormatInvalid + 1, // 1 Format8 + 1, // 2 Format16 + 2, // 3 Format8_8 + 1, // 4 Format32 + 2, // 5 Format16_16 + 3, // 6 Format10_11_11 + 3, // 7 Format11_11_10 + 4, // 8 Format10_10_10_2 + 4, // 9 Format2_10_10_10 + 4, // 10 Format8_8_8_8 + 2, // 11 Format32_32 + 4, // 12 Format16_16_16_16 + 3, // 13 Format32_32_32 + 4, // 14 Format32_32_32_32 + 0, // 15 + 3, // 16 Format5_6_5 + 4, // 17 Format1_5_5_5 + 4, // 18 Format5_5_5_1 + 4, // 19 Format4_4_4_4 + 2, // 20 Format8_24 + 2, // 21 Format24_8 + 2, // 22 FormatX24_8_32 + 0, // 23 + 0, // 24 + 0, // 25 + 0, // 26 + 0, // 27 + 0, // 28 + 0, // 29 + 0, // 30 + 0, // 31 + 3, // 32 FormatGB_GR + 3, // 33 FormatBG_RG + 4, // 34 Format5_9_9_9 + 4, // 35 FormatBc1 + 4, // 36 FormatBc2 + 4, // 37 FormatBc3 + 1, // 38 FormatBc4 + 2, // 39 FormatBc5 + 3, // 40 FormatBc6 + 4, // 41 FormatBc7 }; -u32 ComponentBits(DataFormat format, u32 comp) { +u32 NumComponents(DataFormat format) { const u32 index = static_cast(format); - if (index >= component_bits.size() || comp >= 4) { - return 0; - } - return component_bits[index][comp]; + ASSERT_MSG(index < NUM_COMPONENTS.size(), "Invalid data format = {}", format); + return NUM_COMPONENTS[index]; } -static constexpr std::array component_offset = { - std::array{-1, -1, -1, -1}, // 0 FormatInvalid - std::array{0, -1, -1, -1}, // 1 Format8 - std::array{0, -1, -1, -1}, // 2 Format16 - std::array{0, 8, -1, -1}, // 3 Format8_8 - std::array{0, -1, -1, -1}, // 4 Format32 - std::array{0, 16, -1, -1}, // 5 Format16_16 - std::array{0, 11, 22, -1}, // 6 Format10_11_11 - std::array{0, 10, 21, -1}, // 7 Format11_11_10 - std::array{0, 2, 12, 22}, // 8 Format10_10_10_2 - std::array{0, 10, 20, 30}, // 9 Format2_10_10_10 - std::array{0, 8, 16, 24}, // 10 Format8_8_8_8 - std::array{0, 32, -1, -1}, // 11 Format32_32 - std::array{0, 16, 32, 48}, // 12 Format16_16_16_16 - std::array{0, 32, 64, -1}, // 13 Format32_32_32 - std::array{0, 32, 64, 96}, // 14 Format32_32_32_32 - std::array{-1, -1, -1, -1}, // 15 - std::array{0, 5, 11, -1}, // 16 Format5_6_5 - std::array{0, 5, 10, 15}, // 17 Format1_5_5_5 - std::array{0, 1, 6, 11}, // 18 Format5_5_5_1 - std::array{0, 4, 8, 12}, // 19 Format4_4_4_4 - std::array{0, 24, -1, -1}, // 20 Format8_24 - std::array{0, 8, -1, -1}, // 21 Format24_8 - std::array{0, 8, -1, -1}, // 22 FormatX24_8_32 - std::array{-1, -1, -1, -1}, // 23 - std::array{-1, -1, -1, -1}, // 24 - std::array{-1, -1, -1, -1}, // 25 - std::array{-1, -1, -1, -1}, // 26 - std::array{-1, -1, -1, -1}, // 27 - std::array{-1, -1, -1, -1}, // 28 - std::array{-1, -1, -1, -1}, // 29 - std::array{-1, -1, -1, -1}, // 30 - std::array{-1, -1, -1, -1}, // 31 - std::array{-1, -1, -1, -1}, // 32 FormatGB_GR - std::array{-1, -1, -1, -1}, // 33 FormatBG_RG - std::array{-1, -1, -1, -1}, // 34 Format5_9_9_9 - std::array{-1, -1, -1, -1}, // 35 FormatBc1 - std::array{-1, -1, -1, -1}, // 36 FormatBc2 - std::array{-1, -1, -1, -1}, // 37 FormatBc3 - std::array{-1, -1, -1, -1}, // 38 FormatBc4 - std::array{-1, -1, -1, -1}, // 39 FormatBc5 - std::array{-1, -1, -1, -1}, // 40 FormatBc6 - std::array{-1, -1, -1, -1}, // 41 FormatBc7 +static constexpr std::array BITS_PER_BLOCK = { + 0, // 0 FormatInvalid + 8, // 1 Format8 + 16, // 2 Format16 + 16, // 3 Format8_8 + 32, // 4 Format32 + 32, // 5 Format16_16 + 32, // 6 Format10_11_11 + 32, // 7 Format11_11_10 + 32, // 8 Format10_10_10_2 + 32, // 9 Format2_10_10_10 + 32, // 10 Format8_8_8_8 + 64, // 11 Format32_32 + 64, // 12 Format16_16_16_16 + 96, // 13 Format32_32_32 + 128, // 14 Format32_32_32_32 + 0, // 15 + 16, // 16 Format5_6_5 + 16, // 17 Format1_5_5_5 + 16, // 18 Format5_5_5_1 + 16, // 19 Format4_4_4_4 + 32, // 20 Format8_24 + 32, // 21 Format24_8 + 64, // 22 FormatX24_8_32 + 0, // 23 + 0, // 24 + 0, // 25 + 0, // 26 + 0, // 27 + 0, // 28 + 0, // 29 + 0, // 30 + 0, // 31 + 16, // 32 FormatGB_GR + 16, // 33 FormatBG_RG + 32, // 34 Format5_9_9_9 + 64, // 35 FormatBc1 + 128, // 36 FormatBc2 + 128, // 37 FormatBc3 + 64, // 38 FormatBc4 + 128, // 39 FormatBc5 + 128, // 40 FormatBc6 + 128, // 41 FormatBc7 }; -s32 ComponentOffset(DataFormat format, u32 comp) { +u32 NumBitsPerBlock(DataFormat format) { const u32 index = static_cast(format); - if (index >= component_offset.size() || comp >= 4) { - return -1; - } - return component_offset[index][comp]; + ASSERT_MSG(index < BITS_PER_BLOCK.size(), "Invalid data format = {}", format); + return BITS_PER_BLOCK[index]; } } // namespace AmdGpu diff --git a/src/video_core/amdgpu/pixel_format.h b/src/video_core/amdgpu/pixel_format.h index faba8e285..bd0f778f4 100644 --- a/src/video_core/amdgpu/pixel_format.h +++ b/src/video_core/amdgpu/pixel_format.h @@ -310,10 +310,8 @@ constexpr bool IsInteger(const NumberFormat nfmt) { std::string_view NameOf(DataFormat fmt); std::string_view NameOf(NumberFormat fmt); -int NumComponents(DataFormat format); -int NumBits(DataFormat format); -u32 ComponentBits(DataFormat format, u32 comp); -s32 ComponentOffset(DataFormat format, u32 comp); +u32 NumComponents(DataFormat format); +u32 NumBitsPerBlock(DataFormat format); } // namespace AmdGpu diff --git a/src/video_core/host_shaders/detilers/micro_128bpp.comp b/src/video_core/host_shaders/detilers/micro_128bpp.comp index a09a0b4c4..a43073a8b 100644 --- a/src/video_core/host_shaders/detilers/micro_128bpp.comp +++ b/src/video_core/host_shaders/detilers/micro_128bpp.comp @@ -16,7 +16,7 @@ layout(push_constant) uniform image_info { uint num_levels; uint pitch; uint height; - uint sizes[14]; + uint sizes[16]; } info; // Inverse morton LUT, small enough to fit into K$ diff --git a/src/video_core/host_shaders/detilers/micro_16bpp.comp b/src/video_core/host_shaders/detilers/micro_16bpp.comp index 909a14acc..5f1240d64 100644 --- a/src/video_core/host_shaders/detilers/micro_16bpp.comp +++ b/src/video_core/host_shaders/detilers/micro_16bpp.comp @@ -18,7 +18,7 @@ layout(push_constant) uniform image_info { uint num_levels; uint pitch; uint height; - uint sizes[14]; + uint sizes[16]; } info; #define MICRO_TILE_DIM 8 diff --git a/src/video_core/host_shaders/detilers/micro_32bpp.comp b/src/video_core/host_shaders/detilers/micro_32bpp.comp index cdc8d0018..605523e4d 100644 --- a/src/video_core/host_shaders/detilers/micro_32bpp.comp +++ b/src/video_core/host_shaders/detilers/micro_32bpp.comp @@ -16,7 +16,7 @@ layout(push_constant) uniform image_info { uint num_levels; uint pitch; uint height; - uint sizes[14]; + uint sizes[16]; } info; // Inverse morton LUT, small enough to fit into K$ diff --git a/src/video_core/host_shaders/detilers/micro_64bpp.comp b/src/video_core/host_shaders/detilers/micro_64bpp.comp index c128ba5a1..1bca44067 100644 --- a/src/video_core/host_shaders/detilers/micro_64bpp.comp +++ b/src/video_core/host_shaders/detilers/micro_64bpp.comp @@ -16,7 +16,7 @@ layout(push_constant) uniform image_info { uint num_levels; uint pitch; uint height; - uint sizes[14]; + uint sizes[16]; } info; // Inverse morton LUT, small enough to fit into K$ diff --git a/src/video_core/host_shaders/detilers/micro_8bpp.comp b/src/video_core/host_shaders/detilers/micro_8bpp.comp index ecf706450..1d9b48daa 100644 --- a/src/video_core/host_shaders/detilers/micro_8bpp.comp +++ b/src/video_core/host_shaders/detilers/micro_8bpp.comp @@ -19,7 +19,7 @@ layout(push_constant) uniform image_info { uint num_levels; uint pitch; uint height; - uint sizes[14]; + uint sizes[16]; } info; #define MICRO_TILE_DIM 8 diff --git a/src/video_core/texture_cache/host_compatibility.cpp b/src/video_core/texture_cache/host_compatibility.cpp new file mode 100644 index 000000000..327709e64 --- /dev/null +++ b/src/video_core/texture_cache/host_compatibility.cpp @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/) +// Copyright © 2015-2023 The Khronos Group Inc. +// Copyright © 2015-2023 Valve Corporation +// Copyright © 2015-2023 LunarG, Inc. + +#include +#include "common/enum.h" +#include "video_core/texture_cache/host_compatibility.h" + +namespace VideoCore { + +/** + * @brief All classes of format compatibility according to the Vulkan specification + * @url + * https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/d37c676f/layers/generated/vk_format_utils.h#L47-L131 + */ +enum class CompatibilityClass { + NONE = 0, + _128BIT = 1 << 0, + _16BIT = 1 << 1, + _192BIT = 1 << 2, + _24BIT = 1 << 3, + _256BIT = 1 << 4, + _32BIT = 1 << 5, + _48BIT = 1 << 6, + _64BIT = 1 << 7, + _8BIT = 1 << 8, + _96BIT = 1 << 9, + BC1_RGB = 1 << 10, + BC1_RGBA = 1 << 11, + BC2 = 1 << 12, + BC3 = 1 << 13, + BC4 = 1 << 14, + BC5 = 1 << 15, + BC6H = 1 << 16, + BC7 = 1 << 17, + D16 = 1 << 18, + D16S8 = 1 << 19, + D24 = 1 << 20, + D24S8 = 1 << 21, + D32 = 1 << 22, + D32S8 = 1 << 23, + S8 = 1 << 24, +}; +DECLARE_ENUM_FLAG_OPERATORS(CompatibilityClass) + +/** + * @brief The format compatibility class according to the Vulkan specification + * @url + * https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-compatibility-classes + * @url + * https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/d37c676f/layers/generated/vk_format_utils.cpp#L70-L812 + */ +static const std::unordered_map FORMAT_TABLE = { + {vk::Format::eA1R5G5B5UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eA2B10G10R10SintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2B10G10R10SnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2B10G10R10SscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2B10G10R10UintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2B10G10R10UnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2B10G10R10UscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10SintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10SnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10SscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10UintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10UnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA2R10G10B10UscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA4B4G4R4UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eA4R4G4B4UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eA8B8G8R8SintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8SnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8SrgbPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8SscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8UintPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8UnormPack32, CompatibilityClass::_32BIT}, + {vk::Format::eA8B8G8R8UscaledPack32, CompatibilityClass::_32BIT}, + {vk::Format::eB10G11R11UfloatPack32, CompatibilityClass::_32BIT}, + {vk::Format::eB4G4R4A4UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eB5G5R5A1UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eB5G6R5UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eB8G8R8A8Sint, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Snorm, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Srgb, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Sscaled, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Uint, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Unorm, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8A8Uscaled, CompatibilityClass::_32BIT}, + {vk::Format::eB8G8R8Sint, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Snorm, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Srgb, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Sscaled, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Uint, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Unorm, CompatibilityClass::_24BIT}, + {vk::Format::eB8G8R8Uscaled, CompatibilityClass::_24BIT}, + {vk::Format::eBc1RgbaSrgbBlock, CompatibilityClass::BC1_RGBA | CompatibilityClass::_64BIT}, + {vk::Format::eBc1RgbaUnormBlock, CompatibilityClass::BC1_RGBA | CompatibilityClass::_64BIT}, + {vk::Format::eBc1RgbSrgbBlock, CompatibilityClass::BC1_RGB | CompatibilityClass::_64BIT}, + {vk::Format::eBc1RgbUnormBlock, CompatibilityClass::BC1_RGB | CompatibilityClass::_64BIT}, + {vk::Format::eBc2SrgbBlock, CompatibilityClass::BC2 | CompatibilityClass::_128BIT}, + {vk::Format::eBc2UnormBlock, CompatibilityClass::BC2 | CompatibilityClass::_128BIT}, + {vk::Format::eBc3SrgbBlock, CompatibilityClass::BC3 | CompatibilityClass::_128BIT}, + {vk::Format::eBc3UnormBlock, CompatibilityClass::BC3 | CompatibilityClass::_128BIT}, + {vk::Format::eBc4SnormBlock, CompatibilityClass::BC4 | CompatibilityClass::_64BIT}, + {vk::Format::eBc4UnormBlock, CompatibilityClass::BC4 | CompatibilityClass::_64BIT}, + {vk::Format::eBc5SnormBlock, CompatibilityClass::BC5 | CompatibilityClass::_128BIT}, + {vk::Format::eBc5UnormBlock, CompatibilityClass::BC5 | CompatibilityClass::_128BIT}, + {vk::Format::eBc6HSfloatBlock, CompatibilityClass::BC6H | CompatibilityClass::_128BIT}, + {vk::Format::eBc6HUfloatBlock, CompatibilityClass::BC6H | CompatibilityClass::_128BIT}, + {vk::Format::eBc7SrgbBlock, CompatibilityClass::BC7 | CompatibilityClass::_128BIT}, + {vk::Format::eBc7UnormBlock, CompatibilityClass::BC7 | CompatibilityClass::_128BIT}, + {vk::Format::eD16Unorm, CompatibilityClass::D16}, + {vk::Format::eD16UnormS8Uint, CompatibilityClass::D16S8}, + {vk::Format::eD24UnormS8Uint, CompatibilityClass::D24S8}, + {vk::Format::eD32Sfloat, CompatibilityClass::D32}, + {vk::Format::eD32SfloatS8Uint, CompatibilityClass::D32S8}, + {vk::Format::eE5B9G9R9UfloatPack32, CompatibilityClass::_32BIT}, + {vk::Format::eR10X6G10X6Unorm2Pack16, CompatibilityClass::_32BIT}, + {vk::Format::eR10X6UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eR12X4G12X4Unorm2Pack16, CompatibilityClass::_32BIT}, + {vk::Format::eR12X4UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eR16G16B16A16Sfloat, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Sint, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Snorm, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Sscaled, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Uint, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Unorm, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16A16Uscaled, CompatibilityClass::_64BIT}, + {vk::Format::eR16G16B16Sfloat, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Sint, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Snorm, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Sscaled, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Uint, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Unorm, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16B16Uscaled, CompatibilityClass::_48BIT}, + {vk::Format::eR16G16Sfloat, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Sint, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Snorm, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Sscaled, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Uint, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Unorm, CompatibilityClass::_32BIT}, + {vk::Format::eR16G16Uscaled, CompatibilityClass::_32BIT}, + {vk::Format::eR16Sfloat, CompatibilityClass::_16BIT}, + {vk::Format::eR16Sint, CompatibilityClass::_16BIT}, + {vk::Format::eR16Snorm, CompatibilityClass::_16BIT}, + {vk::Format::eR16Sscaled, CompatibilityClass::_16BIT}, + {vk::Format::eR16Uint, CompatibilityClass::_16BIT}, + {vk::Format::eR16Unorm, CompatibilityClass::_16BIT}, + {vk::Format::eR16Uscaled, CompatibilityClass::_16BIT}, + {vk::Format::eR32G32B32A32Sfloat, CompatibilityClass::_128BIT}, + {vk::Format::eR32G32B32A32Sint, CompatibilityClass::_128BIT}, + {vk::Format::eR32G32B32A32Uint, CompatibilityClass::_128BIT}, + {vk::Format::eR32G32B32Sfloat, CompatibilityClass::_96BIT}, + {vk::Format::eR32G32B32Sint, CompatibilityClass::_96BIT}, + {vk::Format::eR32G32B32Uint, CompatibilityClass::_96BIT}, + {vk::Format::eR32G32Sfloat, CompatibilityClass::_64BIT}, + {vk::Format::eR32G32Sint, CompatibilityClass::_64BIT}, + {vk::Format::eR32G32Uint, CompatibilityClass::_64BIT}, + {vk::Format::eR32Sfloat, CompatibilityClass::_32BIT}, + {vk::Format::eR32Sint, CompatibilityClass::_32BIT}, + {vk::Format::eR32Uint, CompatibilityClass::_32BIT}, + {vk::Format::eR4G4B4A4UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eR4G4UnormPack8, CompatibilityClass::_8BIT}, + {vk::Format::eR5G5B5A1UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eR5G6B5UnormPack16, CompatibilityClass::_16BIT}, + {vk::Format::eR64G64B64A64Sfloat, CompatibilityClass::_256BIT}, + {vk::Format::eR64G64B64A64Sint, CompatibilityClass::_256BIT}, + {vk::Format::eR64G64B64A64Uint, CompatibilityClass::_256BIT}, + {vk::Format::eR64G64B64Sfloat, CompatibilityClass::_192BIT}, + {vk::Format::eR64G64B64Sint, CompatibilityClass::_192BIT}, + {vk::Format::eR64G64B64Uint, CompatibilityClass::_192BIT}, + {vk::Format::eR64G64Sfloat, CompatibilityClass::_128BIT}, + {vk::Format::eR64G64Sint, CompatibilityClass::_128BIT}, + {vk::Format::eR64G64Uint, CompatibilityClass::_128BIT}, + {vk::Format::eR64Sfloat, CompatibilityClass::_64BIT}, + {vk::Format::eR64Sint, CompatibilityClass::_64BIT}, + {vk::Format::eR64Uint, CompatibilityClass::_64BIT}, + {vk::Format::eR8G8B8A8Sint, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Snorm, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Srgb, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Sscaled, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Uint, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Unorm, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8A8Uscaled, CompatibilityClass::_32BIT}, + {vk::Format::eR8G8B8Sint, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Snorm, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Srgb, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Sscaled, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Uint, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Unorm, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8B8Uscaled, CompatibilityClass::_24BIT}, + {vk::Format::eR8G8Sint, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Snorm, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Srgb, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Sscaled, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Uint, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Unorm, CompatibilityClass::_16BIT}, + {vk::Format::eR8G8Uscaled, CompatibilityClass::_16BIT}, + {vk::Format::eR8Sint, CompatibilityClass::_8BIT}, + {vk::Format::eR8Snorm, CompatibilityClass::_8BIT}, + {vk::Format::eR8Srgb, CompatibilityClass::_8BIT}, + {vk::Format::eR8Sscaled, CompatibilityClass::_8BIT}, + {vk::Format::eR8Uint, CompatibilityClass::_8BIT}, + {vk::Format::eR8Unorm, CompatibilityClass::_8BIT}, + {vk::Format::eR8Uscaled, CompatibilityClass::_8BIT}, + {vk::Format::eS8Uint, CompatibilityClass::S8}, + {vk::Format::eX8D24UnormPack32, CompatibilityClass::D24}, + {vk::Format::eUndefined, CompatibilityClass::NONE}, +}; + +bool IsVulkanFormatCompatible(vk::Format base, vk::Format view) { + if (base == view) { + return true; + } + const auto base_comp = FORMAT_TABLE.at(base); + const auto view_comp = FORMAT_TABLE.at(view); + return (base_comp & view_comp) == view_comp; +} + +} // namespace VideoCore diff --git a/src/video_core/texture_cache/host_compatibility.h b/src/video_core/texture_cache/host_compatibility.h index a73f7e6be..b0579137b 100644 --- a/src/video_core/texture_cache/host_compatibility.h +++ b/src/video_core/texture_cache/host_compatibility.h @@ -6,387 +6,11 @@ #pragma once -#include #include "video_core/renderer_vulkan/vk_common.h" namespace VideoCore { -/** - * @brief All classes of format compatibility according to the Vulkan specification - * @url - * https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/d37c676f75f545a3e5a98d7dfb89864391a1db1e/layers/generated/vk_format_utils.h#L47-L131 - * @note This is copied directly from Vulkan Validation Layers and doesn't follow the Skyline naming - * conventions - */ -enum class FORMAT_COMPATIBILITY_CLASS { - NONE = 0, - _10BIT_2PLANE_420, - _10BIT_2PLANE_422, - _10BIT_2PLANE_444, - _10BIT_3PLANE_420, - _10BIT_3PLANE_422, - _10BIT_3PLANE_444, - _12BIT_2PLANE_420, - _12BIT_2PLANE_422, - _12BIT_2PLANE_444, - _12BIT_3PLANE_420, - _12BIT_3PLANE_422, - _12BIT_3PLANE_444, - _128BIT, - _16BIT, - _16BIT_2PLANE_420, - _16BIT_2PLANE_422, - _16BIT_2PLANE_444, - _16BIT_3PLANE_420, - _16BIT_3PLANE_422, - _16BIT_3PLANE_444, - _192BIT, - _24BIT, - _256BIT, - _32BIT, - _32BIT_B8G8R8G8, - _32BIT_G8B8G8R8, - _48BIT, - _64BIT, - _64BIT_B10G10R10G10, - _64BIT_B12G12R12G12, - _64BIT_B16G16R16G16, - _64BIT_G10B10G10R10, - _64BIT_G12B12G12R12, - _64BIT_G16B16G16R16, - _64BIT_R10G10B10A10, - _64BIT_R12G12B12A12, - _8BIT, - _8BIT_2PLANE_420, - _8BIT_2PLANE_422, - _8BIT_2PLANE_444, - _8BIT_3PLANE_420, - _8BIT_3PLANE_422, - _8BIT_3PLANE_444, - _96BIT, - ASTC_10X10, - ASTC_10X5, - ASTC_10X6, - ASTC_10X8, - ASTC_12X10, - ASTC_12X12, - ASTC_4X4, - ASTC_5X4, - ASTC_5X5, - ASTC_6X5, - ASTC_6X6, - ASTC_8X5, - ASTC_8X6, - ASTC_8X8, - BC1_RGB, - BC1_RGBA, - BC2, - BC3, - BC4, - BC5, - BC6H, - BC7, - D16, - D16S8, - D24, - D24S8, - D32, - D32S8, - EAC_R, - EAC_RG, - ETC2_EAC_RGBA, - ETC2_RGB, - ETC2_RGBA, - PVRTC1_2BPP, - PVRTC1_4BPP, - PVRTC2_2BPP, - PVRTC2_4BPP, - S8 -}; -/** - * @brief The format compatibility class according to the Vulkan specification - * @url - * https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-compatibility-classes - * @url - * https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/d37c676f75f545a3e5a98d7dfb89864391a1db1e/layers/generated/vk_format_utils.cpp#L70-L812 - * @note This is copied directly from Vulkan Validation Layers and doesn't follow the Skyline naming - * conventions - */ -static const std::unordered_map vkFormatClassTable{ - {VK_FORMAT_A1R5G5B5_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_A2B10G10R10_SINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2B10G10R10_SNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2B10G10R10_SSCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2B10G10R10_UINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2B10G10R10_UNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2B10G10R10_USCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_SINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_SNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_SSCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_UINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_UNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A2R10G10B10_USCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A4B4G4R4_UNORM_PACK16_EXT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_A4R4G4B4_UNORM_PACK16_EXT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_A8B8G8R8_SINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_SNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_SRGB_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_SSCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_UINT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_UNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_A8B8G8R8_USCALED_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_ASTC_10x10_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_10X10}, - {VK_FORMAT_ASTC_10x10_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X10}, - {VK_FORMAT_ASTC_10x10_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X10}, - {VK_FORMAT_ASTC_10x5_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_10X5}, - {VK_FORMAT_ASTC_10x5_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X5}, - {VK_FORMAT_ASTC_10x5_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X5}, - {VK_FORMAT_ASTC_10x6_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_10X6}, - {VK_FORMAT_ASTC_10x6_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X6}, - {VK_FORMAT_ASTC_10x6_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X6}, - {VK_FORMAT_ASTC_10x8_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_10X8}, - {VK_FORMAT_ASTC_10x8_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X8}, - {VK_FORMAT_ASTC_10x8_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_10X8}, - {VK_FORMAT_ASTC_12x10_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_12X10}, - {VK_FORMAT_ASTC_12x10_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_12X10}, - {VK_FORMAT_ASTC_12x10_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_12X10}, - {VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_12X12}, - {VK_FORMAT_ASTC_12x12_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_12X12}, - {VK_FORMAT_ASTC_12x12_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_12X12}, - {VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_4X4}, - {VK_FORMAT_ASTC_4x4_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_4X4}, - {VK_FORMAT_ASTC_4x4_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_4X4}, - {VK_FORMAT_ASTC_5x4_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_5X4}, - {VK_FORMAT_ASTC_5x4_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_5X4}, - {VK_FORMAT_ASTC_5x4_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_5X4}, - {VK_FORMAT_ASTC_5x5_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_5X5}, - {VK_FORMAT_ASTC_5x5_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_5X5}, - {VK_FORMAT_ASTC_5x5_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_5X5}, - {VK_FORMAT_ASTC_6x5_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_6X5}, - {VK_FORMAT_ASTC_6x5_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_6X5}, - {VK_FORMAT_ASTC_6x5_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_6X5}, - {VK_FORMAT_ASTC_6x6_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_6X6}, - {VK_FORMAT_ASTC_6x6_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_6X6}, - {VK_FORMAT_ASTC_6x6_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_6X6}, - {VK_FORMAT_ASTC_8x5_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_8X5}, - {VK_FORMAT_ASTC_8x5_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X5}, - {VK_FORMAT_ASTC_8x5_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X5}, - {VK_FORMAT_ASTC_8x6_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_8X6}, - {VK_FORMAT_ASTC_8x6_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X6}, - {VK_FORMAT_ASTC_8x6_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X6}, - {VK_FORMAT_ASTC_8x8_SFLOAT_BLOCK_EXT, FORMAT_COMPATIBILITY_CLASS::ASTC_8X8}, - {VK_FORMAT_ASTC_8x8_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X8}, - {VK_FORMAT_ASTC_8x8_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ASTC_8X8}, - {VK_FORMAT_B10G11R11_UFLOAT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B10X6G10X6R10X6G10X6_422_UNORM_4PACK16, - FORMAT_COMPATIBILITY_CLASS::_64BIT_B10G10R10G10}, - {VK_FORMAT_B12X4G12X4R12X4G12X4_422_UNORM_4PACK16, - FORMAT_COMPATIBILITY_CLASS::_64BIT_B12G12R12G12}, - {VK_FORMAT_B16G16R16G16_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_64BIT_B16G16R16G16}, - {VK_FORMAT_B4G4R4A4_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_B5G5R5A1_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_B5G6R5_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_B8G8R8A8_SINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_SNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_SRGB, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_UINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_UNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8A8_USCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_B8G8R8G8_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT_B8G8R8G8}, - {VK_FORMAT_B8G8R8_SINT, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_SNORM, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_SRGB, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_UINT, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_UNORM, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_B8G8R8_USCALED, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_BC1_RGBA_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC1_RGBA}, - {VK_FORMAT_BC1_RGBA_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC1_RGBA}, - {VK_FORMAT_BC1_RGB_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC1_RGB}, - {VK_FORMAT_BC1_RGB_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC1_RGB}, - {VK_FORMAT_BC2_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC2}, - {VK_FORMAT_BC2_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC2}, - {VK_FORMAT_BC3_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC3}, - {VK_FORMAT_BC3_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC3}, - {VK_FORMAT_BC4_SNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC4}, - {VK_FORMAT_BC4_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC4}, - {VK_FORMAT_BC5_SNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC5}, - {VK_FORMAT_BC5_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC5}, - {VK_FORMAT_BC6H_SFLOAT_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC6H}, - {VK_FORMAT_BC6H_UFLOAT_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC6H}, - {VK_FORMAT_BC7_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC7}, - {VK_FORMAT_BC7_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::BC7}, - {VK_FORMAT_D16_UNORM, FORMAT_COMPATIBILITY_CLASS::D16}, - {VK_FORMAT_D16_UNORM_S8_UINT, FORMAT_COMPATIBILITY_CLASS::D16S8}, - {VK_FORMAT_D24_UNORM_S8_UINT, FORMAT_COMPATIBILITY_CLASS::D24S8}, - {VK_FORMAT_D32_SFLOAT, FORMAT_COMPATIBILITY_CLASS::D32}, - {VK_FORMAT_D32_SFLOAT_S8_UINT, FORMAT_COMPATIBILITY_CLASS::D32S8}, - {VK_FORMAT_E5B9G9R9_UFLOAT_PACK32, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_EAC_R11G11_SNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::EAC_RG}, - {VK_FORMAT_EAC_R11G11_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::EAC_RG}, - {VK_FORMAT_EAC_R11_SNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::EAC_R}, - {VK_FORMAT_EAC_R11_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::EAC_R}, - {VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_RGBA}, - {VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_RGBA}, - {VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_EAC_RGBA}, - {VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_EAC_RGBA}, - {VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_RGB}, - {VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK, FORMAT_COMPATIBILITY_CLASS::ETC2_RGB}, - {VK_FORMAT_G10X6B10X6G10X6R10X6_422_UNORM_4PACK16, - FORMAT_COMPATIBILITY_CLASS::_64BIT_G10B10G10R10}, - {VK_FORMAT_G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_10BIT_2PLANE_420}, - {VK_FORMAT_G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_10BIT_2PLANE_422}, - {VK_FORMAT_G10X6_B10X6R10X6_2PLANE_444_UNORM_3PACK16_EXT, - FORMAT_COMPATIBILITY_CLASS::_10BIT_2PLANE_444}, - {VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_10BIT_3PLANE_420}, - {VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_10BIT_3PLANE_422}, - {VK_FORMAT_G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_10BIT_3PLANE_444}, - {VK_FORMAT_G12X4B12X4G12X4R12X4_422_UNORM_4PACK16, - FORMAT_COMPATIBILITY_CLASS::_64BIT_G12B12G12R12}, - {VK_FORMAT_G12X4_B12X4R12X4_2PLANE_420_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_12BIT_2PLANE_420}, - {VK_FORMAT_G12X4_B12X4R12X4_2PLANE_422_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_12BIT_2PLANE_422}, - {VK_FORMAT_G12X4_B12X4R12X4_2PLANE_444_UNORM_3PACK16_EXT, - FORMAT_COMPATIBILITY_CLASS::_12BIT_2PLANE_444}, - {VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_12BIT_3PLANE_420}, - {VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_422_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_12BIT_3PLANE_422}, - {VK_FORMAT_G12X4_B12X4_R12X4_3PLANE_444_UNORM_3PACK16, - FORMAT_COMPATIBILITY_CLASS::_12BIT_3PLANE_444}, - {VK_FORMAT_G16B16G16R16_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_64BIT_G16B16G16R16}, - {VK_FORMAT_G16_B16R16_2PLANE_420_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT_2PLANE_420}, - {VK_FORMAT_G16_B16R16_2PLANE_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT_2PLANE_422}, - {VK_FORMAT_G16_B16R16_2PLANE_444_UNORM_EXT, FORMAT_COMPATIBILITY_CLASS::_16BIT_2PLANE_444}, - {VK_FORMAT_G16_B16_R16_3PLANE_420_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT_3PLANE_420}, - {VK_FORMAT_G16_B16_R16_3PLANE_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT_3PLANE_422}, - {VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT_3PLANE_444}, - {VK_FORMAT_G8B8G8R8_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT_G8B8G8R8}, - {VK_FORMAT_G8_B8R8_2PLANE_420_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT_2PLANE_420}, - {VK_FORMAT_G8_B8R8_2PLANE_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT_2PLANE_422}, - {VK_FORMAT_G8_B8R8_2PLANE_444_UNORM_EXT, FORMAT_COMPATIBILITY_CLASS::_8BIT_2PLANE_444}, - {VK_FORMAT_G8_B8_R8_3PLANE_420_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT_3PLANE_420}, - {VK_FORMAT_G8_B8_R8_3PLANE_422_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT_3PLANE_422}, - {VK_FORMAT_G8_B8_R8_3PLANE_444_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT_3PLANE_444}, - {VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC1_2BPP}, - {VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC1_2BPP}, - {VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC1_4BPP}, - {VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC1_4BPP}, - {VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC2_2BPP}, - {VK_FORMAT_PVRTC2_2BPP_UNORM_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC2_2BPP}, - {VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC2_4BPP}, - {VK_FORMAT_PVRTC2_4BPP_UNORM_BLOCK_IMG, FORMAT_COMPATIBILITY_CLASS::PVRTC2_4BPP}, - {VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16, FORMAT_COMPATIBILITY_CLASS::_64BIT_R10G10B10A10}, - {VK_FORMAT_R10X6G10X6_UNORM_2PACK16, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R10X6_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R12X4G12X4B12X4A12X4_UNORM_4PACK16, FORMAT_COMPATIBILITY_CLASS::_64BIT_R12G12B12A12}, - {VK_FORMAT_R12X4G12X4_UNORM_2PACK16, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R12X4_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16G16B16A16_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_SINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_SNORM, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_SSCALED, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_UINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_UNORM, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16A16_USCALED, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R16G16B16_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_SINT, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_SNORM, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_SSCALED, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_UINT, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_UNORM, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16B16_USCALED, FORMAT_COMPATIBILITY_CLASS::_48BIT}, - {VK_FORMAT_R16G16_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_SINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_SNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_SSCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_UINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_UNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16G16_USCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R16_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_SINT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_SNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_SSCALED, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_UINT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R16_USCALED, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R32G32B32A32_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R32G32B32A32_SINT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R32G32B32A32_UINT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R32G32B32_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_96BIT}, - {VK_FORMAT_R32G32B32_SINT, FORMAT_COMPATIBILITY_CLASS::_96BIT}, - {VK_FORMAT_R32G32B32_UINT, FORMAT_COMPATIBILITY_CLASS::_96BIT}, - {VK_FORMAT_R32G32_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R32G32_SINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R32G32_UINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R32_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R32_SINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R32_UINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R4G4B4A4_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R4G4_UNORM_PACK8, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R5G5B5A1_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R5G6B5_UNORM_PACK16, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R64G64B64A64_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_256BIT}, - {VK_FORMAT_R64G64B64A64_SINT, FORMAT_COMPATIBILITY_CLASS::_256BIT}, - {VK_FORMAT_R64G64B64A64_UINT, FORMAT_COMPATIBILITY_CLASS::_256BIT}, - {VK_FORMAT_R64G64B64_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_192BIT}, - {VK_FORMAT_R64G64B64_SINT, FORMAT_COMPATIBILITY_CLASS::_192BIT}, - {VK_FORMAT_R64G64B64_UINT, FORMAT_COMPATIBILITY_CLASS::_192BIT}, - {VK_FORMAT_R64G64_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R64G64_SINT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R64G64_UINT, FORMAT_COMPATIBILITY_CLASS::_128BIT}, - {VK_FORMAT_R64_SFLOAT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R64_SINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R64_UINT, FORMAT_COMPATIBILITY_CLASS::_64BIT}, - {VK_FORMAT_R8G8B8A8_SINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_SNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_SRGB, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_UINT, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_UNORM, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8A8_USCALED, FORMAT_COMPATIBILITY_CLASS::_32BIT}, - {VK_FORMAT_R8G8B8_SINT, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_SNORM, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_SRGB, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_UINT, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_UNORM, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8B8_USCALED, FORMAT_COMPATIBILITY_CLASS::_24BIT}, - {VK_FORMAT_R8G8_SINT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_SNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_SRGB, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_UINT, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_UNORM, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8G8_USCALED, FORMAT_COMPATIBILITY_CLASS::_16BIT}, - {VK_FORMAT_R8_SINT, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_SNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_SRGB, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_SSCALED, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_UINT, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_UNORM, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_R8_USCALED, FORMAT_COMPATIBILITY_CLASS::_8BIT}, - {VK_FORMAT_S8_UINT, FORMAT_COMPATIBILITY_CLASS::S8}, - {VK_FORMAT_X8_D24_UNORM_PACK32, FORMAT_COMPATIBILITY_CLASS::D24}, - {VK_FORMAT_UNDEFINED, FORMAT_COMPATIBILITY_CLASS::NONE}, -}; +/// Returns true if the two formats are compatible according to Vulkan's format compatibility rules +bool IsVulkanFormatCompatible(vk::Format base, vk::Format view); -/** - * @return If the two formats are compatible according to Vulkan's format compatibility rules - * @url - * https://registry.khronos.org/vulkan/specs/1.3-extensions/html/vkspec.html#formats-compatibility - */ -static bool IsVulkanFormatCompatible(vk::Format lhs, vk::Format rhs) { - if (lhs == rhs) { - return true; - } - return vkFormatClassTable.at(VkFormat(lhs)) == vkFormatClassTable.at(VkFormat(rhs)); -} } // namespace VideoCore diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 522e6fd5b..8522f1307 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -14,62 +14,6 @@ namespace VideoCore { using namespace Vulkan; -bool ImageInfo::IsBlockCoded() const { - switch (pixel_format) { - case vk::Format::eBc1RgbaSrgbBlock: - case vk::Format::eBc1RgbaUnormBlock: - case vk::Format::eBc1RgbSrgbBlock: - case vk::Format::eBc1RgbUnormBlock: - case vk::Format::eBc2SrgbBlock: - case vk::Format::eBc2UnormBlock: - case vk::Format::eBc3SrgbBlock: - case vk::Format::eBc3UnormBlock: - case vk::Format::eBc4SnormBlock: - case vk::Format::eBc4UnormBlock: - case vk::Format::eBc5SnormBlock: - case vk::Format::eBc5UnormBlock: - case vk::Format::eBc6HSfloatBlock: - case vk::Format::eBc6HUfloatBlock: - case vk::Format::eBc7SrgbBlock: - case vk::Format::eBc7UnormBlock: - return true; - default: - return false; - } -} - -bool ImageInfo::IsPacked() const { - switch (pixel_format) { - case vk::Format::eB5G5R5A1UnormPack16: - [[fallthrough]]; - case vk::Format::eB5G6R5UnormPack16: - return true; - default: - return false; - } -} - -bool ImageInfo::IsDepthStencil() const { - switch (pixel_format) { - case vk::Format::eD16Unorm: - case vk::Format::eD16UnormS8Uint: - case vk::Format::eD32Sfloat: - case vk::Format::eD32SfloatS8Uint: - return true; - default: - return false; - } -} - -bool ImageInfo::HasStencil() const { - if (pixel_format == vk::Format::eD32SfloatS8Uint || - pixel_format == vk::Format::eD24UnormS8Uint || - pixel_format == vk::Format::eD16UnormS8Uint) { - return true; - } - return false; -} - static vk::ImageUsageFlags ImageUsageFlags(const ImageInfo& info) { vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | @@ -161,6 +105,9 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, if (info.props.is_volume) { flags |= vk::ImageCreateFlagBits::e2DArrayCompatible; } + if (info.props.is_block) { + flags |= vk::ImageCreateFlagBits::eBlockTexelViewCompatible; + } usage_flags = ImageUsageFlags(info); format_features = FormatFeatureFlags(usage_flags); @@ -372,9 +319,9 @@ void Image::CopyImage(const Image& image) { boost::container::small_vector image_copy{}; for (u32 m = 0; m < image.info.resources.levels; ++m) { - const auto mip_w = std::max(info.size.width >> m, 1u); - const auto mip_h = std::max(info.size.height >> m, 1u); - const auto mip_d = std::max(info.size.depth >> m, 1u); + const auto mip_w = std::max(image.info.size.width >> m, 1u); + const auto mip_h = std::max(image.info.size.height >> m, 1u); + const auto mip_d = std::max(image.info.size.depth >> m, 1u); image_copy.emplace_back(vk::ImageCopy{ .srcSubresource{ diff --git a/src/video_core/texture_cache/image_info.cpp b/src/video_core/texture_cache/image_info.cpp index 39322f449..769c4211f 100644 --- a/src/video_core/texture_cache/image_info.cpp +++ b/src/video_core/texture_cache/image_info.cpp @@ -81,7 +81,7 @@ ImageInfo::ImageInfo(const AmdGpu::Liverpool::ColorBuffer& buffer, tiling_mode = buffer.GetTilingMode(); pixel_format = LiverpoolToVK::SurfaceFormat(buffer.GetDataFmt(), buffer.GetNumberFmt()); num_samples = buffer.NumSamples(); - num_bits = NumBits(buffer.GetDataFmt()); + num_bits = NumBitsPerBlock(buffer.GetDataFmt()); type = vk::ImageType::e2D; size.width = hint.Valid() ? hint.width : buffer.Pitch(); size.height = hint.Valid() ? hint.height : buffer.Height(); @@ -142,7 +142,7 @@ ImageInfo::ImageInfo(const AmdGpu::Image& image, const Shader::ImageResource& de resources.levels = image.NumLevels(); resources.layers = image.NumLayers(); num_samples = image.NumSamples(); - num_bits = NumBits(image.GetDataFmt()); + num_bits = NumBitsPerBlock(image.GetDataFmt()); guest_address = image.Address(); @@ -152,6 +152,80 @@ ImageInfo::ImageInfo(const AmdGpu::Image& image, const Shader::ImageResource& de UpdateSize(); } +bool ImageInfo::IsBlockCoded() const { + switch (pixel_format) { + case vk::Format::eBc1RgbaSrgbBlock: + case vk::Format::eBc1RgbaUnormBlock: + case vk::Format::eBc1RgbSrgbBlock: + case vk::Format::eBc1RgbUnormBlock: + case vk::Format::eBc2SrgbBlock: + case vk::Format::eBc2UnormBlock: + case vk::Format::eBc3SrgbBlock: + case vk::Format::eBc3UnormBlock: + case vk::Format::eBc4SnormBlock: + case vk::Format::eBc4UnormBlock: + case vk::Format::eBc5SnormBlock: + case vk::Format::eBc5UnormBlock: + case vk::Format::eBc6HSfloatBlock: + case vk::Format::eBc6HUfloatBlock: + case vk::Format::eBc7SrgbBlock: + case vk::Format::eBc7UnormBlock: + return true; + default: + return false; + } +} + +bool ImageInfo::IsPacked() const { + switch (pixel_format) { + case vk::Format::eB5G5R5A1UnormPack16: + [[fallthrough]]; + case vk::Format::eB5G6R5UnormPack16: + return true; + default: + return false; + } +} + +bool ImageInfo::IsDepthStencil() const { + switch (pixel_format) { + case vk::Format::eD16Unorm: + case vk::Format::eD16UnormS8Uint: + case vk::Format::eD32Sfloat: + case vk::Format::eD32SfloatS8Uint: + return true; + default: + return false; + } +} + +bool ImageInfo::HasStencil() const { + if (pixel_format == vk::Format::eD32SfloatS8Uint || + pixel_format == vk::Format::eD24UnormS8Uint || + pixel_format == vk::Format::eD16UnormS8Uint) { + return true; + } + return false; +} + +bool ImageInfo::IsCompatible(const ImageInfo& info) const { + return (pixel_format == info.pixel_format && num_samples == info.num_samples && + num_bits == info.num_bits); +} + +bool ImageInfo::IsTilingCompatible(u32 lhs, u32 rhs) const { + if (lhs == rhs) { + return true; + } + if (lhs == 0x0e && rhs == 0x0d) { + return true; + } + if (lhs == 0x0d && rhs == 0x0e) { + return true; + } + return false; +} + void ImageInfo::UpdateSize() { mips_layout.clear(); MipInfo mip_info{}; @@ -163,7 +237,6 @@ void ImageInfo::UpdateSize() { if (props.is_block) { mip_w = (mip_w + 3) / 4; mip_h = (mip_h + 3) / 4; - bpp *= 16; } mip_w = std::max(mip_w, 1u); mip_h = std::max(mip_h, 1u); diff --git a/src/video_core/texture_cache/image_info.h b/src/video_core/texture_cache/image_info.h index ca4d9f5e9..47718a095 100644 --- a/src/video_core/texture_cache/image_info.h +++ b/src/video_core/texture_cache/image_info.h @@ -25,6 +25,11 @@ struct ImageInfo { bool IsTiled() const { return tiling_mode != AmdGpu::TilingMode::Display_Linear; } + Extent3D BlockDim() const { + const u32 shift = props.is_block ? 2 : 0; + return Extent3D{size.width >> shift, size.height >> shift, size.depth}; + } + bool IsBlockCoded() const; bool IsPacked() const; bool IsDepthStencil() const; @@ -33,24 +38,8 @@ struct ImageInfo { s32 MipOf(const ImageInfo& info) const; s32 SliceOf(const ImageInfo& info, s32 mip) const; - /// Verifies if images are compatible for subresource merging. - bool IsCompatible(const ImageInfo& info) const { - return (pixel_format == info.pixel_format && num_samples == info.num_samples && - num_bits == info.num_bits); - } - - bool IsTilingCompatible(u32 lhs, u32 rhs) const { - if (lhs == rhs) { - return true; - } - if (lhs == 0x0e && rhs == 0x0d) { - return true; - } - if (lhs == 0x0d && rhs == 0x0e) { - return true; - } - return false; - } + bool IsCompatible(const ImageInfo& info) const; + bool IsTilingCompatible(u32 lhs, u32 rhs) const; void UpdateSize(); diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index 4b173c313..f070b9132 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -199,7 +199,8 @@ std::tuple TextureCache::ResolveOverlap(const ImageInfo& imag scheduler.CurrentTick() - tex_cache_image.tick_accessed_last > NumFramesBeforeRemoval; if (image_info.guest_address == tex_cache_image.info.guest_address) { // Equal address - if (image_info.size != tex_cache_image.info.size) { + if (image_info.BlockDim() != tex_cache_image.info.BlockDim() || + image_info.num_bits != tex_cache_image.info.num_bits) { // Very likely this kind of overlap is caused by allocation from a pool. if (safe_to_delete) { FreeImage(cache_image_id); @@ -211,15 +212,19 @@ std::tuple TextureCache::ResolveOverlap(const ImageInfo& imag return {depth_image_id, -1, -1}; } + if (image_info.IsBlockCoded() && !tex_cache_image.info.IsBlockCoded()) { + // Compressed view of uncompressed image with same block size. + // We need to recreate the image with compressed format and copy. + return {ExpandImage(image_info, cache_image_id), -1, -1}; + } + if (image_info.pixel_format != tex_cache_image.info.pixel_format || image_info.guest_size <= tex_cache_image.info.guest_size) { auto result_id = merged_image_id ? merged_image_id : cache_image_id; const auto& result_image = slot_images[result_id]; - return { - IsVulkanFormatCompatible(image_info.pixel_format, result_image.info.pixel_format) - ? result_id - : ImageId{}, - -1, -1}; + const bool is_compatible = + IsVulkanFormatCompatible(result_image.info.pixel_format, image_info.pixel_format); + return {is_compatible ? result_id : ImageId{}, -1, -1}; } if (image_info.type == tex_cache_image.info.type && @@ -340,7 +345,7 @@ ImageId TextureCache::FindImage(BaseDesc& desc, FindFlags flags) { continue; } if (False(flags & FindFlags::RelaxFmt) && - (!IsVulkanFormatCompatible(info.pixel_format, cache_image.info.pixel_format) || + (!IsVulkanFormatCompatible(cache_image.info.pixel_format, info.pixel_format) || (cache_image.info.type != info.type && info.size != Extent3D{1, 1, 1}))) { continue; } @@ -512,9 +517,9 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule // So this calculation should be very uncommon and reasonably fast // For now we'll just check up to 64 first pixels const auto addr = std::bit_cast(image.info.guest_address); - const auto w = std::min(image.info.size.width, u32(8)); - const auto h = std::min(image.info.size.height, u32(8)); - const auto size = w * h * image.info.num_bits / 8; + const u32 w = std::min(image.info.size.width, u32(8)); + const u32 h = std::min(image.info.size.height, u32(8)); + const u32 size = w * h * image.info.num_bits >> (3 + image.info.props.is_block ? 4 : 0); const u64 hash = XXH3_64bits(addr, size); if (image.hash == hash) { image.flags &= ~ImageFlagBits::MaybeCpuDirty; diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index d7fc54338..683ac08db 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -25,10 +25,9 @@ namespace VideoCore { const DetilerContext* TileManager::GetDetiler(const ImageInfo& info) const { - const auto bpp = info.num_bits * (info.props.is_block ? 16 : 1); switch (info.tiling_mode) { case AmdGpu::TilingMode::Texture_MicroTiled: - switch (bpp) { + switch (info.num_bits) { case 8: return &detilers[DetilerType::Micro8]; case 16: @@ -43,7 +42,7 @@ const DetilerContext* TileManager::GetDetiler(const ImageInfo& info) const { return nullptr; } case AmdGpu::TilingMode::Texture_Volume: - switch (bpp) { + switch (info.num_bits) { case 8: return &detilers[DetilerType::Macro8]; case 32: @@ -55,7 +54,7 @@ const DetilerContext* TileManager::GetDetiler(const ImageInfo& info) const { } break; case AmdGpu::TilingMode::Display_MicroTiled: - switch (bpp) { + switch (info.num_bits) { case 64: return &detilers[DetilerType::Display_Micro64]; default: @@ -71,7 +70,7 @@ struct DetilerParams { u32 num_levels; u32 pitch0; u32 height; - u32 sizes[14]; + std::array sizes; }; TileManager::TileManager(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler) @@ -276,7 +275,7 @@ std::pair TileManager::TryDetile(vk::Buffer in_buffer, u32 in_o params.sizes[0] = tiles_per_row; params.sizes[1] = tiles_per_slice; } else { - ASSERT(info.resources.levels <= 14); + ASSERT(info.resources.levels <= params.sizes.size()); std::memset(¶ms.sizes, 0, sizeof(params.sizes)); for (int m = 0; m < info.resources.levels; ++m) { params.sizes[m] = info.mips_layout[m].size + (m > 0 ? params.sizes[m - 1] : 0); @@ -287,8 +286,7 @@ std::pair TileManager::TryDetile(vk::Buffer in_buffer, u32 in_o ¶ms); ASSERT((image_size % 64) == 0); - const auto bpp = info.num_bits * (info.props.is_block ? 16u : 1u); - const auto num_tiles = image_size / (64 * (bpp / 8)); + const auto num_tiles = image_size / (64 * (info.num_bits / 8)); cmdbuf.dispatch(num_tiles, 1, 1); return {out_buffer.first, 0}; } From 14b082f5ea1ac520a6b1c5ce5c930e5c7ada5bb3 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Jun 2025 01:28:00 +0300 Subject: [PATCH 112/141] buffer_cache: Inline data to cpu unless gpu modified (#3061) --- src/video_core/buffer_cache/buffer_cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index 8a5283d83..f53c111e9 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -293,7 +293,7 @@ void BufferCache::BindIndexBuffer(u32 index_offset) { void BufferCache::InlineData(VAddr address, const void* value, u32 num_bytes, bool is_gds) { ASSERT_MSG(address % 4 == 0, "GDS offset must be dword aligned"); - if (!is_gds && !IsRegionRegistered(address, num_bytes)) { + if (!is_gds && !IsRegionGpuModified(address, num_bytes)) { memcpy(std::bit_cast(address), value, num_bytes); return; } From ae2053c487e67e16a1f33bf5af9280714bc435fd Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Sun, 8 Jun 2025 15:41:58 -0700 Subject: [PATCH 113/141] fix: Disable eBlockTexelViewCompatible on MoltenVK --- src/video_core/texture_cache/image.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 8522f1307..d8070da61 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -105,7 +105,8 @@ Image::Image(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, if (info.props.is_volume) { flags |= vk::ImageCreateFlagBits::e2DArrayCompatible; } - if (info.props.is_block) { + // Not supported by MoltenVK. + if (info.props.is_block && instance->GetDriverID() != vk::DriverId::eMoltenvk) { flags |= vk::ImageCreateFlagBits::eBlockTexelViewCompatible; } From c20d02dd4094dbab2e990fcd27987e8de1bb4522 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Jun 2025 03:31:51 +0300 Subject: [PATCH 114/141] shader_recompiler: Better handling of geometry shader scenario G (#3064) --- src/shader_recompiler/frontend/copy_shader.cpp | 3 +++ src/shader_recompiler/frontend/copy_shader.h | 5 +++-- .../frontend/translate/translate.h | 1 + .../ir/passes/readlane_elimination_pass.cpp | 1 + .../ir/passes/ring_access_elimination.cpp | 15 ++++++++++++++- src/shader_recompiler/runtime_info.h | 1 + src/video_core/amdgpu/liverpool.h | 10 +++++++++- .../renderer_vulkan/vk_pipeline_cache.cpp | 1 + 8 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/shader_recompiler/frontend/copy_shader.cpp b/src/shader_recompiler/frontend/copy_shader.cpp index 8750e2b18..4b5869e1d 100644 --- a/src/shader_recompiler/frontend/copy_shader.cpp +++ b/src/shader_recompiler/frontend/copy_shader.cpp @@ -67,6 +67,9 @@ CopyShaderData ParseCopyShader(std::span code) { if (last_attr != IR::Attribute::Position0) { data.num_attrs = static_cast(last_attr) - static_cast(IR::Attribute::Param0) + 1; + const auto it = data.attr_map.begin(); + const u32 comp_stride = std::next(it)->first - it->first; + data.output_vertices = comp_stride / 64; } return data; diff --git a/src/shader_recompiler/frontend/copy_shader.h b/src/shader_recompiler/frontend/copy_shader.h index 55cc31ebd..24c7060ed 100644 --- a/src/shader_recompiler/frontend/copy_shader.h +++ b/src/shader_recompiler/frontend/copy_shader.h @@ -3,8 +3,8 @@ #pragma once +#include #include -#include #include "common/types.h" #include "shader_recompiler/ir/attribute.h" @@ -12,8 +12,9 @@ namespace Shader { struct CopyShaderData { - std::unordered_map> attr_map; + std::map> attr_map; u32 num_attrs{0}; + u32 output_vertices{0}; }; CopyShaderData ParseCopyShader(std::span code); diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index f8ffb9638..96ca924a3 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include "shader_recompiler/frontend/instruction.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/basic_block.h" diff --git a/src/shader_recompiler/ir/passes/readlane_elimination_pass.cpp b/src/shader_recompiler/ir/passes/readlane_elimination_pass.cpp index fbe382d41..9c5f64f84 100644 --- a/src/shader_recompiler/ir/passes/readlane_elimination_pass.cpp +++ b/src/shader_recompiler/ir/passes/readlane_elimination_pass.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include "shader_recompiler/ir/program.h" namespace Shader::Optimization { diff --git a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp index 071b94ac0..02745bf9a 100644 --- a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp +++ b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp @@ -91,6 +91,19 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim const auto& gs_info = runtime_info.gs_info; info.gs_copy_data = Shader::ParseCopyShader(gs_info.vs_copy); + u32 output_vertices = gs_info.output_vertices; + if (info.gs_copy_data.output_vertices && + info.gs_copy_data.output_vertices != output_vertices) { + ASSERT_MSG(output_vertices > info.gs_copy_data.output_vertices && + gs_info.mode == AmdGpu::Liverpool::GsMode::Mode::ScenarioG, + "Invalid geometry shader vertex configuration scenario = {}, max_vert_out = " + "{}, output_vertices = {}", + u32(gs_info.mode), output_vertices, info.gs_copy_data.output_vertices); + LOG_WARNING(Render_Vulkan, "MAX_VERT_OUT {} is larger than actual output vertices {}", + output_vertices, info.gs_copy_data.output_vertices); + output_vertices = info.gs_copy_data.output_vertices; + } + ForEachInstruction([&](IR::IREmitter& ir, IR::Inst& inst) { const auto opcode = inst.GetOpcode(); switch (opcode) { @@ -122,7 +135,7 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim const auto offset = inst.Flags().inst_offset.Value(); const auto data = ir.BitCast(IR::U32{inst.Arg(2)}); - const auto comp_ofs = gs_info.output_vertices * 4u; + const auto comp_ofs = output_vertices * 4u; const auto output_size = comp_ofs * gs_info.out_vertex_data_size; const auto vc_read_ofs = (((offset / comp_ofs) * comp_ofs) % output_size) * 16u; diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index 53d2d5303..5a0408e2c 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -149,6 +149,7 @@ struct GeometryRuntimeInfo { u32 out_vertex_data_size{}; AmdGpu::PrimitiveType in_primitive; GsOutputPrimTypes out_primitive; + AmdGpu::Liverpool::GsMode::Mode mode; std::span vs_copy; u64 vs_copy_hash; diff --git a/src/video_core/amdgpu/liverpool.h b/src/video_core/amdgpu/liverpool.h index 2f33c7302..d88a44375 100644 --- a/src/video_core/amdgpu/liverpool.h +++ b/src/video_core/amdgpu/liverpool.h @@ -1179,8 +1179,16 @@ struct Liverpool { }; union GsMode { + enum class Mode : u32 { + Off = 0, + ScenarioA = 1, + ScenarioB = 2, + ScenarioG = 3, + ScenarioC = 4, + }; + u32 raw; - BitField<0, 3, u32> mode; + BitField<0, 3, Mode> mode; BitField<3, 2, u32> cut_mode; BitField<22, 2, u32> onchip; }; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index b72f77e55..cd8552515 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -146,6 +146,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS } gs_info.in_vertex_data_size = regs.vgt_esgs_ring_itemsize; gs_info.out_vertex_data_size = regs.vgt_gs_vert_itemsize[0]; + gs_info.mode = regs.vgt_gs_mode.mode; const auto params_vc = Liverpool::GetParams(regs.vs_program); gs_info.vs_copy = params_vc.code; gs_info.vs_copy_hash = params_vc.hash; From 12f4a8f073804454132c1da38b79636782134705 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Jun 2025 09:35:43 +0300 Subject: [PATCH 115/141] tile_manager: Downgrade assert to error (#3067) --- src/video_core/texture_cache/tile_manager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/video_core/texture_cache/tile_manager.cpp b/src/video_core/texture_cache/tile_manager.cpp index 683ac08db..dd6fae457 100644 --- a/src/video_core/texture_cache/tile_manager.cpp +++ b/src/video_core/texture_cache/tile_manager.cpp @@ -269,7 +269,10 @@ std::pair TileManager::TryDetile(vk::Buffer in_buffer, u32 in_o params.height = info.size.height; if (info.tiling_mode == AmdGpu::TilingMode::Texture_Volume || info.tiling_mode == AmdGpu::TilingMode::Display_MicroTiled) { - ASSERT(info.resources.levels == 1); + if (info.resources.levels != 1) { + LOG_ERROR(Render_Vulkan, "Unexpected mipmaps for volume and display tilings {}", + info.resources.levels); + } const auto tiles_per_row = info.pitch / 8u; const auto tiles_per_slice = tiles_per_row * ((info.size.height + 7u) / 8u); params.sizes[0] = tiles_per_row; From d7051f15f4c97ed093be64c5d743e7af37bf4557 Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Mon, 9 Jun 2025 11:26:10 +0300 Subject: [PATCH 116/141] texture_cache: Basic handling of partially resident images (#3066) * texture_cache: Avoid gpu tracking assert on sparse image At the moment just take the easy way of creating the entire image normally and uploading unmapped subresources are zero * tile_manager: Downgrade assert to error * fix macos --- src/core/libraries/kernel/memory.cpp | 3 ++ src/core/memory.cpp | 40 ++++++++++++++++++++ src/core/memory.h | 16 ++++++++ src/video_core/buffer_cache/buffer_cache.cpp | 7 +++- src/video_core/buffer_cache/buffer_cache.h | 5 +++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index 5e94199e1..f02ddafdc 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -660,6 +660,9 @@ int PS4_SYSV_ABI sceKernelSetPrtAperture(int id, VAddr address, size_t size) { "PRT aperture id = {}, address = {:#x}, size = {:#x} is set but not used", id, address, size); + auto* memory = Core::Memory::Instance(); + memory->SetPrtArea(id, address, size); + PrtApertures[id] = {address, size}; return ORBIS_OK; } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 54cae910b..e738f85a1 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -95,6 +95,46 @@ u64 MemoryManager::ClampRangeSize(VAddr virtual_addr, u64 size) { return clamped_size; } +void MemoryManager::SetPrtArea(u32 id, VAddr address, u64 size) { + PrtArea& area = prt_areas[id]; + if (area.mapped) { + rasterizer->UnmapMemory(area.start, area.end - area.start); + } + + area.start = address; + area.end = address + size; + area.mapped = true; + + // Pretend the entire PRT area is mapped to avoid GPU tracking errors. + // The caches will use CopySparseMemory to fetch data which avoids unmapped areas. + rasterizer->MapMemory(address, size); +} + +void MemoryManager::CopySparseMemory(VAddr virtual_addr, u8* dest, u64 size) { + const bool is_sparse = std::ranges::any_of( + prt_areas, [&](const PrtArea& area) { return area.Overlaps(virtual_addr, size); }); + if (!is_sparse) { + std::memcpy(dest, std::bit_cast(virtual_addr), size); + return; + } + + auto vma = FindVMA(virtual_addr); + ASSERT_MSG(vma->second.Contains(virtual_addr, 0), + "Attempted to access invalid GPU address {:#x}", virtual_addr); + while (size) { + u64 copy_size = std::min(vma->second.size - (virtual_addr - vma->first), size); + if (vma->second.IsFree()) { + std::memset(dest, 0, copy_size); + } else { + std::memcpy(dest, std::bit_cast(virtual_addr), copy_size); + } + size -= copy_size; + virtual_addr += copy_size; + dest += copy_size; + ++vma; + } +} + bool MemoryManager::TryWriteBacking(void* address, const void* data, u32 num_bytes) { const VAddr virtual_addr = std::bit_cast(address); const auto& vma = FindVMA(virtual_addr)->second; diff --git a/src/core/memory.h b/src/core/memory.h index b3ebe3c27..68f9c26c4 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -172,6 +172,10 @@ public: u64 ClampRangeSize(VAddr virtual_addr, u64 size); + void SetPrtArea(u32 id, VAddr address, u64 size); + + void CopySparseMemory(VAddr source, u8* dest, u64 size); + bool TryWriteBacking(void* address, const void* data, u32 num_bytes); void SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1, bool use_extended_mem2); @@ -275,6 +279,18 @@ private: size_t pool_budget{}; Vulkan::Rasterizer* rasterizer{}; + struct PrtArea { + VAddr start; + VAddr end; + bool mapped; + + bool Overlaps(VAddr test_address, u64 test_size) const { + const VAddr overlap_end = test_address + test_size; + return start < overlap_end && test_address < end; + } + }; + std::array prt_areas{}; + friend class ::Core::Devtools::Widget::MemoryMapViewer; }; diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index f53c111e9..e470f8e77 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -6,6 +6,7 @@ #include "common/debug.h" #include "common/scope_exit.h" #include "common/types.h" +#include "core/memory.h" #include "video_core/amdgpu/liverpool.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/host_shaders/fault_buffer_process_comp.h" @@ -28,7 +29,7 @@ BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& s Vulkan::Rasterizer& rasterizer_, AmdGpu::Liverpool* liverpool_, TextureCache& texture_cache_, PageManager& tracker_) : instance{instance_}, scheduler{scheduler_}, rasterizer{rasterizer_}, liverpool{liverpool_}, - texture_cache{texture_cache_}, tracker{tracker_}, + memory{Core::Memory::Instance()}, texture_cache{texture_cache_}, tracker{tracker_}, staging_buffer{instance, scheduler, MemoryUsage::Upload, StagingBufferSize}, stream_buffer{instance, scheduler, MemoryUsage::Stream, UboStreamBufferSize}, download_buffer(instance, scheduler, MemoryUsage::Download, DownloadBufferSize), @@ -365,7 +366,9 @@ std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, return ObtainBuffer(gpu_addr, size, false, false); } // In all other cases, just do a CPU copy to the staging buffer. - const u32 offset = staging_buffer.Copy(gpu_addr, size, 16); + const auto [data, offset] = staging_buffer.Map(size, 16); + memory->CopySparseMemory(gpu_addr, data, size); + staging_buffer.Commit(); return {&staging_buffer, offset}; } diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 2d6551a7f..c2faf12c8 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -17,6 +17,10 @@ namespace AmdGpu { struct Liverpool; } +namespace Core { +class MemoryManager; +} + namespace Shader { namespace Gcn { struct FetchShaderData; @@ -183,6 +187,7 @@ private: Vulkan::Scheduler& scheduler; Vulkan::Rasterizer& rasterizer; AmdGpu::Liverpool* liverpool; + Core::MemoryManager* memory; TextureCache& texture_cache; PageManager& tracker; StreamBuffer staging_buffer; From 046cf5041235cbd413ca9d0ada28ffcdd0651cb3 Mon Sep 17 00:00:00 2001 From: mailwl Date: Mon, 9 Jun 2025 11:27:37 +0300 Subject: [PATCH 117/141] PM4 type 2 in acb (#3047) * Stub PM4 type 0 * fix command size * revert command size to actual * return unreachable for PM4t0 * remove skipping command body --- src/video_core/amdgpu/liverpool.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/video_core/amdgpu/liverpool.cpp b/src/video_core/amdgpu/liverpool.cpp index e031d0ebc..464f02e3a 100644 --- a/src/video_core/amdgpu/liverpool.cpp +++ b/src/video_core/amdgpu/liverpool.cpp @@ -228,9 +228,12 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span dcb, std::spantype; switch (type) { + default: + UNREACHABLE_MSG("Wrong PM4 type {}", type); + break; case 0: - case 1: - UNREACHABLE_MSG("Unsupported PM4 type {}", type); + UNREACHABLE_MSG("Unimplemented PM4 type 0, base reg: {}, size: {}", + header->type0.base.Value(), header->type0.NumWords()); break; case 2: // Type-2 packet are used for padding purposes @@ -826,6 +829,19 @@ Liverpool::Task Liverpool::ProcessCompute(const u32* acb, u32 acb_dwords, u32 vq break; } + if (header->type == 2) { + // Type-2 packet are used for padding purposes + next_dw_off = 1; + acb += next_dw_off; + acb_dwords -= next_dw_off; + + if constexpr (!is_indirect) { + *queue.read_addr += next_dw_off; + *queue.read_addr %= queue.ring_size_dw; + } + continue; + } + if (header->type != 3) { // No other types of packets were spotted so far UNREACHABLE_MSG("Invalid PM4 type {}", header->type.Value()); From a71bfb30a2002eafd8d9bf1a2742875fc1ab4eb0 Mon Sep 17 00:00:00 2001 From: Lander Gallastegi Date: Mon, 9 Jun 2025 12:04:21 +0200 Subject: [PATCH 118/141] shader_recompiler: Patch SRT walker on segfault (#2991) * Patch srt walker access violations * Fix range * clang-format lolz * Lower log from warning to debug --- src/core/signals.h | 1 + .../passes/flatten_extended_userdata_pass.cpp | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/core/signals.h b/src/core/signals.h index 6ee525e10..0409b73ae 100644 --- a/src/core/signals.h +++ b/src/core/signals.h @@ -5,6 +5,7 @@ #include #include "common/singleton.h" +#include "common/types.h" namespace Core { diff --git a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp index bbf3fe8fb..7253e18c1 100644 --- a/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp +++ b/src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp @@ -10,6 +10,8 @@ #include "common/io_file.h" #include "common/logging/log.h" #include "common/path_util.h" +#include "common/signal_context.h" +#include "core/signals.h" #include "shader_recompiler/info.h" #include "shader_recompiler/ir/breadth_first_search.h" #include "shader_recompiler/ir/opcodes.h" @@ -24,6 +26,7 @@ using namespace Xbyak::util; static Xbyak::CodeGenerator g_srt_codegen(32_MB); +static const u8* g_srt_codegen_start = nullptr; namespace { @@ -54,6 +57,57 @@ static void DumpSrtProgram(const Shader::Info& info, const u8* code, size_t code #endif } +static bool SrtWalkerSignalHandler(void* context, void* fault_address) { + // Only handle if the fault address is within the SRT code range + const u8* code_start = g_srt_codegen_start; + const u8* code_end = code_start + g_srt_codegen.getSize(); + const void* code = Common::GetRip(context); + if (code < code_start || code >= code_end) { + return false; // Not in SRT code range + } + + // Patch instruction to zero register + ZydisDecodedInstruction instruction; + ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; + ZyanStatus status = Common::Decoder::Instance()->decodeInstruction(instruction, operands, + const_cast(code), 15); + + ASSERT(ZYAN_SUCCESS(status) && instruction.mnemonic == ZYDIS_MNEMONIC_MOV && + operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY); + + size_t len = instruction.length; + const size_t patch_size = 3; + u8* code_patch = const_cast(reinterpret_cast(code)); + + // We can only encounter rdi or r10d as the first operand in a + // fault memory access for SRT walker. + switch (operands[0].reg.value) { + case ZYDIS_REGISTER_RDI: + // mov rdi, [rdi + (off_dw << 2)] -> xor rdi, rdi + code_patch[0] = 0x48; + code_patch[1] = 0x31; + code_patch[2] = 0xFF; + break; + case ZYDIS_REGISTER_R10D: + // mov r10d, [rdi + (off_dw << 2)] -> xor r10d, r10d + code_patch[0] = 0x45; + code_patch[1] = 0x31; + code_patch[2] = 0xD2; + break; + default: + UNREACHABLE_MSG("Unsupported register for SRT walker patch"); + return false; + } + + // Fill nops + memset(code_patch + patch_size, 0x90, len - patch_size); + + LOG_DEBUG(Render_Recompiler, "Patched SRT walker at {}", code); + + return true; +} + using namespace Shader; struct PassInfo { @@ -141,6 +195,15 @@ static void GenerateSrtProgram(Info& info, PassInfo& pass_info) { return; } + // Register the signal handler for SRT walker, if not already registered + if (g_srt_codegen_start == nullptr) { + g_srt_codegen_start = c.getCurr(); + auto* signals = Core::Signals::Instance(); + // Call after the memory invalidation handler + constexpr u32 priority = 1; + signals->RegisterAccessViolationHandler(SrtWalkerSignalHandler, priority); + } + info.srt_info.walker_func = c.getCurr(); pass_info.dst_off_dw = NumUserDataRegs; From 217d32b5024be79d77de1d541d3c757210e33990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczyk?= Date: Mon, 9 Jun 2025 21:03:38 +0200 Subject: [PATCH 119/141] Handle DS_READ_U16, DS_WRITE_B16, DS_ADD_U64 (#3007) * Handle DS_READ_U16 & DS_WRITE_B16 * Refactor DS translation * Translate DS_ADD_U64 * format * Fix RingAccessElimination after changing WriteShared64 type * Simplify bounds checking in generated SPIR-V --- .../backend/spirv/emit_spirv.cpp | 5 ++ .../backend/spirv/emit_spirv_atomic.cpp | 85 ++++++++++++------- .../backend/spirv/emit_spirv_bounds.h | 48 +++++++++++ .../backend/spirv/emit_spirv_instructions.h | 4 + .../spirv/emit_spirv_shared_memory.cpp | 83 +++++++++++++----- .../backend/spirv/spirv_emit_context.cpp | 31 +++++-- .../backend/spirv/spirv_emit_context.h | 12 +-- .../frontend/translate/data_share.cpp | 56 +++++++++--- .../frontend/translate/translate.h | 1 + src/shader_recompiler/ir/ir_emitter.cpp | 11 ++- src/shader_recompiler/ir/ir_emitter.h | 2 +- src/shader_recompiler/ir/opcodes.inc | 8 +- .../ir/passes/ring_access_elimination.cpp | 6 +- .../ir/passes/shader_info_collection_pass.cpp | 2 + .../passes/shared_memory_to_storage_pass.cpp | 15 +++- src/shader_recompiler/profile.h | 2 +- .../renderer_vulkan/vk_instance.cpp | 29 ++++++- src/video_core/renderer_vulkan/vk_instance.h | 9 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 3 +- 19 files changed, 323 insertions(+), 89 deletions(-) create mode 100644 src/shader_recompiler/backend/spirv/emit_spirv_bounds.h diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index f2e6279f4..37d7eea35 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -303,6 +303,11 @@ void SetupCapabilities(const Info& info, const Profile& profile, EmitContext& ct ctx.AddCapability(spv::Capability::PhysicalStorageBufferAddresses); ctx.AddExtension("SPV_KHR_physical_storage_buffer"); } + if (info.uses_shared && profile.supports_workgroup_explicit_memory_layout) { + ctx.AddExtension("SPV_KHR_workgroup_memory_explicit_layout"); + ctx.AddCapability(spv::Capability::WorkgroupMemoryExplicitLayoutKHR); + ctx.AddCapability(spv::Capability::WorkgroupMemoryExplicitLayout16BitAccessKHR); + } } void DefineEntryPoint(const Info& info, EmitContext& ctx, Id main) { diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index a342b47b6..13fd8e180 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/div_ceil.h" +#include "shader_recompiler/backend/spirv/emit_spirv_bounds.h" #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" @@ -15,42 +17,40 @@ std::pair AtomicArgs(EmitContext& ctx) { Id SharedAtomicU32(EmitContext& ctx, Id offset, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { const Id shift_id{ctx.ConstU32(2U)}; - const Id index{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift_id)}; - const Id pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, index)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index)}; const auto [scope, semantics]{AtomicArgs(ctx)}; - return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics, value); + return AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { + return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics, value); + }); +} + +Id SharedAtomicU64(EmitContext& ctx, Id offset, Id value, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { + const Id shift_id{ctx.ConstU32(3U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 8u)}; + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u64, ctx.shared_memory_u64, ctx.u32_zero_value, index)}; + const auto [scope, semantics]{AtomicArgs(ctx)}; + return AccessBoundsCheck<64>(ctx, index, ctx.ConstU32(num_elements), [&] { + return (ctx.*atomic_func)(ctx.U64, pointer, scope, semantics, value); + }); } Id SharedAtomicU32_IncDec(EmitContext& ctx, Id offset, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id)) { const Id shift_id{ctx.ConstU32(2U)}; - const Id index{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift_id)}; - const Id pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, index)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index)}; const auto [scope, semantics]{AtomicArgs(ctx)}; - return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics); -} - -Id BufferAtomicU32BoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func) { - if (Sirit::ValidId(buffer_size)) { - // Bounds checking enabled, wrap in a conditional branch to make sure that - // the atomic is not mistakenly executed when the index is out of bounds. - const Id in_bounds = ctx.OpULessThan(ctx.U1[1], index, buffer_size); - const Id ib_label = ctx.OpLabel(); - const Id oob_label = ctx.OpLabel(); - const Id end_label = ctx.OpLabel(); - ctx.OpSelectionMerge(end_label, spv::SelectionControlMask::MaskNone); - ctx.OpBranchConditional(in_bounds, ib_label, oob_label); - ctx.AddLabel(ib_label); - const Id ib_result = emit_func(); - ctx.OpBranch(end_label); - ctx.AddLabel(oob_label); - const Id oob_result = ctx.u32_zero_value; - ctx.OpBranch(end_label); - ctx.AddLabel(end_label); - return ctx.OpPhi(ctx.U32[1], ib_result, ib_label, oob_result, oob_label); - } - // Bounds checking not enabled, just perform the atomic operation. - return emit_func(); + return AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { + return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics); + }); } Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, @@ -63,7 +63,7 @@ Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); const auto [scope, semantics]{AtomicArgs(ctx)}; - return BufferAtomicU32BoundsCheck(ctx, index, buffer.size_dwords, [&] { + return AccessBoundsCheck<32>(ctx, index, buffer.size_dwords, [&] { return (ctx.*atomic_func)(ctx.U32[1], ptr, scope, semantics, value); }); } @@ -79,11 +79,26 @@ Id BufferAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); const auto [scope, semantics]{AtomicArgs(ctx)}; - return BufferAtomicU32BoundsCheck(ctx, index, buffer.size_dwords, [&] { + return AccessBoundsCheck<32>(ctx, index, buffer.size_dwords, [&] { return (ctx.*atomic_func)(ctx.U32[1], ptr, scope, semantics, semantics, value, cmp_value); }); } +Id BufferAtomicU64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { + const auto& buffer = ctx.buffers[handle]; + if (Sirit::ValidId(buffer.offset)) { + address = ctx.OpIAdd(ctx.U32[1], address, buffer.offset); + } + const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(3u)); + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U64]; + const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); + const auto [scope, semantics]{AtomicArgs(ctx)}; + return AccessBoundsCheck<64>(ctx, index, buffer.size_qwords, [&] { + return (ctx.*atomic_func)(ctx.U64, ptr, scope, semantics, value); + }); +} + Id ImageAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id coords, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { const auto& texture = ctx.images[handle & 0xFFFF]; @@ -105,6 +120,10 @@ Id EmitSharedAtomicIAdd32(EmitContext& ctx, Id offset, Id value) { return SharedAtomicU32(ctx, offset, value, &Sirit::Module::OpAtomicIAdd); } +Id EmitSharedAtomicIAdd64(EmitContext& ctx, Id offset, Id value) { + return SharedAtomicU64(ctx, offset, value, &Sirit::Module::OpAtomicIAdd); +} + Id EmitSharedAtomicUMax32(EmitContext& ctx, Id offset, Id value) { return SharedAtomicU32(ctx, offset, value, &Sirit::Module::OpAtomicUMax); } @@ -149,6 +168,10 @@ Id EmitBufferAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicIAdd); } +Id EmitBufferAtomicIAdd64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { + return BufferAtomicU64(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicIAdd); +} + Id EmitBufferAtomicSMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicSMin); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h b/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h new file mode 100644 index 000000000..41e70c8c3 --- /dev/null +++ b/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" +#include "shader_recompiler/backend/spirv/spirv_emit_context.h" + +namespace Shader::Backend::SPIRV { + +template +auto AccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func) { + Id zero_value{}; + Id result_type{}; + if constexpr (bit_size == 64) { + zero_value = ctx.u64_zero_value; + result_type = ctx.U64; + } else if constexpr (bit_size == 32) { + zero_value = ctx.u32_zero_value; + result_type = ctx.U32[1]; + } else if constexpr (bit_size == 16) { + zero_value = ctx.u16_zero_value; + result_type = ctx.U16; + } else { + static_assert(false, "type not supported"); + } + if (Sirit::ValidId(buffer_size)) { + // Bounds checking enabled, wrap in a conditional branch to make sure that + // the atomic is not mistakenly executed when the index is out of bounds. + const Id in_bounds = ctx.OpULessThan(ctx.U1[1], index, buffer_size); + const Id ib_label = ctx.OpLabel(); + const Id end_label = ctx.OpLabel(); + ctx.OpSelectionMerge(end_label, spv::SelectionControlMask::MaskNone); + ctx.OpBranchConditional(in_bounds, ib_label, end_label); + const auto last_label = ctx.last_label; + ctx.AddLabel(ib_label); + const auto ib_result = emit_func(); + ctx.OpBranch(end_label); + ctx.AddLabel(end_label); + if (Sirit::ValidId(ib_result)) { + return ctx.OpPhi(result_type, ib_result, ib_label, zero_value, last_label); + } else { + return Id{0}; + } + } + // Bounds checking not enabled, just perform the atomic operation. + return emit_func(); +} + +} // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index b9707224c..3441c5a23 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -86,6 +86,7 @@ void EmitStoreBufferF32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre void EmitStoreBufferF32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); +Id EmitBufferAtomicIAdd64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicSMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicUMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicSMax32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); @@ -120,11 +121,14 @@ Id EmitUndefU8(EmitContext& ctx); Id EmitUndefU16(EmitContext& ctx); Id EmitUndefU32(EmitContext& ctx); Id EmitUndefU64(EmitContext& ctx); +Id EmitLoadSharedU16(EmitContext& ctx, Id offset); Id EmitLoadSharedU32(EmitContext& ctx, Id offset); Id EmitLoadSharedU64(EmitContext& ctx, Id offset); +void EmitWriteSharedU16(EmitContext& ctx, Id offset, Id value); void EmitWriteSharedU32(EmitContext& ctx, Id offset, Id value); void EmitWriteSharedU64(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicIAdd32(EmitContext& ctx, Id offset, Id value); +Id EmitSharedAtomicIAdd64(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicUMax32(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicSMax32(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicUMin32(EmitContext& ctx, Id offset, Id value); diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_shared_memory.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_shared_memory.cpp index 8b1610d61..c59406499 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_shared_memory.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_shared_memory.cpp @@ -1,43 +1,86 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include "common/div_ceil.h" +#include "shader_recompiler/backend/spirv/emit_spirv_bounds.h" #include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" #include "shader_recompiler/backend/spirv/spirv_emit_context.h" namespace Shader::Backend::SPIRV { +Id EmitLoadSharedU16(EmitContext& ctx, Id offset) { + const Id shift_id{ctx.ConstU32(1U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 2u)}; + + return AccessBoundsCheck<16>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer = + ctx.OpAccessChain(ctx.shared_u16, ctx.shared_memory_u16, ctx.u32_zero_value, index); + return ctx.OpLoad(ctx.U16, pointer); + }); +} + Id EmitLoadSharedU32(EmitContext& ctx, Id offset) { const Id shift_id{ctx.ConstU32(2U)}; - const Id index{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift_id)}; - const Id pointer = ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, index); - return ctx.OpLoad(ctx.U32[1], pointer); + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; + + return AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer = + ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index); + return ctx.OpLoad(ctx.U32[1], pointer); + }); } Id EmitLoadSharedU64(EmitContext& ctx, Id offset) { - const Id shift_id{ctx.ConstU32(2U)}; - const Id base_index{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift_id)}; - const Id next_index{ctx.OpIAdd(ctx.U32[1], base_index, ctx.ConstU32(1U))}; - const Id lhs_pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, base_index)}; - const Id rhs_pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, next_index)}; - return ctx.OpCompositeConstruct(ctx.U32[2], ctx.OpLoad(ctx.U32[1], lhs_pointer), - ctx.OpLoad(ctx.U32[1], rhs_pointer)); + const Id shift_id{ctx.ConstU32(3U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 8u)}; + + return AccessBoundsCheck<64>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u64, ctx.shared_memory_u64, ctx.u32_zero_value, index)}; + return ctx.OpLoad(ctx.U64, pointer); + }); +} + +void EmitWriteSharedU16(EmitContext& ctx, Id offset, Id value) { + const Id shift{ctx.ConstU32(1U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 2u)}; + + AccessBoundsCheck<16>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer = + ctx.OpAccessChain(ctx.shared_u16, ctx.shared_memory_u16, ctx.u32_zero_value, index); + ctx.OpStore(pointer, value); + return Id{0}; + }); } void EmitWriteSharedU32(EmitContext& ctx, Id offset, Id value) { const Id shift{ctx.ConstU32(2U)}; - const Id word_offset{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift)}; - const Id pointer = ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, word_offset); - ctx.OpStore(pointer, value); + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; + + AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer = + ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index); + ctx.OpStore(pointer, value); + return Id{0}; + }); } void EmitWriteSharedU64(EmitContext& ctx, Id offset, Id value) { - const Id shift{ctx.ConstU32(2U)}; - const Id word_offset{ctx.OpShiftRightArithmetic(ctx.U32[1], offset, shift)}; - const Id next_offset{ctx.OpIAdd(ctx.U32[1], word_offset, ctx.ConstU32(1U))}; - const Id lhs_pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, word_offset)}; - const Id rhs_pointer{ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, next_offset)}; - ctx.OpStore(lhs_pointer, ctx.OpCompositeExtract(ctx.U32[1], value, 0U)); - ctx.OpStore(rhs_pointer, ctx.OpCompositeExtract(ctx.U32[1], value, 1U)); + const Id shift{ctx.ConstU32(3U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 8u)}; + + AccessBoundsCheck<64>(ctx, index, ctx.ConstU32(num_elements), [&] { + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u64, ctx.shared_memory_u64, ctx.u32_zero_value, index)}; + ctx.OpStore(pointer, value); + return Id{0}; + }); } } // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 9e51f8e60..672856397 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -146,6 +146,7 @@ void EmitContext::DefineArithmeticTypes() { false_value = ConstantFalse(U1[1]); u8_one_value = Constant(U8, 1U); u8_zero_value = Constant(U8, 0U); + u16_zero_value = Constant(U16, 0U); u32_one_value = ConstU32(1U); u32_zero_value = ConstU32(0U); f32_zero_value = ConstF32(0.0f); @@ -285,6 +286,8 @@ void EmitContext::DefineBufferProperties() { Name(buffer.size_shorts, fmt::format("buf{}_short_size", binding)); buffer.size_dwords = OpShiftRightLogical(U32[1], buffer.size, ConstU32(2U)); Name(buffer.size_dwords, fmt::format("buf{}_dword_size", binding)); + buffer.size_qwords = OpShiftRightLogical(U32[1], buffer.size, ConstU32(3U)); + Name(buffer.size_qwords, fmt::format("buf{}_qword_size", binding)); } } } @@ -979,13 +982,27 @@ void EmitContext::DefineSharedMemory() { } ASSERT(info.stage == Stage::Compute); const u32 shared_memory_size = runtime_info.cs_info.shared_memory_size; - const u32 num_elements{Common::DivCeil(shared_memory_size, 4U)}; - const Id type{TypeArray(U32[1], ConstU32(num_elements))}; - shared_memory_u32_type = TypePointer(spv::StorageClass::Workgroup, type); - shared_u32 = TypePointer(spv::StorageClass::Workgroup, U32[1]); - shared_memory_u32 = AddGlobalVariable(shared_memory_u32_type, spv::StorageClass::Workgroup); - Name(shared_memory_u32, "shared_mem"); - interfaces.push_back(shared_memory_u32); + + const auto make_type = [&](Id element_type, u32 element_size) { + const u32 num_elements{Common::DivCeil(shared_memory_size, element_size)}; + const Id array_type{TypeArray(element_type, ConstU32(num_elements))}; + Decorate(array_type, spv::Decoration::ArrayStride, element_size); + + const Id struct_type{TypeStruct(array_type)}; + MemberDecorate(struct_type, 0u, spv::Decoration::Offset, 0u); + Decorate(struct_type, spv::Decoration::Block); + + const Id pointer = TypePointer(spv::StorageClass::Workgroup, struct_type); + const Id element_pointer = TypePointer(spv::StorageClass::Workgroup, element_type); + const Id variable = AddGlobalVariable(pointer, spv::StorageClass::Workgroup); + Decorate(variable, spv::Decoration::Aliased); + interfaces.push_back(variable); + + return std::make_tuple(variable, element_pointer, pointer); + }; + std::tie(shared_memory_u16, shared_u16, shared_memory_u16_type) = make_type(U16, 2u); + std::tie(shared_memory_u32, shared_u32, shared_memory_u32_type) = make_type(U32[1], 4u); + std::tie(shared_memory_u64, shared_u64, shared_memory_u64_type) = make_type(U64, 8u); } Id EmitContext::DefineFloat32ToUfloatM5(u32 mantissa_bits, const std::string_view name) { diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 20d936cf0..93c4ed265 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -235,17 +235,16 @@ public: Id false_value{}; Id u8_one_value{}; Id u8_zero_value{}; + Id u16_zero_value{}; Id u32_one_value{}; Id u32_zero_value{}; Id f32_zero_value{}; Id u64_one_value{}; Id u64_zero_value{}; - Id shared_u8{}; Id shared_u16{}; Id shared_u32{}; - Id shared_u32x2{}; - Id shared_u32x4{}; + Id shared_u64{}; Id input_u32{}; Id input_f32{}; @@ -285,13 +284,13 @@ public: Id image_u32{}; Id image_f32{}; - Id shared_memory_u8{}; Id shared_memory_u16{}; Id shared_memory_u32{}; - Id shared_memory_u32x2{}; - Id shared_memory_u32x4{}; + Id shared_memory_u64{}; + Id shared_memory_u16_type{}; Id shared_memory_u32_type{}; + Id shared_memory_u64_type{}; Id bary_coord_persp_id{}; Id bary_coord_linear_id{}; @@ -320,6 +319,7 @@ public: Id size; Id size_shorts; Id size_dwords; + Id size_qwords; std::array aliases; const BufferSpv& operator[](PointerType alias) const { diff --git a/src/shader_recompiler/frontend/translate/data_share.cpp b/src/shader_recompiler/frontend/translate/data_share.cpp index c29497ada..4b6a58fd0 100644 --- a/src/shader_recompiler/frontend/translate/data_share.cpp +++ b/src/shader_recompiler/frontend/translate/data_share.cpp @@ -13,6 +13,8 @@ void Translator::EmitDataShare(const GcnInst& inst) { // DS case Opcode::DS_ADD_U32: return DS_ADD_U32(inst, false); + case Opcode::DS_ADD_U64: + return DS_ADD_U64(inst, false); case Opcode::DS_SUB_U32: return DS_SUB_U32(inst, false); case Opcode::DS_INC_U32: @@ -61,10 +63,14 @@ void Translator::EmitDataShare(const GcnInst& inst) { return DS_READ(32, false, true, false, inst); case Opcode::DS_READ2ST64_B32: return DS_READ(32, false, true, true, inst); + case Opcode::DS_READ_U16: + return DS_READ(16, false, false, false, inst); case Opcode::DS_CONSUME: return DS_CONSUME(inst); case Opcode::DS_APPEND: return DS_APPEND(inst); + case Opcode::DS_WRITE_B16: + return DS_WRITE(16, false, false, false, inst); case Opcode::DS_WRITE_B64: return DS_WRITE(64, false, false, false, inst); case Opcode::DS_WRITE2_B64: @@ -123,6 +129,18 @@ void Translator::DS_ADD_U32(const GcnInst& inst, bool rtn) { } } +void Translator::DS_ADD_U64(const GcnInst& inst, bool rtn) { + const IR::U32 addr{GetSrc(inst.src[0])}; + const IR::U64 data{GetSrc64(inst.src[1])}; + const IR::U32 offset = + ir.Imm32((u32(inst.control.ds.offset1) << 8u) + u32(inst.control.ds.offset0)); + const IR::U32 addr_offset = ir.IAdd(addr, offset); + const IR::Value original_val = ir.SharedAtomicIAdd(addr_offset, data); + if (rtn) { + SetDst64(inst.dst[0], IR::U64{original_val}); + } +} + void Translator::DS_MIN_U32(const GcnInst& inst, bool is_signed, bool rtn) { const IR::U32 addr{GetSrc(inst.src[0])}; const IR::U32 data{GetSrc(inst.src[1])}; @@ -201,23 +219,28 @@ void Translator::DS_WRITE(int bit_size, bool is_signed, bool is_pair, bool strid if (bit_size == 32) { ir.WriteShared(32, ir.GetVectorReg(data0), addr0); } else { - ir.WriteShared( - 64, ir.CompositeConstruct(ir.GetVectorReg(data0), ir.GetVectorReg(data0 + 1)), - addr0); + ir.WriteShared(64, + ir.PackUint2x32(ir.CompositeConstruct(ir.GetVectorReg(data0), + ir.GetVectorReg(data0 + 1))), + addr0); } const IR::U32 addr1 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset1 * adj))); if (bit_size == 32) { ir.WriteShared(32, ir.GetVectorReg(data1), addr1); } else { - ir.WriteShared( - 64, ir.CompositeConstruct(ir.GetVectorReg(data1), ir.GetVectorReg(data1 + 1)), - addr1); + ir.WriteShared(64, + ir.PackUint2x32(ir.CompositeConstruct(ir.GetVectorReg(data1), + ir.GetVectorReg(data1 + 1))), + addr1); } } else if (bit_size == 64) { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); const IR::Value data = ir.CompositeConstruct(ir.GetVectorReg(data0), ir.GetVectorReg(data0 + 1)); - ir.WriteShared(bit_size, data, addr0); + ir.WriteShared(bit_size, ir.PackUint2x32(data), addr0); + } else if (bit_size == 16) { + const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); + ir.WriteShared(bit_size, ir.GetVectorReg(data0), addr0); } else { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); ir.WriteShared(bit_size, ir.GetVectorReg(data0), addr0); @@ -289,22 +312,29 @@ void Translator::DS_READ(int bit_size, bool is_signed, bool is_pair, bool stride if (bit_size == 32) { ir.SetVectorReg(dst_reg++, IR::U32{data0}); } else { - ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(data0, 0)}); - ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(data0, 1)}); + const auto vector = ir.UnpackUint2x32(IR::U64{data0}); + ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 0)}); + ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 1)}); } const IR::U32 addr1 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset1 * adj))); const IR::Value data1 = ir.LoadShared(bit_size, is_signed, addr1); if (bit_size == 32) { ir.SetVectorReg(dst_reg++, IR::U32{data1}); } else { - ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(data1, 0)}); - ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(data1, 1)}); + const auto vector = ir.UnpackUint2x32(IR::U64{data1}); + ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 0)}); + ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 1)}); } } else if (bit_size == 64) { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); const IR::Value data = ir.LoadShared(bit_size, is_signed, addr0); - ir.SetVectorReg(dst_reg, IR::U32{ir.CompositeExtract(data, 0)}); - ir.SetVectorReg(dst_reg + 1, IR::U32{ir.CompositeExtract(data, 1)}); + const auto vector = ir.UnpackUint2x32(IR::U64{data}); + ir.SetVectorReg(dst_reg, IR::U32{ir.CompositeExtract(vector, 0)}); + ir.SetVectorReg(dst_reg + 1, IR::U32{ir.CompositeExtract(vector, 1)}); + } else if (bit_size == 16) { + const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); + const IR::U16 data = IR::U16{ir.LoadShared(bit_size, is_signed, addr0)}; + ir.SetVectorReg(dst_reg, ir.UConvert(32, data)); } else { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); const IR::U32 data = IR::U32{ir.LoadShared(bit_size, is_signed, addr0)}; diff --git a/src/shader_recompiler/frontend/translate/translate.h b/src/shader_recompiler/frontend/translate/translate.h index 96ca924a3..086b325aa 100644 --- a/src/shader_recompiler/frontend/translate/translate.h +++ b/src/shader_recompiler/frontend/translate/translate.h @@ -271,6 +271,7 @@ public: // Data share // DS void DS_ADD_U32(const GcnInst& inst, bool rtn); + void DS_ADD_U64(const GcnInst& inst, bool rtn); void DS_MIN_U32(const GcnInst& inst, bool is_signed, bool rtn); void DS_MAX_U32(const GcnInst& inst, bool is_signed, bool rtn); void DS_WRITE(int bit_size, bool is_signed, bool is_pair, bool stride64, const GcnInst& inst); diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index e6cc32829..2c37c8099 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -293,10 +293,12 @@ void IREmitter::SetPatch(Patch patch, const F32& value) { Value IREmitter::LoadShared(int bit_size, bool is_signed, const U32& offset) { switch (bit_size) { + case 16: + return Inst(Opcode::LoadSharedU16, offset); case 32: return Inst(Opcode::LoadSharedU32, offset); case 64: - return Inst(Opcode::LoadSharedU64, offset); + return Inst(Opcode::LoadSharedU64, offset); default: UNREACHABLE_MSG("Invalid bit size {}", bit_size); } @@ -304,6 +306,9 @@ Value IREmitter::LoadShared(int bit_size, bool is_signed, const U32& offset) { void IREmitter::WriteShared(int bit_size, const Value& value, const U32& offset) { switch (bit_size) { + case 16: + Inst(Opcode::WriteSharedU16, offset, value); + break; case 32: Inst(Opcode::WriteSharedU32, offset, value); break; @@ -315,10 +320,12 @@ void IREmitter::WriteShared(int bit_size, const Value& value, const U32& offset) } } -U32F32 IREmitter::SharedAtomicIAdd(const U32& address, const U32F32& data) { +U32U64 IREmitter::SharedAtomicIAdd(const U32& address, const U32U64& data) { switch (data.Type()) { case Type::U32: return Inst(Opcode::SharedAtomicIAdd32, address, data); + case Type::U64: + return Inst(Opcode::SharedAtomicIAdd64, address, data); default: ThrowInvalidType(data.Type()); } diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index 0e41f4b2d..eae44ed04 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -99,7 +99,7 @@ public: [[nodiscard]] Value LoadShared(int bit_size, bool is_signed, const U32& offset); void WriteShared(int bit_size, const Value& value, const U32& offset); - [[nodiscard]] U32F32 SharedAtomicIAdd(const U32& address, const U32F32& data); + [[nodiscard]] U32U64 SharedAtomicIAdd(const U32& address, const U32U64& data); [[nodiscard]] U32 SharedAtomicIMin(const U32& address, const U32& data, bool is_signed); [[nodiscard]] U32 SharedAtomicIMax(const U32& address, const U32& data, bool is_signed); [[nodiscard]] U32 SharedAtomicAnd(const U32& address, const U32& data); diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index 5b3216be6..e96e32297 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -30,13 +30,16 @@ OPCODE(EmitVertex, Void, OPCODE(EmitPrimitive, Void, ) // Shared memory operations +OPCODE(LoadSharedU16, U16, U32, ) OPCODE(LoadSharedU32, U32, U32, ) -OPCODE(LoadSharedU64, U32x2, U32, ) +OPCODE(LoadSharedU64, U64, U32, ) +OPCODE(WriteSharedU16, Void, U32, U16, ) OPCODE(WriteSharedU32, Void, U32, U32, ) -OPCODE(WriteSharedU64, Void, U32, U32x2, ) +OPCODE(WriteSharedU64, Void, U32, U64, ) // Shared atomic operations OPCODE(SharedAtomicIAdd32, U32, U32, U32, ) +OPCODE(SharedAtomicIAdd64, U64, U32, U64, ) OPCODE(SharedAtomicSMin32, U32, U32, U32, ) OPCODE(SharedAtomicUMin32, U32, U32, U32, ) OPCODE(SharedAtomicSMax32, U32, U32, U32, ) @@ -116,6 +119,7 @@ OPCODE(StoreBufferFormatF32, Void, Opaq // Buffer atomic operations OPCODE(BufferAtomicIAdd32, U32, Opaque, Opaque, U32 ) +OPCODE(BufferAtomicIAdd64, U64, Opaque, Opaque, U64 ) OPCODE(BufferAtomicSMin32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicUMin32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicSMax32, U32, Opaque, Opaque, U32 ) diff --git a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp index 02745bf9a..b292b41b9 100644 --- a/src/shader_recompiler/ir/passes/ring_access_elimination.cpp +++ b/src/shader_recompiler/ir/passes/ring_access_elimination.cpp @@ -39,11 +39,13 @@ void RingAccessElimination(const IR::Program& program, const RuntimeInfo& runtim ASSERT(addr->Arg(1).IsImmediate()); offset = addr->Arg(1).U32(); } - IR::Value data = inst.Arg(1).Resolve(); + IR::Value data = is_composite ? ir.UnpackUint2x32(IR::U64{inst.Arg(1).Resolve()}) + : inst.Arg(1).Resolve(); for (s32 i = 0; i < num_components; i++) { const auto attrib = IR::Attribute::Param0 + (offset / 16); const auto comp = (offset / 4) % 4; - const IR::U32 value = IR::U32{is_composite ? data.Inst()->Arg(i) : data}; + const IR::U32 value = + IR::U32{is_composite ? ir.CompositeExtract(data, i) : data}; ir.SetAttribute(attrib, ir.BitCast(value), comp); offset += 4; } diff --git a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp index d4759b32e..ba8d1cca6 100644 --- a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp +++ b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp @@ -34,8 +34,10 @@ void Visit(Info& info, const IR::Inst& inst) { info.uses_patches |= 1U << IR::GenericPatchIndex(patch); break; } + case IR::Opcode::LoadSharedU16: case IR::Opcode::LoadSharedU32: case IR::Opcode::LoadSharedU64: + case IR::Opcode::WriteSharedU16: case IR::Opcode::WriteSharedU32: case IR::Opcode::WriteSharedU64: info.uses_shared = true; diff --git a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp index 25aaf257c..409c05940 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp @@ -16,6 +16,7 @@ static bool IsSharedAccess(const IR::Inst& inst) { case IR::Opcode::WriteSharedU64: case IR::Opcode::SharedAtomicAnd32: case IR::Opcode::SharedAtomicIAdd32: + case IR::Opcode::SharedAtomicIAdd64: case IR::Opcode::SharedAtomicOr32: case IR::Opcode::SharedAtomicSMax32: case IR::Opcode::SharedAtomicUMax32: @@ -33,9 +34,11 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ if (program.info.stage != Stage::Compute) { return; } - // Only perform the transform if the host shared memory is insufficient. + // Only perform the transform if the host shared memory is insufficient + // or the device does not support VK_KHR_workgroup_memory_explicit_layout const u32 shared_memory_size = runtime_info.cs_info.shared_memory_size; - if (shared_memory_size <= profile.max_shared_memory_size) { + if (shared_memory_size <= profile.max_shared_memory_size && + profile.supports_workgroup_explicit_memory_layout) { return; } // Add buffer binding for shared memory storage buffer. @@ -60,6 +63,7 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ ir.BufferAtomicAnd(handle, inst.Arg(0), inst.Arg(1), {})); continue; case IR::Opcode::SharedAtomicIAdd32: + case IR::Opcode::SharedAtomicIAdd64: inst.ReplaceUsesWithAndRemove( ir.BufferAtomicIAdd(handle, inst.Arg(0), inst.Arg(1), {})); continue; @@ -93,12 +97,19 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ ir.Imm32(shared_memory_size)); const IR::U32 address = ir.IAdd(IR::U32{inst.Arg(0)}, offset); switch (inst.GetOpcode()) { + case IR::Opcode::LoadSharedU16: + inst.ReplaceUsesWithAndRemove(ir.LoadBufferU16(handle, address, {})); + break; case IR::Opcode::LoadSharedU32: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU32(1, handle, address, {})); break; case IR::Opcode::LoadSharedU64: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU32(2, handle, address, {})); break; + case IR::Opcode::WriteSharedU16: + ir.StoreBufferU16(handle, address, IR::U32{inst.Arg(1)}, {}); + inst.Invalidate(); + break; case IR::Opcode::WriteSharedU32: ir.StoreBufferU32(1, handle, address, inst.Arg(1), {}); inst.Invalidate(); diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h index 853e4854d..7d313180f 100644 --- a/src/shader_recompiler/profile.h +++ b/src/shader_recompiler/profile.h @@ -23,13 +23,13 @@ struct Profile { bool support_fp32_denorm_preserve{}; bool support_fp32_denorm_flush{}; bool support_fp32_round_to_zero{}; - bool support_explicit_workgroup_layout{}; bool support_legacy_vertex_attributes{}; bool supports_image_load_store_lod{}; bool supports_native_cube_calc{}; bool supports_trinary_minmax{}; bool supports_robust_buffer_access{}; bool supports_image_fp32_atomic_min_max{}; + bool supports_workgroup_explicit_memory_layout{}; bool has_broken_spirv_clamp{}; bool lower_left_origin_mode{}; bool needs_manual_interpolation{}; diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 9584329f0..0591e06ce 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -212,7 +212,8 @@ bool Instance::CreateDevice() { vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT, vk::PhysicalDevicePrimitiveTopologyListRestartFeaturesEXT, vk::PhysicalDevicePortabilitySubsetFeaturesKHR, - vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT>(); + vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT, + vk::PhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR>(); features = feature_chain.get().features; const vk::StructureChain properties_chain = physical_device.getProperties2< @@ -283,6 +284,20 @@ bool Instance::CreateDevice() { LOG_INFO(Render_Vulkan, "- shaderImageFloat32AtomicMinMax: {}", shader_atomic_float2_features.shaderImageFloat32AtomicMinMax); } + workgroup_memory_explicit_layout = + add_extension(VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME); + if (workgroup_memory_explicit_layout) { + workgroup_memory_explicit_layout_features = + feature_chain.get(); + LOG_INFO(Render_Vulkan, "- workgroupMemoryExplicitLayout: {}", + workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout); + LOG_INFO(Render_Vulkan, "- workgroupMemoryExplicitLayoutScalarBlockLayout: {}", + workgroup_memory_explicit_layout_features + .workgroupMemoryExplicitLayoutScalarBlockLayout); + LOG_INFO( + Render_Vulkan, "- workgroupMemoryExplicitLayout16BitAccess: {}", + workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout16BitAccess); + } const bool calibrated_timestamps = TRACY_GPU_ENABLED ? add_extension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) : false; @@ -420,6 +435,15 @@ bool Instance::CreateDevice() { .shaderImageFloat32AtomicMinMax = shader_atomic_float2_features.shaderImageFloat32AtomicMinMax, }, + vk::PhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR{ + .workgroupMemoryExplicitLayout = + workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout, + .workgroupMemoryExplicitLayoutScalarBlockLayout = + workgroup_memory_explicit_layout_features + .workgroupMemoryExplicitLayoutScalarBlockLayout, + .workgroupMemoryExplicitLayout16BitAccess = + workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout16BitAccess, + }, #ifdef __APPLE__ portability_features, #endif @@ -452,6 +476,9 @@ bool Instance::CreateDevice() { if (!shader_atomic_float2) { device_chain.unlink(); } + if (!workgroup_memory_explicit_layout) { + device_chain.unlink(); + } auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get()); if (device_result != vk::Result::eSuccess) { diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 30848e8b7..c687e6f67 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -171,6 +171,12 @@ public: return shader_atomic_float2 && shader_atomic_float2_features.shaderImageFloat32AtomicMinMax; } + /// Returns true when VK_KHR_workgroup_memory_explicit_layout is supported. + bool IsWorkgroupMemoryExplicitLayoutSupported() const { + return workgroup_memory_explicit_layout && + workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout16BitAccess; + } + /// Returns true when geometry shaders are supported by the device bool IsGeometryStageSupported() const { return features.geometryShader; @@ -349,6 +355,8 @@ private: vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT dynamic_state_3_features; vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features; vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT shader_atomic_float2_features; + vk::PhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR + workgroup_memory_explicit_layout_features; vk::DriverIdKHR driver_id; vk::UniqueDebugUtilsMessengerEXT debug_callback{}; std::string vendor_name; @@ -374,6 +382,7 @@ private: bool amd_gcn_shader{}; bool amd_shader_trinary_minmax{}; bool shader_atomic_float2{}; + bool workgroup_memory_explicit_layout{}; bool portability_subset{}; }; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index cd8552515..2c3f4ba2f 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -210,7 +210,6 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .support_fp32_denorm_preserve = bool(vk12_props.shaderDenormPreserveFloat32), .support_fp32_denorm_flush = bool(vk12_props.shaderDenormFlushToZeroFloat32), .support_fp32_round_to_zero = bool(vk12_props.shaderRoundingModeRTZFloat32), - .support_explicit_workgroup_layout = true, .support_legacy_vertex_attributes = instance_.IsLegacyVertexAttributesSupported(), .supports_image_load_store_lod = instance_.IsImageLoadStoreLodSupported(), .supports_native_cube_calc = instance_.IsAmdGcnShaderSupported(), @@ -218,6 +217,8 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, // TODO: Emitted bounds checks cause problems with phi control flow; needs to be fixed. .supports_robust_buffer_access = true, // instance_.IsRobustBufferAccess2Supported(), .supports_image_fp32_atomic_min_max = instance_.IsShaderAtomicFloatImage32MinMaxSupported(), + .supports_workgroup_explicit_memory_layout = + instance_.IsWorkgroupMemoryExplicitLayoutSupported(), .needs_manual_interpolation = instance.IsFragmentShaderBarycentricSupported() && instance.GetDriverID() == vk::DriverId::eNvidiaProprietary, .needs_lds_barriers = instance.GetDriverID() == vk::DriverId::eNvidiaProprietary || From 64bbedeb82ce95b4ba328fb3b4597a6b6f3b2dd0 Mon Sep 17 00:00:00 2001 From: Connor Garey Date: Mon, 9 Jun 2025 23:25:57 +0100 Subject: [PATCH 120/141] changed package name to openal-soft-devel reflecting the fedora name package change (#3069) --- documents/building-linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documents/building-linux.md b/documents/building-linux.md index bd07b2eff..61d067881 100644 --- a/documents/building-linux.md +++ b/documents/building-linux.md @@ -25,7 +25,7 @@ sudo apt install build-essential clang git cmake libasound2-dev \ ```bash sudo dnf install clang git cmake libatomic alsa-lib-devel \ - pipewire-jack-audio-connection-kit-devel openal-devel \ + pipewire-jack-audio-connection-kit-devel openal-soft-devel \ openssl-devel libevdev-devel libudev-devel libXext-devel \ qt6-qtbase-devel qt6-qtbase-private-devel \ qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel \ From 0444e590e071c565f548b1fdb48d4e06f0eba3b7 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:29:15 -0700 Subject: [PATCH 121/141] mac: Fix building on macOS 26. (#3073) --- CMakeLists.txt | 13 +++++-------- externals/MoltenVK/MoltenVK | 2 +- externals/MoltenVK/SPIRV-Cross | 2 +- src/core/signals.cpp | 1 + src/core/tls.cpp | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6dfe9348a..7c2739d22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1119,6 +1119,10 @@ if (APPLE) set(MVK_BUNDLE_PATH "Resources/vulkan/icd.d") set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path/../${MVK_BUNDLE_PATH}") set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR}/shadps4.app/Contents/${MVK_BUNDLE_PATH}) + + add_custom_command( + OUTPUT ${MVK_DST} + COMMAND ${CMAKE_COMMAND} -E make_directory ${MVK_DST}) else() set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path") set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR}) @@ -1129,9 +1133,6 @@ if (APPLE) set(MVK_ICD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/externals/MoltenVK/MoltenVK/MoltenVK/icd/MoltenVK_icd.json) set(MVK_ICD_DST ${MVK_DST}/MoltenVK_icd.json) - add_custom_command( - OUTPUT ${MVK_DST} - COMMAND ${CMAKE_COMMAND} -E make_directory ${MVK_DST}) add_custom_command( OUTPUT ${MVK_ICD_DST} DEPENDS ${MVK_ICD_SRC} ${MVK_DST} @@ -1146,17 +1147,13 @@ if (APPLE) if (ARCHITECTURE STREQUAL "x86_64") # Reserve system-managed memory space. - target_link_options(shadps4 PRIVATE -Wl,-no_pie,-no_fixup_chains,-no_huge,-pagezero_size,0x4000,-segaddr,TCB_SPACE,0x4000,-segaddr,SYSTEM_MANAGED,0x400000,-segaddr,SYSTEM_RESERVED,0x7FFFFC000,-image_base,0x20000000000) + target_link_options(shadps4 PRIVATE -Wl,-ld_classic,-no_pie,-no_fixup_chains,-no_huge,-pagezero_size,0x4000,-segaddr,TCB_SPACE,0x4000,-segaddr,SYSTEM_MANAGED,0x400000,-segaddr,SYSTEM_RESERVED,0x7FFFFC000,-image_base,0x20000000000) endif() # Replacement for std::chrono::time_zone target_link_libraries(shadps4 PRIVATE date::date-tz) endif() -if (NOT ENABLE_QT_GUI) - target_link_libraries(shadps4 PRIVATE SDL3::SDL3) -endif() - if (ENABLE_QT_GUI) target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network Qt6::Multimedia) add_definitions(-DENABLE_QT_GUI) diff --git a/externals/MoltenVK/MoltenVK b/externals/MoltenVK/MoltenVK index 3a0b07a24..00abd384c 160000 --- a/externals/MoltenVK/MoltenVK +++ b/externals/MoltenVK/MoltenVK @@ -1 +1 @@ -Subproject commit 3a0b07a24a4a681ffe70b461b1f4333b2729e2ef +Subproject commit 00abd384ce01cbd439045905d2fa6cf799dfa2f6 diff --git a/externals/MoltenVK/SPIRV-Cross b/externals/MoltenVK/SPIRV-Cross index 969e75f7c..1a69a919f 160000 --- a/externals/MoltenVK/SPIRV-Cross +++ b/externals/MoltenVK/SPIRV-Cross @@ -1 +1 @@ -Subproject commit 969e75f7cc0718774231d029f9d52fa87d4ae1b2 +Subproject commit 1a69a919fa302e92b337594bd0a8aaea61037d91 diff --git a/src/core/signals.cpp b/src/core/signals.cpp index e47a78cd2..4099ac237 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.cpp @@ -11,6 +11,7 @@ #include #else #include +#include #ifdef ARCH_X86_64 #include #endif diff --git a/src/core/tls.cpp b/src/core/tls.cpp index e13c683e1..0d1d514cf 100644 --- a/src/core/tls.cpp +++ b/src/core/tls.cpp @@ -51,7 +51,7 @@ Tcb* GetTcbBase() { // Apple x86_64 // Reserve space in the 32-bit address range for allocating TCB pages. -asm(".zerofill TCB_SPACE,TCB_SPACE,__guest_system,0x3FC000"); +asm(".zerofill TCB_SPACE,TCB_SPACE,__tcb_space,0x3FC000"); struct LdtPage { void* tcb; From e2b726382ea4a156eefc52bcb7cac06713563e5e Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:48:20 -0700 Subject: [PATCH 122/141] vulkan: Fix two validation errors introduced by shared memory changes. (#3074) --- .../passes/shared_memory_to_storage_pass.cpp | 8 ++++---- .../renderer_vulkan/vk_instance.cpp | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp index 409c05940..12d4d0659 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp @@ -34,11 +34,11 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ if (program.info.stage != Stage::Compute) { return; } - // Only perform the transform if the host shared memory is insufficient - // or the device does not support VK_KHR_workgroup_memory_explicit_layout + // Only perform the transform if there is shared memory and either host shared memory is + // insufficient or the device does not support VK_KHR_workgroup_memory_explicit_layout const u32 shared_memory_size = runtime_info.cs_info.shared_memory_size; - if (shared_memory_size <= profile.max_shared_memory_size && - profile.supports_workgroup_explicit_memory_layout) { + if (shared_memory_size == 0 || (shared_memory_size <= profile.max_shared_memory_size && + profile.supports_workgroup_explicit_memory_layout)) { return; } // Add buffer binding for shared memory storage buffer. diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 0591e06ce..63c0a38d6 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -445,7 +445,25 @@ bool Instance::CreateDevice() { workgroup_memory_explicit_layout_features.workgroupMemoryExplicitLayout16BitAccess, }, #ifdef __APPLE__ - portability_features, + vk::PhysicalDevicePortabilitySubsetFeaturesKHR{ + .constantAlphaColorBlendFactors = portability_features.constantAlphaColorBlendFactors, + .events = portability_features.events, + .imageViewFormatReinterpretation = portability_features.imageViewFormatReinterpretation, + .imageViewFormatSwizzle = portability_features.imageViewFormatSwizzle, + .imageView2DOn3DImage = portability_features.imageView2DOn3DImage, + .multisampleArrayImage = portability_features.multisampleArrayImage, + .mutableComparisonSamplers = portability_features.mutableComparisonSamplers, + .pointPolygons = portability_features.pointPolygons, + .samplerMipLodBias = portability_features.samplerMipLodBias, + .separateStencilMaskRef = portability_features.separateStencilMaskRef, + .shaderSampleRateInterpolationFunctions = + portability_features.shaderSampleRateInterpolationFunctions, + .tessellationIsolines = portability_features.tessellationIsolines, + .tessellationPointMode = portability_features.tessellationPointMode, + .triangleFans = portability_features.triangleFans, + .vertexAttributeAccessBeyondStride = + portability_features.vertexAttributeAccessBeyondStride, + }, #endif }; From e0c930f2d801e0d2998202760b785a76f9346ecd Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Tue, 10 Jun 2025 18:57:16 +0300 Subject: [PATCH 123/141] shader_recompiler: Cleanup fragment attribute handling (#3076) * image: Take minimum of mip levels Avoids validation error * texture_cache: Update depth target image Avoids using undefined depth target in rendering * shader_recompiler: Cleanup fragment attribute handling --- .../backend/spirv/spirv_emit_context.cpp | 28 ++++++++++--------- .../translate/vector_interpolation.cpp | 12 ++++---- src/video_core/texture_cache/image.cpp | 3 +- .../texture_cache/texture_cache.cpp | 2 +- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 672856397..c47a75739 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -299,8 +299,7 @@ void EmitContext::DefineInterpolatedAttribs() { // Iterate all input attributes, load them and manually interpolate. for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { const auto& input = runtime_info.fs_info.inputs[i]; - const u32 semantic = input.param_index; - auto& params = input_params[semantic]; + auto& params = input_params[i]; if (input.is_flat || params.is_loaded) { continue; } @@ -318,7 +317,7 @@ void EmitContext::DefineInterpolatedAttribs() { const Id p10_y{OpVectorTimesScalar(F32[4], p10, bary_coord_y)}; const Id p20_z{OpVectorTimesScalar(F32[4], p20, bary_coord_z)}; params.id = OpFAdd(F32[4], p0, OpFAdd(F32[4], p10_y, p20_z)); - Name(params.id, fmt::format("fs_in_attr{}", semantic)); + Name(params.id, fmt::format("fs_in_attr{}", i)); params.is_loaded = true; } } @@ -427,25 +426,28 @@ void EmitContext::DefineInputs() { } for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) { const auto& input = runtime_info.fs_info.inputs[i]; - const u32 semantic = input.param_index; - ASSERT(semantic < IR::NumParams); if (input.IsDefault()) { - input_params[semantic] = { - MakeDefaultValue(*this, input.default_value), input_f32, F32[1], 4, false, true, + input_params[i] = { + .id = MakeDefaultValue(*this, input.default_value), + .pointer_type = input_f32, + .component_type = F32[1], + .num_components = 4, + .is_integer = false, + .is_loaded = true, }; continue; } - const IR::Attribute param{IR::Attribute::Param0 + input.param_index}; + const IR::Attribute param{IR::Attribute::Param0 + i}; const u32 num_components = info.loads.NumComponents(param); const Id type{F32[num_components]}; Id attr_id{}; if (profile.needs_manual_interpolation && !input.is_flat) { - attr_id = DefineInput(TypeArray(type, ConstU32(3U)), semantic); + attr_id = DefineInput(TypeArray(type, ConstU32(3U)), input.param_index); Decorate(attr_id, spv::Decoration::PerVertexKHR); - Name(attr_id, fmt::format("fs_in_attr{}_p", semantic)); + Name(attr_id, fmt::format("fs_in_attr{}_p", i)); } else { - attr_id = DefineInput(type, semantic); - Name(attr_id, fmt::format("fs_in_attr{}", semantic)); + attr_id = DefineInput(type, input.param_index); + Name(attr_id, fmt::format("fs_in_attr{}", i)); if (input.is_flat) { Decorate(attr_id, spv::Decoration::Flat); @@ -453,7 +455,7 @@ void EmitContext::DefineInputs() { Decorate(attr_id, spv::Decoration::NoPerspective); } } - input_params[semantic] = + input_params[i] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, num_components, false); } break; diff --git a/src/shader_recompiler/frontend/translate/vector_interpolation.cpp b/src/shader_recompiler/frontend/translate/vector_interpolation.cpp index 2d7297c12..5a287dbe2 100644 --- a/src/shader_recompiler/frontend/translate/vector_interpolation.cpp +++ b/src/shader_recompiler/frontend/translate/vector_interpolation.cpp @@ -22,15 +22,17 @@ void Translator::EmitVectorInterpolation(const GcnInst& inst) { // VINTRP void Translator::V_INTERP_P2_F32(const GcnInst& inst) { - const auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); - info.interp_qualifiers[attr.param_index] = vgpr_to_interp[inst.src[0].code]; - const IR::Attribute attrib{IR::Attribute::Param0 + attr.param_index}; + const u32 attr_index = inst.control.vintrp.attr; + const auto& attr = runtime_info.fs_info.inputs.at(attr_index); + info.interp_qualifiers[attr_index] = vgpr_to_interp[inst.src[0].code]; + const IR::Attribute attrib{IR::Attribute::Param0 + attr_index}; SetDst(inst.dst[0], ir.GetAttribute(attrib, inst.control.vintrp.chan)); } void Translator::V_INTERP_MOV_F32(const GcnInst& inst) { - const auto& attr = runtime_info.fs_info.inputs.at(inst.control.vintrp.attr); - const IR::Attribute attrib{IR::Attribute::Param0 + attr.param_index}; + const u32 attr_index = inst.control.vintrp.attr; + const auto& attr = runtime_info.fs_info.inputs.at(attr_index); + const IR::Attribute attrib{IR::Attribute::Param0 + attr_index}; SetDst(inst.dst[0], ir.GetAttribute(attrib, inst.control.vintrp.chan)); } diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index d8070da61..6241100a0 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -319,7 +319,8 @@ void Image::CopyImage(const Image& image) { auto cmdbuf = scheduler->CommandBuffer(); boost::container::small_vector image_copy{}; - for (u32 m = 0; m < image.info.resources.levels; ++m) { + const u32 num_mips = std::min(image.info.resources.levels, info.resources.levels); + for (u32 m = 0; m < num_mips; ++m) { const auto mip_w = std::max(image.info.size.width >> m, 1u); const auto mip_h = std::max(image.info.size.height >> m, 1u); const auto mip_d = std::max(image.info.size.depth >> m, 1u); diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index f070b9132..cc244eb6b 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -461,9 +461,9 @@ ImageView& TextureCache::FindDepthTarget(BaseDesc& desc) { const ImageId image_id = FindImage(desc); Image& image = slot_images[image_id]; image.flags |= ImageFlagBits::GpuModified; - image.flags &= ~ImageFlagBits::Dirty; image.usage.depth_target = 1u; image.usage.stencil = image.info.HasStencil(); + UpdateImage(image_id); // Register meta data for this depth buffer if (!(image.flags & ImageFlagBits::MetaRegistered)) { From 9981c8df03dcaf00d3e3d6b59f731961424397d5 Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Tue, 10 Jun 2025 21:30:45 +0200 Subject: [PATCH 124/141] Add option to ignore game patch (#3039) * impl * fix * cleanup * more * clang + * why --- src/core/file_sys/fs.cpp | 6 ++++-- src/core/file_sys/fs.h | 1 + src/emulator.cpp | 2 +- src/main.cpp | 26 +++++++++++++++----------- src/qt_gui/main.cpp | 32 ++++++++++++++++++-------------- 5 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/core/file_sys/fs.cpp b/src/core/file_sys/fs.cpp index 4dad44874..b237ab7d9 100644 --- a/src/core/file_sys/fs.cpp +++ b/src/core/file_sys/fs.cpp @@ -10,6 +10,8 @@ namespace Core::FileSys { +bool MntPoints::ignore_game_patches = false; + std::string RemoveTrailingSlashes(const std::string& path) { // Remove trailing slashes to make comparisons simpler. std::string path_sanitized = path; @@ -77,7 +79,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea patch_path /= rel_path; if ((corrected_path.starts_with("/app0") || corrected_path.starts_with("/hostapp")) && - !force_base_path && std::filesystem::exists(patch_path)) { + !force_base_path && !ignore_game_patches && std::filesystem::exists(patch_path)) { return patch_path; } @@ -137,7 +139,7 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea return std::optional(current_path); }; - if (!force_base_path) { + if (!force_base_path && !ignore_game_patches) { if (const auto path = search(patch_path)) { return *path; } diff --git a/src/core/file_sys/fs.h b/src/core/file_sys/fs.h index 6638b48e8..4a2aa56c1 100644 --- a/src/core/file_sys/fs.h +++ b/src/core/file_sys/fs.h @@ -21,6 +21,7 @@ class MntPoints { static constexpr bool NeedsCaseInsensitiveSearch = true; #endif public: + static bool ignore_game_patches; struct MntPair { std::filesystem::path host_path; std::string mount; // e.g /app0 diff --git a/src/emulator.cpp b/src/emulator.cpp index bb50b8686..f50147818 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -75,7 +75,7 @@ void Emulator::Run(std::filesystem::path file, const std::vector ar game_folder_name.ends_with("-UPDATE") || game_folder_name.ends_with("-patch")) { // If an executable was launched from a separate update directory, // use the base game directory as the game folder. - const auto base_name = game_folder_name.substr(0, game_folder_name.size() - 7); + const std::string base_name = game_folder_name.substr(0, game_folder_name.rfind('-')); const auto base_path = game_folder.parent_path() / base_name; if (std::filesystem::is_directory(base_path)) { game_folder = base_path; diff --git a/src/main.cpp b/src/main.cpp index 85581774b..8a251c55a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,17 +35,19 @@ int main(int argc, char* argv[]) { std::unordered_map> arg_map = { {"-h", [&](int&) { - std::cout << "Usage: shadps4 [options] \n" - "Options:\n" - " -g, --game Specify game path to launch\n" - " -- ... Parameters passed to the game ELF. " - "Needs to be at the end of the line, and everything after \"--\" is a " - "game argument.\n" - " -p, --patch Apply specified patch file\n" - " -f, --fullscreen Specify window initial fullscreen " - "state. Does not overwrite the config file.\n" - " --add-game-folder Adds a new game folder to the config.\n" - " -h, --help Display this help message\n"; + std::cout + << "Usage: shadps4 [options] \n" + "Options:\n" + " -g, --game Specify game path to launch\n" + " -- ... Parameters passed to the game ELF. " + "Needs to be at the end of the line, and everything after \"--\" is a " + "game argument.\n" + " -p, --patch Apply specified patch file\n" + " -i, --ignore-game-patch Disable automatic loading of game patch\n" + " -f, --fullscreen Specify window initial fullscreen " + "state. Does not overwrite the config file.\n" + " --add-game-folder Adds a new game folder to the config.\n" + " -h, --help Display this help message\n"; exit(0); }}, {"--help", [&](int& i) { arg_map["-h"](i); }}, @@ -72,6 +74,8 @@ int main(int argc, char* argv[]) { } }}, {"--patch", [&](int& i) { arg_map["-p"](i); }}, + {"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }}, + {"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }}, {"-f", [&](int& i) { if (++i >= argc) { diff --git a/src/qt_gui/main.cpp b/src/qt_gui/main.cpp index bd9dca6ce..b7de517e8 100644 --- a/src/qt_gui/main.cpp +++ b/src/qt_gui/main.cpp @@ -41,20 +41,22 @@ int main(int argc, char* argv[]) { std::unordered_map> arg_map = { {"-h", [&](int&) { - std::cout << "Usage: shadps4 [options]\n" - "Options:\n" - " No arguments: Opens the GUI.\n" - " -g, --game Specify or " - " to launch\n" - " -- ... Parameters passed to the game ELF. " - "Needs to be at the end of the line, and everything after \"--\" is a " - "game argument.\n" - " -p, --patch Apply specified patch file\n" - " -s, --show-gui Show the GUI\n" - " -f, --fullscreen Specify window initial fullscreen " - "state. Does not overwrite the config file.\n" - " --add-game-folder Adds a new game folder to the config.\n" - " -h, --help Display this help message\n"; + std::cout + << "Usage: shadps4 [options]\n" + "Options:\n" + " No arguments: Opens the GUI.\n" + " -g, --game Specify or " + " to launch\n" + " -- ... Parameters passed to the game ELF. " + "Needs to be at the end of the line, and everything after \"--\" is a " + "game argument.\n" + " -p, --patch Apply specified patch file\n" + " -i, --ignore-game-patch Disable automatic loading of game patch\n" + " -s, --show-gui Show the GUI\n" + " -f, --fullscreen Specify window initial fullscreen " + "state. Does not overwrite the config file.\n" + " --add-game-folder Adds a new game folder to the config.\n" + " -h, --help Display this help message\n"; exit(0); }}, {"--help", [&](int& i) { arg_map["-h"](i); }}, // Redirect --help to -h @@ -84,6 +86,8 @@ int main(int argc, char* argv[]) { } }}, {"--patch", [&](int& i) { arg_map["-p"](i); }}, + {"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }}, + {"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }}, {"-f", [&](int& i) { if (++i >= argc) { From b49340dff8e28abcf96fe07ad0e90c4dda0bcaf2 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:22:50 -0500 Subject: [PATCH 125/141] libSceVideodec2: Update structs to match newer firmwares (#3077) * Update file_system.cpp * libSceVideodec2 struct fixes Our code was based on an old version of the libSceVideodec2 library. Based on what I've decompiled, these structs changed somewhere around firmware 6.50, and newer versions of the library have these flexible checks to accommodate both variants of the structs. * Static assert for AvcPictureInfo struct All the other Videodec2 structs have static asserts, might as well use one here too. * Initialize new values Set proper values for frameFormat and framePitchInBytes. `frame->linesize[0]` appears to be in bytes already, I'm not sure if that means framePitch is being set wrong though. --- src/core/libraries/kernel/file_system.cpp | 1 + src/core/libraries/videodec/videodec2.cpp | 6 +++--- src/core/libraries/videodec/videodec2.h | 4 +++- src/core/libraries/videodec/videodec2_avc.h | 17 +++++++++++++++++ src/core/libraries/videodec/videodec2_impl.cpp | 4 ++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index ad372325c..fecc606fd 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -1050,6 +1050,7 @@ void RegisterFileSystem(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("4wSze92BhLI", "libkernel", 1, "libkernel", 1, 1, sceKernelWrite); LIB_FUNCTION("+WRlkKjZvag", "libkernel", 1, "libkernel", 1, 1, readv); LIB_FUNCTION("YSHRBRLn2pI", "libkernel", 1, "libkernel", 1, 1, writev); + LIB_FUNCTION("kAt6VDbHmro", "libkernel", 1, "libkernel", 1, 1, sceKernelWritev); LIB_FUNCTION("Oy6IpwgtYOk", "libScePosix", 1, "libkernel", 1, 1, posix_lseek); LIB_FUNCTION("Oy6IpwgtYOk", "libkernel", 1, "libkernel", 1, 1, posix_lseek); LIB_FUNCTION("oib76F-12fk", "libkernel", 1, "libkernel", 1, 1, sceKernelLseek); diff --git a/src/core/libraries/videodec/videodec2.cpp b/src/core/libraries/videodec/videodec2.cpp index 4f9379151..1c6044fe2 100644 --- a/src/core/libraries/videodec/videodec2.cpp +++ b/src/core/libraries/videodec/videodec2.cpp @@ -140,7 +140,7 @@ s32 PS4_SYSV_ABI sceVideodec2Flush(OrbisVideodec2Decoder decoder, return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } if (frameBuffer->thisSize != sizeof(OrbisVideodec2FrameBuffer) || - outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) { + (outputInfo->thisSize | 8) != sizeof(OrbisVideodec2OutputInfo)) { LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -167,7 +167,7 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp LOG_ERROR(Lib_Vdec2, "Invalid arguments"); return ORBIS_VIDEODEC2_ERROR_ARGUMENT_POINTER; } - if (outputInfo->thisSize != sizeof(OrbisVideodec2OutputInfo)) { + if ((outputInfo->thisSize | 8) != sizeof(OrbisVideodec2OutputInfo)) { LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } @@ -179,7 +179,7 @@ s32 PS4_SYSV_ABI sceVideodec2GetPictureInfo(const OrbisVideodec2OutputInfo* outp if (p1stPictureInfoOut) { OrbisVideodec2AvcPictureInfo* picInfo = static_cast(p1stPictureInfoOut); - if (picInfo->thisSize != sizeof(OrbisVideodec2AvcPictureInfo)) { + if ((picInfo->thisSize | 16) != sizeof(OrbisVideodec2AvcPictureInfo)) { LOG_ERROR(Lib_Vdec2, "Invalid struct size"); return ORBIS_VIDEODEC2_ERROR_STRUCT_SIZE; } diff --git a/src/core/libraries/videodec/videodec2.h b/src/core/libraries/videodec/videodec2.h index abc8f8ab5..410ee8ea6 100644 --- a/src/core/libraries/videodec/videodec2.h +++ b/src/core/libraries/videodec/videodec2.h @@ -73,8 +73,10 @@ struct OrbisVideodec2OutputInfo { u32 frameHeight; void* frameBuffer; u64 frameBufferSize; + u32 frameFormat; + u32 framePitchInBytes; }; -static_assert(sizeof(OrbisVideodec2OutputInfo) == 0x30); +static_assert(sizeof(OrbisVideodec2OutputInfo) == 0x38); struct OrbisVideodec2FrameBuffer { u64 thisSize; diff --git a/src/core/libraries/videodec/videodec2_avc.h b/src/core/libraries/videodec/videodec2_avc.h index 22293ee93..1975209cb 100644 --- a/src/core/libraries/videodec/videodec2_avc.h +++ b/src/core/libraries/videodec/videodec2_avc.h @@ -55,6 +55,23 @@ struct OrbisVideodec2AvcPictureInfo { u8 pic_struct; u8 field_pic_flag; u8 bottom_field_flag; + + u8 sequenceParameterSetPresentFlag; + u8 pictureParameterSetPresentFlag; + u8 auDelimiterPresentFlag; + u8 endOfSequencePresentFlag; + u8 endOfStreamPresentFlag; + u8 fillerDataPresentFlag; + u8 pictureTimingSeiPresentFlag; + u8 bufferingPeriodSeiPresentFlag; + + u8 constraint_set0_flag; + u8 constraint_set1_flag; + u8 constraint_set2_flag; + u8 constraint_set3_flag; + u8 constraint_set4_flag; + u8 constraint_set5_flag; }; +static_assert(sizeof(OrbisVideodec2AvcPictureInfo) == 0x78); } // namespace Libraries::Vdec2 \ No newline at end of file diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index 22b17c86c..a643239a3 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -48,6 +48,7 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, outputInfo.isValid = false; outputInfo.isErrorFrame = true; outputInfo.pictureCount = 0; + outputInfo.frameFormat = 0; if (!inputData.auData) { return ORBIS_VIDEODEC2_ERROR_ACCESS_UNIT_POINTER; @@ -106,6 +107,7 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, outputInfo.frameWidth = frame->width; outputInfo.frameHeight = frame->height; outputInfo.framePitch = frame->linesize[0]; + outputInfo.framePitchInBytes = frame->linesize[0]; outputInfo.frameBufferSize = frameBuffer.frameBufferSize; outputInfo.frameBuffer = frameBuffer.frameBuffer; @@ -144,6 +146,7 @@ s32 VdecDecoder::Flush(OrbisVideodec2FrameBuffer& frameBuffer, outputInfo.isValid = false; outputInfo.isErrorFrame = true; outputInfo.pictureCount = 0; + outputInfo.frameFormat = 0; AVFrame* frame = av_frame_alloc(); if (!frame) { @@ -175,6 +178,7 @@ s32 VdecDecoder::Flush(OrbisVideodec2FrameBuffer& frameBuffer, outputInfo.frameWidth = frame->width; outputInfo.frameHeight = frame->height; outputInfo.framePitch = frame->linesize[0]; + outputInfo.framePitchInBytes = frame->linesize[0]; outputInfo.frameBufferSize = frameBuffer.frameBufferSize; outputInfo.frameBuffer = frameBuffer.frameBuffer; From ca92e72efe6a041ce27c5e7473b62abf99e8f4c2 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:41:58 -0700 Subject: [PATCH 126/141] shader_recompiler: Various fixes to shared memory and atomics. (#3075) * shader_recompiler: Various fixes to shared memory and atomics. * shader_recompiler: Re-type non-32bit load/stores. --- .../backend/spirv/emit_spirv_atomic.cpp | 63 ++++++---- .../backend/spirv/emit_spirv_bounds.h | 66 ++++++++-- .../spirv/emit_spirv_context_get_set.cpp | 118 +++++++++--------- .../backend/spirv/emit_spirv_convert.cpp | 8 ++ .../backend/spirv/emit_spirv_instructions.h | 13 +- .../frontend/translate/data_share.cpp | 76 +++++------ .../frontend/translate/vector_memory.cpp | 4 +- src/shader_recompiler/ir/ir_emitter.cpp | 64 +++++++--- src/shader_recompiler/ir/ir_emitter.h | 26 ++-- src/shader_recompiler/ir/microinstruction.cpp | 11 +- src/shader_recompiler/ir/opcodes.inc | 25 ++-- .../ir/passes/hull_shader_transform.cpp | 12 +- .../ir/passes/lower_buffer_format_to_raw.cpp | 16 +-- .../ir/passes/resource_tracking_pass.cpp | 9 ++ .../ir/passes/shared_memory_barrier_pass.cpp | 6 +- .../passes/shared_memory_to_storage_pass.cpp | 100 ++++++++++----- src/shader_recompiler/ir/value.h | 1 + 17 files changed, 391 insertions(+), 227 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp index 13fd8e180..47290e7e8 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_atomic.cpp @@ -27,6 +27,19 @@ Id SharedAtomicU32(EmitContext& ctx, Id offset, Id value, }); } +Id SharedAtomicU32IncDec(EmitContext& ctx, Id offset, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id)) { + const Id shift_id{ctx.ConstU32(2U)}; + const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; + const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; + const Id pointer{ + ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index)}; + const auto [scope, semantics]{AtomicArgs(ctx)}; + return AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { + return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics); + }); +} + Id SharedAtomicU64(EmitContext& ctx, Id offset, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { const Id shift_id{ctx.ConstU32(3U)}; @@ -40,19 +53,6 @@ Id SharedAtomicU64(EmitContext& ctx, Id offset, Id value, }); } -Id SharedAtomicU32_IncDec(EmitContext& ctx, Id offset, - Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id)) { - const Id shift_id{ctx.ConstU32(2U)}; - const Id index{ctx.OpShiftRightLogical(ctx.U32[1], offset, shift_id)}; - const u32 num_elements{Common::DivCeil(ctx.runtime_info.cs_info.shared_memory_size, 4u)}; - const Id pointer{ - ctx.OpAccessChain(ctx.shared_u32, ctx.shared_memory_u32, ctx.u32_zero_value, index)}; - const auto [scope, semantics]{AtomicArgs(ctx)}; - return AccessBoundsCheck<32>(ctx, index, ctx.ConstU32(num_elements), [&] { - return (ctx.*atomic_func)(ctx.U32[1], pointer, scope, semantics); - }); -} - Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id)) { const auto& buffer = ctx.buffers[handle]; @@ -68,6 +68,21 @@ Id BufferAtomicU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id }); } +Id BufferAtomicU32IncDec(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, + Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id)) { + const auto& buffer = ctx.buffers[handle]; + if (Sirit::ValidId(buffer.offset)) { + address = ctx.OpIAdd(ctx.U32[1], address, buffer.offset); + } + const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(2u)); + const auto [id, pointer_type] = buffer[EmitContext::PointerType::U32]; + const Id ptr = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index); + const auto [scope, semantics]{AtomicArgs(ctx)}; + return AccessBoundsCheck<32>(ctx, index, buffer.size_dwords, [&] { + return (ctx.*atomic_func)(ctx.U32[1], ptr, scope, semantics); + }); +} + Id BufferAtomicU32CmpSwap(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value, Id cmp_value, Id (Sirit::Module::*atomic_func)(Id, Id, Id, Id, Id, Id, Id)) { @@ -156,12 +171,12 @@ Id EmitSharedAtomicISub32(EmitContext& ctx, Id offset, Id value) { return SharedAtomicU32(ctx, offset, value, &Sirit::Module::OpAtomicISub); } -Id EmitSharedAtomicIIncrement32(EmitContext& ctx, Id offset) { - return SharedAtomicU32_IncDec(ctx, offset, &Sirit::Module::OpAtomicIIncrement); +Id EmitSharedAtomicInc32(EmitContext& ctx, Id offset) { + return SharedAtomicU32IncDec(ctx, offset, &Sirit::Module::OpAtomicIIncrement); } -Id EmitSharedAtomicIDecrement32(EmitContext& ctx, Id offset) { - return SharedAtomicU32_IncDec(ctx, offset, &Sirit::Module::OpAtomicIDecrement); +Id EmitSharedAtomicDec32(EmitContext& ctx, Id offset) { + return SharedAtomicU32IncDec(ctx, offset, &Sirit::Module::OpAtomicIDecrement); } Id EmitBufferAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { @@ -172,6 +187,10 @@ Id EmitBufferAtomicIAdd64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre return BufferAtomicU64(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicIAdd); } +Id EmitBufferAtomicISub32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { + return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicISub); +} + Id EmitBufferAtomicSMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicSMin); } @@ -188,14 +207,12 @@ Id EmitBufferAtomicUMax32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre return BufferAtomicU32(ctx, inst, handle, address, value, &Sirit::Module::OpAtomicUMax); } -Id EmitBufferAtomicInc32(EmitContext&, IR::Inst*, u32, Id, Id) { - // TODO - UNREACHABLE_MSG("Unsupported BUFFER_ATOMIC opcode: ", IR::Opcode::BufferAtomicInc32); +Id EmitBufferAtomicInc32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { + return BufferAtomicU32IncDec(ctx, inst, handle, address, &Sirit::Module::OpAtomicIIncrement); } -Id EmitBufferAtomicDec32(EmitContext&, IR::Inst*, u32, Id, Id) { - // TODO - UNREACHABLE_MSG("Unsupported BUFFER_ATOMIC opcode: ", IR::Opcode::BufferAtomicDec32); +Id EmitBufferAtomicDec32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { + return BufferAtomicU32IncDec(ctx, inst, handle, address, &Sirit::Module::OpAtomicIDecrement); } Id EmitBufferAtomicAnd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h b/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h index 41e70c8c3..e66467c6b 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_bounds.h @@ -1,31 +1,54 @@ // SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "shader_recompiler/backend/spirv/emit_spirv_instructions.h" +#pragma once + #include "shader_recompiler/backend/spirv/spirv_emit_context.h" namespace Shader::Backend::SPIRV { -template -auto AccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func) { - Id zero_value{}; +template +std::tuple ResolveTypeAndZero(EmitContext& ctx) { Id result_type{}; - if constexpr (bit_size == 64) { - zero_value = ctx.u64_zero_value; + Id zero_value{}; + if constexpr (bit_size == 64 && num_components == 1 && !is_float) { result_type = ctx.U64; + zero_value = ctx.u64_zero_value; } else if constexpr (bit_size == 32) { - zero_value = ctx.u32_zero_value; - result_type = ctx.U32[1]; - } else if constexpr (bit_size == 16) { - zero_value = ctx.u16_zero_value; + if (is_float) { + result_type = ctx.F32[num_components]; + zero_value = ctx.f32_zero_value; + } else { + result_type = ctx.U32[num_components]; + zero_value = ctx.u32_zero_value; + } + } else if constexpr (bit_size == 16 && num_components == 1 && !is_float) { result_type = ctx.U16; + zero_value = ctx.u16_zero_value; + } else if constexpr (bit_size == 8 && num_components == 1 && !is_float) { + result_type = ctx.U8; + zero_value = ctx.u8_zero_value; } else { - static_assert(false, "type not supported"); + static_assert(false, "Type not supported."); } + if (num_components > 1) { + std::array zero_ids; + zero_ids.fill(zero_value); + zero_value = ctx.ConstantComposite(result_type, zero_ids); + } + return {result_type, zero_value}; +} + +template +auto AccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func) { if (Sirit::ValidId(buffer_size)) { // Bounds checking enabled, wrap in a conditional branch to make sure that // the atomic is not mistakenly executed when the index is out of bounds. - const Id in_bounds = ctx.OpULessThan(ctx.U1[1], index, buffer_size); + auto compare_index = index; + if (num_components > 1) { + compare_index = ctx.OpIAdd(ctx.U32[1], index, ctx.ConstU32(num_components - 1)); + } + const Id in_bounds = ctx.OpULessThan(ctx.U1[1], compare_index, buffer_size); const Id ib_label = ctx.OpLabel(); const Id end_label = ctx.OpLabel(); ctx.OpSelectionMerge(end_label, spv::SelectionControlMask::MaskNone); @@ -36,6 +59,8 @@ auto AccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_fun ctx.OpBranch(end_label); ctx.AddLabel(end_label); if (Sirit::ValidId(ib_result)) { + const auto [result_type, zero_value] = + ResolveTypeAndZero(ctx); return ctx.OpPhi(result_type, ib_result, ib_label, zero_value, last_label); } else { return Id{0}; @@ -45,4 +70,21 @@ auto AccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_fun return emit_func(); } +template +static Id LoadAccessBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, Id result) { + if (Sirit::ValidId(buffer_size)) { + // Bounds checking enabled, wrap in a select. + auto compare_index = index; + if (num_components > 1) { + compare_index = ctx.OpIAdd(ctx.U32[1], index, ctx.ConstU32(num_components - 1)); + } + const Id in_bounds = ctx.OpULessThan(ctx.U1[1], compare_index, buffer_size); + const auto [result_type, zero_value] = + ResolveTypeAndZero(ctx); + return ctx.OpSelect(result_type, in_bounds, result, zero_value); + } + // Bounds checking not enabled, just return the plain value. + return result; +} + } // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 658d4759f..ccbe54d0a 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -11,6 +11,8 @@ #include +#include "emit_spirv_bounds.h" + namespace Shader::Backend::SPIRV { namespace { @@ -239,8 +241,8 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, Id index) { } if (IR::IsParam(attr)) { - const u32 index{u32(attr) - u32(IR::Attribute::Param0)}; - const auto& param{ctx.input_params.at(index)}; + const u32 param_index{u32(attr) - u32(IR::Attribute::Param0)}; + const auto& param{ctx.input_params.at(param_index)}; if (param.buffer_handle >= 0) { const auto step_rate = EmitReadStepRate(ctx, param.id.value); const auto offset = ctx.OpIAdd( @@ -415,27 +417,6 @@ void EmitSetPatch(EmitContext& ctx, IR::Patch patch, Id value) { ctx.OpStore(pointer, value); } -template -static Id EmitLoadBufferBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, Id result, - bool is_float) { - if (Sirit::ValidId(buffer_size)) { - // Bounds checking enabled, wrap in a select. - const auto result_type = is_float ? ctx.F32[N] : ctx.U32[N]; - auto compare_index = index; - auto zero_value = is_float ? ctx.f32_zero_value : ctx.u32_zero_value; - if (N > 1) { - compare_index = ctx.OpIAdd(ctx.U32[1], index, ctx.ConstU32(N - 1)); - std::array zero_ids; - zero_ids.fill(zero_value); - zero_value = ctx.ConstantComposite(result_type, zero_ids); - } - const Id in_bounds = ctx.OpULessThan(ctx.U1[1], compare_index, buffer_size); - return ctx.OpSelect(result_type, in_bounds, result, zero_value); - } - // Bounds checking not enabled, just return the plain value. - return result; -} - template static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { const auto flags = inst->Flags(); @@ -454,8 +435,9 @@ static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id a const Id result_i = ctx.OpLoad(data_types[1], ptr_i); if (!flags.typed) { // Untyped loads have bounds checking per-component. - ids.push_back(EmitLoadBufferBoundsCheck<1>(ctx, index_i, spv_buffer.size_dwords, - result_i, alias == PointerType::F32)); + ids.push_back(LoadAccessBoundsCheck < 32, 1, + alias == + PointerType::F32 > (ctx, index_i, spv_buffer.size_dwords, result_i)); } else { ids.push_back(result_i); } @@ -464,8 +446,8 @@ static Id EmitLoadBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id a const Id result = N == 1 ? ids[0] : ctx.OpCompositeConstruct(data_types[N], ids); if (flags.typed) { // Typed loads have single bounds check for the whole load. - return EmitLoadBufferBoundsCheck(ctx, index, spv_buffer.size_dwords, result, - alias == PointerType::F32); + return LoadAccessBoundsCheck < 32, N, + alias == PointerType::F32 > (ctx, index, spv_buffer.size_dwords, result); } return result; } @@ -477,8 +459,8 @@ Id EmitLoadBufferU8(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { } const auto [id, pointer_type] = spv_buffer[PointerType::U8]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, address)}; - const Id result{ctx.OpUConvert(ctx.U32[1], ctx.OpLoad(ctx.U8, ptr))}; - return EmitLoadBufferBoundsCheck<1>(ctx, address, spv_buffer.size, result, false); + const Id result{ctx.OpLoad(ctx.U8, ptr)}; + return LoadAccessBoundsCheck<8>(ctx, address, spv_buffer.size, result); } Id EmitLoadBufferU16(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { @@ -489,8 +471,8 @@ Id EmitLoadBufferU16(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { const auto [id, pointer_type] = spv_buffer[PointerType::U16]; const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(1u)); const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; - const Id result{ctx.OpUConvert(ctx.U32[1], ctx.OpLoad(ctx.U16, ptr))}; - return EmitLoadBufferBoundsCheck<1>(ctx, index, spv_buffer.size_shorts, result, false); + const Id result{ctx.OpLoad(ctx.U16, ptr)}; + return LoadAccessBoundsCheck<16>(ctx, index, spv_buffer.size_shorts, result); } Id EmitLoadBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { @@ -509,6 +491,18 @@ Id EmitLoadBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) return EmitLoadBufferB32xN<4, PointerType::U32>(ctx, inst, handle, address); } +Id EmitLoadBufferU64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { + const auto& spv_buffer = ctx.buffers[handle]; + if (Sirit::ValidId(spv_buffer.offset)) { + address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); + } + const auto [id, pointer_type] = spv_buffer[PointerType::U64]; + const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(3u)); + const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u64_zero_value, index)}; + const Id result{ctx.OpLoad(ctx.U64, ptr)}; + return LoadAccessBoundsCheck<64>(ctx, index, spv_buffer.size_qwords, result); +} + Id EmitLoadBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address) { return EmitLoadBufferB32xN<1, PointerType::F32>(ctx, inst, handle, address); } @@ -529,29 +523,6 @@ Id EmitLoadBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addr UNREACHABLE_MSG("SPIR-V instruction"); } -template -void EmitStoreBufferBoundsCheck(EmitContext& ctx, Id index, Id buffer_size, auto emit_func) { - if (Sirit::ValidId(buffer_size)) { - // Bounds checking enabled, wrap in a conditional branch. - auto compare_index = index; - if (N > 1) { - compare_index = ctx.OpIAdd(ctx.U32[1], index, ctx.ConstU32(N - 1)); - } - const Id in_bounds = ctx.OpULessThan(ctx.U1[1], compare_index, buffer_size); - const Id in_bounds_label = ctx.OpLabel(); - const Id merge_label = ctx.OpLabel(); - ctx.OpSelectionMerge(merge_label, spv::SelectionControlMask::MaskNone); - ctx.OpBranchConditional(in_bounds, in_bounds_label, merge_label); - ctx.AddLabel(in_bounds_label); - emit_func(); - ctx.OpBranch(merge_label); - ctx.AddLabel(merge_label); - return; - } - // Bounds checking not enabled, just perform the store. - emit_func(); -} - template static void EmitStoreBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { @@ -569,19 +540,25 @@ static void EmitStoreBufferB32xN(EmitContext& ctx, IR::Inst* inst, u32 handle, I const Id index_i = i == 0 ? index : ctx.OpIAdd(ctx.U32[1], index, ctx.ConstU32(i)); const Id ptr_i = ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index_i); const Id value_i = N == 1 ? value : ctx.OpCompositeExtract(data_types[1], value, i); - auto store_i = [&]() { ctx.OpStore(ptr_i, value_i); }; + auto store_i = [&] { + ctx.OpStore(ptr_i, value_i); + return Id{}; + }; if (!flags.typed) { // Untyped stores have bounds checking per-component. - EmitStoreBufferBoundsCheck<1>(ctx, index_i, spv_buffer.size_dwords, store_i); + AccessBoundsCheck<32, 1, alias == PointerType::F32>( + ctx, index_i, spv_buffer.size_dwords, store_i); } else { store_i(); } } + return Id{}; }; if (flags.typed) { // Typed stores have single bounds check for the whole store. - EmitStoreBufferBoundsCheck(ctx, index, spv_buffer.size_dwords, store); + AccessBoundsCheck<32, N, alias == PointerType::F32>(ctx, index, spv_buffer.size_dwords, + store); } else { store(); } @@ -594,8 +571,10 @@ void EmitStoreBufferU8(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id v } const auto [id, pointer_type] = spv_buffer[PointerType::U8]; const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, address)}; - const Id result{ctx.OpUConvert(ctx.U8, value)}; - EmitStoreBufferBoundsCheck<1>(ctx, address, spv_buffer.size, [&] { ctx.OpStore(ptr, result); }); + AccessBoundsCheck<8>(ctx, address, spv_buffer.size, [&] { + ctx.OpStore(ptr, value); + return Id{}; + }); } void EmitStoreBufferU16(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id value) { @@ -606,9 +585,10 @@ void EmitStoreBufferU16(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id const auto [id, pointer_type] = spv_buffer[PointerType::U16]; const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(1u)); const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u32_zero_value, index)}; - const Id result{ctx.OpUConvert(ctx.U16, value)}; - EmitStoreBufferBoundsCheck<1>(ctx, index, spv_buffer.size_shorts, - [&] { ctx.OpStore(ptr, result); }); + AccessBoundsCheck<16>(ctx, index, spv_buffer.size_shorts, [&] { + ctx.OpStore(ptr, value); + return Id{}; + }); } void EmitStoreBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { @@ -627,6 +607,20 @@ void EmitStoreBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre EmitStoreBufferB32xN<4, PointerType::U32>(ctx, inst, handle, address, value); } +void EmitStoreBufferU64(EmitContext& ctx, IR::Inst*, u32 handle, Id address, Id value) { + const auto& spv_buffer = ctx.buffers[handle]; + if (Sirit::ValidId(spv_buffer.offset)) { + address = ctx.OpIAdd(ctx.U32[1], address, spv_buffer.offset); + } + const auto [id, pointer_type] = spv_buffer[PointerType::U64]; + const Id index = ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.ConstU32(3u)); + const Id ptr{ctx.OpAccessChain(pointer_type, id, ctx.u64_zero_value, index)}; + AccessBoundsCheck<64>(ctx, index, spv_buffer.size_qwords, [&] { + ctx.OpStore(ptr, value); + return Id{}; + }); +} + void EmitStoreBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value) { EmitStoreBufferB32xN<1, PointerType::F32>(ctx, inst, handle, address, value); } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_convert.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_convert.cpp index 945fa6877..c75f43393 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_convert.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_convert.cpp @@ -263,4 +263,12 @@ Id EmitConvertU32U16(EmitContext& ctx, Id value) { return ctx.OpUConvert(ctx.U32[1], value); } +Id EmitConvertU8U32(EmitContext& ctx, Id value) { + return ctx.OpUConvert(ctx.U8, value); +} + +Id EmitConvertU32U8(EmitContext& ctx, Id value) { + return ctx.OpUConvert(ctx.U32[1], value); +} + } // namespace Shader::Backend::SPIRV diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h index 3441c5a23..daf1b973e 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h +++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h @@ -69,6 +69,7 @@ Id EmitLoadBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferU32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferU32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); +Id EmitLoadBufferU64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferF32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitLoadBufferF32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); @@ -80,6 +81,7 @@ void EmitStoreBufferU32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address void EmitStoreBufferU32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferU32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferU32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); +void EmitStoreBufferU64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferF32x2(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); void EmitStoreBufferF32x3(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); @@ -87,12 +89,13 @@ void EmitStoreBufferF32x4(EmitContext& ctx, IR::Inst* inst, u32 handle, Id addre void EmitStoreBufferFormatF32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicIAdd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicIAdd64(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); +Id EmitBufferAtomicISub32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicSMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicUMin32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicSMax32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicUMax32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); -Id EmitBufferAtomicInc32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); -Id EmitBufferAtomicDec32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); +Id EmitBufferAtomicInc32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); +Id EmitBufferAtomicDec32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address); Id EmitBufferAtomicAnd32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicOr32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); Id EmitBufferAtomicXor32(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address, Id value); @@ -136,8 +139,8 @@ Id EmitSharedAtomicSMin32(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicAnd32(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicOr32(EmitContext& ctx, Id offset, Id value); Id EmitSharedAtomicXor32(EmitContext& ctx, Id offset, Id value); -Id EmitSharedAtomicIIncrement32(EmitContext& ctx, Id offset); -Id EmitSharedAtomicIDecrement32(EmitContext& ctx, Id offset); +Id EmitSharedAtomicInc32(EmitContext& ctx, Id offset); +Id EmitSharedAtomicDec32(EmitContext& ctx, Id offset); Id EmitSharedAtomicISub32(EmitContext& ctx, Id offset, Id value); Id EmitCompositeConstructU32x2(EmitContext& ctx, IR::Inst* inst, Id e1, Id e2); @@ -461,6 +464,8 @@ Id EmitConvertF64U32(EmitContext& ctx, Id value); Id EmitConvertF64U64(EmitContext& ctx, Id value); Id EmitConvertU16U32(EmitContext& ctx, Id value); Id EmitConvertU32U16(EmitContext& ctx, Id value); +Id EmitConvertU8U32(EmitContext& ctx, Id value); +Id EmitConvertU32U8(EmitContext& ctx, Id value); Id EmitImageSampleRaw(EmitContext& ctx, IR::Inst* inst, u32 handle, Id address1, Id address2, Id address3, Id address4); diff --git a/src/shader_recompiler/frontend/translate/data_share.cpp b/src/shader_recompiler/frontend/translate/data_share.cpp index 4b6a58fd0..8ead93f78 100644 --- a/src/shader_recompiler/frontend/translate/data_share.cpp +++ b/src/shader_recompiler/frontend/translate/data_share.cpp @@ -216,34 +216,38 @@ void Translator::DS_WRITE(int bit_size, bool is_signed, bool is_pair, bool strid if (is_pair) { const u32 adj = (bit_size == 32 ? 4 : 8) * (stride64 ? 64 : 1); const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset0 * adj))); - if (bit_size == 32) { - ir.WriteShared(32, ir.GetVectorReg(data0), addr0); - } else { + if (bit_size == 64) { ir.WriteShared(64, ir.PackUint2x32(ir.CompositeConstruct(ir.GetVectorReg(data0), ir.GetVectorReg(data0 + 1))), addr0); + } else if (bit_size == 32) { + ir.WriteShared(32, ir.GetVectorReg(data0), addr0); + } else if (bit_size == 16) { + ir.WriteShared(16, ir.UConvert(16, ir.GetVectorReg(data0)), addr0); } const IR::U32 addr1 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset1 * adj))); - if (bit_size == 32) { - ir.WriteShared(32, ir.GetVectorReg(data1), addr1); - } else { + if (bit_size == 64) { ir.WriteShared(64, ir.PackUint2x32(ir.CompositeConstruct(ir.GetVectorReg(data1), ir.GetVectorReg(data1 + 1))), addr1); + } else if (bit_size == 32) { + ir.WriteShared(32, ir.GetVectorReg(data1), addr1); + } else if (bit_size == 16) { + ir.WriteShared(16, ir.UConvert(16, ir.GetVectorReg(data1)), addr1); } - } else if (bit_size == 64) { - const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - const IR::Value data = - ir.CompositeConstruct(ir.GetVectorReg(data0), ir.GetVectorReg(data0 + 1)); - ir.WriteShared(bit_size, ir.PackUint2x32(data), addr0); - } else if (bit_size == 16) { - const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - ir.WriteShared(bit_size, ir.GetVectorReg(data0), addr0); } else { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - ir.WriteShared(bit_size, ir.GetVectorReg(data0), addr0); + if (bit_size == 64) { + const IR::Value data = + ir.CompositeConstruct(ir.GetVectorReg(data0), ir.GetVectorReg(data0 + 1)); + ir.WriteShared(bit_size, ir.PackUint2x32(data), addr0); + } else if (bit_size == 32) { + ir.WriteShared(bit_size, ir.GetVectorReg(data0), addr0); + } else if (bit_size == 16) { + ir.WriteShared(bit_size, ir.UConvert(16, ir.GetVectorReg(data0)), addr0); + } } } @@ -264,7 +268,7 @@ void Translator::DS_INC_U32(const GcnInst& inst, bool rtn) { const IR::U32 offset = ir.Imm32((u32(inst.control.ds.offset1) << 8u) + u32(inst.control.ds.offset0)); const IR::U32 addr_offset = ir.IAdd(addr, offset); - const IR::Value original_val = ir.SharedAtomicIIncrement(addr_offset); + const IR::Value original_val = ir.SharedAtomicInc(addr_offset); if (rtn) { SetDst(inst.dst[0], IR::U32{original_val}); } @@ -275,7 +279,7 @@ void Translator::DS_DEC_U32(const GcnInst& inst, bool rtn) { const IR::U32 offset = ir.Imm32((u32(inst.control.ds.offset1) << 8u) + u32(inst.control.ds.offset0)); const IR::U32 addr_offset = ir.IAdd(addr, offset); - const IR::Value original_val = ir.SharedAtomicIDecrement(addr_offset); + const IR::Value original_val = ir.SharedAtomicDec(addr_offset); if (rtn) { SetDst(inst.dst[0], IR::U32{original_val}); } @@ -309,36 +313,38 @@ void Translator::DS_READ(int bit_size, bool is_signed, bool is_pair, bool stride const u32 adj = (bit_size == 32 ? 4 : 8) * (stride64 ? 64 : 1); const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset0 * adj))); const IR::Value data0 = ir.LoadShared(bit_size, is_signed, addr0); - if (bit_size == 32) { - ir.SetVectorReg(dst_reg++, IR::U32{data0}); - } else { + if (bit_size == 64) { const auto vector = ir.UnpackUint2x32(IR::U64{data0}); ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 0)}); ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 1)}); + } else if (bit_size == 32) { + ir.SetVectorReg(dst_reg++, IR::U32{data0}); + } else if (bit_size == 16) { + ir.SetVectorReg(dst_reg++, IR::U32{ir.UConvert(32, IR::U16{data0})}); } const IR::U32 addr1 = ir.IAdd(addr, ir.Imm32(u32(inst.control.ds.offset1 * adj))); const IR::Value data1 = ir.LoadShared(bit_size, is_signed, addr1); - if (bit_size == 32) { - ir.SetVectorReg(dst_reg++, IR::U32{data1}); - } else { + if (bit_size == 64) { const auto vector = ir.UnpackUint2x32(IR::U64{data1}); ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 0)}); ir.SetVectorReg(dst_reg++, IR::U32{ir.CompositeExtract(vector, 1)}); + } else if (bit_size == 32) { + ir.SetVectorReg(dst_reg++, IR::U32{data1}); + } else if (bit_size == 16) { + ir.SetVectorReg(dst_reg++, IR::U32{ir.UConvert(32, IR::U16{data1})}); } - } else if (bit_size == 64) { - const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - const IR::Value data = ir.LoadShared(bit_size, is_signed, addr0); - const auto vector = ir.UnpackUint2x32(IR::U64{data}); - ir.SetVectorReg(dst_reg, IR::U32{ir.CompositeExtract(vector, 0)}); - ir.SetVectorReg(dst_reg + 1, IR::U32{ir.CompositeExtract(vector, 1)}); - } else if (bit_size == 16) { - const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - const IR::U16 data = IR::U16{ir.LoadShared(bit_size, is_signed, addr0)}; - ir.SetVectorReg(dst_reg, ir.UConvert(32, data)); } else { const IR::U32 addr0 = ir.IAdd(addr, ir.Imm32(offset)); - const IR::U32 data = IR::U32{ir.LoadShared(bit_size, is_signed, addr0)}; - ir.SetVectorReg(dst_reg, data); + const IR::Value data = ir.LoadShared(bit_size, is_signed, addr0); + if (bit_size == 64) { + const auto vector = ir.UnpackUint2x32(IR::U64{data}); + ir.SetVectorReg(dst_reg, IR::U32{ir.CompositeExtract(vector, 0)}); + ir.SetVectorReg(dst_reg + 1, IR::U32{ir.CompositeExtract(vector, 1)}); + } else if (bit_size == 32) { + ir.SetVectorReg(dst_reg, IR::U32{data}); + } else if (bit_size == 16) { + ir.SetVectorReg(dst_reg++, IR::U32{ir.UConvert(32, IR::U16{data})}); + } } } diff --git a/src/shader_recompiler/frontend/translate/vector_memory.cpp b/src/shader_recompiler/frontend/translate/vector_memory.cpp index 5eb2079a4..54e8b8ee8 100644 --- a/src/shader_recompiler/frontend/translate/vector_memory.cpp +++ b/src/shader_recompiler/frontend/translate/vector_memory.cpp @@ -354,9 +354,9 @@ void Translator::BUFFER_ATOMIC(AtomicOp op, const GcnInst& inst) { case AtomicOp::Xor: return ir.BufferAtomicXor(handle, address, vdata_val, buffer_info); case AtomicOp::Inc: - return ir.BufferAtomicInc(handle, address, vdata_val, buffer_info); + return ir.BufferAtomicInc(handle, address, buffer_info); case AtomicOp::Dec: - return ir.BufferAtomicDec(handle, address, vdata_val, buffer_info); + return ir.BufferAtomicDec(handle, address, buffer_info); default: UNREACHABLE(); } diff --git a/src/shader_recompiler/ir/ir_emitter.cpp b/src/shader_recompiler/ir/ir_emitter.cpp index 2c37c8099..3d7cf71dc 100644 --- a/src/shader_recompiler/ir/ir_emitter.cpp +++ b/src/shader_recompiler/ir/ir_emitter.cpp @@ -353,12 +353,12 @@ U32 IREmitter::SharedAtomicXor(const U32& address, const U32& data) { return Inst(Opcode::SharedAtomicXor32, address, data); } -U32 IREmitter::SharedAtomicIIncrement(const U32& address) { - return Inst(Opcode::SharedAtomicIIncrement32, address); +U32 IREmitter::SharedAtomicInc(const U32& address) { + return Inst(Opcode::SharedAtomicInc32, address); } -U32 IREmitter::SharedAtomicIDecrement(const U32& address) { - return Inst(Opcode::SharedAtomicIDecrement32, address); +U32 IREmitter::SharedAtomicDec(const U32& address) { + return Inst(Opcode::SharedAtomicDec32, address); } U32 IREmitter::SharedAtomicISub(const U32& address, const U32& data) { @@ -373,12 +373,12 @@ U32 IREmitter::ReadConstBuffer(const Value& handle, const U32& index) { return Inst(Opcode::ReadConstBuffer, handle, index); } -U32 IREmitter::LoadBufferU8(const Value& handle, const Value& address, BufferInstInfo info) { - return Inst(Opcode::LoadBufferU8, Flags{info}, handle, address); +U8 IREmitter::LoadBufferU8(const Value& handle, const Value& address, BufferInstInfo info) { + return Inst(Opcode::LoadBufferU8, Flags{info}, handle, address); } -U32 IREmitter::LoadBufferU16(const Value& handle, const Value& address, BufferInstInfo info) { - return Inst(Opcode::LoadBufferU16, Flags{info}, handle, address); +U16 IREmitter::LoadBufferU16(const Value& handle, const Value& address, BufferInstInfo info) { + return Inst(Opcode::LoadBufferU16, Flags{info}, handle, address); } Value IREmitter::LoadBufferU32(int num_dwords, const Value& handle, const Value& address, @@ -397,6 +397,10 @@ Value IREmitter::LoadBufferU32(int num_dwords, const Value& handle, const Value& } } +U64 IREmitter::LoadBufferU64(const Value& handle, const Value& address, BufferInstInfo info) { + return Inst(Opcode::LoadBufferU64, Flags{info}, handle, address); +} + Value IREmitter::LoadBufferF32(int num_dwords, const Value& handle, const Value& address, BufferInstInfo info) { switch (num_dwords) { @@ -417,12 +421,12 @@ Value IREmitter::LoadBufferFormat(const Value& handle, const Value& address, Buf return Inst(Opcode::LoadBufferFormatF32, Flags{info}, handle, address); } -void IREmitter::StoreBufferU8(const Value& handle, const Value& address, const U32& data, +void IREmitter::StoreBufferU8(const Value& handle, const Value& address, const U8& data, BufferInstInfo info) { Inst(Opcode::StoreBufferU8, Flags{info}, handle, address, data); } -void IREmitter::StoreBufferU16(const Value& handle, const Value& address, const U32& data, +void IREmitter::StoreBufferU16(const Value& handle, const Value& address, const U16& data, BufferInstInfo info) { Inst(Opcode::StoreBufferU16, Flags{info}, handle, address, data); } @@ -447,6 +451,11 @@ void IREmitter::StoreBufferU32(int num_dwords, const Value& handle, const Value& } } +void IREmitter::StoreBufferU64(const Value& handle, const Value& address, const U64& data, + BufferInstInfo info) { + Inst(Opcode::StoreBufferU64, Flags{info}, handle, address, data); +} + void IREmitter::StoreBufferF32(int num_dwords, const Value& handle, const Value& address, const Value& data, BufferInstInfo info) { switch (num_dwords) { @@ -474,7 +483,19 @@ void IREmitter::StoreBufferFormat(const Value& handle, const Value& address, con Value IREmitter::BufferAtomicIAdd(const Value& handle, const Value& address, const Value& value, BufferInstInfo info) { - return Inst(Opcode::BufferAtomicIAdd32, Flags{info}, handle, address, value); + switch (value.Type()) { + case Type::U32: + return Inst(Opcode::BufferAtomicIAdd32, Flags{info}, handle, address, value); + case Type::U64: + return Inst(Opcode::BufferAtomicIAdd64, Flags{info}, handle, address, value); + default: + ThrowInvalidType(value.Type()); + } +} + +Value IREmitter::BufferAtomicISub(const Value& handle, const Value& address, const Value& value, + BufferInstInfo info) { + return Inst(Opcode::BufferAtomicISub32, Flags{info}, handle, address, value); } Value IREmitter::BufferAtomicIMin(const Value& handle, const Value& address, const Value& value, @@ -489,14 +510,12 @@ Value IREmitter::BufferAtomicIMax(const Value& handle, const Value& address, con : Inst(Opcode::BufferAtomicUMax32, Flags{info}, handle, address, value); } -Value IREmitter::BufferAtomicInc(const Value& handle, const Value& address, const Value& value, - BufferInstInfo info) { - return Inst(Opcode::BufferAtomicInc32, Flags{info}, handle, address, value); +Value IREmitter::BufferAtomicInc(const Value& handle, const Value& address, BufferInstInfo info) { + return Inst(Opcode::BufferAtomicInc32, Flags{info}, handle, address); } -Value IREmitter::BufferAtomicDec(const Value& handle, const Value& address, const Value& value, - BufferInstInfo info) { - return Inst(Opcode::BufferAtomicDec32, Flags{info}, handle, address, value); +Value IREmitter::BufferAtomicDec(const Value& handle, const Value& address, BufferInstInfo info) { + return Inst(Opcode::BufferAtomicDec32, Flags{info}, handle, address); } Value IREmitter::BufferAtomicAnd(const Value& handle, const Value& address, const Value& value, @@ -1804,8 +1823,15 @@ F32F64 IREmitter::ConvertIToF(size_t dest_bitsize, size_t src_bitsize, bool is_s : ConvertUToF(dest_bitsize, src_bitsize, value); } -U16U32U64 IREmitter::UConvert(size_t result_bitsize, const U16U32U64& value) { +U8U16U32U64 IREmitter::UConvert(size_t result_bitsize, const U8U16U32U64& value) { switch (result_bitsize) { + case 8: + switch (value.Type()) { + case Type::U32: + return Inst(Opcode::ConvertU8U32, value); + default: + break; + } case 16: switch (value.Type()) { case Type::U32: @@ -1815,6 +1841,8 @@ U16U32U64 IREmitter::UConvert(size_t result_bitsize, const U16U32U64& value) { } case 32: switch (value.Type()) { + case Type::U8: + return Inst(Opcode::ConvertU32U8, value); case Type::U16: return Inst(Opcode::ConvertU32U16, value); default: diff --git a/src/shader_recompiler/ir/ir_emitter.h b/src/shader_recompiler/ir/ir_emitter.h index eae44ed04..215a35ee9 100644 --- a/src/shader_recompiler/ir/ir_emitter.h +++ b/src/shader_recompiler/ir/ir_emitter.h @@ -100,33 +100,35 @@ public: void WriteShared(int bit_size, const Value& value, const U32& offset); [[nodiscard]] U32U64 SharedAtomicIAdd(const U32& address, const U32U64& data); + [[nodiscard]] U32 SharedAtomicISub(const U32& address, const U32& data); [[nodiscard]] U32 SharedAtomicIMin(const U32& address, const U32& data, bool is_signed); [[nodiscard]] U32 SharedAtomicIMax(const U32& address, const U32& data, bool is_signed); + [[nodiscard]] U32 SharedAtomicInc(const U32& address); + [[nodiscard]] U32 SharedAtomicDec(const U32& address); [[nodiscard]] U32 SharedAtomicAnd(const U32& address, const U32& data); [[nodiscard]] U32 SharedAtomicOr(const U32& address, const U32& data); [[nodiscard]] U32 SharedAtomicXor(const U32& address, const U32& data); - [[nodiscard]] U32 SharedAtomicIIncrement(const U32& address); - [[nodiscard]] U32 SharedAtomicIDecrement(const U32& address); - [[nodiscard]] U32 SharedAtomicISub(const U32& address, const U32& data); - [[nodiscard]] U32 ReadConst(const Value& base, const U32& offset); [[nodiscard]] U32 ReadConstBuffer(const Value& handle, const U32& index); - [[nodiscard]] U32 LoadBufferU8(const Value& handle, const Value& address, BufferInstInfo info); - [[nodiscard]] U32 LoadBufferU16(const Value& handle, const Value& address, BufferInstInfo info); + [[nodiscard]] U8 LoadBufferU8(const Value& handle, const Value& address, BufferInstInfo info); + [[nodiscard]] U16 LoadBufferU16(const Value& handle, const Value& address, BufferInstInfo info); [[nodiscard]] Value LoadBufferU32(int num_dwords, const Value& handle, const Value& address, BufferInstInfo info); + [[nodiscard]] U64 LoadBufferU64(const Value& handle, const Value& address, BufferInstInfo info); [[nodiscard]] Value LoadBufferF32(int num_dwords, const Value& handle, const Value& address, BufferInstInfo info); [[nodiscard]] Value LoadBufferFormat(const Value& handle, const Value& address, BufferInstInfo info); - void StoreBufferU8(const Value& handle, const Value& address, const U32& data, + void StoreBufferU8(const Value& handle, const Value& address, const U8& data, BufferInstInfo info); - void StoreBufferU16(const Value& handle, const Value& address, const U32& data, + void StoreBufferU16(const Value& handle, const Value& address, const U16& data, BufferInstInfo info); void StoreBufferU32(int num_dwords, const Value& handle, const Value& address, const Value& data, BufferInstInfo info); + void StoreBufferU64(const Value& handle, const Value& address, const U64& data, + BufferInstInfo info); void StoreBufferF32(int num_dwords, const Value& handle, const Value& address, const Value& data, BufferInstInfo info); void StoreBufferFormat(const Value& handle, const Value& address, const Value& data, @@ -134,14 +136,16 @@ public: [[nodiscard]] Value BufferAtomicIAdd(const Value& handle, const Value& address, const Value& value, BufferInstInfo info); + [[nodiscard]] Value BufferAtomicISub(const Value& handle, const Value& address, + const Value& value, BufferInstInfo info); [[nodiscard]] Value BufferAtomicIMin(const Value& handle, const Value& address, const Value& value, bool is_signed, BufferInstInfo info); [[nodiscard]] Value BufferAtomicIMax(const Value& handle, const Value& address, const Value& value, bool is_signed, BufferInstInfo info); [[nodiscard]] Value BufferAtomicInc(const Value& handle, const Value& address, - const Value& value, BufferInstInfo info); + BufferInstInfo info); [[nodiscard]] Value BufferAtomicDec(const Value& handle, const Value& address, - const Value& value, BufferInstInfo info); + BufferInstInfo info); [[nodiscard]] Value BufferAtomicAnd(const Value& handle, const Value& address, const Value& value, BufferInstInfo info); [[nodiscard]] Value BufferAtomicOr(const Value& handle, const Value& address, @@ -309,7 +313,7 @@ public: [[nodiscard]] F32F64 ConvertIToF(size_t dest_bitsize, size_t src_bitsize, bool is_signed, const Value& value); - [[nodiscard]] U16U32U64 UConvert(size_t result_bitsize, const U16U32U64& value); + [[nodiscard]] U8U16U32U64 UConvert(size_t result_bitsize, const U8U16U32U64& value); [[nodiscard]] F16F32F64 FPConvert(size_t result_bitsize, const F16F32F64& value); [[nodiscard]] Value ImageAtomicIAdd(const Value& handle, const Value& coords, diff --git a/src/shader_recompiler/ir/microinstruction.cpp b/src/shader_recompiler/ir/microinstruction.cpp index a57310fb9..c2311afea 100644 --- a/src/shader_recompiler/ir/microinstruction.cpp +++ b/src/shader_recompiler/ir/microinstruction.cpp @@ -60,12 +60,15 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::StoreBufferU32x2: case Opcode::StoreBufferU32x3: case Opcode::StoreBufferU32x4: + case Opcode::StoreBufferU64: case Opcode::StoreBufferF32: case Opcode::StoreBufferF32x2: case Opcode::StoreBufferF32x3: case Opcode::StoreBufferF32x4: case Opcode::StoreBufferFormatF32: case Opcode::BufferAtomicIAdd32: + case Opcode::BufferAtomicIAdd64: + case Opcode::BufferAtomicISub32: case Opcode::BufferAtomicSMin32: case Opcode::BufferAtomicUMin32: case Opcode::BufferAtomicSMax32: @@ -76,15 +79,21 @@ bool Inst::MayHaveSideEffects() const noexcept { case Opcode::BufferAtomicOr32: case Opcode::BufferAtomicXor32: case Opcode::BufferAtomicSwap32: + case Opcode::BufferAtomicCmpSwap32: case Opcode::DataAppend: case Opcode::DataConsume: - case Opcode::WriteSharedU64: + case Opcode::WriteSharedU16: case Opcode::WriteSharedU32: + case Opcode::WriteSharedU64: case Opcode::SharedAtomicIAdd32: + case Opcode::SharedAtomicIAdd64: + case Opcode::SharedAtomicISub32: case Opcode::SharedAtomicSMin32: case Opcode::SharedAtomicUMin32: case Opcode::SharedAtomicSMax32: case Opcode::SharedAtomicUMax32: + case Opcode::SharedAtomicInc32: + case Opcode::SharedAtomicDec32: case Opcode::SharedAtomicAnd32: case Opcode::SharedAtomicOr32: case Opcode::SharedAtomicXor32: diff --git a/src/shader_recompiler/ir/opcodes.inc b/src/shader_recompiler/ir/opcodes.inc index e96e32297..1621d2acf 100644 --- a/src/shader_recompiler/ir/opcodes.inc +++ b/src/shader_recompiler/ir/opcodes.inc @@ -35,21 +35,21 @@ OPCODE(LoadSharedU32, U32, U32, OPCODE(LoadSharedU64, U64, U32, ) OPCODE(WriteSharedU16, Void, U32, U16, ) OPCODE(WriteSharedU32, Void, U32, U32, ) -OPCODE(WriteSharedU64, Void, U32, U64, ) +OPCODE(WriteSharedU64, Void, U32, U64, ) // Shared atomic operations OPCODE(SharedAtomicIAdd32, U32, U32, U32, ) OPCODE(SharedAtomicIAdd64, U64, U32, U64, ) +OPCODE(SharedAtomicISub32, U32, U32, U32, ) OPCODE(SharedAtomicSMin32, U32, U32, U32, ) OPCODE(SharedAtomicUMin32, U32, U32, U32, ) OPCODE(SharedAtomicSMax32, U32, U32, U32, ) OPCODE(SharedAtomicUMax32, U32, U32, U32, ) +OPCODE(SharedAtomicInc32, U32, U32, ) +OPCODE(SharedAtomicDec32, U32, U32, ) OPCODE(SharedAtomicAnd32, U32, U32, U32, ) OPCODE(SharedAtomicOr32, U32, U32, U32, ) OPCODE(SharedAtomicXor32, U32, U32, U32, ) -OPCODE(SharedAtomicISub32, U32, U32, U32, ) -OPCODE(SharedAtomicIIncrement32, U32, U32, ) -OPCODE(SharedAtomicIDecrement32, U32, U32, ) // Context getters/setters OPCODE(GetUserData, U32, ScalarReg, ) @@ -94,23 +94,25 @@ OPCODE(UndefU32, U32, OPCODE(UndefU64, U64, ) // Buffer operations -OPCODE(LoadBufferU8, U32, Opaque, Opaque, ) -OPCODE(LoadBufferU16, U32, Opaque, Opaque, ) +OPCODE(LoadBufferU8, U8, Opaque, Opaque, ) +OPCODE(LoadBufferU16, U16, Opaque, Opaque, ) OPCODE(LoadBufferU32, U32, Opaque, Opaque, ) OPCODE(LoadBufferU32x2, U32x2, Opaque, Opaque, ) OPCODE(LoadBufferU32x3, U32x3, Opaque, Opaque, ) OPCODE(LoadBufferU32x4, U32x4, Opaque, Opaque, ) +OPCODE(LoadBufferU64, U64, Opaque, Opaque, ) OPCODE(LoadBufferF32, F32, Opaque, Opaque, ) OPCODE(LoadBufferF32x2, F32x2, Opaque, Opaque, ) OPCODE(LoadBufferF32x3, F32x3, Opaque, Opaque, ) OPCODE(LoadBufferF32x4, F32x4, Opaque, Opaque, ) OPCODE(LoadBufferFormatF32, F32x4, Opaque, Opaque, ) -OPCODE(StoreBufferU8, Void, Opaque, Opaque, U32, ) -OPCODE(StoreBufferU16, Void, Opaque, Opaque, U32, ) +OPCODE(StoreBufferU8, Void, Opaque, Opaque, U8, ) +OPCODE(StoreBufferU16, Void, Opaque, Opaque, U16, ) OPCODE(StoreBufferU32, Void, Opaque, Opaque, U32, ) OPCODE(StoreBufferU32x2, Void, Opaque, Opaque, U32x2, ) OPCODE(StoreBufferU32x3, Void, Opaque, Opaque, U32x3, ) OPCODE(StoreBufferU32x4, Void, Opaque, Opaque, U32x4, ) +OPCODE(StoreBufferU64, Void, Opaque, Opaque, U64, ) OPCODE(StoreBufferF32, Void, Opaque, Opaque, F32, ) OPCODE(StoreBufferF32x2, Void, Opaque, Opaque, F32x2, ) OPCODE(StoreBufferF32x3, Void, Opaque, Opaque, F32x3, ) @@ -120,12 +122,13 @@ OPCODE(StoreBufferFormatF32, Void, Opaq // Buffer atomic operations OPCODE(BufferAtomicIAdd32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicIAdd64, U64, Opaque, Opaque, U64 ) +OPCODE(BufferAtomicISub32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicSMin32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicUMin32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicSMax32, U32, Opaque, Opaque, U32 ) OPCODE(BufferAtomicUMax32, U32, Opaque, Opaque, U32 ) -OPCODE(BufferAtomicInc32, U32, Opaque, Opaque, U32, ) -OPCODE(BufferAtomicDec32, U32, Opaque, Opaque, U32, ) +OPCODE(BufferAtomicInc32, U32, Opaque, Opaque, ) +OPCODE(BufferAtomicDec32, U32, Opaque, Opaque, ) OPCODE(BufferAtomicAnd32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicOr32, U32, Opaque, Opaque, U32, ) OPCODE(BufferAtomicXor32, U32, Opaque, Opaque, U32, ) @@ -405,6 +408,8 @@ OPCODE(ConvertF64U32, F64, U32, OPCODE(ConvertF32U16, F32, U16, ) OPCODE(ConvertU16U32, U16, U32, ) OPCODE(ConvertU32U16, U32, U16, ) +OPCODE(ConvertU8U32, U8, U32, ) +OPCODE(ConvertU32U8, U32, U8, ) // Image operations OPCODE(ImageSampleRaw, F32x4, Opaque, F32x4, F32x4, F32x4, F32, ) diff --git a/src/shader_recompiler/ir/passes/hull_shader_transform.cpp b/src/shader_recompiler/ir/passes/hull_shader_transform.cpp index 5cf8a1525..156cb6628 100644 --- a/src/shader_recompiler/ir/passes/hull_shader_transform.cpp +++ b/src/shader_recompiler/ir/passes/hull_shader_transform.cpp @@ -438,7 +438,9 @@ void HullShaderTransform(IR::Program& program, RuntimeInfo& runtime_info) { IR::IREmitter ir{*block, IR::Block::InstructionList::s_iterator_to(inst)}; const u32 num_dwords = opcode == IR::Opcode::WriteSharedU32 ? 1 : 2; const IR::U32 addr{inst.Arg(0)}; - const IR::U32 data{inst.Arg(1).Resolve()}; + const IR::Value data = num_dwords == 2 + ? ir.UnpackUint2x32(IR::U64{inst.Arg(1).Resolve()}) + : inst.Arg(1).Resolve(); const auto SetOutput = [&](IR::U32 addr, IR::U32 value, AttributeRegion output_kind, u32 off_dw) { @@ -466,10 +468,10 @@ void HullShaderTransform(IR::Program& program, RuntimeInfo& runtime_info) { AttributeRegion region = GetAttributeRegionKind(&inst, info, runtime_info); if (num_dwords == 1) { - SetOutput(addr, data, region, 0); + SetOutput(addr, IR::U32{data}, region, 0); } else { for (auto i = 0; i < num_dwords; i++) { - SetOutput(addr, IR::U32{data.Inst()->Arg(i)}, region, i); + SetOutput(addr, IR::U32{ir.CompositeExtract(data, i)}, region, i); } } inst.Invalidate(); @@ -499,7 +501,7 @@ void HullShaderTransform(IR::Program& program, RuntimeInfo& runtime_info) { ReadTessControlPointAttribute(addr, stride, ir, i, is_tcs_output_read); read_components.push_back(ir.BitCast(component)); } - attr_read = ir.CompositeConstruct(read_components); + attr_read = ir.PackUint2x32(ir.CompositeConstruct(read_components)); } inst.ReplaceUsesWithAndRemove(attr_read); break; @@ -578,7 +580,7 @@ void DomainShaderTransform(IR::Program& program, RuntimeInfo& runtime_info) { const IR::F32 component = GetInput(addr, i); read_components.push_back(ir.BitCast(component)); } - attr_read = ir.CompositeConstruct(read_components); + attr_read = ir.PackUint2x32(ir.CompositeConstruct(read_components)); } inst.ReplaceUsesWithAndRemove(attr_read); break; diff --git a/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp b/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp index fcb86e3fb..bb36e2748 100644 --- a/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp +++ b/src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp @@ -34,13 +34,13 @@ static IR::Value LoadBufferFormat(IR::IREmitter& ir, const IR::Value handle, con interpreted = ir.Imm32(0.f); break; case AmdGpu::DataFormat::Format8: { - const auto unpacked = - ir.Unpack4x8(format_info.num_format, ir.LoadBufferU8(handle, address, info)); + const auto raw = ir.UConvert(32, ir.LoadBufferU8(handle, address, info)); + const auto unpacked = ir.Unpack4x8(format_info.num_format, raw); interpreted = ir.CompositeExtract(unpacked, 0); break; } case AmdGpu::DataFormat::Format8_8: { - const auto raw = ir.LoadBufferU16(handle, address, info); + const auto raw = ir.UConvert(32, ir.LoadBufferU16(handle, address, info)); const auto unpacked = ir.Unpack4x8(format_info.num_format, raw); interpreted = ir.CompositeConstruct(ir.CompositeExtract(unpacked, 0), ir.CompositeExtract(unpacked, 1)); @@ -51,8 +51,8 @@ static IR::Value LoadBufferFormat(IR::IREmitter& ir, const IR::Value handle, con IR::U32{ir.LoadBufferU32(1, handle, address, info)}); break; case AmdGpu::DataFormat::Format16: { - const auto unpacked = - ir.Unpack2x16(format_info.num_format, ir.LoadBufferU16(handle, address, info)); + const auto raw = ir.UConvert(32, ir.LoadBufferU16(handle, address, info)); + const auto unpacked = ir.Unpack2x16(format_info.num_format, raw); interpreted = ir.CompositeExtract(unpacked, 0); break; } @@ -126,7 +126,7 @@ static void StoreBufferFormat(IR::IREmitter& ir, const IR::Value handle, const I const auto packed = ir.Pack4x8(format_info.num_format, ir.CompositeConstruct(real_value, ir.Imm32(0.f), ir.Imm32(0.f), ir.Imm32(0.f))); - ir.StoreBufferU8(handle, address, packed, info); + ir.StoreBufferU8(handle, address, ir.UConvert(8, packed), info); break; } case AmdGpu::DataFormat::Format8_8: { @@ -134,7 +134,7 @@ static void StoreBufferFormat(IR::IREmitter& ir, const IR::Value handle, const I ir.CompositeConstruct(ir.CompositeExtract(real_value, 0), ir.CompositeExtract(real_value, 1), ir.Imm32(0.f), ir.Imm32(0.f))); - ir.StoreBufferU16(handle, address, packed, info); + ir.StoreBufferU16(handle, address, ir.UConvert(16, packed), info); break; } case AmdGpu::DataFormat::Format8_8_8_8: { @@ -145,7 +145,7 @@ static void StoreBufferFormat(IR::IREmitter& ir, const IR::Value handle, const I case AmdGpu::DataFormat::Format16: { const auto packed = ir.Pack2x16(format_info.num_format, ir.CompositeConstruct(real_value, ir.Imm32(0.f))); - ir.StoreBufferU16(handle, address, packed, info); + ir.StoreBufferU16(handle, address, ir.UConvert(16, packed), info); break; } case AmdGpu::DataFormat::Format16_16: { diff --git a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp index 18c77e600..ba96d1034 100644 --- a/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp +++ b/src/shader_recompiler/ir/passes/resource_tracking_pass.cpp @@ -17,6 +17,8 @@ using SharpLocation = u32; bool IsBufferAtomic(const IR::Inst& inst) { switch (inst.GetOpcode()) { case IR::Opcode::BufferAtomicIAdd32: + case IR::Opcode::BufferAtomicIAdd64: + case IR::Opcode::BufferAtomicISub32: case IR::Opcode::BufferAtomicSMin32: case IR::Opcode::BufferAtomicUMin32: case IR::Opcode::BufferAtomicSMax32: @@ -27,6 +29,7 @@ bool IsBufferAtomic(const IR::Inst& inst) { case IR::Opcode::BufferAtomicOr32: case IR::Opcode::BufferAtomicXor32: case IR::Opcode::BufferAtomicSwap32: + case IR::Opcode::BufferAtomicCmpSwap32: return true; default: return false; @@ -41,6 +44,7 @@ bool IsBufferStore(const IR::Inst& inst) { case IR::Opcode::StoreBufferU32x2: case IR::Opcode::StoreBufferU32x3: case IR::Opcode::StoreBufferU32x4: + case IR::Opcode::StoreBufferU64: case IR::Opcode::StoreBufferF32: case IR::Opcode::StoreBufferF32x2: case IR::Opcode::StoreBufferF32x3: @@ -60,6 +64,7 @@ bool IsBufferInstruction(const IR::Inst& inst) { case IR::Opcode::LoadBufferU32x2: case IR::Opcode::LoadBufferU32x3: case IR::Opcode::LoadBufferU32x4: + case IR::Opcode::LoadBufferU64: case IR::Opcode::LoadBufferF32: case IR::Opcode::LoadBufferF32x2: case IR::Opcode::LoadBufferF32x3: @@ -85,6 +90,10 @@ IR::Type BufferDataType(const IR::Inst& inst, AmdGpu::NumberFormat num_format) { case IR::Opcode::LoadBufferU16: case IR::Opcode::StoreBufferU16: return IR::Type::U16; + case IR::Opcode::LoadBufferU64: + case IR::Opcode::StoreBufferU64: + case IR::Opcode::BufferAtomicIAdd64: + return IR::Type::U64; case IR::Opcode::LoadBufferFormatF32: case IR::Opcode::StoreBufferFormatF32: // Formatted buffer loads can use a variety of types. diff --git a/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp index baf6ad0d1..10d6a285c 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp @@ -9,12 +9,14 @@ namespace Shader::Optimization { static bool IsLoadShared(const IR::Inst& inst) { - return inst.GetOpcode() == IR::Opcode::LoadSharedU32 || + return inst.GetOpcode() == IR::Opcode::LoadSharedU16 || + inst.GetOpcode() == IR::Opcode::LoadSharedU32 || inst.GetOpcode() == IR::Opcode::LoadSharedU64; } static bool IsWriteShared(const IR::Inst& inst) { - return inst.GetOpcode() == IR::Opcode::WriteSharedU32 || + return inst.GetOpcode() == IR::Opcode::WriteSharedU16 || + inst.GetOpcode() == IR::Opcode::WriteSharedU32 || inst.GetOpcode() == IR::Opcode::WriteSharedU64; } diff --git a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp index 12d4d0659..839a8ddc5 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp @@ -10,18 +10,23 @@ namespace Shader::Optimization { static bool IsSharedAccess(const IR::Inst& inst) { const auto opcode = inst.GetOpcode(); switch (opcode) { + case IR::Opcode::LoadSharedU16: case IR::Opcode::LoadSharedU32: case IR::Opcode::LoadSharedU64: + case IR::Opcode::WriteSharedU16: case IR::Opcode::WriteSharedU32: case IR::Opcode::WriteSharedU64: - case IR::Opcode::SharedAtomicAnd32: case IR::Opcode::SharedAtomicIAdd32: case IR::Opcode::SharedAtomicIAdd64: - case IR::Opcode::SharedAtomicOr32: - case IR::Opcode::SharedAtomicSMax32: - case IR::Opcode::SharedAtomicUMax32: + case IR::Opcode::SharedAtomicISub32: case IR::Opcode::SharedAtomicSMin32: case IR::Opcode::SharedAtomicUMin32: + case IR::Opcode::SharedAtomicSMax32: + case IR::Opcode::SharedAtomicUMax32: + case IR::Opcode::SharedAtomicInc32: + case IR::Opcode::SharedAtomicDec32: + case IR::Opcode::SharedAtomicAnd32: + case IR::Opcode::SharedAtomicOr32: case IR::Opcode::SharedAtomicXor32: return true; default: @@ -41,14 +46,8 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ profile.supports_workgroup_explicit_memory_layout)) { return; } - // Add buffer binding for shared memory storage buffer. const u32 binding = static_cast(program.info.buffers.size()); - program.info.buffers.push_back({ - .used_types = IR::Type::U32, - .inline_cbuf = AmdGpu::Buffer::Null(), - .buffer_type = BufferType::SharedMemory, - .is_written = true, - }); + IR::Type used_types{}; for (IR::Block* const block : program.blocks) { for (IR::Inst& inst : block->Instructions()) { if (!IsSharedAccess(inst)) { @@ -56,73 +55,106 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ } IR::IREmitter ir{*block, IR::Block::InstructionList::s_iterator_to(inst)}; const IR::U32 handle = ir.Imm32(binding); + const IR::U32 offset = ir.IMul(ir.GetAttributeU32(IR::Attribute::WorkgroupIndex), + ir.Imm32(shared_memory_size)); + const IR::U32 address = ir.IAdd(IR::U32{inst.Arg(0)}, offset); // Replace shared atomics first switch (inst.GetOpcode()) { - case IR::Opcode::SharedAtomicAnd32: - inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicAnd(handle, inst.Arg(0), inst.Arg(1), {})); - continue; case IR::Opcode::SharedAtomicIAdd32: + inst.ReplaceUsesWithAndRemove( + ir.BufferAtomicIAdd(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U32; + continue; case IR::Opcode::SharedAtomicIAdd64: inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicIAdd(handle, inst.Arg(0), inst.Arg(1), {})); + ir.BufferAtomicIAdd(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U64; continue; - case IR::Opcode::SharedAtomicOr32: + case IR::Opcode::SharedAtomicISub32: inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicOr(handle, inst.Arg(0), inst.Arg(1), {})); + ir.BufferAtomicISub(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U32; continue; - case IR::Opcode::SharedAtomicSMax32: - case IR::Opcode::SharedAtomicUMax32: { - const bool is_signed = inst.GetOpcode() == IR::Opcode::SharedAtomicSMax32; - inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicIMax(handle, inst.Arg(0), inst.Arg(1), is_signed, {})); - continue; - } case IR::Opcode::SharedAtomicSMin32: case IR::Opcode::SharedAtomicUMin32: { const bool is_signed = inst.GetOpcode() == IR::Opcode::SharedAtomicSMin32; inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicIMin(handle, inst.Arg(0), inst.Arg(1), is_signed, {})); + ir.BufferAtomicIMin(handle, address, inst.Arg(1), is_signed, {})); + used_types |= IR::Type::U32; continue; } - case IR::Opcode::SharedAtomicXor32: + case IR::Opcode::SharedAtomicSMax32: + case IR::Opcode::SharedAtomicUMax32: { + const bool is_signed = inst.GetOpcode() == IR::Opcode::SharedAtomicSMax32; inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicXor(handle, inst.Arg(0), inst.Arg(1), {})); + ir.BufferAtomicIMax(handle, address, inst.Arg(1), is_signed, {})); + used_types |= IR::Type::U32; + continue; + } + case IR::Opcode::SharedAtomicInc32: + inst.ReplaceUsesWithAndRemove(ir.BufferAtomicInc(handle, address, {})); + used_types |= IR::Type::U32; + continue; + case IR::Opcode::SharedAtomicDec32: + inst.ReplaceUsesWithAndRemove(ir.BufferAtomicDec(handle, address, {})); + used_types |= IR::Type::U32; + continue; + case IR::Opcode::SharedAtomicAnd32: + inst.ReplaceUsesWithAndRemove(ir.BufferAtomicAnd(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U32; + continue; + case IR::Opcode::SharedAtomicOr32: + inst.ReplaceUsesWithAndRemove(ir.BufferAtomicOr(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U32; + continue; + case IR::Opcode::SharedAtomicXor32: + inst.ReplaceUsesWithAndRemove(ir.BufferAtomicXor(handle, address, inst.Arg(1), {})); + used_types |= IR::Type::U32; continue; default: break; } // Replace shared operations. - const IR::U32 offset = ir.IMul(ir.GetAttributeU32(IR::Attribute::WorkgroupIndex), - ir.Imm32(shared_memory_size)); - const IR::U32 address = ir.IAdd(IR::U32{inst.Arg(0)}, offset); switch (inst.GetOpcode()) { case IR::Opcode::LoadSharedU16: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU16(handle, address, {})); + used_types |= IR::Type::U16; break; case IR::Opcode::LoadSharedU32: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU32(1, handle, address, {})); + used_types |= IR::Type::U32; break; case IR::Opcode::LoadSharedU64: - inst.ReplaceUsesWithAndRemove(ir.LoadBufferU32(2, handle, address, {})); + inst.ReplaceUsesWithAndRemove(ir.LoadBufferU64(handle, address, {})); + used_types |= IR::Type::U64; break; case IR::Opcode::WriteSharedU16: - ir.StoreBufferU16(handle, address, IR::U32{inst.Arg(1)}, {}); + ir.StoreBufferU16(handle, address, IR::U16{inst.Arg(1)}, {}); inst.Invalidate(); + used_types |= IR::Type::U16; break; case IR::Opcode::WriteSharedU32: ir.StoreBufferU32(1, handle, address, inst.Arg(1), {}); inst.Invalidate(); + used_types |= IR::Type::U32; break; case IR::Opcode::WriteSharedU64: - ir.StoreBufferU32(2, handle, address, inst.Arg(1), {}); + ir.StoreBufferU64(handle, address, IR::U64{inst.Arg(1)}, {}); inst.Invalidate(); + used_types |= IR::Type::U64; break; default: break; } } } + // Add buffer binding for shared memory storage buffer. + program.info.buffers.push_back({ + .used_types = used_types, + .inline_cbuf = AmdGpu::Buffer::Null(), + .buffer_type = BufferType::SharedMemory, + .is_written = true, + }); } } // namespace Shader::Optimization diff --git a/src/shader_recompiler/ir/value.h b/src/shader_recompiler/ir/value.h index ed1e5536a..b92c5d555 100644 --- a/src/shader_recompiler/ir/value.h +++ b/src/shader_recompiler/ir/value.h @@ -265,6 +265,7 @@ using U32F32 = TypedValue; using U64F64 = TypedValue; using U32U64 = TypedValue; using U16U32U64 = TypedValue; +using U8U16U32U64 = TypedValue; using F32F64 = TypedValue; using F16F32F64 = TypedValue; using UAny = TypedValue; From fc4fd0107d5bde21e2a4fbdff6b502e6b39e9b7f Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:43:11 -0500 Subject: [PATCH 127/141] libSceNpTrophy: Change initial context and handle values (#3080) * Change default context and handle values libSceNpToolkit internally uses context/handle values of zero to indicate NpTrophy calls failed. This PR returns handle/context as index + 1 instead, avoiding this issue. * Fix log message --- src/core/libraries/np_trophy/np_trophy.cpp | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/core/libraries/np_trophy/np_trophy.cpp b/src/core/libraries/np_trophy/np_trophy.cpp index 6de84bd93..e3c5ce35e 100644 --- a/src/core/libraries/np_trophy/np_trophy.cpp +++ b/src/core/libraries/np_trophy/np_trophy.cpp @@ -164,10 +164,12 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, int32_t } const auto ctx_id = trophy_contexts.insert(user_id, service_label); - contexts_internal[key].context_id = ctx_id.index; - LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", ctx_id.index, - user_id, service_label); - *context = ctx_id.index; + + *context = ctx_id.index + 1; + contexts_internal[key].context_id = *context; + LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id, + service_label); + return ORBIS_OK; } @@ -179,21 +181,23 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateHandle(OrbisNpTrophyHandle* handle) { if (trophy_handles.size() >= MaxTrophyHandles) { return ORBIS_NP_TROPHY_ERROR_HANDLE_EXCEEDS_MAX; } - const auto handle_id = trophy_handles.insert(); - LOG_INFO(Lib_NpTrophy, "New handle = {}", handle_id.index); - *handle = handle_id.index; + const auto handle_id = trophy_handles.insert(); + + *handle = handle_id.index + 1; + LOG_INFO(Lib_NpTrophy, "New handle = {}", *handle); return ORBIS_OK; } int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) { LOG_INFO(Lib_NpTrophy, "Destroyed Context {}", context); - if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) + if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT) { return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT; + } Common::SlotId contextId; - contextId.index = context; + contextId.index = context - 1; ContextKey contextkey = trophy_contexts[contextId]; trophy_contexts.erase(contextId); @@ -206,15 +210,17 @@ s32 PS4_SYSV_ABI sceNpTrophyDestroyHandle(OrbisNpTrophyHandle handle) { if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE) return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; - if (handle >= trophy_handles.size()) { + s32 handle_index = handle - 1; + if (handle_index >= trophy_handles.size()) { LOG_ERROR(Lib_NpTrophy, "Invalid handle {}", handle); return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; } - if (!trophy_handles.is_allocated({static_cast(handle)})) { + + if (!trophy_handles.is_allocated({static_cast(handle_index)})) { return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE; } - trophy_handles.erase({static_cast(handle)}); + trophy_handles.erase({static_cast(handle_index)}); LOG_INFO(Lib_NpTrophy, "Handle {} destroyed", handle); return ORBIS_OK; } From dedf6de2ac13b6543339ee5cdedc44ee0efd963c Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Wed, 11 Jun 2025 11:34:37 +0300 Subject: [PATCH 128/141] texture_cache: Implement color<->depth copies (#3079) * texture_cache: Implement color to depth copies and vise versa * ir_passes: Adjust shared memory barrier pass to cover more cases * texture_cache: Remove unused code * review comment --- .../ir/passes/shared_memory_barrier_pass.cpp | 35 ++++-- src/video_core/buffer_cache/buffer_cache.cpp | 10 +- src/video_core/buffer_cache/buffer_cache.h | 23 ++-- .../renderer_vulkan/vk_rasterizer.cpp | 4 +- src/video_core/texture_cache/image.cpp | 113 +++++++++++++++--- src/video_core/texture_cache/image.h | 3 +- .../texture_cache/texture_cache.cpp | 23 ++-- 7 files changed, 157 insertions(+), 54 deletions(-) diff --git a/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp index 10d6a285c..11713d099 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include "shader_recompiler/ir/breadth_first_search.h" #include "shader_recompiler/ir/ir_emitter.h" #include "shader_recompiler/ir/program.h" @@ -51,11 +52,14 @@ static void EmitBarrierInBlock(IR::Block* block) { } } +using NodeSet = std::unordered_set; + // Inserts a barrier after divergent conditional blocks to avoid undefined // behavior when some threads write and others read from shared memory. -static void EmitBarrierInMergeBlock(const IR::AbstractSyntaxNode::Data& data) { +static void EmitBarrierInMergeBlock(const IR::AbstractSyntaxNode::Data& data, + NodeSet& divergence_end, u32& divergence_depth) { const IR::U1 cond = data.if_node.cond; - const auto insert_barrier = + const auto is_divergent_cond = IR::BreadthFirstSearch(cond, [](IR::Inst* inst) -> std::optional { if (inst->GetOpcode() == IR::Opcode::GetAttributeU32 && inst->Arg(0).Attribute() == IR::Attribute::LocalInvocationId) { @@ -63,11 +67,15 @@ static void EmitBarrierInMergeBlock(const IR::AbstractSyntaxNode::Data& data) { } return std::nullopt; }); - if (insert_barrier) { - IR::Block* const merge = data.if_node.merge; - auto insert_point = std::ranges::find_if_not(merge->Instructions(), IR::IsPhi); - IR::IREmitter ir{*merge, insert_point}; - ir.Barrier(); + if (is_divergent_cond) { + if (divergence_depth == 0) { + IR::Block* const merge = data.if_node.merge; + auto insert_point = std::ranges::find_if_not(merge->Instructions(), IR::IsPhi); + IR::IREmitter ir{*merge, insert_point}; + ir.Barrier(); + } + ++divergence_depth; + divergence_end.emplace(data.if_node.merge); } } @@ -89,19 +97,22 @@ void SharedMemoryBarrierPass(IR::Program& program, const RuntimeInfo& runtime_in return; } using Type = IR::AbstractSyntaxNode::Type; - u32 branch_depth{}; + u32 divergence_depth{}; + NodeSet divergence_end; for (const IR::AbstractSyntaxNode& node : program.syntax_list) { if (node.type == Type::EndIf) { - --branch_depth; + if (divergence_end.contains(node.data.end_if.merge)) { + --divergence_depth; + } continue; } // Check if branch depth is zero, we don't want to insert barrier in potentially divergent // code. - if (node.type == Type::If && branch_depth++ == 0) { - EmitBarrierInMergeBlock(node.data); + if (node.type == Type::If) { + EmitBarrierInMergeBlock(node.data, divergence_end, divergence_depth); continue; } - if (node.type == Type::Block && branch_depth == 0) { + if (node.type == Type::Block && divergence_depth == 0) { EmitBarrierInBlock(node.data.block); } } diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index e470f8e77..ffa744b31 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -23,6 +23,7 @@ static constexpr size_t DataShareBufferSize = 64_KB; static constexpr size_t StagingBufferSize = 512_MB; static constexpr size_t UboStreamBufferSize = 128_MB; static constexpr size_t DownloadBufferSize = 128_MB; +static constexpr size_t DeviceBufferSize = 16_MB; static constexpr size_t MaxPageFaults = 1024; BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, @@ -32,7 +33,8 @@ BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& s memory{Core::Memory::Instance()}, texture_cache{texture_cache_}, tracker{tracker_}, staging_buffer{instance, scheduler, MemoryUsage::Upload, StagingBufferSize}, stream_buffer{instance, scheduler, MemoryUsage::Stream, UboStreamBufferSize}, - download_buffer(instance, scheduler, MemoryUsage::Download, DownloadBufferSize), + download_buffer{instance, scheduler, MemoryUsage::Download, DownloadBufferSize}, + device_buffer{instance, scheduler, MemoryUsage::DeviceLocal, DeviceBufferSize}, gds_buffer{instance, scheduler, MemoryUsage::Stream, 0, AllFlags, DataShareBufferSize}, bda_pagetable_buffer{instance, scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, BDA_PAGETABLE_SIZE}, @@ -348,7 +350,7 @@ std::pair BufferCache::ObtainBuffer(VAddr device_addr, u32 size, b return {&buffer, buffer.Offset(device_addr)}; } -std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, bool prefer_gpu) { +std::pair BufferCache::ObtainBufferForImage(VAddr gpu_addr, u32 size) { // Check if any buffer contains the full requested range. const u64 page = gpu_addr >> CACHING_PAGEBITS; const BufferId buffer_id = page_table[page].buffer_id; @@ -361,10 +363,10 @@ std::pair BufferCache::ObtainViewBuffer(VAddr gpu_addr, u32 size, } // If no buffer contains the full requested range but some buffer within was GPU-modified, // fall back to ObtainBuffer to create a full buffer and avoid losing GPU modifications. - // This is only done if the request prefers to use GPU memory, otherwise we can skip it. - if (prefer_gpu && memory_tracker.IsRegionGpuModified(gpu_addr, size)) { + if (memory_tracker.IsRegionGpuModified(gpu_addr, size)) { return ObtainBuffer(gpu_addr, size, false, false); } + // In all other cases, just do a CPU copy to the staging buffer. const auto [data, offset] = staging_buffer.Map(size, 16); memory->CopySparseMemory(gpu_addr, data, size); diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index c2faf12c8..d7d753213 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -80,11 +80,6 @@ public: return &gds_buffer; } - /// Retrieves the host visible device local stream buffer. - [[nodiscard]] StreamBuffer& GetStreamBuffer() noexcept { - return stream_buffer; - } - /// Retrieves the device local DBA page table buffer. [[nodiscard]] Buffer* GetBdaPageTableBuffer() noexcept { return &bda_pagetable_buffer; @@ -100,6 +95,20 @@ public: return slot_buffers[id]; } + /// Retrieves a utility buffer optimized for specified memory usage. + StreamBuffer& GetUtilityBuffer(MemoryUsage usage) noexcept { + switch (usage) { + case MemoryUsage::Stream: + return stream_buffer; + case MemoryUsage::Download: + return download_buffer; + case MemoryUsage::Upload: + return staging_buffer; + case MemoryUsage::DeviceLocal: + return device_buffer; + } + } + /// Invalidates any buffer in the logical page range. void InvalidateMemory(VAddr device_addr, u64 size, bool unmap); @@ -121,8 +130,7 @@ public: BufferId buffer_id = {}); /// Attempts to obtain a buffer without modifying the cache contents. - [[nodiscard]] std::pair ObtainViewBuffer(VAddr gpu_addr, u32 size, - bool prefer_gpu); + [[nodiscard]] std::pair ObtainBufferForImage(VAddr gpu_addr, u32 size); /// Return true when a region is registered on the cache [[nodiscard]] bool IsRegionRegistered(VAddr addr, size_t size); @@ -193,6 +201,7 @@ private: StreamBuffer staging_buffer; StreamBuffer stream_buffer; StreamBuffer download_buffer; + StreamBuffer device_buffer; Buffer gds_buffer; Buffer bda_pagetable_buffer; Buffer fault_buffer; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index dff4e5a5f..9dea5ceea 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -549,7 +549,7 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding const auto* gds_buf = buffer_cache.GetGdsBuffer(); buffer_infos.emplace_back(gds_buf->Handle(), 0, gds_buf->SizeBytes()); } else if (desc.buffer_type == Shader::BufferType::Flatbuf) { - auto& vk_buffer = buffer_cache.GetStreamBuffer(); + auto& vk_buffer = buffer_cache.GetUtilityBuffer(VideoCore::MemoryUsage::Stream); const u32 ubo_size = stage.flattened_ud_buf.size() * sizeof(u32); const u64 offset = vk_buffer.Copy(stage.flattened_ud_buf.data(), ubo_size, instance.UniformMinAlignment()); @@ -561,7 +561,7 @@ void Rasterizer::BindBuffers(const Shader::Info& stage, Shader::Backend::Binding const auto* fault_buffer = buffer_cache.GetFaultBuffer(); buffer_infos.emplace_back(fault_buffer->Handle(), 0, fault_buffer->SizeBytes()); } else if (desc.buffer_type == Shader::BufferType::SharedMemory) { - auto& lds_buffer = buffer_cache.GetStreamBuffer(); + auto& lds_buffer = buffer_cache.GetUtilityBuffer(VideoCore::MemoryUsage::Stream); const auto& cs_program = liverpool->GetCsRegs(); const auto lds_size = cs_program.SharedMemSize() * cs_program.NumWorkgroups(); const auto [data, offset] = diff --git a/src/video_core/texture_cache/image.cpp b/src/video_core/texture_cache/image.cpp index 6241100a0..ab9111e6b 100644 --- a/src/video_core/texture_cache/image.cpp +++ b/src/video_core/texture_cache/image.cpp @@ -312,43 +312,121 @@ void Image::Upload(vk::Buffer buffer, u64 offset) { vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } -void Image::CopyImage(const Image& image) { +void Image::CopyImage(const Image& src_image) { scheduler->EndRendering(); Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}); auto cmdbuf = scheduler->CommandBuffer(); + const auto& src_info = src_image.info; boost::container::small_vector image_copy{}; - const u32 num_mips = std::min(image.info.resources.levels, info.resources.levels); + const u32 num_mips = std::min(src_info.resources.levels, info.resources.levels); for (u32 m = 0; m < num_mips; ++m) { - const auto mip_w = std::max(image.info.size.width >> m, 1u); - const auto mip_h = std::max(image.info.size.height >> m, 1u); - const auto mip_d = std::max(image.info.size.depth >> m, 1u); + const auto mip_w = std::max(src_info.size.width >> m, 1u); + const auto mip_h = std::max(src_info.size.height >> m, 1u); + const auto mip_d = std::max(src_info.size.depth >> m, 1u); image_copy.emplace_back(vk::ImageCopy{ .srcSubresource{ - .aspectMask = image.aspect_mask, + .aspectMask = src_image.aspect_mask, .mipLevel = m, .baseArrayLayer = 0, - .layerCount = image.info.resources.layers, + .layerCount = src_info.resources.layers, }, .dstSubresource{ - .aspectMask = image.aspect_mask, + .aspectMask = src_image.aspect_mask, .mipLevel = m, .baseArrayLayer = 0, - .layerCount = image.info.resources.layers, + .layerCount = src_info.resources.layers, }, .extent = {mip_w, mip_h, mip_d}, }); } - cmdbuf.copyImage(image.image, image.last_state.layout, this->image, this->last_state.layout, + cmdbuf.copyImage(src_image.image, src_image.last_state.layout, image, last_state.layout, image_copy); Transit(vk::ImageLayout::eGeneral, vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eTransferRead, {}); } -void Image::CopyMip(const Image& image, u32 mip, u32 slice) { +void Image::CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset) { + const auto& src_info = src_image.info; + + vk::BufferImageCopy buffer_image_copy = { + .bufferOffset = offset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = + { + .aspectMask = src_info.IsDepthStencil() ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eColor, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset = + { + .x = 0, + .y = 0, + .z = 0, + }, + .imageExtent = + { + .width = src_info.size.width, + .height = src_info.size.height, + .depth = src_info.size.depth, + }, + }; + + const vk::BufferMemoryBarrier2 pre_copy_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferRead, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferWrite, + .buffer = buffer, + .offset = offset, + .size = VK_WHOLE_SIZE, + }; + + const vk::BufferMemoryBarrier2 post_copy_barrier = { + .srcStageMask = vk::PipelineStageFlagBits2::eTransfer, + .srcAccessMask = vk::AccessFlagBits2::eTransferWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eTransfer, + .dstAccessMask = vk::AccessFlagBits2::eTransferRead, + .buffer = buffer, + .offset = offset, + .size = VK_WHOLE_SIZE, + }; + + scheduler->EndRendering(); + src_image.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead, {}); + Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}); + + auto cmdbuf = scheduler->CommandBuffer(); + + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &pre_copy_barrier, + }); + + cmdbuf.copyImageToBuffer(src_image.image, vk::ImageLayout::eTransferSrcOptimal, buffer, + buffer_image_copy); + + cmdbuf.pipelineBarrier2(vk::DependencyInfo{ + .dependencyFlags = vk::DependencyFlagBits::eByRegion, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &post_copy_barrier, + }); + + buffer_image_copy.imageSubresource.aspectMask = + info.IsDepthStencil() ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor; + + cmdbuf.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, + buffer_image_copy); +} + +void Image::CopyMip(const Image& src_image, u32 mip, u32 slice) { scheduler->EndRendering(); Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {}); @@ -358,26 +436,27 @@ void Image::CopyMip(const Image& image, u32 mip, u32 slice) { const auto mip_h = std::max(info.size.height >> mip, 1u); const auto mip_d = std::max(info.size.depth >> mip, 1u); - ASSERT(mip_w == image.info.size.width); - ASSERT(mip_h == image.info.size.height); + const auto& src_info = src_image.info; + ASSERT(mip_w == src_info.size.width); + ASSERT(mip_h == src_info.size.height); - const u32 num_layers = std::min(image.info.resources.layers, info.resources.layers); + const u32 num_layers = std::min(src_info.resources.layers, info.resources.layers); const vk::ImageCopy image_copy{ .srcSubresource{ - .aspectMask = image.aspect_mask, + .aspectMask = src_image.aspect_mask, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = num_layers, }, .dstSubresource{ - .aspectMask = image.aspect_mask, + .aspectMask = src_image.aspect_mask, .mipLevel = mip, .baseArrayLayer = slice, .layerCount = num_layers, }, .extent = {mip_w, mip_h, mip_d}, }; - cmdbuf.copyImage(image.image, image.last_state.layout, this->image, this->last_state.layout, + cmdbuf.copyImage(src_image.image, src_image.last_state.layout, image, last_state.layout, image_copy); Transit(vk::ImageLayout::eGeneral, diff --git a/src/video_core/texture_cache/image.h b/src/video_core/texture_cache/image.h index 404e25e88..31b67e021 100644 --- a/src/video_core/texture_cache/image.h +++ b/src/video_core/texture_cache/image.h @@ -104,7 +104,8 @@ struct Image { std::optional range, vk::CommandBuffer cmdbuf = {}); void Upload(vk::Buffer buffer, u64 offset); - void CopyImage(const Image& image); + void CopyImage(const Image& src_image); + void CopyImageWithBuffer(Image& src_image, vk::Buffer buffer, u64 offset); void CopyMip(const Image& src_image, u32 mip, u32 slice); bool IsTracked() { diff --git a/src/video_core/texture_cache/texture_cache.cpp b/src/video_core/texture_cache/texture_cache.cpp index cc244eb6b..a47e858ab 100644 --- a/src/video_core/texture_cache/texture_cache.cpp +++ b/src/video_core/texture_cache/texture_cache.cpp @@ -8,7 +8,6 @@ #include "common/debug.h" #include "video_core/buffer_cache/buffer_cache.h" #include "video_core/page_manager.h" -#include "video_core/renderer_vulkan/liverpool_to_vk.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/texture_cache/host_compatibility.h" @@ -126,7 +125,7 @@ void TextureCache::UnmapMemory(VAddr cpu_addr, size_t size) { ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested_info, BindingType binding, ImageId cache_image_id) { - const auto& cache_image = slot_images[cache_image_id]; + auto& cache_image = slot_images[cache_image_id]; if (!cache_image.info.IsDepthStencil() && !requested_info.IsDepthStencil()) { return {}; @@ -169,18 +168,21 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested_info, Bindi } if (recreate) { - auto new_info{requested_info}; - new_info.resources = std::max(requested_info.resources, cache_image.info.resources); - new_info.UpdateSize(); + auto new_info = requested_info; + new_info.resources = std::min(requested_info.resources, cache_image.info.resources); const auto new_image_id = slot_images.insert(instance, scheduler, new_info); RegisterImage(new_image_id); // Inherit image usage - auto& new_image = GetImage(new_image_id); + auto& new_image = slot_images[new_image_id]; new_image.usage = cache_image.usage; + new_image.flags &= ~ImageFlagBits::Dirty; - // TODO: perform a depth copy here + // Perform depth<->color copy using the intermediate copy buffer. + const auto& copy_buffer = buffer_cache.GetUtilityBuffer(MemoryUsage::DeviceLocal); + new_image.CopyImageWithBuffer(cache_image, copy_buffer.Handle(), 0); + // Free the cache image. FreeImage(cache_image_id); return new_image_id; } @@ -584,12 +586,11 @@ void TextureCache::RefreshImage(Image& image, Vulkan::Scheduler* custom_schedule const VAddr image_addr = image.info.guest_address; const size_t image_size = image.info.guest_size; - const auto [vk_buffer, buf_offset] = - buffer_cache.ObtainViewBuffer(image_addr, image_size, is_gpu_dirty); + const auto [vk_buffer, buf_offset] = buffer_cache.ObtainBufferForImage(image_addr, image_size); const auto cmdbuf = sched_ptr->CommandBuffer(); - // The obtained buffer may be written by a shader so we need to emit a barrier to prevent RAW - // hazard + + // The obtained buffer may be GPU modified so we need to emit a barrier to prevent RAW hazard if (auto barrier = vk_buffer->GetBarrier(vk::AccessFlagBits2::eTransferRead, vk::PipelineStageFlagBits2::eTransfer)) { cmdbuf.pipelineBarrier2(vk::DependencyInfo{ From 274182954551d429c77e8b88ec395ae8726a0127 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Wed, 11 Jun 2025 12:02:59 +0300 Subject: [PATCH 129/141] New translations en_us.ts (Arabic) (#3081) --- src/qt_gui/translations/ar_SA.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt_gui/translations/ar_SA.ts b/src/qt_gui/translations/ar_SA.ts index 26e768720..7d0c15e6b 100644 --- a/src/qt_gui/translations/ar_SA.ts +++ b/src/qt_gui/translations/ar_SA.ts @@ -2049,7 +2049,7 @@ Nightly: نُسخ تحتوي على أحدث الميزات، لكنها أقل * Unsupported Vulkan Version - * Unsupported Vulkan Version + نسخ Vulkan غير مدعومة From 3e0ec9ebef8c6b7d752d4538e42b36b571c983a6 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:34:00 -0500 Subject: [PATCH 130/141] Core: Merge Direct Memory Areas (#3084) * Merge dmem areas * Fix DirectMemoryArea::CanMergeWith Don't merge dmem areas if the memory types are different. * Reduce some warnings to info Both functions should behave properly now, there's no reason to warn about their use. * Clang --- src/core/libraries/kernel/memory.cpp | 6 +++--- src/core/memory.cpp | 1 + src/core/memory.h | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/libraries/kernel/memory.cpp b/src/core/libraries/kernel/memory.cpp index f02ddafdc..ea3998ddd 100644 --- a/src/core/libraries/kernel/memory.cpp +++ b/src/core/libraries/kernel/memory.cpp @@ -99,8 +99,8 @@ s32 PS4_SYSV_ABI sceKernelReleaseDirectMemory(u64 start, size_t len) { s32 PS4_SYSV_ABI sceKernelAvailableDirectMemorySize(u64 searchStart, u64 searchEnd, size_t alignment, u64* physAddrOut, size_t* sizeOut) { - LOG_WARNING(Kernel_Vmm, "called searchStart = {:#x}, searchEnd = {:#x}, alignment = {:#x}", - searchStart, searchEnd, alignment); + LOG_INFO(Kernel_Vmm, "called searchStart = {:#x}, searchEnd = {:#x}, alignment = {:#x}", + searchStart, searchEnd, alignment); if (physAddrOut == nullptr || sizeOut == nullptr) { return ORBIS_KERNEL_ERROR_EINVAL; @@ -287,7 +287,7 @@ s32 PS4_SYSV_ABI sceKernelMtypeprotect(const void* addr, u64 size, s32 mtype, s3 int PS4_SYSV_ABI sceKernelDirectMemoryQuery(u64 offset, int flags, OrbisQueryInfo* query_info, size_t infoSize) { - LOG_WARNING(Kernel_Vmm, "called offset = {:#x}, flags = {:#x}", offset, flags); + LOG_INFO(Kernel_Vmm, "called offset = {:#x}, flags = {:#x}", offset, flags); auto* memory = Core::Memory::Instance(); return memory->DirectMemoryQuery(offset, flags == 1, query_info); } diff --git a/src/core/memory.cpp b/src/core/memory.cpp index e738f85a1..dad42347a 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -222,6 +222,7 @@ PAddr MemoryManager::Allocate(PAddr search_start, PAddr search_end, size_t size, auto& area = CarveDmemArea(mapping_start, size)->second; area.memory_type = memory_type; area.is_free = false; + MergeAdjacent(dmem_map, dmem_area); return mapping_start; } diff --git a/src/core/memory.h b/src/core/memory.h index 68f9c26c4..6a9b29382 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -75,6 +75,9 @@ struct DirectMemoryArea { if (base + size != next.base) { return false; } + if (memory_type != next.memory_type) { + return false; + } if (is_free != next.is_free) { return false; } From 34a1ffbcda67aaa136535744da2ea29cb6d00848 Mon Sep 17 00:00:00 2001 From: Missake212 Date: Wed, 11 Jun 2025 20:21:55 +0100 Subject: [PATCH 131/141] Few changes to the README.md (#3086) * Update README.md * backslash --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 985bba586..9079ead73 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ SPDX-License-Identifier: GPL-2.0-or-later **shadPS4** is an early **PlayStation 4** emulator for **Windows**, **Linux** and **macOS** written in C++. -If you encounter problems or have doubts, do not hesitate to look at the [**Quickstart**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Quickstart/Quickstart.md).\ +If you encounter problems or have doubts, do not hesitate to look at the [**Quickstart**](https://github.com/shadps4-emu/shadPS4/wiki/I.-Quick-start-%5BUsers%5D).\ To verify that a game works, you can look at [**shadPS4 Game Compatibility**](https://github.com/shadps4-emu/shadps4-game-compatibility).\ To discuss shadPS4 development, suggest ideas or to ask for help, join our [**Discord server**](https://discord.gg/bFJxfftGW6).\ To get the latest news, go to our [**X (Twitter)**](https://x.com/shadps4) or our [**website**](https://shadps4.net/).\ @@ -124,8 +124,8 @@ Keyboard and mouse inputs can be customized in the settings menu by clicking the # Firmware files -shadPS4 can load some PlayStation 4 firmware files, these must be dumped from your legally owned PlayStation 4 console.\ -The following firmware modules are supported and must be placed in shadPS4's `user/sys_modules` folder. +shadPS4 can load some PlayStation 4 firmware files, these must be dumped from your legally owned PlayStation 4 console. +The following firmware modules are supported and must be placed in shadPS4's `sys_modules` folder.
@@ -139,7 +139,6 @@ The following firmware modules are supported and must be placed in shadPS4's `us > [!Caution] > The above modules are required to run the games properly and must be extracted from your PlayStation 4.\ -> **We do not provide any information or support on how to do this**. @@ -148,7 +147,7 @@ The following firmware modules are supported and must be placed in shadPS4's `us - [**georgemoralis**](https://github.com/georgemoralis) - [**psucien**](https://github.com/psucien) - [**viniciuslrangel**](https://github.com/viniciuslrangel) -- [**roamic**](https://github.com/vladmikhalin) +- [**roamic**](https://github.com/roamic) - [**squidbus**](https://github.com/squidbus) - [**frodo**](https://github.com/baggins183) - [**Stephen Miller**](https://github.com/StevenMiller123) @@ -158,7 +157,7 @@ Logo is done by [**Xphalnos**](https://github.com/Xphalnos) # Contributing -If you want to contribute, please look the [**CONTRIBUTING.md**](https://github.com/shadps4-emu/shadPS4/blob/main/CONTRIBUTING.md) file.\ +If you want to contribute, please read the [**CONTRIBUTING.md**](https://github.com/shadps4-emu/shadPS4/blob/main/CONTRIBUTING.md) file.\ Open a PR and we'll check it :) # Translations From 69a50fa7132f27f73754aebe15be953546f5ace2 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:22:34 -0500 Subject: [PATCH 132/141] Struct update fixes (#3087) Neither sceVideodec2Decode or sceVideodec2Flush should be modifying the output's `thisSize`, doing so breaks older games now that we have the updated structs. We should also only set frameFormat and framePitchInBytes if the game inputted the newer struct, since otherwise we're modifying memory the game never gave us. These changes might fix the regression in Hatsune Miku Project Diva X, though it's hard to tell due to some weird caching issue with Windows, and the ancient regression this game had on Linux. --- .../libraries/videodec/videodec2_impl.cpp | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/core/libraries/videodec/videodec2_impl.cpp b/src/core/libraries/videodec/videodec2_impl.cpp index a643239a3..373809c14 100644 --- a/src/core/libraries/videodec/videodec2_impl.cpp +++ b/src/core/libraries/videodec/videodec2_impl.cpp @@ -44,11 +44,14 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, OrbisVideodec2FrameBuffer& frameBuffer, OrbisVideodec2OutputInfo& outputInfo) { frameBuffer.isAccepted = false; - outputInfo.thisSize = sizeof(OrbisVideodec2OutputInfo); outputInfo.isValid = false; outputInfo.isErrorFrame = true; outputInfo.pictureCount = 0; - outputInfo.frameFormat = 0; + + // Only set frameFormat if the game uses the newer struct version. + if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) { + outputInfo.frameFormat = 0; + } if (!inputData.auData) { return ORBIS_VIDEODEC2_ERROR_ACCESS_UNIT_POINTER; @@ -107,7 +110,6 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, outputInfo.frameWidth = frame->width; outputInfo.frameHeight = frame->height; outputInfo.framePitch = frame->linesize[0]; - outputInfo.framePitchInBytes = frame->linesize[0]; outputInfo.frameBufferSize = frameBuffer.frameBufferSize; outputInfo.frameBuffer = frameBuffer.frameBuffer; @@ -115,6 +117,11 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, outputInfo.isErrorFrame = false; outputInfo.pictureCount = 1; // TODO: 2 pictures for interlaced video + // Only set framePitchInBytes if the game uses the newer struct version. + if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) { + outputInfo.framePitchInBytes = frame->linesize[0]; + } + if (outputInfo.isValid) { OrbisVideodec2AvcPictureInfo pictureInfo = {}; @@ -142,11 +149,14 @@ s32 VdecDecoder::Decode(const OrbisVideodec2InputData& inputData, s32 VdecDecoder::Flush(OrbisVideodec2FrameBuffer& frameBuffer, OrbisVideodec2OutputInfo& outputInfo) { frameBuffer.isAccepted = false; - outputInfo.thisSize = sizeof(OrbisVideodec2OutputInfo); outputInfo.isValid = false; outputInfo.isErrorFrame = true; outputInfo.pictureCount = 0; - outputInfo.frameFormat = 0; + + // Only set frameFormat if the game uses the newer struct version. + if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) { + outputInfo.frameFormat = 0; + } AVFrame* frame = av_frame_alloc(); if (!frame) { @@ -178,7 +188,6 @@ s32 VdecDecoder::Flush(OrbisVideodec2FrameBuffer& frameBuffer, outputInfo.frameWidth = frame->width; outputInfo.frameHeight = frame->height; outputInfo.framePitch = frame->linesize[0]; - outputInfo.framePitchInBytes = frame->linesize[0]; outputInfo.frameBufferSize = frameBuffer.frameBufferSize; outputInfo.frameBuffer = frameBuffer.frameBuffer; @@ -186,6 +195,11 @@ s32 VdecDecoder::Flush(OrbisVideodec2FrameBuffer& frameBuffer, outputInfo.isErrorFrame = false; outputInfo.pictureCount = 1; // TODO: 2 pictures for interlaced video + // Only set framePitchInBytes if the game uses the newer struct version. + if (outputInfo.thisSize == sizeof(OrbisVideodec2OutputInfo)) { + outputInfo.framePitchInBytes = frame->linesize[0]; + } + // FIXME: Should we add picture info here too? } From c71dc740e20ec917ac06092cf938f66b62153e48 Mon Sep 17 00:00:00 2001 From: squidbus <175574877+squidbus@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:24:41 -0700 Subject: [PATCH 133/141] shader_recompiler: Reduce cases where shared memory to buffer pass is needed. (#3082) --- CMakeLists.txt | 1 + .../backend/spirv/emit_spirv.cpp | 3 +- .../backend/spirv/spirv_emit_context.cpp | 28 +++- src/shader_recompiler/info.h | 2 +- src/shader_recompiler/ir/passes/ir_passes.h | 1 + .../ir/passes/shader_info_collection_pass.cpp | 22 ++- .../ir/passes/shared_memory_simplify_pass.cpp | 127 ++++++++++++++++++ .../passes/shared_memory_to_storage_pass.cpp | 96 ++++++++----- src/shader_recompiler/recompiler.cpp | 1 + 9 files changed, 232 insertions(+), 49 deletions(-) create mode 100644 src/shader_recompiler/ir/passes/shared_memory_simplify_pass.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c2739d22..0d89524cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -870,6 +870,7 @@ set(SHADER_RECOMPILER src/shader_recompiler/exception.h src/shader_recompiler/ir/passes/ring_access_elimination.cpp src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp src/shader_recompiler/ir/passes/shared_memory_barrier_pass.cpp + src/shader_recompiler/ir/passes/shared_memory_simplify_pass.cpp src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp src/shader_recompiler/ir/passes/ssa_rewrite_pass.cpp src/shader_recompiler/ir/abstract_syntax_list.cpp diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index 37d7eea35..93fb81df4 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -303,7 +303,8 @@ void SetupCapabilities(const Info& info, const Profile& profile, EmitContext& ct ctx.AddCapability(spv::Capability::PhysicalStorageBufferAddresses); ctx.AddExtension("SPV_KHR_physical_storage_buffer"); } - if (info.uses_shared && profile.supports_workgroup_explicit_memory_layout) { + const auto shared_type_count = std::popcount(static_cast(info.shared_types)); + if (shared_type_count > 1 && profile.supports_workgroup_explicit_memory_layout) { ctx.AddExtension("SPV_KHR_workgroup_memory_explicit_layout"); ctx.AddCapability(spv::Capability::WorkgroupMemoryExplicitLayoutKHR); ctx.AddCapability(spv::Capability::WorkgroupMemoryExplicitLayout16BitAccessKHR); diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index c47a75739..0a8f78f72 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -979,32 +979,46 @@ void EmitContext::DefineImagesAndSamplers() { } void EmitContext::DefineSharedMemory() { - if (!info.uses_shared) { + const auto num_types = std::popcount(static_cast(info.shared_types)); + if (num_types == 0) { return; } ASSERT(info.stage == Stage::Compute); const u32 shared_memory_size = runtime_info.cs_info.shared_memory_size; - const auto make_type = [&](Id element_type, u32 element_size) { + const auto make_type = [&](IR::Type type, Id element_type, u32 element_size, + std::string_view name) { + if (False(info.shared_types & type)) { + // Skip unused shared memory types. + return std::make_tuple(Id{}, Id{}, Id{}); + } + const u32 num_elements{Common::DivCeil(shared_memory_size, element_size)}; const Id array_type{TypeArray(element_type, ConstU32(num_elements))}; Decorate(array_type, spv::Decoration::ArrayStride, element_size); const Id struct_type{TypeStruct(array_type)}; MemberDecorate(struct_type, 0u, spv::Decoration::Offset, 0u); - Decorate(struct_type, spv::Decoration::Block); const Id pointer = TypePointer(spv::StorageClass::Workgroup, struct_type); const Id element_pointer = TypePointer(spv::StorageClass::Workgroup, element_type); const Id variable = AddGlobalVariable(pointer, spv::StorageClass::Workgroup); - Decorate(variable, spv::Decoration::Aliased); + Name(variable, name); interfaces.push_back(variable); + if (num_types > 1) { + Decorate(struct_type, spv::Decoration::Block); + Decorate(variable, spv::Decoration::Aliased); + } + return std::make_tuple(variable, element_pointer, pointer); }; - std::tie(shared_memory_u16, shared_u16, shared_memory_u16_type) = make_type(U16, 2u); - std::tie(shared_memory_u32, shared_u32, shared_memory_u32_type) = make_type(U32[1], 4u); - std::tie(shared_memory_u64, shared_u64, shared_memory_u64_type) = make_type(U64, 8u); + std::tie(shared_memory_u16, shared_u16, shared_memory_u16_type) = + make_type(IR::Type::U16, U16, 2u, "shared_mem_u16"); + std::tie(shared_memory_u32, shared_u32, shared_memory_u32_type) = + make_type(IR::Type::U32, U32[1], 4u, "shared_mem_u32"); + std::tie(shared_memory_u64, shared_u64, shared_memory_u64_type) = + make_type(IR::Type::U64, U64, 8u, "shared_mem_u64"); } Id EmitContext::DefineFloat32ToUfloatM5(u32 mantissa_bits, const std::string_view name) { diff --git a/src/shader_recompiler/info.h b/src/shader_recompiler/info.h index e14c7988d..f25111350 100644 --- a/src/shader_recompiler/info.h +++ b/src/shader_recompiler/info.h @@ -214,7 +214,7 @@ struct Info { bool uses_lane_id{}; bool uses_group_quad{}; bool uses_group_ballot{}; - bool uses_shared{}; + IR::Type shared_types{}; bool uses_fp16{}; bool uses_fp64{}; bool uses_pack_10_11_11{}; diff --git a/src/shader_recompiler/ir/passes/ir_passes.h b/src/shader_recompiler/ir/passes/ir_passes.h index 06e4ac850..57d36f6df 100644 --- a/src/shader_recompiler/ir/passes/ir_passes.h +++ b/src/shader_recompiler/ir/passes/ir_passes.h @@ -28,6 +28,7 @@ void HullShaderTransform(IR::Program& program, RuntimeInfo& runtime_info); void DomainShaderTransform(IR::Program& program, RuntimeInfo& runtime_info); void SharedMemoryBarrierPass(IR::Program& program, const RuntimeInfo& runtime_info, const Profile& profile); +void SharedMemorySimplifyPass(IR::Program& program, const Profile& profile); void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_info, const Profile& profile); diff --git a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp index ba8d1cca6..4cd16d18f 100644 --- a/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp +++ b/src/shader_recompiler/ir/passes/shader_info_collection_pass.cpp @@ -35,12 +35,28 @@ void Visit(Info& info, const IR::Inst& inst) { break; } case IR::Opcode::LoadSharedU16: - case IR::Opcode::LoadSharedU32: - case IR::Opcode::LoadSharedU64: case IR::Opcode::WriteSharedU16: + info.shared_types |= IR::Type::U16; + break; + case IR::Opcode::LoadSharedU32: case IR::Opcode::WriteSharedU32: + case IR::Opcode::SharedAtomicIAdd32: + case IR::Opcode::SharedAtomicISub32: + case IR::Opcode::SharedAtomicSMin32: + case IR::Opcode::SharedAtomicUMin32: + case IR::Opcode::SharedAtomicSMax32: + case IR::Opcode::SharedAtomicUMax32: + case IR::Opcode::SharedAtomicInc32: + case IR::Opcode::SharedAtomicDec32: + case IR::Opcode::SharedAtomicAnd32: + case IR::Opcode::SharedAtomicOr32: + case IR::Opcode::SharedAtomicXor32: + info.shared_types |= IR::Type::U32; + break; + case IR::Opcode::LoadSharedU64: case IR::Opcode::WriteSharedU64: - info.uses_shared = true; + case IR::Opcode::SharedAtomicIAdd64: + info.shared_types |= IR::Type::U64; break; case IR::Opcode::ConvertF16F32: case IR::Opcode::ConvertF32F16: diff --git a/src/shader_recompiler/ir/passes/shared_memory_simplify_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_simplify_pass.cpp new file mode 100644 index 000000000..0f80a3b28 --- /dev/null +++ b/src/shader_recompiler/ir/passes/shared_memory_simplify_pass.cpp @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "shader_recompiler/ir/ir_emitter.h" +#include "shader_recompiler/ir/program.h" +#include "shader_recompiler/profile.h" + +namespace Shader::Optimization { + +static bool Requires16BitSharedAtomic(const IR::Inst& inst) { + // Nothing yet + return false; +} + +static bool Requires64BitSharedAtomic(const IR::Inst& inst) { + switch (inst.GetOpcode()) { + case IR::Opcode::SharedAtomicIAdd64: + return true; + default: + return false; + } +} + +static bool IsNon32BitSharedLoadStore(const IR::Inst& inst) { + switch (inst.GetOpcode()) { + case IR::Opcode::LoadSharedU16: + case IR::Opcode::LoadSharedU64: + case IR::Opcode::WriteSharedU16: + case IR::Opcode::WriteSharedU64: + return true; + default: + return false; + } +} + +IR::Type CalculateSpecialSharedAtomicTypes(IR::Program& program) { + IR::Type extra_atomic_types{IR::Type::Void}; + for (IR::Block* const block : program.blocks) { + for (IR::Inst& inst : block->Instructions()) { + if (Requires16BitSharedAtomic(inst)) { + extra_atomic_types |= IR::Type::U16; + } + if (Requires64BitSharedAtomic(inst)) { + extra_atomic_types |= IR::Type::U64; + } + } + } + return extra_atomic_types; +} + +// Simplifies down U16 and U64 shared memory operations to U32 when aliasing is not supported and +// atomics of the same type are not used. +void SharedMemorySimplifyPass(IR::Program& program, const Profile& profile) { + if (program.info.stage != Stage::Compute || profile.supports_workgroup_explicit_memory_layout) { + return; + } + + const auto atomic_types = CalculateSpecialSharedAtomicTypes(program); + if (True(atomic_types & IR::Type::U16) && True(atomic_types & IR::Type::U64)) { + // If both other atomic types are used, there is nothing to do. + return; + } + + // Iterate through shared load/store U16/U64 instructions, replacing with + // equivalent U32 ops when the types are not needed for atomics. + for (IR::Block* const block : program.blocks) { + for (IR::Inst& inst : block->Instructions()) { + if (!IsNon32BitSharedLoadStore(inst)) { + continue; + } + IR::IREmitter ir{*block, IR::Block::InstructionList::s_iterator_to(inst)}; + const IR::U32 offset{inst.Arg(0)}; + if (False(atomic_types & IR::Type::U16)) { + switch (inst.GetOpcode()) { + case IR::Opcode::LoadSharedU16: { + const IR::U32 dword_offset{ir.BitwiseAnd(offset, ir.Imm32(~3U))}; + const IR::U32 dword_value{ir.LoadShared(32, false, dword_offset)}; + const IR::U32 bit_offset{ + ir.IMul(ir.BitwiseAnd(offset, ir.Imm32(2U)), ir.Imm32(8U))}; + const IR::U32 value{ir.BitFieldExtract(dword_value, bit_offset, ir.Imm32(16U))}; + inst.ReplaceUsesWithAndRemove(ir.UConvert(16, value)); + continue; + } + case IR::Opcode::WriteSharedU16: { + const IR::U32 value{ir.UConvert(32, IR::U16{inst.Arg(1)})}; + const IR::U32 bit_offset{ + ir.IMul(ir.BitwiseAnd(offset, ir.Imm32(2U)), ir.Imm32(8U))}; + const IR::U32 dword_offset{ir.BitwiseAnd(offset, ir.Imm32(~3U))}; + const IR::U32 dword_value{ + ir.LoadShared(32, false, ir.BitwiseAnd(offset, dword_offset))}; + const IR::U32 new_dword_value{ + ir.BitFieldInsert(dword_value, value, bit_offset, ir.Imm32(16U))}; + ir.WriteShared(32, new_dword_value, dword_offset); + inst.Invalidate(); + continue; + } + default: + break; + } + } + if (False(atomic_types & IR::Type::U64)) { + switch (inst.GetOpcode()) { + case IR::Opcode::LoadSharedU64: { + const IR::U32 value0{ir.LoadShared(32, false, offset)}; + const IR::U32 value1{ir.LoadShared(32, false, ir.IAdd(offset, ir.Imm32(4U)))}; + const IR::Value value{ir.PackUint2x32(ir.CompositeConstruct(value0, value1))}; + inst.ReplaceUsesWithAndRemove(value); + continue; + } + case IR::Opcode::WriteSharedU64: { + const IR::Value value{ir.UnpackUint2x32(IR::U64{inst.Arg(1)})}; + const IR::U32 value0{ir.CompositeExtract(value, 0)}; + const IR::U32 value1{ir.CompositeExtract(value, 1)}; + ir.WriteShared(32, value0, offset); + ir.WriteShared(32, value1, ir.IAdd(offset, ir.Imm32(4U))); + inst.Invalidate(); + continue; + } + default: + break; + } + } + } + } +} + +} // namespace Shader::Optimization diff --git a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp index 839a8ddc5..a6900e180 100644 --- a/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp +++ b/src/shader_recompiler/ir/passes/shared_memory_to_storage_pass.cpp @@ -34,20 +34,74 @@ static bool IsSharedAccess(const IR::Inst& inst) { } } +IR::Type CalculateSharedMemoryTypes(IR::Program& program) { + IR::Type used_types{IR::Type::Void}; + for (IR::Block* const block : program.blocks) { + for (IR::Inst& inst : block->Instructions()) { + if (!IsSharedAccess(inst)) { + continue; + } + switch (inst.GetOpcode()) { + case IR::Opcode::LoadSharedU16: + case IR::Opcode::WriteSharedU16: + used_types |= IR::Type::U16; + break; + case IR::Opcode::LoadSharedU32: + case IR::Opcode::WriteSharedU32: + case IR::Opcode::SharedAtomicIAdd32: + case IR::Opcode::SharedAtomicISub32: + case IR::Opcode::SharedAtomicSMin32: + case IR::Opcode::SharedAtomicUMin32: + case IR::Opcode::SharedAtomicSMax32: + case IR::Opcode::SharedAtomicUMax32: + case IR::Opcode::SharedAtomicInc32: + case IR::Opcode::SharedAtomicDec32: + case IR::Opcode::SharedAtomicAnd32: + case IR::Opcode::SharedAtomicOr32: + case IR::Opcode::SharedAtomicXor32: + used_types |= IR::Type::U32; + break; + case IR::Opcode::LoadSharedU64: + case IR::Opcode::WriteSharedU64: + case IR::Opcode::SharedAtomicIAdd64: + used_types |= IR::Type::U64; + break; + default: + break; + } + } + } + return used_types; +} + void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_info, const Profile& profile) { if (program.info.stage != Stage::Compute) { return; } - // Only perform the transform if there is shared memory and either host shared memory is - // insufficient or the device does not support VK_KHR_workgroup_memory_explicit_layout + + // Run this pass if: + // * There are shared memory instructions. + // * One of the following is true: + // * Requested shared memory size is too large for the host shared memory. + // * Workgroup explicit memory is not supported and multiple shared memory types are used. const u32 shared_memory_size = runtime_info.cs_info.shared_memory_size; - if (shared_memory_size == 0 || (shared_memory_size <= profile.max_shared_memory_size && - profile.supports_workgroup_explicit_memory_layout)) { + const auto used_types = CalculateSharedMemoryTypes(program); + if (used_types == IR::Type::Void || (shared_memory_size <= profile.max_shared_memory_size && + (profile.supports_workgroup_explicit_memory_layout || + std::popcount(static_cast(used_types)) == 1))) { return; } + + // Add a buffer binding for shared memory storage buffer. const u32 binding = static_cast(program.info.buffers.size()); - IR::Type used_types{}; + program.info.buffers.push_back({ + .used_types = used_types, + .inline_cbuf = AmdGpu::Buffer::Null(), + .buffer_type = BufferType::SharedMemory, + .is_written = true, + }); + for (IR::Block* const block : program.blocks) { for (IR::Inst& inst : block->Instructions()) { if (!IsSharedAccess(inst)) { @@ -58,29 +112,21 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ const IR::U32 offset = ir.IMul(ir.GetAttributeU32(IR::Attribute::WorkgroupIndex), ir.Imm32(shared_memory_size)); const IR::U32 address = ir.IAdd(IR::U32{inst.Arg(0)}, offset); - // Replace shared atomics first switch (inst.GetOpcode()) { case IR::Opcode::SharedAtomicIAdd32: - inst.ReplaceUsesWithAndRemove( - ir.BufferAtomicIAdd(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U32; - continue; case IR::Opcode::SharedAtomicIAdd64: inst.ReplaceUsesWithAndRemove( ir.BufferAtomicIAdd(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U64; continue; case IR::Opcode::SharedAtomicISub32: inst.ReplaceUsesWithAndRemove( ir.BufferAtomicISub(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U32; continue; case IR::Opcode::SharedAtomicSMin32: case IR::Opcode::SharedAtomicUMin32: { const bool is_signed = inst.GetOpcode() == IR::Opcode::SharedAtomicSMin32; inst.ReplaceUsesWithAndRemove( ir.BufferAtomicIMin(handle, address, inst.Arg(1), is_signed, {})); - used_types |= IR::Type::U32; continue; } case IR::Opcode::SharedAtomicSMax32: @@ -88,73 +134,49 @@ void SharedMemoryToStoragePass(IR::Program& program, const RuntimeInfo& runtime_ const bool is_signed = inst.GetOpcode() == IR::Opcode::SharedAtomicSMax32; inst.ReplaceUsesWithAndRemove( ir.BufferAtomicIMax(handle, address, inst.Arg(1), is_signed, {})); - used_types |= IR::Type::U32; continue; } case IR::Opcode::SharedAtomicInc32: inst.ReplaceUsesWithAndRemove(ir.BufferAtomicInc(handle, address, {})); - used_types |= IR::Type::U32; continue; case IR::Opcode::SharedAtomicDec32: inst.ReplaceUsesWithAndRemove(ir.BufferAtomicDec(handle, address, {})); - used_types |= IR::Type::U32; continue; case IR::Opcode::SharedAtomicAnd32: inst.ReplaceUsesWithAndRemove(ir.BufferAtomicAnd(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U32; continue; case IR::Opcode::SharedAtomicOr32: inst.ReplaceUsesWithAndRemove(ir.BufferAtomicOr(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U32; continue; case IR::Opcode::SharedAtomicXor32: inst.ReplaceUsesWithAndRemove(ir.BufferAtomicXor(handle, address, inst.Arg(1), {})); - used_types |= IR::Type::U32; continue; - default: - break; - } - // Replace shared operations. - switch (inst.GetOpcode()) { case IR::Opcode::LoadSharedU16: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU16(handle, address, {})); - used_types |= IR::Type::U16; break; case IR::Opcode::LoadSharedU32: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU32(1, handle, address, {})); - used_types |= IR::Type::U32; break; case IR::Opcode::LoadSharedU64: inst.ReplaceUsesWithAndRemove(ir.LoadBufferU64(handle, address, {})); - used_types |= IR::Type::U64; break; case IR::Opcode::WriteSharedU16: ir.StoreBufferU16(handle, address, IR::U16{inst.Arg(1)}, {}); inst.Invalidate(); - used_types |= IR::Type::U16; break; case IR::Opcode::WriteSharedU32: ir.StoreBufferU32(1, handle, address, inst.Arg(1), {}); inst.Invalidate(); - used_types |= IR::Type::U32; break; case IR::Opcode::WriteSharedU64: ir.StoreBufferU64(handle, address, IR::U64{inst.Arg(1)}, {}); inst.Invalidate(); - used_types |= IR::Type::U64; break; default: break; } } } - // Add buffer binding for shared memory storage buffer. - program.info.buffers.push_back({ - .used_types = used_types, - .inline_cbuf = AmdGpu::Buffer::Null(), - .buffer_type = BufferType::SharedMemory, - .is_written = true, - }); } } // namespace Shader::Optimization diff --git a/src/shader_recompiler/recompiler.cpp b/src/shader_recompiler/recompiler.cpp index 9f92857d6..e17fb1c9e 100644 --- a/src/shader_recompiler/recompiler.cpp +++ b/src/shader_recompiler/recompiler.cpp @@ -78,6 +78,7 @@ IR::Program TranslateProgram(std::span code, Pools& pools, Info& info Shader::Optimization::FlattenExtendedUserdataPass(program); Shader::Optimization::ResourceTrackingPass(program); Shader::Optimization::LowerBufferFormatToRaw(program); + Shader::Optimization::SharedMemorySimplifyPass(program, profile); Shader::Optimization::SharedMemoryToStoragePass(program, runtime_info, profile); Shader::Optimization::SharedMemoryBarrierPass(program, runtime_info, profile); Shader::Optimization::IdentityRemovalPass(program.blocks); From 34d0d85c1532079e44bb6610dd3fba19fbee441b Mon Sep 17 00:00:00 2001 From: TheTurtle Date: Thu, 12 Jun 2025 13:05:25 +0300 Subject: [PATCH 134/141] buffer_cache: Bump device local staging buffer size (#3088) --- src/video_core/buffer_cache/buffer_cache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video_core/buffer_cache/buffer_cache.cpp b/src/video_core/buffer_cache/buffer_cache.cpp index ffa744b31..23f9dc0bc 100644 --- a/src/video_core/buffer_cache/buffer_cache.cpp +++ b/src/video_core/buffer_cache/buffer_cache.cpp @@ -23,7 +23,7 @@ static constexpr size_t DataShareBufferSize = 64_KB; static constexpr size_t StagingBufferSize = 512_MB; static constexpr size_t UboStreamBufferSize = 128_MB; static constexpr size_t DownloadBufferSize = 128_MB; -static constexpr size_t DeviceBufferSize = 16_MB; +static constexpr size_t DeviceBufferSize = 128_MB; static constexpr size_t MaxPageFaults = 1024; BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_, From a1d6cd15f4ef33e77b225dd80dc96e21d24295b4 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 12 Jun 2025 13:27:52 +0300 Subject: [PATCH 135/141] qt: save gui settings to separate file (#2984) * initial save classes for gui save file * fixup * some more settings passed to the new saving file * even more variables parsing * more settings * fixup * more settings * more settings * clang fix * fixed wrong setting * more setting * more setting * added ca_ES * rename to general_settings * added set-addon-folder in main * fixup * fixup2 * added sr_CS * Update CMakeLists.txt --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com> --- CMakeLists.txt | 4 + src/common/config.cpp | 223 +-------------------------------- src/common/config.h | 35 ------ src/main.cpp | 20 ++- src/qt_gui/check_update.cpp | 26 ++-- src/qt_gui/check_update.h | 5 +- src/qt_gui/game_grid_frame.cpp | 22 ++-- src/qt_gui/game_grid_frame.h | 5 +- src/qt_gui/game_list_frame.cpp | 17 +-- src/qt_gui/game_list_frame.h | 5 +- src/qt_gui/gui_settings.cpp | 9 ++ src/qt_gui/gui_settings.h | 46 +++++++ src/qt_gui/main_window.cpp | 113 +++++++++-------- src/qt_gui/main_window.h | 4 +- src/qt_gui/main_window_ui.h | 1 - src/qt_gui/settings.cpp | 77 ++++++++++++ src/qt_gui/settings.h | 55 ++++++++ src/qt_gui/settings_dialog.cpp | 80 ++++++++---- src/qt_gui/settings_dialog.h | 6 +- 19 files changed, 383 insertions(+), 370 deletions(-) create mode 100644 src/qt_gui/gui_settings.cpp create mode 100644 src/qt_gui/gui_settings.h create mode 100644 src/qt_gui/settings.cpp create mode 100644 src/qt_gui/settings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d89524cc..12ff0b53a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1055,6 +1055,10 @@ set(QT_GUI src/qt_gui/about_dialog.cpp src/qt_gui/settings_dialog.h src/qt_gui/settings_dialog.ui src/qt_gui/main.cpp + src/qt_gui/gui_settings.cpp + src/qt_gui/gui_settings.h + src/qt_gui/settings.cpp + src/qt_gui/settings.h ${EMULATOR} ${RESOURCE_FILES} ${TRANSLATIONS} diff --git a/src/common/config.cpp b/src/common/config.cpp index 6565ab82a..d8f46a17d 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -33,9 +33,7 @@ namespace Config { static bool isNeo = false; static bool isDevKit = false; -static bool playBGM = false; static bool isTrophyPopupDisabled = false; -static int BGMvolume = 50; static bool enableDiscordRPC = false; static u32 screenWidth = 1280; static u32 screenHeight = 720; @@ -43,7 +41,6 @@ static s32 gpuId = -1; // Vulkan physical device index. Set to negative for auto static std::string logFilter; static std::string logType = "sync"; static std::string userName = "shadPS4"; -static std::string updateChannel; static std::string chooseHomeTab; static std::string backButtonBehavior = "left"; static bool useSpecialPad = false; @@ -52,8 +49,6 @@ static bool isMotionControlsEnabled = true; static bool isDebugDump = false; static bool isShaderDebug = false; static bool isShowSplash = false; -static bool isAutoUpdate = false; -static bool isAlwaysShowChangelog = false; static std::string isSideTrophy = "right"; static bool isNullGpu = false; static bool shouldCopyGPUBuffers = false; @@ -86,27 +81,13 @@ static std::vector settings_install_dirs = {}; std::vector install_dirs_enabled = {}; std::filesystem::path settings_addon_install_dir = {}; std::filesystem::path save_data_path = {}; -u32 main_window_geometry_x = 400; -u32 main_window_geometry_y = 400; -u32 main_window_geometry_w = 1280; -u32 main_window_geometry_h = 720; u32 mw_themes = 0; -u32 m_icon_size = 36; -u32 m_icon_size_grid = 69; -u32 m_slider_pos = 0; -u32 m_slider_pos_grid = 0; -u32 m_table_mode = 0; -u32 m_window_size_W = 1280; -u32 m_window_size_H = 720; std::vector m_elf_viewer; std::vector m_recent_files; std::string emulator_language = "en_US"; -static int backgroundImageOpacity = 50; -static bool showBackgroundImage = true; static bool isFullscreen = false; static std::string fullscreenMode = "Windowed"; static bool isHDRAllowed = false; -static bool showLabelsUnderIcons = true; // Language u32 m_language = 1; // english @@ -176,14 +157,6 @@ bool getIsFullscreen() { return isFullscreen; } -bool getShowLabelsUnderIcons() { - return showLabelsUnderIcons; -} - -bool setShowLabelsUnderIcons() { - return false; -} - std::string getFullscreenMode() { return fullscreenMode; } @@ -192,14 +165,6 @@ bool getisTrophyPopupDisabled() { return isTrophyPopupDisabled; } -bool getPlayBGM() { - return playBGM; -} - -int getBGMvolume() { - return BGMvolume; -} - bool getEnableDiscordRPC() { return enableDiscordRPC; } @@ -240,10 +205,6 @@ std::string getUserName() { return userName; } -std::string getUpdateChannel() { - return updateChannel; -} - std::string getChooseHomeTab() { return chooseHomeTab; } @@ -276,14 +237,6 @@ bool showSplash() { return isShowSplash; } -bool autoUpdate() { - return isAutoUpdate; -} - -bool alwaysShowChangelog() { - return isAlwaysShowChangelog; -} - std::string sideTrophy() { return isSideTrophy; } @@ -384,14 +337,6 @@ void setShowSplash(bool enable) { isShowSplash = enable; } -void setAutoUpdate(bool enable) { - isAutoUpdate = enable; -} - -void setAlwaysShowChangelog(bool enable) { - isAlwaysShowChangelog = enable; -} - void setSideTrophy(std::string side) { isSideTrophy = side; } @@ -431,9 +376,6 @@ void setVblankDiv(u32 value) { void setIsFullscreen(bool enable) { isFullscreen = enable; } -static void setShowLabelsUnderIcons(bool enable) { - showLabelsUnderIcons = enable; -} void setFullscreenMode(std::string mode) { fullscreenMode = mode; @@ -443,14 +385,6 @@ void setisTrophyPopupDisabled(bool disable) { isTrophyPopupDisabled = disable; } -void setPlayBGM(bool enable) { - playBGM = enable; -} - -void setBGMvolume(int volume) { - BGMvolume = volume; -} - void setEnableDiscordRPC(bool enable) { enableDiscordRPC = enable; } @@ -490,9 +424,6 @@ void setUserName(const std::string& type) { userName = type; } -void setUpdateChannel(const std::string& type) { - updateChannel = type; -} void setChooseHomeTab(const std::string& type) { chooseHomeTab = type; } @@ -521,13 +452,6 @@ void setCheckCompatibilityOnStartup(bool use) { checkCompatibilityOnStartup = use; } -void setMainWindowGeometry(u32 x, u32 y, u32 w, u32 h) { - main_window_geometry_x = x; - main_window_geometry_y = y; - main_window_geometry_w = w; - main_window_geometry_h = h; -} - bool addGameInstallDir(const std::filesystem::path& dir, bool enabled) { for (const auto& install_dir : settings_install_dirs) { if (install_dir.path == dir) { @@ -564,34 +488,6 @@ void setMainWindowTheme(u32 theme) { mw_themes = theme; } -void setIconSize(u32 size) { - m_icon_size = size; -} - -void setIconSizeGrid(u32 size) { - m_icon_size_grid = size; -} - -void setSliderPosition(u32 pos) { - m_slider_pos = pos; -} - -void setSliderPositionGrid(u32 pos) { - m_slider_pos_grid = pos; -} - -void setTableMode(u32 mode) { - m_table_mode = mode; -} - -void setMainWindowWidth(u32 width) { - m_window_size_W = width; -} - -void setMainWindowHeight(u32 height) { - m_window_size_H = height; -} - void setElfViewer(const std::vector& elfList) { m_elf_viewer.resize(elfList.size()); m_elf_viewer = elfList; @@ -621,22 +517,6 @@ void setSaveDataPath(const std::filesystem::path& path) { save_data_path = path; } -u32 getMainWindowGeometryX() { - return main_window_geometry_x; -} - -u32 getMainWindowGeometryY() { - return main_window_geometry_y; -} - -u32 getMainWindowGeometryW() { - return main_window_geometry_w; -} - -u32 getMainWindowGeometryH() { - return main_window_geometry_h; -} - const std::vector getGameInstallDirs() { std::vector enabled_dirs; for (const auto& dir : settings_install_dirs) { @@ -667,34 +547,6 @@ u32 getMainWindowTheme() { return mw_themes; } -u32 getIconSize() { - return m_icon_size; -} - -u32 getIconSizeGrid() { - return m_icon_size_grid; -} - -u32 getSliderPosition() { - return m_slider_pos; -} - -u32 getSliderPositionGrid() { - return m_slider_pos_grid; -} - -u32 getTableMode() { - return m_table_mode; -} - -u32 getMainWindowWidth() { - return m_window_size_W; -} - -u32 getMainWindowHeight() { - return m_window_size_H; -} - std::vector getElfViewer() { return m_elf_viewer; } @@ -715,22 +567,6 @@ bool getSeparateLogFilesEnabled() { return isSeparateLogFilesEnabled; } -int getBackgroundImageOpacity() { - return backgroundImageOpacity; -} - -void setBackgroundImageOpacity(int opacity) { - backgroundImageOpacity = std::clamp(opacity, 0, 100); -} - -bool getShowBackgroundImage() { - return showBackgroundImage; -} - -void setShowBackgroundImage(bool show) { - showBackgroundImage = show; -} - bool getPSNSignedIn() { return isPSNSignedIn; } @@ -764,23 +600,14 @@ void load(const std::filesystem::path& path) { isNeo = toml::find_or(general, "isPS4Pro", false); isDevKit = toml::find_or(general, "isDevKit", false); isPSNSignedIn = toml::find_or(general, "isPSNSignedIn", false); - playBGM = toml::find_or(general, "playBGM", false); isTrophyPopupDisabled = toml::find_or(general, "isTrophyPopupDisabled", false); trophyNotificationDuration = toml::find_or(general, "trophyNotificationDuration", 5.0); - BGMvolume = toml::find_or(general, "BGMvolume", 50); enableDiscordRPC = toml::find_or(general, "enableDiscordRPC", true); logFilter = toml::find_or(general, "logFilter", ""); logType = toml::find_or(general, "logType", "sync"); userName = toml::find_or(general, "userName", "shadPS4"); - if (Common::g_is_release) { - updateChannel = toml::find_or(general, "updateChannel", "Release"); - } else { - updateChannel = toml::find_or(general, "updateChannel", "Nightly"); - } isShowSplash = toml::find_or(general, "showSplash", true); - isAutoUpdate = toml::find_or(general, "autoUpdate", false); - isAlwaysShowChangelog = toml::find_or(general, "alwaysShowChangelog", false); isSideTrophy = toml::find_or(general, "sideTrophy", "right"); compatibilityData = toml::find_or(general, "compatibilityEnabled", false); checkCompatibilityOnStartup = @@ -841,13 +668,7 @@ void load(const std::filesystem::path& path) { const toml::value& gui = data.at("GUI"); load_game_size = toml::find_or(gui, "loadGameSizeEnabled", true); - m_icon_size = toml::find_or(gui, "iconSize", 0); - m_icon_size_grid = toml::find_or(gui, "iconSizeGrid", 0); - m_slider_pos = toml::find_or(gui, "sliderPos", 0); - m_slider_pos_grid = toml::find_or(gui, "sliderPosGrid", 0); mw_themes = toml::find_or(gui, "theme", 0); - m_window_size_W = toml::find_or(gui, "mw_width", 0); - m_window_size_H = toml::find_or(gui, "mw_height", 0); const auto install_dir_array = toml::find_or>(gui, "installDirs", {}); @@ -872,16 +693,9 @@ void load(const std::filesystem::path& path) { save_data_path = toml::find_fs_path_or(gui, "saveDataPath", {}); settings_addon_install_dir = toml::find_fs_path_or(gui, "addonInstallDir", {}); - main_window_geometry_x = toml::find_or(gui, "geometry_x", 0); - main_window_geometry_y = toml::find_or(gui, "geometry_y", 0); - main_window_geometry_w = toml::find_or(gui, "geometry_w", 0); - main_window_geometry_h = toml::find_or(gui, "geometry_h", 0); m_elf_viewer = toml::find_or>(gui, "elfDirs", {}); m_recent_files = toml::find_or>(gui, "recentFiles", {}); - m_table_mode = toml::find_or(gui, "gameTableMode", 0); emulator_language = toml::find_or(gui, "emulatorLanguage", "en_US"); - backgroundImageOpacity = toml::find_or(gui, "backgroundImageOpacity", 50); - showBackgroundImage = toml::find_or(gui, "showBackgroundImage", true); } if (data.contains("Settings")) { @@ -897,9 +711,10 @@ void load(const std::filesystem::path& path) { // Check if the loaded language is in the allowed list const std::vector allowed_languages = { - "ar_SA", "da_DK", "de_DE", "el_GR", "en_US", "es_ES", "fa_IR", "fi_FI", "fr_FR", "hu_HU", - "id_ID", "it_IT", "ja_JP", "ko_KR", "lt_LT", "nb_NO", "nl_NL", "pl_PL", "pt_BR", "pt_PT", - "ro_RO", "ru_RU", "sq_AL", "sv_SE", "tr_TR", "uk_UA", "vi_VN", "zh_CN", "zh_TW"}; + "ar_SA", "da_DK", "de_DE", "el_GR", "en_US", "es_ES", "fa_IR", "fi_FI", + "fr_FR", "hu_HU", "id_ID", "it_IT", "ja_JP", "ko_KR", "lt_LT", "nb_NO", + "nl_NL", "pl_PL", "pt_BR", "pt_PT", "ro_RO", "ru_RU", "sq_AL", "sv_SE", + "tr_TR", "uk_UA", "vi_VN", "zh_CN", "zh_TW", "ca_ES", "sr_CS"}; if (std::find(allowed_languages.begin(), allowed_languages.end(), emulator_language) == allowed_languages.end()) { @@ -966,17 +781,12 @@ void save(const std::filesystem::path& path) { data["General"]["isPSNSignedIn"] = isPSNSignedIn; data["General"]["isTrophyPopupDisabled"] = isTrophyPopupDisabled; data["General"]["trophyNotificationDuration"] = trophyNotificationDuration; - data["General"]["playBGM"] = playBGM; - data["General"]["BGMvolume"] = BGMvolume; data["General"]["enableDiscordRPC"] = enableDiscordRPC; data["General"]["logFilter"] = logFilter; data["General"]["logType"] = logType; data["General"]["userName"] = userName; - data["General"]["updateChannel"] = updateChannel; data["General"]["chooseHomeTab"] = chooseHomeTab; data["General"]["showSplash"] = isShowSplash; - data["General"]["autoUpdate"] = isAutoUpdate; - data["General"]["alwaysShowChangelog"] = isAlwaysShowChangelog; data["General"]["sideTrophy"] = isSideTrophy; data["General"]["compatibilityEnabled"] = compatibilityData; data["General"]["checkCompatibilityOnStartup"] = checkCompatibilityOnStartup; @@ -1046,8 +856,6 @@ void save(const std::filesystem::path& path) { data["GUI"]["addonInstallDir"] = std::string{fmt::UTF(settings_addon_install_dir.u8string()).data}; data["GUI"]["emulatorLanguage"] = emulator_language; - data["GUI"]["backgroundImageOpacity"] = backgroundImageOpacity; - data["GUI"]["showBackgroundImage"] = showBackgroundImage; data["Settings"]["consoleLanguage"] = m_language; // Sorting of TOML sections @@ -1082,18 +890,7 @@ void saveMainWindow(const std::filesystem::path& path) { fmt::print("Saving new configuration file {}\n", fmt::UTF(path.u8string())); } - data["GUI"]["mw_width"] = m_window_size_W; - data["GUI"]["mw_height"] = m_window_size_H; data["GUI"]["theme"] = mw_themes; - data["GUI"]["iconSize"] = m_icon_size; - data["GUI"]["sliderPos"] = m_slider_pos; - data["GUI"]["iconSizeGrid"] = m_icon_size_grid; - data["GUI"]["sliderPosGrid"] = m_slider_pos_grid; - data["GUI"]["gameTableMode"] = m_table_mode; - data["GUI"]["geometry_x"] = main_window_geometry_x; - data["GUI"]["geometry_y"] = main_window_geometry_y; - data["GUI"]["geometry_w"] = main_window_geometry_w; - data["GUI"]["geometry_h"] = main_window_geometry_h; data["GUI"]["elfDirs"] = m_elf_viewer; data["GUI"]["recentFiles"] = m_recent_files; @@ -1112,19 +909,13 @@ void setDefaultValues() { isPSNSignedIn = false; isFullscreen = false; isTrophyPopupDisabled = false; - playBGM = false; - BGMvolume = 50; enableDiscordRPC = true; screenWidth = 1280; screenHeight = 720; logFilter = ""; logType = "sync"; userName = "shadPS4"; - if (Common::g_is_release) { - updateChannel = "Release"; - } else { - updateChannel = "Nightly"; - } + chooseHomeTab = "General"; cursorState = HideCursorState::Idle; cursorHideTimeout = 5; @@ -1135,8 +926,6 @@ void setDefaultValues() { isDebugDump = false; isShaderDebug = false; isShowSplash = false; - isAutoUpdate = false; - isAlwaysShowChangelog = false; isSideTrophy = "right"; isNullGpu = false; shouldDumpShaders = false; @@ -1153,8 +942,6 @@ void setDefaultValues() { gpuId = -1; compatibilityData = false; checkCompatibilityOnStartup = false; - backgroundImageOpacity = 50; - showBackgroundImage = true; } constexpr std::string_view GetDefaultKeyboardConfig() { diff --git a/src/common/config.h b/src/common/config.h index 404854ae2..414bc122e 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -26,25 +26,18 @@ bool GetLoadGameSizeEnabled(); std::filesystem::path GetSaveDataPath(); void setLoadGameSizeEnabled(bool enable); bool getIsFullscreen(); -bool getShowLabelsUnderIcons(); -bool setShowLabelsUnderIcons(); std::string getFullscreenMode(); bool isNeoModeConsole(); bool isDevKitConsole(); -bool getPlayBGM(); -int getBGMvolume(); bool getisTrophyPopupDisabled(); bool getEnableDiscordRPC(); bool getCompatibilityEnabled(); bool getCheckCompatibilityOnStartup(); -int getBackgroundImageOpacity(); -bool getShowBackgroundImage(); bool getPSNSignedIn(); std::string getLogFilter(); std::string getLogType(); std::string getUserName(); -std::string getUpdateChannel(); std::string getChooseHomeTab(); s16 getCursorState(); @@ -69,8 +62,6 @@ bool allowHDR(); bool debugDump(); bool collectShadersForDebug(); bool showSplash(); -bool autoUpdate(); -bool alwaysShowChangelog(); std::string sideTrophy(); bool nullGpu(); bool copyGPUCmdBuffers(); @@ -83,8 +74,6 @@ u32 vblankDiv(); void setDebugDump(bool enable); void setCollectShaderForDebug(bool enable); void setShowSplash(bool enable); -void setAutoUpdate(bool enable); -void setAlwaysShowChangelog(bool enable); void setSideTrophy(std::string side); void setNullGpu(bool enable); void setAllowHDR(bool enable); @@ -97,21 +86,16 @@ void setScreenHeight(u32 height); void setIsFullscreen(bool enable); void setFullscreenMode(std::string mode); void setisTrophyPopupDisabled(bool disable); -void setPlayBGM(bool enable); -void setBGMvolume(int volume); void setEnableDiscordRPC(bool enable); void setLanguage(u32 language); void setNeoMode(bool enable); void setUserName(const std::string& type); -void setUpdateChannel(const std::string& type); void setChooseHomeTab(const std::string& type); void setGameInstallDirs(const std::vector& dirs_config); void setAllGameInstallDirs(const std::vector& dirs_config); void setSaveDataPath(const std::filesystem::path& path); void setCompatibilityEnabled(bool use); void setCheckCompatibilityOnStartup(bool use); -void setBackgroundImageOpacity(int opacity); -void setShowBackgroundImage(bool show); void setPSNSignedIn(bool sign); void setCursorState(s16 cursorState); @@ -141,38 +125,19 @@ void setVkHostMarkersEnabled(bool enable); void setVkGuestMarkersEnabled(bool enable); // Gui -void setMainWindowGeometry(u32 x, u32 y, u32 w, u32 h); bool addGameInstallDir(const std::filesystem::path& dir, bool enabled = true); void removeGameInstallDir(const std::filesystem::path& dir); void setGameInstallDirEnabled(const std::filesystem::path& dir, bool enabled); void setAddonInstallDir(const std::filesystem::path& dir); void setMainWindowTheme(u32 theme); -void setIconSize(u32 size); -void setIconSizeGrid(u32 size); -void setSliderPosition(u32 pos); -void setSliderPositionGrid(u32 pos); -void setTableMode(u32 mode); -void setMainWindowWidth(u32 width); -void setMainWindowHeight(u32 height); void setElfViewer(const std::vector& elfList); void setRecentFiles(const std::vector& recentFiles); void setEmulatorLanguage(std::string language); -u32 getMainWindowGeometryX(); -u32 getMainWindowGeometryY(); -u32 getMainWindowGeometryW(); -u32 getMainWindowGeometryH(); const std::vector getGameInstallDirs(); const std::vector getGameInstallDirsEnabled(); std::filesystem::path getAddonInstallDir(); u32 getMainWindowTheme(); -u32 getIconSize(); -u32 getIconSizeGrid(); -u32 getSliderPosition(); -u32 getSliderPositionGrid(); -u32 getTableMode(); -u32 getMainWindowWidth(); -u32 getMainWindowHeight(); std::vector getElfViewer(); std::vector getRecentFiles(); std::string getEmulatorLanguage(); diff --git a/src/main.cpp b/src/main.cpp index 8a251c55a..fe245d104 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,6 +47,7 @@ int main(int argc, char* argv[]) { " -f, --fullscreen Specify window initial fullscreen " "state. Does not overwrite the config file.\n" " --add-game-folder Adds a new game folder to the config.\n" + " --set-addon-folder Sets the addon folder to the config.\n" " -h, --help Display this help message\n"; exit(0); }}, @@ -116,7 +117,24 @@ int main(int argc, char* argv[]) { std::cout << "Game folder successfully saved.\n"; exit(0); }}, - }; + {"--set-addon-folder", [&](int& i) { + if (++i >= argc) { + std::cerr << "Error: Missing argument for --add-addon-folder\n"; + exit(1); + } + std::string config_dir(argv[i]); + std::filesystem::path config_path = std::filesystem::path(config_dir); + std::error_code discard; + if (!std::filesystem::exists(config_path, discard)) { + std::cerr << "Error: File does not exist: " << config_path << "\n"; + exit(1); + } + + Config::setAddonInstallDir(config_path); + Config::save(Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "config.toml"); + std::cout << "Addon folder successfully saved.\n"; + exit(0); + }}}; if (argc == 1) { int dummy = 0; // one does not simply pass 0 directly diff --git a/src/qt_gui/check_update.cpp b/src/qt_gui/check_update.cpp index b0858840a..10c986411 100644 --- a/src/qt_gui/check_update.cpp +++ b/src/qt_gui/check_update.cpp @@ -28,8 +28,10 @@ using namespace Common::FS; -CheckUpdate::CheckUpdate(const bool showMessage, QWidget* parent) - : QDialog(parent), networkManager(new QNetworkAccessManager(this)) { +CheckUpdate::CheckUpdate(std::shared_ptr gui_settings, const bool showMessage, + QWidget* parent) + : QDialog(parent), m_gui_settings(std::move(gui_settings)), + networkManager(new QNetworkAccessManager(this)) { setWindowTitle(tr("Auto Updater")); setFixedSize(0, 0); CheckForUpdates(showMessage); @@ -43,7 +45,7 @@ void CheckUpdate::CheckForUpdates(const bool showMessage) { bool checkName = true; while (checkName) { - updateChannel = QString::fromStdString(Config::getUpdateChannel()); + updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString(); if (updateChannel == "Nightly") { url = QUrl("https://api.github.com/repos/shadps4-emu/shadPS4/releases"); checkName = false; @@ -52,12 +54,10 @@ void CheckUpdate::CheckForUpdates(const bool showMessage) { checkName = false; } else { if (Common::g_is_release) { - Config::setUpdateChannel("Release"); + m_gui_settings->SetValue(gui::gen_updateChannel, "Release"); } else { - Config::setUpdateChannel("Nightly"); + m_gui_settings->SetValue(gui::gen_updateChannel, "Nightly"); } - const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::save(config_dir / "config.toml"); } } @@ -198,7 +198,7 @@ void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate, titleLayout->addWidget(titleLabel); layout->addLayout(titleLayout); - QString updateChannel = QString::fromStdString(Config::getUpdateChannel()); + QString updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString(); QString updateText = QString("

" + tr("Update Channel") + ": " + updateChannel + "
" @@ -273,7 +273,7 @@ void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate, } }); - if (Config::alwaysShowChangelog()) { + if (m_gui_settings->GetValue(gui::gen_showChangeLog).toBool()) { requestChangelog(currentRev, latestRev, downloadUrl, latestDate, currentDate); textField->setVisible(true); toggleButton->setText(tr("Hide Changelog")); @@ -290,14 +290,14 @@ void CheckUpdate::setupUI(const QString& downloadUrl, const QString& latestDate, connect(noButton, &QPushButton::clicked, this, [this]() { close(); }); - autoUpdateCheckBox->setChecked(Config::autoUpdate()); + autoUpdateCheckBox->setChecked(m_gui_settings->GetValue(gui::gen_checkForUpdates).toBool()); #if (QT_VERSION < QT_VERSION_CHECK(6, 7, 0)) - connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [](int state) { + connect(autoUpdateCheckBox, &QCheckBox::stateChanged, this, [this](int state) { #else - connect(autoUpdateCheckBox, &QCheckBox::checkStateChanged, this, [](Qt::CheckState state) { + connect(autoUpdateCheckBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) { #endif const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); - Config::setAutoUpdate(state == Qt::Checked); + m_gui_settings->SetValue(gui::gen_checkForUpdates, (state == Qt::Checked)); Config::save(user_dir / "config.toml"); }); diff --git a/src/qt_gui/check_update.h b/src/qt_gui/check_update.h index dfeb95e73..139059c41 100644 --- a/src/qt_gui/check_update.h +++ b/src/qt_gui/check_update.h @@ -8,12 +8,14 @@ #include #include #include +#include "gui_settings.h" class CheckUpdate : public QDialog { Q_OBJECT public: - explicit CheckUpdate(const bool showMessage, QWidget* parent = nullptr); + explicit CheckUpdate(std::shared_ptr gui_settings, const bool showMessage, + QWidget* parent = nullptr); ~CheckUpdate(); private slots: @@ -35,6 +37,7 @@ private: QString updateDownloadUrl; QNetworkAccessManager* networkManager; + std::shared_ptr m_gui_settings; }; #endif // CHECKUPDATE_H diff --git a/src/qt_gui/game_grid_frame.cpp b/src/qt_gui/game_grid_frame.cpp index e06fea090..66679dc71 100644 --- a/src/qt_gui/game_grid_frame.cpp +++ b/src/qt_gui/game_grid_frame.cpp @@ -5,11 +5,13 @@ #include "game_grid_frame.h" #include "qt_gui/compatibility_info.h" -GameGridFrame::GameGridFrame(std::shared_ptr game_info_get, +GameGridFrame::GameGridFrame(std::shared_ptr gui_settings, + std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent) - : QTableWidget(parent), m_game_info(game_info_get), m_compat_info(compat_info_get) { - icon_size = Config::getIconSizeGrid(); + : QTableWidget(parent), m_gui_settings(std::move(gui_settings)), m_game_info(game_info_get), + m_compat_info(compat_info_get) { + icon_size = m_gui_settings->GetValue(gui::gg_icon_size).toInt(); windowWidth = parent->width(); this->setShowGrid(false); this->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -74,7 +76,7 @@ void GameGridFrame::onCurrentCellChanged(int currentRow, int currentColumn, int } void GameGridFrame::PlayBackgroundMusic(QString path) { - if (path.isEmpty() || !Config::getPlayBGM()) { + if (path.isEmpty() || !m_gui_settings->GetValue(gui::gl_playBackgroundMusic).toBool()) { BackgroundMusicPlayer::getInstance().stopMusic(); return; } @@ -91,7 +93,8 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from else m_games_ = m_game_info->m_games; m_games_shared = std::make_shared>(m_games_); - icon_size = Config::getIconSizeGrid(); // update icon size for resize event. + icon_size = + m_gui_settings->GetValue(gui::gg_icon_size).toInt(); // update icon size for resize event. int gamesPerRow = windowWidth / (icon_size + 20); // 2 x cell widget border size. int row = 0; @@ -118,7 +121,7 @@ void GameGridFrame::PopulateGameGrid(QVector m_games_search, bool from layout->addWidget(name_label); // Resizing of font-size. - float fontSize = (Config::getIconSizeGrid() / 5.5f); + float fontSize = (m_gui_settings->GetValue(gui::gg_icon_size).toInt() / 5.5f); QString styleSheet = QString("color: white; font-weight: bold; font-size: %1px;").arg(fontSize); name_label->setStyleSheet(styleSheet); @@ -168,7 +171,7 @@ void GameGridFrame::SetGridBackgroundImage(int row, int column) { } // If background images are hidden, clear the background image - if (!Config::getShowBackgroundImage()) { + if (!m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) { backgroundImage = QImage(); m_last_opacity = -1; // Reset opacity tracking when disabled m_current_game_path.clear(); // Reset current game path @@ -177,7 +180,7 @@ void GameGridFrame::SetGridBackgroundImage(int row, int column) { } const auto& game = (*m_games_shared)[itemID]; - const int opacity = Config::getBackgroundImageOpacity(); + const int opacity = m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt(); // Recompute if opacity changed or we switched to a different game if (opacity != m_last_opacity || game.pic_path != m_current_game_path) { @@ -195,7 +198,8 @@ void GameGridFrame::SetGridBackgroundImage(int row, int column) { void GameGridFrame::RefreshGridBackgroundImage() { QPalette palette; - if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) { + if (!backgroundImage.isNull() && + m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) { QSize widgetSize = size(); QPixmap scaledPixmap = QPixmap::fromImage(backgroundImage) diff --git a/src/qt_gui/game_grid_frame.h b/src/qt_gui/game_grid_frame.h index 14596f8e1..22d278a21 100644 --- a/src/qt_gui/game_grid_frame.h +++ b/src/qt_gui/game_grid_frame.h @@ -11,6 +11,7 @@ #include "game_info.h" #include "game_list_utils.h" #include "gui_context_menus.h" +#include "gui_settings.h" #include "qt_gui/compatibility_info.h" class GameGridFrame : public QTableWidget { @@ -37,9 +38,11 @@ private: bool validCellSelected = false; int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation std::filesystem::path m_current_game_path; // Track current game path to detect changes + std::shared_ptr m_gui_settings; public: - explicit GameGridFrame(std::shared_ptr game_info_get, + explicit GameGridFrame(std::shared_ptr gui_settings, + std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent = nullptr); void PopulateGameGrid(QVector m_games, bool fromSearch); diff --git a/src/qt_gui/game_list_frame.cpp b/src/qt_gui/game_list_frame.cpp index 170215f3d..dd10e0f8b 100644 --- a/src/qt_gui/game_list_frame.cpp +++ b/src/qt_gui/game_list_frame.cpp @@ -9,11 +9,13 @@ #include "game_list_frame.h" #include "game_list_utils.h" -GameListFrame::GameListFrame(std::shared_ptr game_info_get, +GameListFrame::GameListFrame(std::shared_ptr gui_settings, + std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent) - : QTableWidget(parent), m_game_info(game_info_get), m_compat_info(compat_info_get) { - icon_size = Config::getIconSize(); + : QTableWidget(parent), m_gui_settings(std::move(gui_settings)), m_game_info(game_info_get), + m_compat_info(compat_info_get) { + icon_size = m_gui_settings->GetValue(gui::gl_icon_size).toInt(); this->setShowGrid(false); this->setEditTriggers(QAbstractItemView::NoEditTriggers); this->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -97,7 +99,7 @@ void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int } void GameListFrame::PlayBackgroundMusic(QTableWidgetItem* item) { - if (!item || !Config::getPlayBGM()) { + if (!item || !m_gui_settings->GetValue(gui::gl_playBackgroundMusic).toBool()) { BackgroundMusicPlayer::getInstance().stopMusic(); return; } @@ -172,7 +174,7 @@ void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { } // If background images are hidden, clear the background image - if (!Config::getShowBackgroundImage()) { + if (!m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) { backgroundImage = QImage(); m_last_opacity = -1; // Reset opacity tracking when disabled m_current_game_path.clear(); // Reset current game path @@ -181,7 +183,7 @@ void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { } const auto& game = m_game_info->m_games[item->row()]; - const int opacity = Config::getBackgroundImageOpacity(); + const int opacity = m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt(); // Recompute if opacity changed or we switched to a different game if (opacity != m_last_opacity || game.pic_path != m_current_game_path) { @@ -200,7 +202,8 @@ void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) { void GameListFrame::RefreshListBackgroundImage() { QPalette palette; - if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) { + if (!backgroundImage.isNull() && + m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()) { QSize widgetSize = size(); QPixmap scaledPixmap = QPixmap::fromImage(backgroundImage) diff --git a/src/qt_gui/game_list_frame.h b/src/qt_gui/game_list_frame.h index 782db6bae..f70d73054 100644 --- a/src/qt_gui/game_list_frame.h +++ b/src/qt_gui/game_list_frame.h @@ -17,11 +17,13 @@ #include "game_info.h" #include "game_list_utils.h" #include "gui_context_menus.h" +#include "gui_settings.h" class GameListFrame : public QTableWidget { Q_OBJECT public: - explicit GameListFrame(std::shared_ptr game_info_get, + explicit GameListFrame(std::shared_ptr gui_settings, + std::shared_ptr game_info_get, std::shared_ptr compat_info_get, QWidget* parent = nullptr); Q_SIGNALS: @@ -48,6 +50,7 @@ private: QTableWidgetItem* m_current_item = nullptr; int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation std::filesystem::path m_current_game_path; // Track current game path to detect changes + std::shared_ptr m_gui_settings; public: void PopulateGameList(bool isInitialPopulation = true); diff --git a/src/qt_gui/gui_settings.cpp b/src/qt_gui/gui_settings.cpp new file mode 100644 index 000000000..4de6b7f19 --- /dev/null +++ b/src/qt_gui/gui_settings.cpp @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "gui_settings.h" + +gui_settings::gui_settings(QObject* parent) : settings(parent) { + m_settings = std::make_unique(ComputeSettingsDir() + "qt_ui.ini", + QSettings::Format::IniFormat, parent); +} diff --git a/src/qt_gui/gui_settings.h b/src/qt_gui/gui_settings.h new file mode 100644 index 000000000..da5542956 --- /dev/null +++ b/src/qt_gui/gui_settings.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "settings.h" + +namespace gui { +// categories +const QString general_settings = "general_settings"; +const QString main_window = "main_window"; +const QString game_list = "game_list"; +const QString game_grid = "game_grid"; + +// general +const gui_value gen_checkForUpdates = gui_value(general_settings, "checkForUpdates", false); +const gui_value gen_showChangeLog = gui_value(general_settings, "showChangeLog", false); +const gui_value gen_updateChannel = gui_value(general_settings, "updateChannel", "Release"); + +// main window settings +const gui_value mw_geometry = gui_value(main_window, "geometry", QByteArray()); +const gui_value mw_showLabelsUnderIcons = gui_value(main_window, "showLabelsUnderIcons", true); + +// game list settings +const gui_value gl_mode = gui_value(game_list, "tableMode", 0); +const gui_value gl_icon_size = gui_value(game_list, "icon_size", 36); +const gui_value gl_slider_pos = gui_value(game_list, "slider_pos", 0); +const gui_value gl_showBackgroundImage = gui_value(game_list, "showBackgroundImage", true); +const gui_value gl_backgroundImageOpacity = gui_value(game_list, "backgroundImageOpacity", 50); +const gui_value gl_playBackgroundMusic = gui_value(game_list, "playBackgroundMusic", true); +const gui_value gl_backgroundMusicVolume = gui_value(game_list, "backgroundMusicVolume", 50); + +// game grid settings +const gui_value gg_icon_size = gui_value(game_grid, "icon_size", 69); +const gui_value gg_slider_pos = gui_value(game_grid, "slider_pos", 0); + +} // namespace gui + +class gui_settings : public settings { + Q_OBJECT + +public: + explicit gui_settings(QObject* parent = nullptr); + ~gui_settings() override = default; +}; diff --git a/src/qt_gui/main_window.cpp b/src/qt_gui/main_window.cpp index 906a3066e..c6da49182 100644 --- a/src/qt_gui/main_window.cpp +++ b/src/qt_gui/main_window.cpp @@ -32,6 +32,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->setupUi(this); installEventFilter(this); setAttribute(Qt::WA_DeleteOnClose); + m_gui_settings = std::make_shared(); + ui->toggleLabelsAct->setChecked( + m_gui_settings->GetValue(gui::mw_showLabelsUnderIcons).toBool()); } MainWindow::~MainWindow() { @@ -139,7 +142,7 @@ void MainWindow::PauseGame() { void MainWindow::toggleLabelsUnderIcons() { bool showLabels = ui->toggleLabelsAct->isChecked(); - Config::setShowLabelsUnderIcons(); + m_gui_settings->SetValue(gui::mw_showLabelsUnderIcons, showLabels); UpdateToolbarLabels(); if (isGameRunning) { UpdateToolbarButtons(); @@ -290,21 +293,21 @@ void MainWindow::CreateDockWindows() { setCentralWidget(phCentralWidget); m_dock_widget.reset(new QDockWidget(tr("Game List"), this)); - m_game_list_frame.reset(new GameListFrame(m_game_info, m_compat_info, this)); + m_game_list_frame.reset(new GameListFrame(m_gui_settings, m_game_info, m_compat_info, this)); m_game_list_frame->setObjectName("gamelist"); - m_game_grid_frame.reset(new GameGridFrame(m_game_info, m_compat_info, this)); + m_game_grid_frame.reset(new GameGridFrame(m_gui_settings, m_game_info, m_compat_info, this)); m_game_grid_frame->setObjectName("gamegridlist"); m_elf_viewer.reset(new ElfViewer(this)); m_elf_viewer->setObjectName("elflist"); - int table_mode = Config::getTableMode(); + int table_mode = m_gui_settings->GetValue(gui::gl_mode).toInt(); int slider_pos = 0; if (table_mode == 0) { // List m_game_grid_frame->hide(); m_elf_viewer->hide(); m_game_list_frame->show(); m_dock_widget->setWidget(m_game_list_frame.data()); - slider_pos = Config::getSliderPosition(); + slider_pos = m_gui_settings->GetValue(gui::gl_slider_pos).toInt(); ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; isTableList = true; } else if (table_mode == 1) { // Grid @@ -312,7 +315,7 @@ void MainWindow::CreateDockWindows() { m_elf_viewer->hide(); m_game_grid_frame->show(); m_dock_widget->setWidget(m_game_grid_frame.data()); - slider_pos = Config::getSliderPositionGrid(); + slider_pos = m_gui_settings->GetValue(gui::gg_slider_pos).toInt(); ui->sizeSlider->setSliderPosition(slider_pos); // set slider pos at start; isTableList = false; } else { @@ -356,11 +359,11 @@ void MainWindow::LoadGameLists() { #ifdef ENABLE_UPDATER void MainWindow::CheckUpdateMain(bool checkSave) { if (checkSave) { - if (!Config::autoUpdate()) { + if (!m_gui_settings->GetValue(gui::gen_checkForUpdates).toBool()) { return; } } - auto checkUpdate = new CheckUpdate(false); + auto checkUpdate = new CheckUpdate(m_gui_settings, false); checkUpdate->exec(); } #endif @@ -380,13 +383,13 @@ void MainWindow::CreateConnects() { m_game_list_frame->icon_size = 48 + value; // 48 is the minimum icon size to use due to text disappearing. m_game_list_frame->ResizeIcons(48 + value); - Config::setIconSize(48 + value); - Config::setSliderPosition(value); + m_gui_settings->SetValue(gui::gl_icon_size, 48 + value); + m_gui_settings->SetValue(gui::gl_slider_pos, value); } else { m_game_grid_frame->icon_size = 69 + value; m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); - Config::setIconSizeGrid(69 + value); - Config::setSliderPositionGrid(value); + m_gui_settings->SetValue(gui::gg_icon_size, 69 + value); + m_gui_settings->SetValue(gui::gg_slider_pos, value); } }); @@ -404,7 +407,7 @@ void MainWindow::CreateConnects() { &MainWindow::StartGame); connect(ui->configureAct, &QAction::triggered, this, [this]() { - auto settingsDialog = new SettingsDialog(m_compat_info, this); + auto settingsDialog = new SettingsDialog(m_gui_settings, m_compat_info, this); connect(settingsDialog, &SettingsDialog::LanguageChanged, this, &MainWindow::OnLanguageChanged); @@ -418,7 +421,8 @@ void MainWindow::CreateConnects() { connect(settingsDialog, &SettingsDialog::BackgroundOpacityChanged, this, [this](int opacity) { - Config::setBackgroundImageOpacity(opacity); + m_gui_settings->SetValue(gui::gl_backgroundImageOpacity, + std::clamp(opacity, 0, 100)); if (m_game_list_frame) { QTableWidgetItem* current = m_game_list_frame->GetCurrentItem(); if (current) { @@ -437,7 +441,7 @@ void MainWindow::CreateConnects() { }); connect(ui->settingsButton, &QPushButton::clicked, this, [this]() { - auto settingsDialog = new SettingsDialog(m_compat_info, this); + auto settingsDialog = new SettingsDialog(m_gui_settings, m_compat_info, this); connect(settingsDialog, &SettingsDialog::LanguageChanged, this, &MainWindow::OnLanguageChanged); @@ -451,7 +455,8 @@ void MainWindow::CreateConnects() { connect(settingsDialog, &SettingsDialog::BackgroundOpacityChanged, this, [this](int opacity) { - Config::setBackgroundImageOpacity(opacity); + m_gui_settings->SetValue(gui::gl_backgroundImageOpacity, + std::clamp(opacity, 0, 100)); if (m_game_list_frame) { QTableWidgetItem* current = m_game_list_frame->GetCurrentItem(); if (current) { @@ -481,7 +486,7 @@ void MainWindow::CreateConnects() { #ifdef ENABLE_UPDATER connect(ui->updaterAct, &QAction::triggered, this, [this]() { - auto checkUpdate = new CheckUpdate(true); + auto checkUpdate = new CheckUpdate(m_gui_settings, true); checkUpdate->exec(); }); #endif @@ -496,13 +501,13 @@ void MainWindow::CreateConnects() { m_game_list_frame->icon_size = 36; // 36 is the minimum icon size to use due to text disappearing. ui->sizeSlider->setValue(0); // icone_size - 36 - Config::setIconSize(36); - Config::setSliderPosition(0); + m_gui_settings->SetValue(gui::gl_icon_size, 36); + m_gui_settings->SetValue(gui::gl_slider_pos, 0); } else { m_game_grid_frame->icon_size = 69; ui->sizeSlider->setValue(0); // icone_size - 36 - Config::setIconSizeGrid(69); - Config::setSliderPositionGrid(0); + m_gui_settings->SetValue(gui::gg_icon_size, 69); + m_gui_settings->SetValue(gui::gg_slider_pos, 9); m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } }); @@ -511,13 +516,13 @@ void MainWindow::CreateConnects() { if (isTableList) { m_game_list_frame->icon_size = 64; ui->sizeSlider->setValue(28); - Config::setIconSize(64); - Config::setSliderPosition(28); + m_gui_settings->SetValue(gui::gl_icon_size, 64); + m_gui_settings->SetValue(gui::gl_slider_pos, 28); } else { m_game_grid_frame->icon_size = 97; ui->sizeSlider->setValue(28); - Config::setIconSizeGrid(97); - Config::setSliderPositionGrid(28); + m_gui_settings->SetValue(gui::gg_icon_size, 97); + m_gui_settings->SetValue(gui::gg_slider_pos, 28); m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } }); @@ -526,13 +531,13 @@ void MainWindow::CreateConnects() { if (isTableList) { m_game_list_frame->icon_size = 128; ui->sizeSlider->setValue(92); - Config::setIconSize(128); - Config::setSliderPosition(92); + m_gui_settings->SetValue(gui::gl_icon_size, 128); + m_gui_settings->SetValue(gui::gl_slider_pos, 92); } else { m_game_grid_frame->icon_size = 161; ui->sizeSlider->setValue(92); - Config::setIconSizeGrid(161); - Config::setSliderPositionGrid(92); + m_gui_settings->SetValue(gui::gg_icon_size, 161); + m_gui_settings->SetValue(gui::gg_slider_pos, 92); m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } }); @@ -541,13 +546,13 @@ void MainWindow::CreateConnects() { if (isTableList) { m_game_list_frame->icon_size = 256; ui->sizeSlider->setValue(220); - Config::setIconSize(256); - Config::setSliderPosition(220); + m_gui_settings->SetValue(gui::gl_icon_size, 256); + m_gui_settings->SetValue(gui::gl_slider_pos, 220); } else { m_game_grid_frame->icon_size = 256; ui->sizeSlider->setValue(220); - Config::setIconSizeGrid(256); - Config::setSliderPositionGrid(220); + m_gui_settings->SetValue(gui::gg_icon_size, 256); + m_gui_settings->SetValue(gui::gg_slider_pos, 220); m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } }); @@ -563,8 +568,8 @@ void MainWindow::CreateConnects() { m_game_list_frame->PopulateGameList(); } isTableList = true; - Config::setTableMode(0); - int slider_pos = Config::getSliderPosition(); + m_gui_settings->SetValue(gui::gl_mode, 0); + int slider_pos = m_gui_settings->GetValue(gui::gl_slider_pos).toInt(); ui->sizeSlider->setEnabled(true); ui->sizeSlider->setSliderPosition(slider_pos); ui->mw_searchbar->setText(""); @@ -582,8 +587,8 @@ void MainWindow::CreateConnects() { m_game_grid_frame->PopulateGameGrid(m_game_info->m_games, false); } isTableList = false; - Config::setTableMode(1); - int slider_pos_grid = Config::getSliderPositionGrid(); + m_gui_settings->SetValue(gui::gl_mode, 1); + int slider_pos_grid = m_gui_settings->GetValue(gui::gg_slider_pos).toInt(); ui->sizeSlider->setEnabled(true); ui->sizeSlider->setSliderPosition(slider_pos_grid); ui->mw_searchbar->setText(""); @@ -598,7 +603,7 @@ void MainWindow::CreateConnects() { m_elf_viewer->show(); isTableList = false; ui->sizeSlider->setDisabled(true); - Config::setTableMode(2); + m_gui_settings->SetValue(gui::gl_mode, 2); SetLastIconSizeBullet(); }); @@ -840,7 +845,7 @@ void MainWindow::CreateConnects() { void MainWindow::StartGame() { BackgroundMusicPlayer::getInstance().stopMusic(); QString gamePath = ""; - int table_mode = Config::getTableMode(); + int table_mode = m_gui_settings->GetValue(gui::gl_mode).toInt(); if (table_mode == 0) { if (m_game_list_frame->currentItem()) { int itemID = m_game_list_frame->currentItem()->row(); @@ -925,25 +930,25 @@ void MainWindow::RefreshGameTable() { } void MainWindow::ConfigureGuiFromSettings() { - setGeometry(Config::getMainWindowGeometryX(), Config::getMainWindowGeometryY(), - Config::getMainWindowGeometryW(), Config::getMainWindowGeometryH()); - + if (!restoreGeometry(m_gui_settings->GetValue(gui::mw_geometry).toByteArray())) { + // By default, set the window to 70% of the screen + resize(QGuiApplication::primaryScreen()->availableSize() * 0.7); + } ui->showGameListAct->setChecked(true); - if (Config::getTableMode() == 0) { + int table_mode = m_gui_settings->GetValue(gui::gl_mode).toInt(); + if (table_mode == 0) { ui->setlistModeListAct->setChecked(true); - } else if (Config::getTableMode() == 1) { + } else if (table_mode == 1) { ui->setlistModeGridAct->setChecked(true); - } else if (Config::getTableMode() == 2) { + } else if (table_mode == 2) { ui->setlistElfAct->setChecked(true); } - BackgroundMusicPlayer::getInstance().setVolume(Config::getBGMvolume()); + BackgroundMusicPlayer::getInstance().setVolume( + m_gui_settings->GetValue(gui::gl_backgroundMusicVolume).toInt()); } -void MainWindow::SaveWindowState() const { - Config::setMainWindowWidth(this->width()); - Config::setMainWindowHeight(this->height()); - Config::setMainWindowGeometry(this->geometry().x(), this->geometry().y(), - this->geometry().width(), this->geometry().height()); +void MainWindow::SaveWindowState() { + m_gui_settings->SetValue(gui::mw_geometry, saveGeometry(), false); } void MainWindow::BootGame() { @@ -1024,8 +1029,8 @@ void MainWindow::SetLastUsedTheme() { void MainWindow::SetLastIconSizeBullet() { // set QAction bullet point if applicable - int lastSize = Config::getIconSize(); - int lastSizeGrid = Config::getIconSizeGrid(); + int lastSize = m_gui_settings->GetValue(gui::gl_icon_size).toInt(); + int lastSizeGrid = m_gui_settings->GetValue(gui::gg_icon_size).toInt(); if (isTableList) { switch (lastSize) { case 36: @@ -1195,7 +1200,7 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { - auto tblMode = Config::getTableMode(); + auto tblMode = m_gui_settings->GetValue(gui::gl_mode).toInt(); if (tblMode != 2 && (tblMode != 1 || m_game_grid_frame->IsValidCellSelected())) { StartGame(); return true; diff --git a/src/qt_gui/main_window.h b/src/qt_gui/main_window.h index 97c56433d..7f11f7310 100644 --- a/src/qt_gui/main_window.h +++ b/src/qt_gui/main_window.h @@ -20,6 +20,7 @@ #include "game_info.h" #include "game_list_frame.h" #include "game_list_utils.h" +#include "gui_settings.h" #include "main_window_themes.h" #include "main_window_ui.h" @@ -41,7 +42,7 @@ public: private Q_SLOTS: void ConfigureGuiFromSettings(); - void SaveWindowState() const; + void SaveWindowState(); void SearchGameTable(const QString& text); void ShowGameList(); void RefreshGameTable(); @@ -102,6 +103,7 @@ private: std::make_shared(); QTranslator* translator; + std::shared_ptr m_gui_settings; protected: bool eventFilter(QObject* obj, QEvent* event) override; diff --git a/src/qt_gui/main_window_ui.h b/src/qt_gui/main_window_ui.h index 4d3481c07..4ce71013e 100644 --- a/src/qt_gui/main_window_ui.h +++ b/src/qt_gui/main_window_ui.h @@ -107,7 +107,6 @@ public: toggleLabelsAct = new QAction(MainWindow); toggleLabelsAct->setObjectName("toggleLabelsAct"); toggleLabelsAct->setCheckable(true); - toggleLabelsAct->setChecked(Config::getShowLabelsUnderIcons()); setIconSizeTinyAct = new QAction(MainWindow); setIconSizeTinyAct->setObjectName("setIconSizeTinyAct"); diff --git a/src/qt_gui/settings.cpp b/src/qt_gui/settings.cpp new file mode 100644 index 000000000..44133dac5 --- /dev/null +++ b/src/qt_gui/settings.cpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "settings.h" + +settings::settings(QObject* parent) : QObject(parent), m_settings_dir(ComputeSettingsDir()) {} + +settings::~settings() { + sync(); +} + +void settings::sync() { + if (m_settings) { + m_settings->sync(); + } +} + +QString settings::GetSettingsDir() const { + return m_settings_dir.absolutePath(); +} + +QString settings::ComputeSettingsDir() { + const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir); + return QString::fromStdString(config_dir.string() + "/"); +} + +void settings::RemoveValue(const QString& key, const QString& name, bool sync) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->remove(name); + m_settings->endGroup(); + + if (sync) { + m_settings->sync(); + } + } +} + +void settings::RemoveValue(const gui_value& entry, bool sync) const { + RemoveValue(entry.key, entry.name, sync); +} + +QVariant settings::GetValue(const QString& key, const QString& name, const QVariant& def) const { + return m_settings ? m_settings->value(key + "/" + name, def) : def; +} + +QVariant settings::GetValue(const gui_value& entry) const { + return GetValue(entry.key, entry.name, entry.def); +} + +void settings::SetValue(const gui_value& entry, const QVariant& value, bool sync) const { + SetValue(entry.key, entry.name, value, sync); +} + +void settings::SetValue(const QString& key, const QVariant& value, bool sync) const { + if (m_settings) { + m_settings->setValue(key, value); + + if (sync) { + m_settings->sync(); + } + } +} + +void settings::SetValue(const QString& key, const QString& name, const QVariant& value, + bool sync) const { + if (m_settings) { + m_settings->beginGroup(key); + m_settings->setValue(name, value); + m_settings->endGroup(); + + if (sync) { + m_settings->sync(); + } + } +} diff --git a/src/qt_gui/settings.h b/src/qt_gui/settings.h new file mode 100644 index 000000000..da71fe01a --- /dev/null +++ b/src/qt_gui/settings.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +struct gui_value { + QString key; + QString name; + QVariant def; + + gui_value() {} + + gui_value(const QString& k, const QString& n, const QVariant& d) : key(k), name(n), def(d) {} + + bool operator==(const gui_value& rhs) const noexcept { + return key == rhs.key && name == rhs.name && def == rhs.def; + } +}; + +class settings : public QObject { + Q_OBJECT + +public: + explicit settings(QObject* parent = nullptr); + ~settings(); + + void sync(); + + QString GetSettingsDir() const; + + QVariant GetValue(const QString& key, const QString& name, const QVariant& def) const; + QVariant GetValue(const gui_value& entry) const; + +public Q_SLOTS: + /** Remove entry */ + void RemoveValue(const QString& key, const QString& name, bool sync = true) const; + void RemoveValue(const gui_value& entry, bool sync = true) const; + + /** Write value to entry */ + void SetValue(const gui_value& entry, const QVariant& value, bool sync = true) const; + void SetValue(const QString& key, const QVariant& value, bool sync = true) const; + void SetValue(const QString& key, const QString& name, const QVariant& value, + bool sync = true) const; + +protected: + static QString ComputeSettingsDir(); + + std::unique_ptr m_settings; + QDir m_settings_dir; +}; diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index 914cc5470..fc00c6f75 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -71,9 +71,10 @@ int bgm_volume_backup; static std::vector m_physical_devices; -SettingsDialog::SettingsDialog(std::shared_ptr m_compat_info, +SettingsDialog::SettingsDialog(std::shared_ptr gui_settings, + std::shared_ptr m_compat_info, QWidget* parent) - : QDialog(parent), ui(new Ui::SettingsDialog) { + : QDialog(parent), ui(new Ui::SettingsDialog), m_gui_settings(std::move(gui_settings)) { ui->setupUi(this); ui->tabWidgetSettings->setUsesScrollButtons(false); @@ -147,6 +148,7 @@ SettingsDialog::SettingsDialog(std::shared_ptr m_compat_ Config::save(config_dir / "config.toml"); } else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { Config::setDefaultValues(); + setDefaultValues(); Config::save(config_dir / "config.toml"); LoadValuesFromConfig(); } else if (button == ui->buttonBox->button(QDialogButtonBox::Close)) { @@ -175,28 +177,34 @@ SettingsDialog::SettingsDialog(std::shared_ptr m_compat_ { #ifdef ENABLE_UPDATER #if (QT_VERSION < QT_VERSION_CHECK(6, 7, 0)) - connect(ui->updateCheckBox, &QCheckBox::stateChanged, this, - [](int state) { Config::setAutoUpdate(state == Qt::Checked); }); + connect(ui->updateCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + m_gui_settings->SetValue(gui::gen_checkForUpdates, state == Qt::Checked); + }); - connect(ui->changelogCheckBox, &QCheckBox::stateChanged, this, - [](int state) { Config::setAlwaysShowChangelog(state == Qt::Checked); }); + connect(ui->changelogCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + m_gui_settings->SetValue(gui::gen_showChangeLog, state == Qt::Checked); + }); #else connect(ui->updateCheckBox, &QCheckBox::checkStateChanged, this, - [](Qt::CheckState state) { Config::setAutoUpdate(state == Qt::Checked); }); + [this](Qt::CheckState state) { + m_gui_settings->SetValue(gui::gen_checkForUpdates, state == Qt::Checked); + }); connect(ui->changelogCheckBox, &QCheckBox::checkStateChanged, this, - [](Qt::CheckState state) { Config::setAlwaysShowChangelog(state == Qt::Checked); }); + [this](Qt::CheckState state) { + m_gui_settings->SetValue(gui::gen_showChangeLog, state == Qt::Checked); + }); #endif connect(ui->updateComboBox, &QComboBox::currentTextChanged, this, [this](const QString& channel) { if (channelMap.contains(channel)) { - Config::setUpdateChannel(channelMap.value(channel).toStdString()); + m_gui_settings->SetValue(gui::gen_updateChannel, channelMap.value(channel)); } }); - connect(ui->checkUpdateButton, &QPushButton::clicked, this, []() { - auto checkUpdate = new CheckUpdate(true); + connect(ui->checkUpdateButton, &QPushButton::clicked, this, [this]() { + auto checkUpdate = new CheckUpdate(m_gui_settings, true); checkUpdate->exec(); }); #else @@ -235,12 +243,12 @@ SettingsDialog::SettingsDialog(std::shared_ptr m_compat_ [](const QString& hometab) { Config::setChooseHomeTab(hometab.toStdString()); }); #if (QT_VERSION < QT_VERSION_CHECK(6, 7, 0)) - connect(ui->showBackgroundImageCheckBox, &QCheckBox::stateChanged, this, [](int state) { + connect(ui->showBackgroundImageCheckBox, &QCheckBox::stateChanged, this, [this](int state) { #else connect(ui->showBackgroundImageCheckBox, &QCheckBox::checkStateChanged, this, - [](Qt::CheckState state) { + [this](Qt::CheckState state) { #endif - Config::setShowBackgroundImage(state == Qt::Checked); + m_gui_settings->SetValue(gui::gl_showBackgroundImage, state == Qt::Checked); }); } @@ -505,7 +513,7 @@ void SettingsDialog::LoadValuesFromConfig() { ui->changelogCheckBox->setChecked( toml::find_or(data, "General", "alwaysShowChangelog", false)); - QString updateChannel = QString::fromStdString(Config::getUpdateChannel()); + QString updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString(); ui->updateComboBox->setCurrentText( channelMap.key(updateChannel != "Release" && updateChannel != "Nightly" ? (Common::g_is_release ? "Release" : "Nightly") @@ -536,11 +544,14 @@ void SettingsDialog::LoadValuesFromConfig() { ui->removeFolderButton->setEnabled(!ui->gameFoldersListWidget->selectedItems().isEmpty()); ResetInstallFolders(); - ui->backgroundImageOpacitySlider->setValue(Config::getBackgroundImageOpacity()); - ui->showBackgroundImageCheckBox->setChecked(Config::getShowBackgroundImage()); + ui->backgroundImageOpacitySlider->setValue( + m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt()); + ui->showBackgroundImageCheckBox->setChecked( + m_gui_settings->GetValue(gui::gl_showBackgroundImage).toBool()); - backgroundImageOpacitySlider_backup = Config::getBackgroundImageOpacity(); - bgm_volume_backup = Config::getBGMvolume(); + backgroundImageOpacitySlider_backup = + m_gui_settings->GetValue(gui::gl_backgroundImageOpacity).toInt(); + bgm_volume_backup = m_gui_settings->GetValue(gui::gl_backgroundMusicVolume).toInt(); } void SettingsDialog::InitializeEmulatorLanguages() { @@ -754,8 +765,7 @@ void SettingsDialog::UpdateSettings() { } else if (ui->radioButton_Bottom->isChecked()) { Config::setSideTrophy("bottom"); } - - Config::setPlayBGM(ui->playBGMCheckBox->isChecked()); + m_gui_settings->SetValue(gui::gl_playBackgroundMusic, ui->playBGMCheckBox->isChecked()); Config::setAllowHDR(ui->enableHDRCheckBox->isChecked()); Config::setLogType(logTypeMap.value(ui->logTypeComboBox->currentText()).toStdString()); Config::setLogFilter(ui->logFilterLineEdit->text().toStdString()); @@ -764,7 +774,7 @@ void SettingsDialog::UpdateSettings() { Config::setCursorState(ui->hideCursorComboBox->currentIndex()); Config::setCursorHideTimeout(ui->idleTimeoutSpinBox->value()); Config::setGpuId(ui->graphicsAdapterBox->currentIndex() - 1); - Config::setBGMvolume(ui->BGMVolumeSlider->value()); + m_gui_settings->SetValue(gui::gl_backgroundMusicVolume, ui->BGMVolumeSlider->value()); Config::setLanguage(languageIndexes[ui->consoleLanguageComboBox->currentIndex()]); Config::setEnableDiscordRPC(ui->discordRPCCheckbox->isChecked()); Config::setScreenWidth(ui->widthSpinBox->value()); @@ -784,16 +794,19 @@ void SettingsDialog::UpdateSettings() { Config::setVkCrashDiagnosticEnabled(ui->crashDiagnosticsCheckBox->isChecked()); Config::setCollectShaderForDebug(ui->collectShaderCheckBox->isChecked()); Config::setCopyGPUCmdBuffers(ui->copyGPUBuffersCheckBox->isChecked()); - Config::setAutoUpdate(ui->updateCheckBox->isChecked()); - Config::setAlwaysShowChangelog(ui->changelogCheckBox->isChecked()); - Config::setUpdateChannel(channelMap.value(ui->updateComboBox->currentText()).toStdString()); + m_gui_settings->SetValue(gui::gen_checkForUpdates, ui->updateCheckBox->isChecked()); + m_gui_settings->SetValue(gui::gen_showChangeLog, ui->changelogCheckBox->isChecked()); + m_gui_settings->SetValue(gui::gen_updateChannel, + channelMap.value(ui->updateComboBox->currentText())); Config::setChooseHomeTab( chooseHomeTabMap.value(ui->chooseHomeTabComboBox->currentText()).toStdString()); Config::setCompatibilityEnabled(ui->enableCompatibilityCheckBox->isChecked()); Config::setCheckCompatibilityOnStartup(ui->checkCompatibilityOnStartupCheckBox->isChecked()); - Config::setBackgroundImageOpacity(ui->backgroundImageOpacitySlider->value()); + m_gui_settings->SetValue(gui::gl_backgroundImageOpacity, + std::clamp(ui->backgroundImageOpacitySlider->value(), 0, 100)); emit BackgroundOpacityChanged(ui->backgroundImageOpacitySlider->value()); - Config::setShowBackgroundImage(ui->showBackgroundImageCheckBox->isChecked()); + m_gui_settings->SetValue(gui::gl_showBackgroundImage, + ui->showBackgroundImageCheckBox->isChecked()); std::vector dirs_with_states; for (int i = 0; i < ui->gameFoldersListWidget->count(); i++) { @@ -862,3 +875,16 @@ void SettingsDialog::ResetInstallFolders() { Config::setAllGameInstallDirs(settings_install_dirs_config); } } +void SettingsDialog::setDefaultValues() { + m_gui_settings->SetValue(gui::gl_showBackgroundImage, true); + m_gui_settings->SetValue(gui::gl_backgroundImageOpacity, 50); + m_gui_settings->SetValue(gui::gl_playBackgroundMusic, false); + m_gui_settings->SetValue(gui::gl_backgroundMusicVolume, 50); + m_gui_settings->SetValue(gui::gen_checkForUpdates, false); + m_gui_settings->SetValue(gui::gen_showChangeLog, false); + if (Common::g_is_release) { + m_gui_settings->SetValue(gui::gen_updateChannel, "Release"); + } else { + m_gui_settings->SetValue(gui::gen_updateChannel, "Nightly"); + } +} \ No newline at end of file diff --git a/src/qt_gui/settings_dialog.h b/src/qt_gui/settings_dialog.h index cdf9be80e..db1bcf772 100644 --- a/src/qt_gui/settings_dialog.h +++ b/src/qt_gui/settings_dialog.h @@ -11,6 +11,7 @@ #include "common/config.h" #include "common/path_util.h" +#include "gui_settings.h" #include "qt_gui/compatibility_info.h" namespace Ui { @@ -20,7 +21,8 @@ class SettingsDialog; class SettingsDialog : public QDialog { Q_OBJECT public: - explicit SettingsDialog(std::shared_ptr m_compat_info, + explicit SettingsDialog(std::shared_ptr gui_settings, + std::shared_ptr m_compat_info, QWidget* parent = nullptr); ~SettingsDialog(); @@ -42,6 +44,7 @@ private: void OnLanguageChanged(int index); void OnCursorStateChanged(s16 index); void closeEvent(QCloseEvent* event) override; + void setDefaultValues(); std::unique_ptr ui; @@ -52,4 +55,5 @@ private: int initialHeight; bool is_saving = false; + std::shared_ptr m_gui_settings; }; From c35141b33f3e376f9123e3595cec0f2184f5351d Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 12 Jun 2025 13:28:14 +0300 Subject: [PATCH 136/141] New Crowdin updates (#3085) * New translations en_us.ts (Persian) * New translations en_us.ts (Catalan) * New translations en_us.ts (Persian) * New translations en_us.ts (Persian) * New translations en_us.ts (Persian) * New translations en_us.ts (Catalan) * New translations en_us.ts (Catalan) * New translations en_us.ts (Serbian (Latin)) --- src/qt_gui/translations/ca_ES.ts | 2081 ++++++++++++++++++++++++++++++ src/qt_gui/translations/fa_IR.ts | 108 +- src/qt_gui/translations/sr_CS.ts | 2081 ++++++++++++++++++++++++++++++ 3 files changed, 4216 insertions(+), 54 deletions(-) create mode 100644 src/qt_gui/translations/ca_ES.ts create mode 100644 src/qt_gui/translations/sr_CS.ts diff --git a/src/qt_gui/translations/ca_ES.ts b/src/qt_gui/translations/ca_ES.ts new file mode 100644 index 000000000..412cefa2c --- /dev/null +++ b/src/qt_gui/translations/ca_ES.ts @@ -0,0 +1,2081 @@ + + + + + + AboutDialog + + About shadPS4 + Sobre shadPS4 + + + shadPS4 is an experimental open-source emulator for the PlayStation 4. + shadPS4 is an experimental open-source emulator for the PlayStation 4. + + + This software should not be used to play games you have not legally obtained. + This software should not be used to play games you have not legally obtained. + + + + CheatsPatches + + Cheats / Patches for + Trucs / Correccions per + + + Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + + + No Image Available + No hi ha imatge disponible + + + Serial: + Número de sèrie: + + + Version: + Versió: + + + Size: + Mida: + + + Select Cheat File: + Selecciona el fitxer de trucs: + + + Repository: + Repositori: + + + Download Cheats + Descarrega els trucs + + + Delete File + Elimina el fitxer + + + No files selected. + No hi ha cap fitxer seleccionat. + + + You can delete the cheats you don't want after downloading them. + You can delete the cheats you don't want after downloading them. + + + Do you want to delete the selected file?\n%1 + Do you want to delete the selected file?\n%1 + + + Select Patch File: + Selecciona un fitxer de correcció: + + + Download Patches + Descarrega les correccions + + + Save + Desa + + + Cheats + Trucs + + + Patches + Correccions + + + Error + Error + + + No patch selected. + No s'ha seleccionat cap correcció. + + + Unable to open files.json for reading. + Unable to open files.json for reading. + + + No patch file found for the current serial. + No patch file found for the current serial. + + + Unable to open the file for reading. + Unable to open the file for reading. + + + Unable to open the file for writing. + Unable to open the file for writing. + + + Failed to parse XML: + Error en analitzar XML: + + + Success + Realitzat amb èxit + + + Options saved successfully. + Options saved successfully. + + + Invalid Source + Font no vàlida + + + The selected source is invalid. + The selected source is invalid. + + + File Exists + El fitxer ja existeix + + + File already exists. Do you want to replace it? + File already exists. Do you want to replace it? + + + Failed to save file: + Error en desar el fitxer: + + + Failed to download file: + Failed to download file: + + + Cheats Not Found + No s'han trobat els trucs + + + No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. + No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. + + + Cheats Downloaded Successfully + Cheats Downloaded Successfully + + + You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. + You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. + + + Failed to save: + Error en desar: + + + Failed to download: + Error en la descàrrega: + + + Download Complete + Descàrrega completa + + + Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. + Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. + + + Failed to parse JSON data from HTML. + Failed to parse JSON data from HTML. + + + Failed to retrieve HTML page. + Failed to retrieve HTML page. + + + The game is in version: %1 + The game is in version: %1 + + + The downloaded patch only works on version: %1 + The downloaded patch only works on version: %1 + + + You may need to update your game. + You may need to update your game. + + + Incompatibility Notice + Avís d'incompatibilitat + + + Failed to open file: + Error en obrir el fitxer: + + + XML ERROR: + Error XML: + + + Failed to open files.json for writing + Failed to open files.json for writing + + + Author: + Autor: + + + Directory does not exist: + Directory does not exist: + + + Failed to open files.json for reading. + Failed to open files.json for reading. + + + Name: + Nom: + + + Can't apply cheats before the game is started + Can't apply cheats before the game is started + + + Close + Tanca + + + + CheckUpdate + + Auto Updater + Actualització automàtica + + + Error + Error + + + Network error: + Error de xarxa: + + + The Auto Updater allows up to 60 update checks per hour.\nYou have reached this limit. Please try again later. + The Auto Updater allows up to 60 update checks per hour.\nYou have reached this limit. Please try again later. + + + Failed to parse update information. + Failed to parse update information. + + + No pre-releases found. + No s'han trobat llançaments previs. + + + Invalid release data. + Dades de la versió no vàlides. + + + No download URL found for the specified asset. + No download URL found for the specified asset. + + + Your version is already up to date! + Your version is already up to date! + + + Update Available + Hi ha una actualització disponible + + + Update Channel + Actualitza el canal + + + Current Version + Versió actual + + + Latest Version + Última versió + + + Do you want to update? + Estàs segur que vols actualitzar? + + + Show Changelog + Mostra el registre de canvis + + + Check for Updates at Startup + Check for Updates at Startup + + + Update + Actualitza + + + No + No + + + Hide Changelog + Oculta el registre de canvis + + + Changes + Canvis + + + Network error occurred while trying to access the URL + Network error occurred while trying to access the URL + + + Download Complete + Descàrrega completa + + + The update has been downloaded, press OK to install. + The update has been downloaded, press OK to install. + + + Failed to save the update file at + Failed to save the update file at + + + Starting Update... + Iniciant l'actualització... + + + Failed to create the update script file + Failed to create the update script file + + + + CompatibilityInfoClass + + Fetching compatibility data, please wait + Fetching compatibility data, please wait + + + Cancel + Cancel·la + + + Loading... + Carregant... + + + Error + Error + + + Unable to update compatibility data! Try again later. + Unable to update compatibility data! Try again later. + + + Unable to open compatibility_data.json for writing. + Unable to open compatibility_data.json for writing. + + + Unknown + Desconegut + + + Nothing + Res + + + Boots + Executa + + + Menus + Menús + + + Ingame + En joc + + + Playable + Jugable + + + + ControlSettings + + Configure Controls + Configura els controladors + + + D-Pad + Botons de direcció + + + Up + Amunt + + + Left + Esquerra + + + Right + Dreta + + + Down + Avall + + + Left Stick Deadzone (def:2 max:127) + Left Stick Deadzone (def:2 max:127) + + + Left Deadzone + Zona morta de la palanca esquerra + + + Left Stick + Palanca esquerra + + + Config Selection + Configura la selecció + + + Common Config + Configuració estàndard + + + Use per-game configs + Fes servir configuracions per cada joc + + + L1 / LB + L1 / LB + + + L2 / LT + L2 / LT + + + Back + Torna + + + R1 / RB + R1 / RB + + + R2 / RT + R2 / RT + + + L3 + L3 + + + Options / Start + Opcions / Executa + + + R3 + R3 + + + Face Buttons + Botons d'acció + + + Triangle / Y + Triangle / Y + + + Square / X + Quadrat / X + + + Circle / B + Cercle / B + + + Cross / A + Creu / A + + + Right Stick Deadzone (def:2, max:127) + Right Stick Deadzone (def:2, max:127) + + + Right Deadzone + Zona morta de la palanca dreta + + + Right Stick + Palanca dreta + + + Color Adjustment + Ajust del color + + + R: + R: + + + G: + G: + + + B: + B: + + + Override Lightbar Color + Override Lightbar Color + + + Override Color + Reemplaça el color + + + Unable to Save + No s'ha pogut desar + + + Cannot bind axis values more than once + Cannot bind axis values more than once + + + Save + Desa + + + Apply + Aplica + + + Restore Defaults + Restaura als valors predeterminats + + + Cancel + Cancel·la + + + + EditorDialog + + Edit Keyboard + Mouse and Controller input bindings + Edit Keyboard + Mouse and Controller input bindings + + + Use Per-Game configs + Fes servir configuracions per cada joc + + + Error + Error + + + Could not open the file for reading + Could not open the file for reading + + + Could not open the file for writing + Could not open the file for writing + + + Save Changes + Desa els canvis + + + Do you want to save changes? + Do you want to save changes? + + + Help + Ajuda + + + Do you want to reset your custom default config to the original default config? + Do you want to reset your custom default config to the original default config? + + + Do you want to reset this config to your custom default config? + Do you want to reset this config to your custom default config? + + + Reset to Default + Reinicia ala valors predeterminats + + + + ElfViewer + + Open Folder + Obre la carpeta + + + + GameInfoClass + + Loading game list, please wait :3 + Loading game list, please wait :3 + + + Cancel + Cancel·la + + + Loading... + Carregant... + + + + GameInstallDialog + + shadPS4 - Choose directory + shadPS4 - Choose directory + + + Directory to install games + Directory to install games + + + Browse + Cercart + + + Error + Error + + + Directory to install DLC + Directory to install DLC + + + + GameListFrame + + Icon + Icona + + + Name + Nom + + + Serial + Número de sèrie + + + Compatibility + Compatibilitat + + + Region + Regió + + + Firmware + Firmware + + + Size + Mida + + + Version + Versió + + + Path + Camí + + + Play Time + Temps de joc + + + Never Played + Mai jugat + + + h + h + + + m + m + + + s + s + + + Compatibility is untested + Compatibility is untested + + + Game does not initialize properly / crashes the emulator + Game does not initialize properly / crashes the emulator + + + Game boots, but only displays a blank screen + Game boots, but only displays a blank screen + + + Game displays an image but does not go past the menu + Game displays an image but does not go past the menu + + + Game has game-breaking glitches or unplayable performance + Game has game-breaking glitches or unplayable performance + + + Game can be completed with playable performance and no major glitches + Game can be completed with playable performance and no major glitches + + + Click to see details on github + Click to see details on github + + + Last updated + Darrera actualització + + + + GameListUtils + + B + B + + + KB + KB + + + MB + MB + + + GB + GB + + + TB + TB + + + + GuiContextMenus + + Create Shortcut + Crear una drecera + + + Cheats / Patches + Trucs / Correccions + + + SFO Viewer + Visualitzador SFO + + + Trophy Viewer + Visualitzador de trofeus + + + Open Folder... + Obre la carpeta... + + + Open Game Folder + Obre la carpeta de jocs + + + Open Save Data Folder + Obre la carpeta de dades desades + + + Open Log Folder + Obre la carpeta de registres + + + Copy info... + Copia la informació... + + + Copy Name + Copia el nom + + + Copy Serial + Copia el número de sèrie + + + Copy Version + Copia la versió + + + Copy Size + Copia la mida + + + Copy All + Copia tot + + + Delete... + Esborra... + + + Delete Game + Esborra el joc + + + Delete Update + Suprimeix l'actualització + + + Delete DLC + Esborra el DLC + + + Delete Trophy + Suprimeix el trofeu + + + Compatibility... + Compatibilitat... + + + Update database + Actualitza la base de dades + + + View report + Visualitza l'informe + + + Submit a report + Envia un informe + + + Shortcut creation + Crea una drecera + + + Shortcut created successfully! + Shortcut created successfully! + + + Error + Error + + + Error creating shortcut! + Error creating shortcut! + + + Game + Joc + + + This game has no update to delete! + This game has no update to delete! + + + Update + Actualitza + + + This game has no DLC to delete! + This game has no DLC to delete! + + + DLC + DLC + + + Delete %1 + Esborra %1 + + + Are you sure you want to delete %1's %2 directory? + Are you sure you want to delete %1's %2 directory? + + + Open Update Folder + Obre la carpeta d'actualitzacions + + + Delete Save Data + Elimina les dades desades + + + This game has no update folder to open! + This game has no update folder to open! + + + No log file found for this game! + No log file found for this game! + + + Failed to convert icon. + Failed to convert icon. + + + This game has no save data to delete! + This game has no save data to delete! + + + This game has no saved trophies to delete! + This game has no saved trophies to delete! + + + Save Data + Desa les dades + + + Trophy + Trofeu + + + SFO Viewer for + Visualitzador SFO per + + + + HelpDialog + + Quickstart + Inici ràpid + + + FAQ + Preguntes freqüents + + + Syntax + Sintaxi + + + Special Bindings + Assignació de tecles especials + + + Keybindings + Dreceres de teclat + + + + KBMSettings + + Configure Controls + Configura els controladors + + + D-Pad + Botons de direcció + + + Up + Amunt + + + unmapped + sense assignar + + + Left + Esquerra + + + Right + Dreta + + + Down + Avall + + + Left Analog Halfmode + Mode reduït de la palanca esquerra + + + hold to move left stick at half-speed + hold to move left stick at half-speed + + + Left Stick + Palanca esquerra + + + Config Selection + Configura la selecció + + + Common Config + Configuració estàndard + + + Use per-game configs + Fes servir configuracions per cada joc + + + L1 + L1 + + + L2 + L2 + + + Text Editor + Editor de text + + + Help + Ajuda + + + R1 + R1 + + + R2 + R2 + + + L3 + L3 + + + Touchpad Click + Click al touchpad + + + Mouse to Joystick + Ratolí a palanca + + + *press F7 ingame to activate + *press F7 ingame to activate + + + R3 + R3 + + + Options + Opcions + + + Mouse Movement Parameters + Mouse Movement Parameters + + + note: click Help Button/Special Keybindings for more information + note: click Help Button/Special Keybindings for more information + + + Face Buttons + Botons d'acció + + + Triangle + Triangle + + + Square + Quadrat + + + Circle + Cercle + + + Cross + Creu + + + Right Analog Halfmode + Mode reduït de la palanca dreta + + + hold to move right stick at half-speed + hold to move right stick at half-speed + + + Right Stick + Palanca dreta + + + Speed Offset (def 0.125): + Speed Offset (def 0.125): + + + Copy from Common Config + Copy from Common Config + + + Deadzone Offset (def 0.50): + Deadzone Offset (def 0.50): + + + Speed Multiplier (def 1.0): + Speed Multiplier (def 1.0): + + + Common Config Selected + Configuració estàndard seleccionada + + + This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. + This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. + + + Copy values from Common Config + Copy values from Common Config + + + Do you want to overwrite existing mappings with the mappings from the Common Config? + Do you want to overwrite existing mappings with the mappings from the Common Config? + + + Unable to Save + No s'ha pogut desar + + + Cannot bind any unique input more than once + Cannot bind any unique input more than once + + + Press a key + Premeu una tecla + + + Cannot set mapping + No s'ha pogut fer l'assignació + + + Mousewheel cannot be mapped to stick outputs + Mousewheel cannot be mapped to stick outputs + + + Save + Desa + + + Apply + Aplica + + + Restore Defaults + Restaura els valors per defecte + + + Cancel + Cancel·la + + + + MainWindow + + Open/Add Elf Folder + Obre/Afegeix la carpeta Elf + + + Boot Game + Executa el joc + + + Check for Updates + Comprova si hi ha actualitzacions + + + About shadPS4 + Sobre shadPS4 + + + Configure... + Configura... + + + Recent Games + Jocs recents + + + Open shadPS4 Folder + Obre la carpeta de shadPS4 + + + Exit + Sortida + + + Exit shadPS4 + Surt de shadPS4 + + + Exit the application. + Surt de l'aplicació. + + + Show Game List + Mostra la llista de jocs + + + Game List Refresh + Actualitza la llista de jocs + + + Tiny + Molt petita + + + Small + Petita + + + Medium + Mitjà + + + Large + Gran + + + List View + Visualització en llista + + + Grid View + Visualització en graella + + + Elf Viewer + Visualitzador Elf + + + Game Install Directory + Carpeta d'instal·lació de jocs + + + Download Cheats/Patches + Download Cheats/Patches + + + Dump Game List + Aboca la llista de jocs + + + Trophy Viewer + Visualitzador de trofeus + + + No games found. Please add your games to your library first. + No games found. Please add your games to your library first. + + + Search... + Cerca... + + + File + Fitxer + + + View + Visualitza + + + Game List Icons + Icones de la llista de jocs + + + Game List Mode + Mode de la llista de jocs + + + Settings + Configuració + + + Utils + Útils + + + Themes + Temes + + + Help + Ajuda + + + Dark + Fosc + + + Light + Clar + + + Green + Verd + + + Blue + Blau + + + Violet + Violeta + + + toolBar + Barra d'eines + + + Game List + Llista de jocs + + + Download Cheats For All Installed Games + Download Cheats For All Installed Games + + + Download Patches For All Games + Download Patches For All Games + + + Download Complete + Descàrrega completa + + + You have downloaded cheats for all the games you have installed. + You have downloaded cheats for all the games you have installed. + + + Patches Downloaded Successfully! + Patches Downloaded Successfully! + + + All Patches available for all games have been downloaded. + All Patches available for all games have been downloaded. + + + Games: + Jocs: + + + ELF files (*.bin *.elf *.oelf) + ELF files (*.bin *.elf *.oelf) + + + Game Boot + Executa el joc + + + Only one file can be selected! + Only one file can be selected! + + + Run Game + Executa el joc + + + Eboot.bin file not found + Eboot.bin file not found + + + Game is already running! + Game is already running! + + + shadPS4 + shadPS4 + + + Play + Reprodueix + + + Pause + Pausa + + + Stop + Atura + + + Restart + Reinicia + + + Full Screen + Pantalla completa + + + Controllers + Controladors + + + Keyboard + Teclat + + + Refresh List + Actualitza la llista + + + Resume + Reprendre + + + Show Labels Under Icons + Show Labels Under Icons + + + + SettingsDialog + + Settings + Configuració + + + General + General + + + System + Sistema + + + Console Language + Idioma de la consola + + + Emulator Language + Idioma de l'emulador + + + Emulator + Emulador + + + Default tab when opening settings + Default tab when opening settings + + + Show Game Size In List + Mostra la mida del joc a la llista + + + Show Splash + Mostra missatge de benvinguda + + + Enable Discord Rich Presence + Enable Discord Rich Presence + + + Username + Nom d’usuari + + + Trophy Key + Clau dels trofeus + + + Trophy + Trofeu + + + Open the custom trophy images/sounds folder + Open the custom trophy images/sounds folder + + + Logger + Registre + + + Log Type + Registre de tipus + + + Log Filter + Filtre del registre + + + Open Log Location + Obre la ubicació del registre + + + Input + Entrada + + + Cursor + Cursor + + + Hide Cursor + Amaga el ratolí + + + Hide Cursor Idle Timeout + Hide Cursor Idle Timeout + + + s + s + + + Controller + Controlador + + + Back Button Behavior + Comportament del botó de retrocés + + + Graphics + Gràfics + + + GUI + Interfície gràfica + + + User + Usuari + + + Graphics Device + Dispositiu de gràfics + + + Vblank Divider + Divisor Vblank + + + Advanced + Avançat + + + Enable Shaders Dumping + Habilita l'abocat de shaders + + + Enable NULL GPU + Activa NULL GPU + + + Enable HDR + Activa el HDR + + + Paths + Camins + + + Game Folders + Carpetes dels jocs + + + Add... + Afegir... + + + Remove + Suprimeix + + + Debug + Depuració + + + Enable Debug Dumping + Activa l'abocat de depuració + + + Enable Vulkan Validation Layers + Enable Vulkan Validation Layers + + + Enable Vulkan Synchronization Validation + Enable Vulkan Synchronization Validation + + + Enable RenderDoc Debugging + Enable RenderDoc Debugging + + + Enable Crash Diagnostics + Enable Crash Diagnostics + + + Collect Shaders + Recopila Shaders + + + Copy GPU Buffers + Copia la memòria intermèdia de la GPU + + + Host Debug Markers + Marcardors de depuració + + + Guest Debug Markers + Marcadors de depuració + + + Update + Actualitza + + + Check for Updates at Startup + Check for Updates at Startup + + + Always Show Changelog + Mostra sempre el registre de canvis + + + Update Channel + Actualitza el canal + + + Check for Updates + Comprova si hi ha actualitzacions + + + GUI Settings + Configuració de la interfície + + + Title Music + Música de títol + + + Disable Trophy Notification + Disable Trophy Notification + + + Background Image + Imatge de fons + + + Show Background Image + Mostra imatge de fons + + + Opacity + Opacitat + + + Play title music + Reprodueix la música del títol + + + Update Compatibility Database On Startup + Update Compatibility Database On Startup + + + Game Compatibility + Compatibilitat dels jocs + + + Display Compatibility Data + Display Compatibility Data + + + Update Compatibility Database + Update Compatibility Database + + + Volume + Volum + + + Save + Desa + + + Apply + Aplica + + + Restore Defaults + Restaura els valors per defecte + + + Close + Tanca + + + Point your mouse at an option to display its description. + Point your mouse at an option to display its description. + + + Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. + Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. + + + Emulator Language:\nSets the language of the emulator's user interface. + Emulator Language:\nSets the language of the emulator's user interface. + + + Show Splash Screen:\nShows the game's splash screen (a special image) while the game is starting. + Show Splash Screen:\nShows the game's splash screen (a special image) while the game is starting. + + + Enable Discord Rich Presence:\nDisplays the emulator icon and relevant information on your Discord profile. + Enable Discord Rich Presence:\nDisplays the emulator icon and relevant information on your Discord profile. + + + Username:\nSets the PS4's account username, which may be displayed by some games. + Username:\nSets the PS4's account username, which may be displayed by some games. + + + Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. + Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. + + + Log Type:\nSets whether to synchronize the output of the log window for performance. May have adverse effects on emulation. + Log Type:\nSets whether to synchronize the output of the log window for performance. May have adverse effects on emulation. + + + Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. + Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. + + + Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. + Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. + + + Background Image:\nControl the opacity of the game background image. + Background Image:\nControl the opacity of the game background image. + + + Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. + Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. + + + Disable Trophy Pop-ups:\nDisable in-game trophy notifications. Trophy progress can still be tracked using the Trophy Viewer (right-click the game in the main window). + Disable Trophy Pop-ups:\nDisable in-game trophy notifications. Trophy progress can still be tracked using the Trophy Viewer (right-click the game in the main window). + + + Hide Cursor:\nChoose when the cursor will disappear:\nNever: You will always see the mouse.\nidle: Set a time for it to disappear after being idle.\nAlways: you will never see the mouse. + Hide Cursor:\nChoose when the cursor will disappear:\nNever: You will always see the mouse.\nidle: Set a time for it to disappear after being idle.\nAlways: you will never see the mouse. + + + Hide Idle Cursor Timeout:\nThe duration (seconds) after which the cursor that has been idle hides itself. + Hide Idle Cursor Timeout:\nThe duration (seconds) after which the cursor that has been idle hides itself. + + + Back Button Behavior:\nSets the controller's back button to emulate tapping the specified position on the PS4 touchpad. + Back Button Behavior:\nSets the controller's back button to emulate tapping the specified position on the PS4 touchpad. + + + Display Compatibility Data:\nDisplays game compatibility information in table view. Enable "Update Compatibility On Startup" to get up-to-date information. + Display Compatibility Data:\nDisplays game compatibility information in table view. Enable "Update Compatibility On Startup" to get up-to-date information. + + + Update Compatibility On Startup:\nAutomatically update the compatibility database when shadPS4 starts. + Update Compatibility On Startup:\nAutomatically update the compatibility database when shadPS4 starts. + + + Update Compatibility Database:\nImmediately update the compatibility database. + Update Compatibility Database:\nImmediately update the compatibility database. + + + Never + Mai + + + Idle + Inactiu + + + Always + Sempre + + + Touchpad Left + Touchpad esquerra + + + Touchpad Right + Touchpad dret + + + Touchpad Center + Centre del Touchpad + + + None + Cap + + + Graphics Device:\nOn multiple GPU systems, select the GPU the emulator will use from the drop down list,\nor select "Auto Select" to automatically determine it. + Graphics Device:\nOn multiple GPU systems, select the GPU the emulator will use from the drop down list,\nor select "Auto Select" to automatically determine it. + + + Width/Height:\nSets the size of the emulator window at launch, which can be resized during gameplay.\nThis is different from the in-game resolution. + Width/Height:\nSets the size of the emulator window at launch, which can be resized during gameplay.\nThis is different from the in-game resolution. + + + Vblank Divider:\nThe frame rate at which the emulator refreshes at is multiplied by this number. Changing this may have adverse effects, such as increasing the game speed, or breaking critical game functionality that does not expect this to change! + Vblank Divider:\nThe frame rate at which the emulator refreshes at is multiplied by this number. Changing this may have adverse effects, such as increasing the game speed, or breaking critical game functionality that does not expect this to change! + + + Enable Shaders Dumping:\nFor the sake of technical debugging, saves the games shaders to a folder as they render. + Enable Shaders Dumping:\nFor the sake of technical debugging, saves the games shaders to a folder as they render. + + + Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. + Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. + + + Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. + Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. + + + Game Folders:\nThe list of folders to check for installed games. + Game Folders:\nThe list of folders to check for installed games. + + + Add:\nAdd a folder to the list. + Add:\nAdd a folder to the list. + + + Remove:\nRemove a folder from the list. + Remove:\nRemove a folder from the list. + + + Enable Debug Dumping:\nSaves the import and export symbols and file header information of the currently running PS4 program to a directory. + Enable Debug Dumping:\nSaves the import and export symbols and file header information of the currently running PS4 program to a directory. + + + Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. + Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. + + + Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. + Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. + + + Enable RenderDoc Debugging:\nIf enabled, the emulator will provide compatibility with Renderdoc to allow capture and analysis of the currently rendered frame. + Enable RenderDoc Debugging:\nIf enabled, the emulator will provide compatibility with Renderdoc to allow capture and analysis of the currently rendered frame. + + + Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + + + Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. + Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. + + + Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. + Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. + + + Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + + + Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + + + Save Data Path:\nThe folder where game save data will be saved. + Save Data Path:\nThe folder where game save data will be saved. + + + Browse:\nBrowse for a folder to set as the save data path. + Browse:\nBrowse for a folder to set as the save data path. + + + Release + Publicació + + + Nightly + Publicació diària + + + Set the volume of the background music. + Set the volume of the background music. + + + Enable Motion Controls + Habilita els controls de moviment + + + Save Data Path + Desa la ruta a les dades + + + Browse + Navega + + + async + asíncron + + + sync + sincronitzar + + + Auto Select + Selecciona automàticament + + + Directory to install games + Directory to install games + + + Directory to save data + Directory to save data + + + Video + Vídeo + + + Display Mode + Mode de visualització + + + Windowed + En finestra + + + Fullscreen + Pantalla completa + + + Fullscreen (Borderless) + Fullscreen (Borderless) + + + Window Size + Mida de la finestra + + + W: + W: + + + H: + H: + + + Separate Log Files + Fitxers de registre independents + + + Separate Log Files:\nWrites a separate logfile for each game. + Separate Log Files:\nWrites a separate logfile for each game. + + + Trophy Notification Position + Trophy Notification Position + + + Left + Esquerra + + + Right + Dreta + + + Top + Amunt + + + Bottom + Sota + + + Notification Duration + Duració de les notificacions + + + Portable User Folder + Carpeta de l'usuari portàtil + + + Create Portable User Folder from Common User Folder + Create Portable User Folder from Common User Folder + + + Portable user folder:\nStores shadPS4 settings and data that will be applied only to the shadPS4 build located in the current folder. Restart the app after creating the portable user folder to begin using it. + Portable user folder:\nStores shadPS4 settings and data that will be applied only to the shadPS4 build located in the current folder. Restart the app after creating the portable user folder to begin using it. + + + Cannot create portable user folder + Cannot create portable user folder + + + %1 already exists + %1 ja existeix + + + Portable user folder created + Portable user folder created + + + %1 successfully created. + %1 successfully created. + + + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + + + + TrophyViewer + + Trophy Viewer + Visualitzador de trofeus + + + Select Game: + Selecciona un joc: + + + Progress + Progrés + + + Show Earned Trophies + Mostra els trofeus aconseguits + + + Show Not Earned Trophies + Show Not Earned Trophies + + + Show Hidden Trophies + Mostra els trofeus ocults + + + diff --git a/src/qt_gui/translations/fa_IR.ts b/src/qt_gui/translations/fa_IR.ts index b9c2282fa..115632444 100644 --- a/src/qt_gui/translations/fa_IR.ts +++ b/src/qt_gui/translations/fa_IR.ts @@ -26,7 +26,7 @@ Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n - Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + تقلب‌ها/پچ‌ها آزمایشی هستند.\n با احتیاط استفاده کنید.\n\n با انتخاب مخزن و کلیک روی دکمه دانلود، تقلب‌ها را به‌صورت جداگانه دانلود کنید.\n در تب پچ‌ها، می‌توانید همه پچ‌ها را به‌طور همزمان دانلود کنید، انتخاب کنید که می‌خواهید از کدام استفاده کنید و انتخاب خود را ذخیره کنید.\n\n از آنجایی که ما تقلب‌ها/پچ‌ها را توسعه نمی‌دهیم،\n لطفاً مشکلات را به نویسنده تقلب گزارش دهید.\n\n تقلب جدیدی ایجاد کرده‌اید؟ به این صفحه مراجعه کنید: \n No Image Available @@ -214,7 +214,7 @@ XML ERROR: - XML ERROR: + XML خطای : Failed to open files.json for writing @@ -407,43 +407,43 @@ ControlSettings Configure Controls - Configure Controls + پیکربندی دسته ها D-Pad - D-Pad + D-Pad Up - Up + بالا Left - Left + چپ Right - Right + راست Down - Down + پایین Left Stick Deadzone (def:2 max:127) - Left Stick Deadzone (def:2 max:127) + منطقه‌ی حساس به حرکت چپ (def:2 max:127) Left Deadzone - Left Deadzone + منطقه مرده چپ Left Stick - Left Stick + جواستیک چپ Config Selection - Config Selection + انتخاب پیکربندی Common Config @@ -451,7 +451,7 @@ Use per-game configs - Use per-game configs + از پیکربندی‌های مخصوص هر بازی استفاده کنید L1 / LB @@ -483,7 +483,7 @@ R3 - R3 + R3 Face Buttons @@ -491,7 +491,7 @@ Triangle / Y - Triangle / Y + مثلث / Y Square / X @@ -531,7 +531,7 @@ B: - B: + B: Override Lightbar Color @@ -543,7 +543,7 @@ Unable to Save - Unable to Save + ذخیره امکان پذیر نیست Cannot bind axis values more than once @@ -570,7 +570,7 @@ EditorDialog Edit Keyboard + Mouse and Controller input bindings - Edit Keyboard + Mouse and Controller input bindings + تغییر دکمه های کیبرد + ماوس و دسته Use Per-Game configs @@ -582,7 +582,7 @@ Could not open the file for reading - Could not open the file for reading + نمی تواند فایل را برای خواندن باز کند Could not open the file for writing @@ -602,7 +602,7 @@ Do you want to reset your custom default config to the original default config? - Do you want to reset your custom default config to the original default config? + آیا می‌خواهید پیکربندی سفارشی خود را به پیکربندی پیش‌فرض اصلی بازگردانید ؟ Do you want to reset this config to your custom default config? @@ -860,7 +860,7 @@ View report - View report + مشاهده گزارش Submit a report @@ -916,11 +916,11 @@ Delete Save Data - Delete Save Data + پاک کردن داده های ذخیره شده This game has no update folder to open! - This game has no update folder to open! + این بازی هیچ پوشه‌ی به‌روزرسانی برای باز کردن ندارد! No log file found for this game! @@ -948,7 +948,7 @@ SFO Viewer for - SFO Viewer for + SFO مشاهده @@ -986,7 +986,7 @@ Up - Up + unmapped @@ -1058,7 +1058,7 @@ Touchpad Click - Touchpad Click + کلیک روی تاچ‌پد Mouse to Joystick @@ -1078,7 +1078,7 @@ Mouse Movement Parameters - Mouse Movement Parameters + note: click Help Button/Special Keybindings for more information @@ -1102,7 +1102,7 @@ Cross - Cross + ضربدر Right Analog Halfmode @@ -1122,7 +1122,7 @@ Copy from Common Config - Copy from Common Config + کپی از پیکربندی مشترک Deadzone Offset (def 0.50): @@ -1130,23 +1130,23 @@ Speed Multiplier (def 1.0): - Speed Multiplier (def 1.0): + ضریب سرعت (def 1.0): Common Config Selected - Common Config Selected + پیکربندی مشترک انتخاب شده This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. - This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. + این دکمه نگاشت‌ها را از پیکربندی مشترک به پروفایل انتخاب‌شده‌ی فعلی کپی می‌کند و وقتی پروفایل انتخاب‌شده‌ی فعلی پیکربندی مشترک باشد، نمی‌توان از آن استفاده کرد. Copy values from Common Config - Copy values from Common Config + کپی کردن مقادیر از پیکربندی مشترک Do you want to overwrite existing mappings with the mappings from the Common Config? - Do you want to overwrite existing mappings with the mappings from the Common Config? + آیا می‌خواهید نگاشت‌های موجود را با نگاشت‌های پیکربندی مشترک جایگزین کنید؟ Unable to Save @@ -1170,7 +1170,7 @@ Save - Save + ذخیره‌سازی Apply @@ -1213,7 +1213,7 @@ Open shadPS4 Folder - Open shadPS4 Folder + پوشه shadPS4 را باز کنید Exit @@ -1624,7 +1624,7 @@ Collect Shaders - Collect Shaders + جمع آوری شیدرها Copy GPU Buffers @@ -1664,7 +1664,7 @@ Title Music - Title Music + Disable Trophy Notification @@ -1728,7 +1728,7 @@ Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. - Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. + زبان کنسول:\nزبانی را که بازی PS4 استفاده می‌کند تنظیم می‌کند.\nتوصیه می‌شود این را روی زبانی که بازی پشتیبانی می‌کند تنظیم کنید، که بسته به منطقه متفاوت خواهد بود. Emulator Language:\nSets the language of the emulator's user interface. @@ -1748,7 +1748,7 @@ Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. - Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. + کلید تروفی:\و کلیدی که برای رمزگشایی تروفی‌ها استفاده می‌شود. باید از کنسول جیلبریک شده شما دریافت شود.\باید فقط شامل کاراکترهای هگز باشد. Log Type:\nSets whether to synchronize the output of the log window for performance. May have adverse effects on emulation. @@ -1756,7 +1756,7 @@ Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. - Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. + فیلتر گزارش:\nگزارش را فیلتر می‌کند تا فقط اطلاعات خاصی چاپ شود.\nمثال‌ها: "هسته:ردیابی" "Lib.Pad:اشکال‌زدایی Common.Filesystem:خطا" "*:بحرانی"\nسطوح: ردیابی، اشکال‌زدایی، اطلاعات، هشدار، خطا، بحرانی - به این ترتیب، یک سطح خاص تمام سطوح قبل از خود را در لیست بی‌صدا می‌کند و هر سطح بعد از خود را ثبت می‌کند. Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. @@ -1764,7 +1764,7 @@ Background Image:\nControl the opacity of the game background image. - Background Image:\nControl the opacity of the game background image. + تصویر پس‌زمینه: میزان شفافیت تصویر پس‌زمینه بازی را کنترل کنید. Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. @@ -1844,11 +1844,11 @@ Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. - Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. + فعال کردن پردازنده گرافیکی خالی:\برای رفع اشکال فنی، رندر بازی را طوری غیرفعال کنید که انگار هیچ کارت گرافیکی وجود ندارد. Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. - Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. + فعال کردن HDR و :\n این گزینه HDR را در بازی‌هایی که از آن پشتیبانی می‌کنند فعال می‌کند.\n مانیتور شما باید از فضای رنگی BT2020 PQ و فرمت swapchain RGB10A2 پشتیبانی کند. Game Folders:\nThe list of folders to check for installed games. @@ -1860,7 +1860,7 @@ Remove:\nRemove a folder from the list. - حذف:\nیک پوشه را از لیست حذف کنید. + حذف:\n یک پوشه را از لیست حذف کنید. Enable Debug Dumping:\nSaves the import and export symbols and file header information of the currently running PS4 program to a directory. @@ -1868,11 +1868,11 @@ Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. - Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. + فعال کردن لایه‌های اعتبارسنجی Vulkan: \nسیستمی را فعال می‌کند که وضعیت رندرکننده Vulkan را اعتبارسنجی کرده و اطلاعات مربوط به وضعیت داخلی آن را ثبت می‌کند.\n این کار باعث کاهش عملکرد و احتمالاً تغییر رفتار شبیه‌سازی می‌شود. Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. - Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. + فعال کردن اعتبارسنجی همگام‌سازی Vulkan: \nسیستمی را فعال می‌کند که زمان‌بندی وظایف رندر Vulkan را اعتبارسنجی می‌کند.\n این کار باعث کاهش عملکرد و احتمالاً تغییر رفتار شبیه‌سازی می‌شود. Enable RenderDoc Debugging:\nIf enabled, the emulator will provide compatibility with Renderdoc to allow capture and analysis of the currently rendered frame. @@ -1880,7 +1880,7 @@ Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). - Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + جمع‌آوری سایه‌زن‌ها:\n برای ویرایش سایه‌زن‌ها با منوی اشکال‌زدایی (Ctrl + F10) باید این گزینه فعال باشد. Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. @@ -1912,7 +1912,7 @@ Nightly - Nightly + اخرین نسخه شبانه Set the volume of the background music. @@ -1936,7 +1936,7 @@ sync - sync + همزمان Auto Select @@ -2000,7 +2000,7 @@ Right - Right + راست Top @@ -2032,7 +2032,7 @@ %1 already exists - %1 already exists + %1 از قبل وجود دارد Portable user folder created @@ -2044,7 +2044,7 @@ Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. - Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + پوشه تصاویر/صداهای تروفی سفارشی را باز کنید:\n شما می‌توانید تصاویر و صدای سفارشی به تروفی‌ها اضافه کنید.\n فایل‌ها را با نام‌های زیر به custom_trophy اضافه کنید:\ntrophy.wav یا trophy.mp3، bronze.png، gold.png، platinum.png، silver.png \nتوجه: صدا فقط در نسخه‌های QT کار می‌کند. * Unsupported Vulkan Version @@ -2075,7 +2075,7 @@ Show Hidden Trophies - Show Hidden Trophies + نمایش جوایز مخفی diff --git a/src/qt_gui/translations/sr_CS.ts b/src/qt_gui/translations/sr_CS.ts new file mode 100644 index 000000000..4277728e6 --- /dev/null +++ b/src/qt_gui/translations/sr_CS.ts @@ -0,0 +1,2081 @@ + + + + + + AboutDialog + + About shadPS4 + About shadPS4 + + + shadPS4 is an experimental open-source emulator for the PlayStation 4. + shadPS4 is an experimental open-source emulator for the PlayStation 4. + + + This software should not be used to play games you have not legally obtained. + This software should not be used to play games you have not legally obtained. + + + + CheatsPatches + + Cheats / Patches for + Cheats / Patches for + + + Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + Cheats/Patches are experimental.\nUse with caution.\n\nDownload cheats individually by selecting the repository and clicking the download button.\nIn the Patches tab, you can download all patches at once, choose which ones you want to use, and save your selection.\n\nSince we do not develop the Cheats/Patches,\nplease report issues to the cheat author.\n\nCreated a new cheat? Visit:\n + + + No Image Available + No Image Available + + + Serial: + Serial: + + + Version: + Version: + + + Size: + Size: + + + Select Cheat File: + Select Cheat File: + + + Repository: + Repository: + + + Download Cheats + Download Cheats + + + Delete File + Delete File + + + No files selected. + No files selected. + + + You can delete the cheats you don't want after downloading them. + You can delete the cheats you don't want after downloading them. + + + Do you want to delete the selected file?\n%1 + Do you want to delete the selected file?\n%1 + + + Select Patch File: + Select Patch File: + + + Download Patches + Download Patches + + + Save + Save + + + Cheats + Cheats + + + Patches + Patches + + + Error + Error + + + No patch selected. + No patch selected. + + + Unable to open files.json for reading. + Unable to open files.json for reading. + + + No patch file found for the current serial. + No patch file found for the current serial. + + + Unable to open the file for reading. + Unable to open the file for reading. + + + Unable to open the file for writing. + Unable to open the file for writing. + + + Failed to parse XML: + Failed to parse XML: + + + Success + Success + + + Options saved successfully. + Options saved successfully. + + + Invalid Source + Invalid Source + + + The selected source is invalid. + The selected source is invalid. + + + File Exists + File Exists + + + File already exists. Do you want to replace it? + File already exists. Do you want to replace it? + + + Failed to save file: + Failed to save file: + + + Failed to download file: + Failed to download file: + + + Cheats Not Found + Cheats Not Found + + + No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. + No Cheats found for this game in this version of the selected repository,try another repository or a different version of the game. + + + Cheats Downloaded Successfully + Cheats Downloaded Successfully + + + You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. + You have successfully downloaded the cheats for this version of the game from the selected repository. You can try downloading from another repository, if it is available it will also be possible to use it by selecting the file from the list. + + + Failed to save: + Failed to save: + + + Failed to download: + Failed to download: + + + Download Complete + Download Complete + + + Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. + Patches Downloaded Successfully! All Patches available for all games have been downloaded, there is no need to download them individually for each game as happens in Cheats. If the patch does not appear, it may be that it does not exist for the specific serial and version of the game. + + + Failed to parse JSON data from HTML. + Failed to parse JSON data from HTML. + + + Failed to retrieve HTML page. + Failed to retrieve HTML page. + + + The game is in version: %1 + The game is in version: %1 + + + The downloaded patch only works on version: %1 + The downloaded patch only works on version: %1 + + + You may need to update your game. + You may need to update your game. + + + Incompatibility Notice + Incompatibility Notice + + + Failed to open file: + Failed to open file: + + + XML ERROR: + XML ERROR: + + + Failed to open files.json for writing + Failed to open files.json for writing + + + Author: + Author: + + + Directory does not exist: + Directory does not exist: + + + Failed to open files.json for reading. + Failed to open files.json for reading. + + + Name: + Name: + + + Can't apply cheats before the game is started + Can't apply cheats before the game is started + + + Close + Close + + + + CheckUpdate + + Auto Updater + Auto Updater + + + Error + Error + + + Network error: + Network error: + + + The Auto Updater allows up to 60 update checks per hour.\nYou have reached this limit. Please try again later. + The Auto Updater allows up to 60 update checks per hour.\nYou have reached this limit. Please try again later. + + + Failed to parse update information. + Failed to parse update information. + + + No pre-releases found. + No pre-releases found. + + + Invalid release data. + Invalid release data. + + + No download URL found for the specified asset. + No download URL found for the specified asset. + + + Your version is already up to date! + Your version is already up to date! + + + Update Available + Update Available + + + Update Channel + Update Channel + + + Current Version + Current Version + + + Latest Version + Latest Version + + + Do you want to update? + Do you want to update? + + + Show Changelog + Show Changelog + + + Check for Updates at Startup + Check for Updates at Startup + + + Update + Update + + + No + No + + + Hide Changelog + Hide Changelog + + + Changes + Changes + + + Network error occurred while trying to access the URL + Network error occurred while trying to access the URL + + + Download Complete + Download Complete + + + The update has been downloaded, press OK to install. + The update has been downloaded, press OK to install. + + + Failed to save the update file at + Failed to save the update file at + + + Starting Update... + Starting Update... + + + Failed to create the update script file + Failed to create the update script file + + + + CompatibilityInfoClass + + Fetching compatibility data, please wait + Fetching compatibility data, please wait + + + Cancel + Cancel + + + Loading... + Loading... + + + Error + Error + + + Unable to update compatibility data! Try again later. + Unable to update compatibility data! Try again later. + + + Unable to open compatibility_data.json for writing. + Unable to open compatibility_data.json for writing. + + + Unknown + Unknown + + + Nothing + Nothing + + + Boots + Boots + + + Menus + Menus + + + Ingame + Ingame + + + Playable + Playable + + + + ControlSettings + + Configure Controls + Configure Controls + + + D-Pad + D-Pad + + + Up + Up + + + Left + Left + + + Right + Right + + + Down + Down + + + Left Stick Deadzone (def:2 max:127) + Left Stick Deadzone (def:2 max:127) + + + Left Deadzone + Left Deadzone + + + Left Stick + Left Stick + + + Config Selection + Config Selection + + + Common Config + Common Config + + + Use per-game configs + Use per-game configs + + + L1 / LB + L1 / LB + + + L2 / LT + L2 / LT + + + Back + Back + + + R1 / RB + R1 / RB + + + R2 / RT + R2 / RT + + + L3 + L3 + + + Options / Start + Options / Start + + + R3 + R3 + + + Face Buttons + Face Buttons + + + Triangle / Y + Triangle / Y + + + Square / X + Square / X + + + Circle / B + Circle / B + + + Cross / A + Cross / A + + + Right Stick Deadzone (def:2, max:127) + Right Stick Deadzone (def:2, max:127) + + + Right Deadzone + Right Deadzone + + + Right Stick + Right Stick + + + Color Adjustment + Color Adjustment + + + R: + R: + + + G: + G: + + + B: + B: + + + Override Lightbar Color + Override Lightbar Color + + + Override Color + Override Color + + + Unable to Save + Unable to Save + + + Cannot bind axis values more than once + Cannot bind axis values more than once + + + Save + Save + + + Apply + Apply + + + Restore Defaults + Restore Defaults + + + Cancel + Cancel + + + + EditorDialog + + Edit Keyboard + Mouse and Controller input bindings + Edit Keyboard + Mouse and Controller input bindings + + + Use Per-Game configs + Use Per-Game configs + + + Error + Error + + + Could not open the file for reading + Could not open the file for reading + + + Could not open the file for writing + Could not open the file for writing + + + Save Changes + Save Changes + + + Do you want to save changes? + Do you want to save changes? + + + Help + Help + + + Do you want to reset your custom default config to the original default config? + Do you want to reset your custom default config to the original default config? + + + Do you want to reset this config to your custom default config? + Do you want to reset this config to your custom default config? + + + Reset to Default + Reset to Default + + + + ElfViewer + + Open Folder + Open Folder + + + + GameInfoClass + + Loading game list, please wait :3 + Loading game list, please wait :3 + + + Cancel + Cancel + + + Loading... + Loading... + + + + GameInstallDialog + + shadPS4 - Choose directory + shadPS4 - Choose directory + + + Directory to install games + Directory to install games + + + Browse + Browse + + + Error + Error + + + Directory to install DLC + Directory to install DLC + + + + GameListFrame + + Icon + Icon + + + Name + Name + + + Serial + Serial + + + Compatibility + Compatibility + + + Region + Region + + + Firmware + Firmware + + + Size + Size + + + Version + Version + + + Path + Path + + + Play Time + Play Time + + + Never Played + Never Played + + + h + h + + + m + m + + + s + s + + + Compatibility is untested + Compatibility is untested + + + Game does not initialize properly / crashes the emulator + Game does not initialize properly / crashes the emulator + + + Game boots, but only displays a blank screen + Game boots, but only displays a blank screen + + + Game displays an image but does not go past the menu + Game displays an image but does not go past the menu + + + Game has game-breaking glitches or unplayable performance + Game has game-breaking glitches or unplayable performance + + + Game can be completed with playable performance and no major glitches + Game can be completed with playable performance and no major glitches + + + Click to see details on github + Click to see details on github + + + Last updated + Last updated + + + + GameListUtils + + B + B + + + KB + KB + + + MB + MB + + + GB + GB + + + TB + TB + + + + GuiContextMenus + + Create Shortcut + Create Shortcut + + + Cheats / Patches + Cheats / Patches + + + SFO Viewer + SFO Viewer + + + Trophy Viewer + Trophy Viewer + + + Open Folder... + Open Folder... + + + Open Game Folder + Open Game Folder + + + Open Save Data Folder + Open Save Data Folder + + + Open Log Folder + Open Log Folder + + + Copy info... + Copy info... + + + Copy Name + Copy Name + + + Copy Serial + Copy Serial + + + Copy Version + Copy Version + + + Copy Size + Copy Size + + + Copy All + Copy All + + + Delete... + Delete... + + + Delete Game + Delete Game + + + Delete Update + Delete Update + + + Delete DLC + Delete DLC + + + Delete Trophy + Delete Trophy + + + Compatibility... + Compatibility... + + + Update database + Update database + + + View report + View report + + + Submit a report + Submit a report + + + Shortcut creation + Shortcut creation + + + Shortcut created successfully! + Shortcut created successfully! + + + Error + Error + + + Error creating shortcut! + Error creating shortcut! + + + Game + Game + + + This game has no update to delete! + This game has no update to delete! + + + Update + Update + + + This game has no DLC to delete! + This game has no DLC to delete! + + + DLC + DLC + + + Delete %1 + Delete %1 + + + Are you sure you want to delete %1's %2 directory? + Are you sure you want to delete %1's %2 directory? + + + Open Update Folder + Open Update Folder + + + Delete Save Data + Delete Save Data + + + This game has no update folder to open! + This game has no update folder to open! + + + No log file found for this game! + No log file found for this game! + + + Failed to convert icon. + Failed to convert icon. + + + This game has no save data to delete! + This game has no save data to delete! + + + This game has no saved trophies to delete! + This game has no saved trophies to delete! + + + Save Data + Save Data + + + Trophy + Trophy + + + SFO Viewer for + SFO Viewer for + + + + HelpDialog + + Quickstart + Quickstart + + + FAQ + FAQ + + + Syntax + Syntax + + + Special Bindings + Special Bindings + + + Keybindings + Keybindings + + + + KBMSettings + + Configure Controls + Configure Controls + + + D-Pad + D-Pad + + + Up + Up + + + unmapped + unmapped + + + Left + Left + + + Right + Right + + + Down + Down + + + Left Analog Halfmode + Left Analog Halfmode + + + hold to move left stick at half-speed + hold to move left stick at half-speed + + + Left Stick + Left Stick + + + Config Selection + Config Selection + + + Common Config + Common Config + + + Use per-game configs + Use per-game configs + + + L1 + L1 + + + L2 + L2 + + + Text Editor + Text Editor + + + Help + Help + + + R1 + R1 + + + R2 + R2 + + + L3 + L3 + + + Touchpad Click + Touchpad Click + + + Mouse to Joystick + Mouse to Joystick + + + *press F7 ingame to activate + *press F7 ingame to activate + + + R3 + R3 + + + Options + Options + + + Mouse Movement Parameters + Mouse Movement Parameters + + + note: click Help Button/Special Keybindings for more information + note: click Help Button/Special Keybindings for more information + + + Face Buttons + Face Buttons + + + Triangle + Triangle + + + Square + Square + + + Circle + Circle + + + Cross + Cross + + + Right Analog Halfmode + Right Analog Halfmode + + + hold to move right stick at half-speed + hold to move right stick at half-speed + + + Right Stick + Right Stick + + + Speed Offset (def 0.125): + Speed Offset (def 0.125): + + + Copy from Common Config + Copy from Common Config + + + Deadzone Offset (def 0.50): + Deadzone Offset (def 0.50): + + + Speed Multiplier (def 1.0): + Speed Multiplier (def 1.0): + + + Common Config Selected + Common Config Selected + + + This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. + This button copies mappings from the Common Config to the currently selected profile, and cannot be used when the currently selected profile is the Common Config. + + + Copy values from Common Config + Copy values from Common Config + + + Do you want to overwrite existing mappings with the mappings from the Common Config? + Do you want to overwrite existing mappings with the mappings from the Common Config? + + + Unable to Save + Unable to Save + + + Cannot bind any unique input more than once + Cannot bind any unique input more than once + + + Press a key + Press a key + + + Cannot set mapping + Cannot set mapping + + + Mousewheel cannot be mapped to stick outputs + Mousewheel cannot be mapped to stick outputs + + + Save + Save + + + Apply + Apply + + + Restore Defaults + Restore Defaults + + + Cancel + Cancel + + + + MainWindow + + Open/Add Elf Folder + Open/Add Elf Folder + + + Boot Game + Boot Game + + + Check for Updates + Check for Updates + + + About shadPS4 + About shadPS4 + + + Configure... + Configure... + + + Recent Games + Recent Games + + + Open shadPS4 Folder + Open shadPS4 Folder + + + Exit + Exit + + + Exit shadPS4 + Exit shadPS4 + + + Exit the application. + Exit the application. + + + Show Game List + Show Game List + + + Game List Refresh + Game List Refresh + + + Tiny + Tiny + + + Small + Small + + + Medium + Medium + + + Large + Large + + + List View + List View + + + Grid View + Grid View + + + Elf Viewer + Elf Viewer + + + Game Install Directory + Game Install Directory + + + Download Cheats/Patches + Download Cheats/Patches + + + Dump Game List + Dump Game List + + + Trophy Viewer + Trophy Viewer + + + No games found. Please add your games to your library first. + No games found. Please add your games to your library first. + + + Search... + Search... + + + File + File + + + View + View + + + Game List Icons + Game List Icons + + + Game List Mode + Game List Mode + + + Settings + Settings + + + Utils + Utils + + + Themes + Themes + + + Help + Help + + + Dark + Dark + + + Light + Light + + + Green + Green + + + Blue + Blue + + + Violet + Violet + + + toolBar + toolBar + + + Game List + Game List + + + Download Cheats For All Installed Games + Download Cheats For All Installed Games + + + Download Patches For All Games + Download Patches For All Games + + + Download Complete + Download Complete + + + You have downloaded cheats for all the games you have installed. + You have downloaded cheats for all the games you have installed. + + + Patches Downloaded Successfully! + Patches Downloaded Successfully! + + + All Patches available for all games have been downloaded. + All Patches available for all games have been downloaded. + + + Games: + Games: + + + ELF files (*.bin *.elf *.oelf) + ELF files (*.bin *.elf *.oelf) + + + Game Boot + Game Boot + + + Only one file can be selected! + Only one file can be selected! + + + Run Game + Run Game + + + Eboot.bin file not found + Eboot.bin file not found + + + Game is already running! + Game is already running! + + + shadPS4 + shadPS4 + + + Play + Play + + + Pause + Pause + + + Stop + Stop + + + Restart + Restart + + + Full Screen + Full Screen + + + Controllers + Controllers + + + Keyboard + Keyboard + + + Refresh List + Refresh List + + + Resume + Resume + + + Show Labels Under Icons + Show Labels Under Icons + + + + SettingsDialog + + Settings + Settings + + + General + General + + + System + System + + + Console Language + Console Language + + + Emulator Language + Emulator Language + + + Emulator + Emulator + + + Default tab when opening settings + Default tab when opening settings + + + Show Game Size In List + Show Game Size In List + + + Show Splash + Show Splash + + + Enable Discord Rich Presence + Enable Discord Rich Presence + + + Username + Username + + + Trophy Key + Trophy Key + + + Trophy + Trophy + + + Open the custom trophy images/sounds folder + Open the custom trophy images/sounds folder + + + Logger + Logger + + + Log Type + Log Type + + + Log Filter + Log Filter + + + Open Log Location + Open Log Location + + + Input + Input + + + Cursor + Cursor + + + Hide Cursor + Hide Cursor + + + Hide Cursor Idle Timeout + Hide Cursor Idle Timeout + + + s + s + + + Controller + Controller + + + Back Button Behavior + Back Button Behavior + + + Graphics + Graphics + + + GUI + GUI + + + User + User + + + Graphics Device + Graphics Device + + + Vblank Divider + Vblank Divider + + + Advanced + Advanced + + + Enable Shaders Dumping + Enable Shaders Dumping + + + Enable NULL GPU + Enable NULL GPU + + + Enable HDR + Enable HDR + + + Paths + Paths + + + Game Folders + Game Folders + + + Add... + Add... + + + Remove + Remove + + + Debug + Debug + + + Enable Debug Dumping + Enable Debug Dumping + + + Enable Vulkan Validation Layers + Enable Vulkan Validation Layers + + + Enable Vulkan Synchronization Validation + Enable Vulkan Synchronization Validation + + + Enable RenderDoc Debugging + Enable RenderDoc Debugging + + + Enable Crash Diagnostics + Enable Crash Diagnostics + + + Collect Shaders + Collect Shaders + + + Copy GPU Buffers + Copy GPU Buffers + + + Host Debug Markers + Host Debug Markers + + + Guest Debug Markers + Guest Debug Markers + + + Update + Update + + + Check for Updates at Startup + Check for Updates at Startup + + + Always Show Changelog + Always Show Changelog + + + Update Channel + Update Channel + + + Check for Updates + Check for Updates + + + GUI Settings + GUI Settings + + + Title Music + Title Music + + + Disable Trophy Notification + Disable Trophy Notification + + + Background Image + Background Image + + + Show Background Image + Show Background Image + + + Opacity + Opacity + + + Play title music + Play title music + + + Update Compatibility Database On Startup + Update Compatibility Database On Startup + + + Game Compatibility + Game Compatibility + + + Display Compatibility Data + Display Compatibility Data + + + Update Compatibility Database + Update Compatibility Database + + + Volume + Volume + + + Save + Save + + + Apply + Apply + + + Restore Defaults + Restore Defaults + + + Close + Close + + + Point your mouse at an option to display its description. + Point your mouse at an option to display its description. + + + Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. + Console Language:\nSets the language that the PS4 game uses.\nIt's recommended to set this to a language the game supports, which will vary by region. + + + Emulator Language:\nSets the language of the emulator's user interface. + Emulator Language:\nSets the language of the emulator's user interface. + + + Show Splash Screen:\nShows the game's splash screen (a special image) while the game is starting. + Show Splash Screen:\nShows the game's splash screen (a special image) while the game is starting. + + + Enable Discord Rich Presence:\nDisplays the emulator icon and relevant information on your Discord profile. + Enable Discord Rich Presence:\nDisplays the emulator icon and relevant information on your Discord profile. + + + Username:\nSets the PS4's account username, which may be displayed by some games. + Username:\nSets the PS4's account username, which may be displayed by some games. + + + Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. + Trophy Key:\nKey used to decrypt trophies. Must be obtained from your jailbroken console.\nMust contain only hex characters. + + + Log Type:\nSets whether to synchronize the output of the log window for performance. May have adverse effects on emulation. + Log Type:\nSets whether to synchronize the output of the log window for performance. May have adverse effects on emulation. + + + Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. + Log Filter:\nFilters the log to only print specific information.\nExamples: "Core:Trace" "Lib.Pad:Debug Common.Filesystem:Error" "*:Critical"\nLevels: Trace, Debug, Info, Warning, Error, Critical - in this order, a specific level silences all levels preceding it in the list and logs every level after it. + + + Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. + Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable. + + + Background Image:\nControl the opacity of the game background image. + Background Image:\nControl the opacity of the game background image. + + + Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. + Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI. + + + Disable Trophy Pop-ups:\nDisable in-game trophy notifications. Trophy progress can still be tracked using the Trophy Viewer (right-click the game in the main window). + Disable Trophy Pop-ups:\nDisable in-game trophy notifications. Trophy progress can still be tracked using the Trophy Viewer (right-click the game in the main window). + + + Hide Cursor:\nChoose when the cursor will disappear:\nNever: You will always see the mouse.\nidle: Set a time for it to disappear after being idle.\nAlways: you will never see the mouse. + Hide Cursor:\nChoose when the cursor will disappear:\nNever: You will always see the mouse.\nidle: Set a time for it to disappear after being idle.\nAlways: you will never see the mouse. + + + Hide Idle Cursor Timeout:\nThe duration (seconds) after which the cursor that has been idle hides itself. + Hide Idle Cursor Timeout:\nThe duration (seconds) after which the cursor that has been idle hides itself. + + + Back Button Behavior:\nSets the controller's back button to emulate tapping the specified position on the PS4 touchpad. + Back Button Behavior:\nSets the controller's back button to emulate tapping the specified position on the PS4 touchpad. + + + Display Compatibility Data:\nDisplays game compatibility information in table view. Enable "Update Compatibility On Startup" to get up-to-date information. + Display Compatibility Data:\nDisplays game compatibility information in table view. Enable "Update Compatibility On Startup" to get up-to-date information. + + + Update Compatibility On Startup:\nAutomatically update the compatibility database when shadPS4 starts. + Update Compatibility On Startup:\nAutomatically update the compatibility database when shadPS4 starts. + + + Update Compatibility Database:\nImmediately update the compatibility database. + Update Compatibility Database:\nImmediately update the compatibility database. + + + Never + Never + + + Idle + Idle + + + Always + Always + + + Touchpad Left + Touchpad Left + + + Touchpad Right + Touchpad Right + + + Touchpad Center + Touchpad Center + + + None + None + + + Graphics Device:\nOn multiple GPU systems, select the GPU the emulator will use from the drop down list,\nor select "Auto Select" to automatically determine it. + Graphics Device:\nOn multiple GPU systems, select the GPU the emulator will use from the drop down list,\nor select "Auto Select" to automatically determine it. + + + Width/Height:\nSets the size of the emulator window at launch, which can be resized during gameplay.\nThis is different from the in-game resolution. + Width/Height:\nSets the size of the emulator window at launch, which can be resized during gameplay.\nThis is different from the in-game resolution. + + + Vblank Divider:\nThe frame rate at which the emulator refreshes at is multiplied by this number. Changing this may have adverse effects, such as increasing the game speed, or breaking critical game functionality that does not expect this to change! + Vblank Divider:\nThe frame rate at which the emulator refreshes at is multiplied by this number. Changing this may have adverse effects, such as increasing the game speed, or breaking critical game functionality that does not expect this to change! + + + Enable Shaders Dumping:\nFor the sake of technical debugging, saves the games shaders to a folder as they render. + Enable Shaders Dumping:\nFor the sake of technical debugging, saves the games shaders to a folder as they render. + + + Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. + Enable Null GPU:\nFor the sake of technical debugging, disables game rendering as if there were no graphics card. + + + Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. + Enable HDR:\nEnables HDR in games that support it.\nYour monitor must have support for the BT2020 PQ color space and the RGB10A2 swapchain format. + + + Game Folders:\nThe list of folders to check for installed games. + Game Folders:\nThe list of folders to check for installed games. + + + Add:\nAdd a folder to the list. + Add:\nAdd a folder to the list. + + + Remove:\nRemove a folder from the list. + Remove:\nRemove a folder from the list. + + + Enable Debug Dumping:\nSaves the import and export symbols and file header information of the currently running PS4 program to a directory. + Enable Debug Dumping:\nSaves the import and export symbols and file header information of the currently running PS4 program to a directory. + + + Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. + Enable Vulkan Validation Layers:\nEnables a system that validates the state of the Vulkan renderer and logs information about its internal state.\nThis will reduce performance and likely change the behavior of emulation. + + + Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. + Enable Vulkan Synchronization Validation:\nEnables a system that validates the timing of Vulkan rendering tasks.\nThis will reduce performance and likely change the behavior of emulation. + + + Enable RenderDoc Debugging:\nIf enabled, the emulator will provide compatibility with Renderdoc to allow capture and analysis of the currently rendered frame. + Enable RenderDoc Debugging:\nIf enabled, the emulator will provide compatibility with Renderdoc to allow capture and analysis of the currently rendered frame. + + + Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + Collect Shaders:\nYou need this enabled to edit shaders with the debug menu (Ctrl + F10). + + + Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. + Crash Diagnostics:\nCreates a .yaml file with info about the Vulkan state at the time of crashing.\nUseful for debugging 'Device lost' errors. If you have this enabled, you should enable Host AND Guest Debug Markers.\nDoes not work on Intel GPUs.\nYou need Vulkan Validation Layers enabled and the Vulkan SDK for this to work. + + + Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. + Copy GPU Buffers:\nGets around race conditions involving GPU submits.\nMay or may not help with PM4 type 0 crashes. + + + Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Host Debug Markers:\nInserts emulator-side information like markers for specific AMDGPU commands around Vulkan commands, as well as giving resources debug names.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + + + Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + Guest Debug Markers:\nInserts any debug markers the game itself has added to the command buffer.\nIf you have this enabled, you should enable Crash Diagnostics.\nUseful for programs like RenderDoc. + + + Save Data Path:\nThe folder where game save data will be saved. + Save Data Path:\nThe folder where game save data will be saved. + + + Browse:\nBrowse for a folder to set as the save data path. + Browse:\nBrowse for a folder to set as the save data path. + + + Release + Release + + + Nightly + Nightly + + + Set the volume of the background music. + Set the volume of the background music. + + + Enable Motion Controls + Enable Motion Controls + + + Save Data Path + Save Data Path + + + Browse + Browse + + + async + async + + + sync + sync + + + Auto Select + Auto Select + + + Directory to install games + Directory to install games + + + Directory to save data + Directory to save data + + + Video + Video + + + Display Mode + Display Mode + + + Windowed + Windowed + + + Fullscreen + Fullscreen + + + Fullscreen (Borderless) + Fullscreen (Borderless) + + + Window Size + Window Size + + + W: + W: + + + H: + H: + + + Separate Log Files + Separate Log Files + + + Separate Log Files:\nWrites a separate logfile for each game. + Separate Log Files:\nWrites a separate logfile for each game. + + + Trophy Notification Position + Trophy Notification Position + + + Left + Left + + + Right + Right + + + Top + Top + + + Bottom + Bottom + + + Notification Duration + Notification Duration + + + Portable User Folder + Portable User Folder + + + Create Portable User Folder from Common User Folder + Create Portable User Folder from Common User Folder + + + Portable user folder:\nStores shadPS4 settings and data that will be applied only to the shadPS4 build located in the current folder. Restart the app after creating the portable user folder to begin using it. + Portable user folder:\nStores shadPS4 settings and data that will be applied only to the shadPS4 build located in the current folder. Restart the app after creating the portable user folder to begin using it. + + + Cannot create portable user folder + Cannot create portable user folder + + + %1 already exists + %1 already exists + + + Portable user folder created + Portable user folder created + + + %1 successfully created. + %1 successfully created. + + + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + Open the custom trophy images/sounds folder:\nYou can add custom images to the trophies and an audio.\nAdd the files to custom_trophy with the following names:\ntrophy.wav OR trophy.mp3, bronze.png, gold.png, platinum.png, silver.png\nNote: The sound will only work in QT versions. + + + * Unsupported Vulkan Version + * Unsupported Vulkan Version + + + + TrophyViewer + + Trophy Viewer + Trophy Viewer + + + Select Game: + Select Game: + + + Progress + Progress + + + Show Earned Trophies + Show Earned Trophies + + + Show Not Earned Trophies + Show Not Earned Trophies + + + Show Hidden Trophies + Show Hidden Trophies + + + From 1a27af6951fb916608fbaad2a5c3b8d9dc0d14d5 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 12 Jun 2025 19:06:54 +0300 Subject: [PATCH 137/141] fixed non updated values (#3092) --- src/qt_gui/settings_dialog.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index fc00c6f75..f7cbe7af6 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -509,9 +509,8 @@ void SettingsDialog::LoadValuesFromConfig() { toml::find_or(data, "General", "checkCompatibilityOnStartup", false)); #ifdef ENABLE_UPDATER - ui->updateCheckBox->setChecked(toml::find_or(data, "General", "autoUpdate", false)); - ui->changelogCheckBox->setChecked( - toml::find_or(data, "General", "alwaysShowChangelog", false)); + ui->updateCheckBox->setChecked(m_gui_settings->GetValue(gui::gen_checkForUpdates).toBool()); + ui->changelogCheckBox->setChecked(m_gui_settings->GetValue(gui::gen_showChangeLog).toBool()); QString updateChannel = m_gui_settings->GetValue(gui::gen_updateChannel).toString(); ui->updateComboBox->setCurrentText( From ae6f7b8d5ad65a252543161f03733c9f8a62ad76 Mon Sep 17 00:00:00 2001 From: UltraDaCat <113462733+UltraDaCat@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:20:39 +0200 Subject: [PATCH 138/141] remove the accidental backslash in README.md (#3093) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9079ead73..22fc27a33 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ The following firmware modules are supported and must be placed in shadPS4's `sy

> [!Caution] -> The above modules are required to run the games properly and must be extracted from your PlayStation 4.\ +> The above modules are required to run the games properly and must be extracted from your PlayStation 4. From 226058d2e97f353d777032a74db89b1e0de91948 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 12 Jun 2025 20:29:39 +0300 Subject: [PATCH 139/141] fixed nonload issues with background music (#3094) --- src/qt_gui/settings_dialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qt_gui/settings_dialog.cpp b/src/qt_gui/settings_dialog.cpp index f7cbe7af6..da2b0dde3 100644 --- a/src/qt_gui/settings_dialog.cpp +++ b/src/qt_gui/settings_dialog.cpp @@ -456,7 +456,7 @@ void SettingsDialog::LoadValuesFromConfig() { ui->dumpShadersCheckBox->setChecked(toml::find_or(data, "GPU", "dumpShaders", false)); ui->nullGpuCheckBox->setChecked(toml::find_or(data, "GPU", "nullGpu", false)); ui->enableHDRCheckBox->setChecked(toml::find_or(data, "GPU", "allowHDR", false)); - ui->playBGMCheckBox->setChecked(toml::find_or(data, "General", "playBGM", false)); + ui->playBGMCheckBox->setChecked(m_gui_settings->GetValue(gui::gl_playBackgroundMusic).toBool()); ui->disableTrophycheckBox->setChecked( toml::find_or(data, "General", "isTrophyPopupDisabled", false)); ui->popUpDurationSpinBox->setValue(Config::getTrophyNotificationDuration()); @@ -468,7 +468,7 @@ void SettingsDialog::LoadValuesFromConfig() { ui->radioButton_Top->setChecked(side == "top"); ui->radioButton_Bottom->setChecked(side == "bottom"); - ui->BGMVolumeSlider->setValue(toml::find_or(data, "General", "BGMvolume", 50)); + ui->BGMVolumeSlider->setValue(m_gui_settings->GetValue(gui::gl_backgroundMusicVolume).toInt()); ui->discordRPCCheckbox->setChecked( toml::find_or(data, "General", "enableDiscordRPC", true)); QString translatedText_FullscreenMode = From 3f40a8d46edc28d07a9aaf29a389d0d5d305375d Mon Sep 17 00:00:00 2001 From: rainmakerv2 <30595646+rainmakerv3@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:06:30 +0800 Subject: [PATCH 140/141] Qt: If duplicate unique inputs found, show which buttons have duplicates (#3098) * If duplicate unique inputs found, show which buttons have duplicates * remove duplicate button from list * cleanup * Set clang-format off for long translatable string --- src/qt_gui/kbm_gui.cpp | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/qt_gui/kbm_gui.cpp b/src/qt_gui/kbm_gui.cpp index 15e9008ab..596de6d30 100644 --- a/src/qt_gui/kbm_gui.cpp +++ b/src/qt_gui/kbm_gui.cpp @@ -33,13 +33,13 @@ KBMSettings::KBMSettings(std::shared_ptr game_info_get, QWidget* } ButtonsList = { - ui->CrossButton, ui->CircleButton, ui->TriangleButton, ui->SquareButton, - ui->L1Button, ui->R1Button, ui->L2Button, ui->R2Button, - ui->L3Button, ui->R3Button, ui->TouchpadButton, ui->OptionsButton, - ui->TouchpadButton, ui->DpadUpButton, ui->DpadDownButton, ui->DpadLeftButton, - ui->DpadRightButton, ui->LStickUpButton, ui->LStickDownButton, ui->LStickLeftButton, - ui->LStickRightButton, ui->RStickUpButton, ui->RStickDownButton, ui->RStickLeftButton, - ui->RStickRightButton, ui->LHalfButton, ui->RHalfButton}; + ui->CrossButton, ui->CircleButton, ui->TriangleButton, ui->SquareButton, + ui->L1Button, ui->R1Button, ui->L2Button, ui->R2Button, + ui->L3Button, ui->R3Button, ui->OptionsButton, ui->TouchpadButton, + ui->DpadUpButton, ui->DpadDownButton, ui->DpadLeftButton, ui->DpadRightButton, + ui->LStickUpButton, ui->LStickDownButton, ui->LStickLeftButton, ui->LStickRightButton, + ui->RStickUpButton, ui->RStickDownButton, ui->RStickLeftButton, ui->RStickRightButton, + ui->LHalfButton, ui->RHalfButton}; ButtonConnects(); SetUIValuestoMappings("default"); @@ -372,14 +372,31 @@ void KBMSettings::SaveKBMConfig(bool CloseOnSave) { file.close(); // Prevent duplicate inputs for KBM as this breaks the engine + bool duplicateFound = false; + QSet duplicateMappings; for (auto it = inputs.begin(); it != inputs.end(); ++it) { if (std::find(it + 1, inputs.end(), *it) != inputs.end()) { - QMessageBox::information(this, tr("Unable to Save"), - tr("Cannot bind any unique input more than once")); - return; + duplicateFound = true; + duplicateMappings.insert(QString::fromStdString(*it)); } } + if (duplicateFound) { + QStringList duplicatesList; + for (const QString mapping : duplicateMappings) { + for (const auto& button : ButtonsList) { + if (button->text() == mapping) + duplicatesList.append(button->objectName() + " - " + mapping); + } + } + QMessageBox::information( + this, tr("Unable to Save"), + // clang-format off +QString(tr("Cannot bind any unique input more than once. Duplicate inputs mapped to the following buttons:\n\n%1").arg(duplicatesList.join("\n")))); + // clang-format on + return; + } + std::vector save; bool CurrentLineEmpty = false, LastLineEmpty = false; for (auto const& line : lines) { From de69f2b40b88d27284305dc806ea6fd7c2698b7f Mon Sep 17 00:00:00 2001 From: Fire Cube Date: Sun, 15 Jun 2025 18:03:57 +0200 Subject: [PATCH 141/141] Equeue: HrTimer fixes (#2987) * initial changes * tmp * impl * support wait for multiple timers * cleanup --- src/core/libraries/kernel/equeue.cpp | 45 +++++++++++++++++----------- src/core/libraries/kernel/equeue.h | 20 +++++++++---- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/core/libraries/kernel/equeue.cpp b/src/core/libraries/kernel/equeue.cpp index 911ae4cd5..4d1b116c5 100644 --- a/src/core/libraries/kernel/equeue.cpp +++ b/src/core/libraries/kernel/equeue.cpp @@ -125,7 +125,6 @@ int EqueueInternal::WaitForEvents(SceKernelEvent* ev, int num, u32 micros) { .count(); count = WaitForSmallTimer(ev, num, std::max(0l, long(micros - time_waited))); } - small_timer_event.event.data = 0; } if (ev->flags & SceKernelEvent::Flags::OneShot) { @@ -179,39 +178,46 @@ int EqueueInternal::GetTriggeredEvents(SceKernelEvent* ev, int num) { } bool EqueueInternal::AddSmallTimer(EqueueEvent& ev) { - // We assume that only one timer event (with the same ident across calls) - // can be posted to the queue, based on observations so far. In the opposite case, - // the small timer storage and wait logic should be reworked. - ASSERT(!HasSmallTimer() || small_timer_event.event.ident == ev.event.ident); - ev.time_added = std::chrono::steady_clock::now(); - small_timer_event = std::move(ev); + SmallTimer st; + st.event = ev.event; + st.added = std::chrono::steady_clock::now(); + st.interval = std::chrono::microseconds{ev.event.data}; + { + std::scoped_lock lock{m_mutex}; + m_small_timers[st.event.ident] = std::move(st); + } return true; } int EqueueInternal::WaitForSmallTimer(SceKernelEvent* ev, int num, u32 micros) { - int count{}; - - ASSERT(num == 1); + ASSERT(num >= 1); auto curr_clock = std::chrono::steady_clock::now(); const auto wait_end_us = (micros == 0) ? std::chrono::steady_clock::time_point::max() : curr_clock + std::chrono::microseconds{micros}; - + int count = 0; do { curr_clock = std::chrono::steady_clock::now(); { std::scoped_lock lock{m_mutex}; - if ((curr_clock - small_timer_event.time_added) > - std::chrono::microseconds{small_timer_event.event.data}) { - ev[count++] = small_timer_event.event; - small_timer_event.event.data = 0; - break; + for (auto it = m_small_timers.begin(); it != m_small_timers.end() && count < num;) { + const SmallTimer& st = it->second; + + if (curr_clock - st.added >= st.interval) { + ev[count++] = st.event; + it = m_small_timers.erase(it); + } else { + ++it; + } } + + if (count > 0) + return count; } std::this_thread::yield(); } while (curr_clock < wait_end_us); - return count; + return 0; } bool EqueueInternal::EventExists(u64 id, s16 filter) { @@ -326,6 +332,11 @@ s32 PS4_SYSV_ABI sceKernelAddHRTimerEvent(SceKernelEqueue eq, int id, timespec* // `HrTimerSpinlockThresholdUs`) and fall back to boost asio timers if the time to tick is // large. Even for large delays, we truncate a small portion to complete the wait // using the spinlock, prioritizing precision. + + if (eq->EventExists(event.event.ident, event.event.filter)) { + eq->RemoveEvent(id, SceKernelEvent::Filter::HrTimer); + } + if (total_us < HrTimerSpinlockThresholdUs) { return eq->AddSmallTimer(event) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOMEM; } diff --git a/src/core/libraries/kernel/equeue.h b/src/core/libraries/kernel/equeue.h index e6e3c0c53..fbc209265 100644 --- a/src/core/libraries/kernel/equeue.h +++ b/src/core/libraries/kernel/equeue.h @@ -9,6 +9,7 @@ #include #include +#include #include "common/rdtsc.h" #include "common/types.h" @@ -135,6 +136,12 @@ private: }; class EqueueInternal { + struct SmallTimer { + SceKernelEvent event; + std::chrono::steady_clock::time_point added; + std::chrono::microseconds interval; + }; + public: explicit EqueueInternal(std::string_view name) : m_name(name) {} @@ -151,13 +158,14 @@ public: int GetTriggeredEvents(SceKernelEvent* ev, int num); bool AddSmallTimer(EqueueEvent& event); - bool HasSmallTimer() const { - return small_timer_event.event.data != 0; + bool HasSmallTimer() { + std::scoped_lock lock{m_mutex}; + return !m_small_timers.empty(); } bool RemoveSmallTimer(u64 id) { - if (HasSmallTimer() && small_timer_event.event.ident == id) { - small_timer_event = {}; - return true; + if (HasSmallTimer()) { + std::scoped_lock lock{m_mutex}; + return m_small_timers.erase(id) > 0; } return false; } @@ -170,8 +178,8 @@ private: std::string m_name; std::mutex m_mutex; std::vector m_events; - EqueueEvent small_timer_event{}; std::condition_variable m_cond; + std::unordered_map m_small_timers; }; u64 PS4_SYSV_ABI sceKernelGetEventData(const SceKernelEvent* ev);