Merge branch 'main' into user_and_settings
Some checks failed
Build and Release / reuse (push) Has been cancelled
Build and Release / clang-format (push) Has been cancelled
Build and Release / get-info (push) Has been cancelled
Build and Release / windows-sdl (push) Has been cancelled
Build and Release / macos-sdl (push) Has been cancelled
Build and Release / linux-sdl (push) Has been cancelled
Build and Release / linux-sdl-gcc (push) Has been cancelled
Build and Release / pre-release (push) Has been cancelled

This commit is contained in:
georgemoralis 2026-01-27 11:25:42 +02:00 committed by GitHub
commit 8c17de032d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1549 additions and 1058 deletions

4
.gitmodules vendored
View File

@ -123,3 +123,7 @@
[submodule "externals/aacdec/fdk-aac"]
path = externals/aacdec/fdk-aac
url = https://android.googlesource.com/platform/external/aac
[submodule "externals/ext-CLI11"]
path = externals/ext-CLI11
url = https://github.com/shadexternals/ext-CLI11.git
branch = main

View File

@ -920,6 +920,7 @@ set(SHADER_RECOMPILER src/shader_recompiler/profile.h
src/shader_recompiler/ir/passes/flatten_extended_userdata_pass.cpp
src/shader_recompiler/ir/passes/hull_shader_transform.cpp
src/shader_recompiler/ir/passes/identity_removal_pass.cpp
src/shader_recompiler/ir/passes/inject_clip_distance_attributes.cpp
src/shader_recompiler/ir/passes/ir_passes.h
src/shader_recompiler/ir/passes/lower_buffer_format_to_raw.cpp
src/shader_recompiler/ir/passes/lower_fp64_to_fp32.cpp
@ -1101,7 +1102,7 @@ create_target_directory_groups(shadps4)
target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half ZLIB::ZLIB PNG::PNG)
target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::glslang SDL3::SDL3 SDL3_mixer::SDL3_mixer pugixml::pugixml)
target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac)
target_link_libraries(shadps4 PRIVATE stb::headers libusb::usb lfreist-hwinfo::hwinfo nlohmann_json::nlohmann_json miniz fdk-aac CLI11::CLI11)
target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h")
target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h")

View File

@ -1,5 +1,5 @@
<!--
SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project
SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@ -58,6 +58,11 @@ This project began for fun. Given our limited free time, it may take some time b
# Building
## Docker
For building shadPS4 in a containerized environment using Docker and VSCode, check the instructions here:
[**Docker Build Instructions**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-docker.md)
## Windows
Check the build instructions for [**Windows**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-windows.md).

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
{
"name": "shadPS4-dev",
"dockerComposeFile": [
"../docker-compose.yml"
],
"containerEnv": {
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
"GITHUB_USER": "${localEnv:GITHUB_USER}"
},
"service": "shadps4",
"workspaceFolder": "/workspaces/shadPS4",
"remoteUser": "root",
"shutdownAction": "none",
"customizations": {
"vscode": {
"extensions": [
"llvm-vs-code-extensions.vscode-clangd"
],
"settings": {
"C_Cpp.intelliSenseEngine": "disabled",
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=never"
]
}
}
},
"settings": {
"cmake.configureOnOpen": false,
"cmake.generator": "Unix Makefiles",
"cmake.environment": {
"CC": "clang",
"CXX": "clang++"
},
"cmake.configureSettings": {
"CMAKE_CXX_STANDARD": "23",
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
}
}
}

View File

@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
build-essential \
clang \
git \
ca-certificates \
wget \
libasound2-dev \
libpulse-dev \
libopenal-dev \
libssl-dev \
zlib1g-dev \
libedit-dev \
libudev-dev \
libevdev-dev \
libsdl2-dev \
libjack-dev \
libsndio-dev \
libxtst-dev \
libvulkan-dev \
vulkan-validationlayers \
libpng-dev \
clang-tidy \
&& rm -rf /var/lib/apt/lists/*
RUN wget -qO - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor -o /usr/share/keyrings/kitware-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main" > /etc/apt/sources.list.d/kitware.list \
&& apt-get update \
&& apt-get install -y cmake \
&& rm -rf /var/lib/apt/lists/*/*
WORKDIR /workspaces/shadPS4

View File

@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
services:
shadps4:
build:
context: ./.docker
volumes:
- ./emu:/workspaces/shadPS4:cached
tty: true

View File

@ -0,0 +1,91 @@
<!--
SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Building shadPS4 with Docker and VSCode Support
This guide explains how to build **shadPS4** using Docker while keeping full compatibility with **VSCode** development.
---
## Prerequisites
Before starting, ensure you have:
- **Docker Engine** or **Docker Desktop** installed
[Installation Guide](https://docs.docker.com/engine/install/)
- **Git** installed on your system.
---
## Step 1: Prepare the Docker Environment
Inside the container (or on your host if mounting volumes):
1. Navigate to the repository folder containing the Docker Builder folder:
```bash
cd <path-to-repo>
```
2. Start the Docker container:
```bash
docker compose up -d
```
This will spin up a container with all the necessary build dependencies, including Clang, CMake, SDL2, Vulkan, and more.
## Step 2: Clone shadPS4 Source
```bash
mkdir emu
cd emu
git clone --recursive https://github.com/shadps4-emu/shadPS4.git .
or your fork link.
```
3. Initialize submodules:
```bash
git submodule update --init --recursive
```
## Step 3: Build with CMake
Generate the build directory and configure the project using Clang:
```bash
cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
```
Then build the project:
```bash
cmake --build ./build --parallel $(nproc)
```
* Tip: To enable debug builds, add -DCMAKE_BUILD_TYPE=Debug to the CMake command.
---
After a successful build, the executable is located at:
```bash
./build/shadps4
```
## Step 4: VSCode Integration
1. Open the repository in VSCode.
2. The CMake Tools extension should automatically detect the build directory inside the container or on your host.
3. You can configure build options, build, and debug directly from the VSCode interface without extra manual setup.
# Notes
* The Docker environment contains all dependencies, so you dont need to install anything manually.
* Using Clang inside Docker ensures consistent builds across Linux and macOS runners.
* GitHub Actions are recommended for cross-platform builds, including Windows .exe output, which is not trivial to produce locally without Visual Studio or clang-cl.

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
# SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(BUILD_SHARED_LIBS OFF)
@ -204,6 +204,7 @@ add_subdirectory(tracy)
# pugixml
if (NOT TARGET pugixml::pugixml)
option(PUGIXML_NO_EXCEPTIONS "" ON)
add_subdirectory(pugixml)
endif()
@ -268,3 +269,9 @@ add_subdirectory(json)
# miniz
add_subdirectory(miniz)
# cli11
set(CLI11_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(ext-CLI11)

1
externals/ext-CLI11 vendored Submodule

@ -0,0 +1 @@
Subproject commit 1cce1483345e60997b87720948c37d6a34db2658

View File

@ -94,7 +94,10 @@ static u64 BackingSize = ORBIS_KERNEL_TOTAL_MEM_DEV_PRO;
struct MemoryRegion {
VAddr base;
size_t size;
PAddr phys_base;
u64 size;
u32 prot;
s32 fd;
bool is_mapped;
};
@ -160,7 +163,8 @@ struct AddressSpace::Impl {
// Restrict region size to avoid overly fragmenting the virtual memory space.
if (info.State == MEM_FREE && info.RegionSize > 0x1000000) {
VAddr addr = Common::AlignUp(reinterpret_cast<VAddr>(info.BaseAddress), alignment);
regions.emplace(addr, MemoryRegion{addr, size, false});
regions.emplace(addr,
MemoryRegion{addr, PAddr(-1), size, PAGE_NOACCESS, -1, false});
}
}
@ -208,46 +212,52 @@ struct AddressSpace::Impl {
~Impl() {
if (virtual_base) {
if (!VirtualFree(virtual_base, 0, MEM_RELEASE)) {
LOG_CRITICAL(Render, "Failed to free virtual memory");
LOG_CRITICAL(Core, "Failed to free virtual memory");
}
}
if (backing_base) {
if (!UnmapViewOfFile2(process, backing_base, MEM_PRESERVE_PLACEHOLDER)) {
LOG_CRITICAL(Render, "Failed to unmap backing memory placeholder");
LOG_CRITICAL(Core, "Failed to unmap backing memory placeholder");
}
if (!VirtualFreeEx(process, backing_base, 0, MEM_RELEASE)) {
LOG_CRITICAL(Render, "Failed to free backing memory");
LOG_CRITICAL(Core, "Failed to free backing memory");
}
}
if (!CloseHandle(backing_handle)) {
LOG_CRITICAL(Render, "Failed to free backing memory file handle");
LOG_CRITICAL(Core, "Failed to free backing memory file handle");
}
}
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, ULONG prot, uintptr_t fd = 0) {
// Before mapping we must carve a placeholder with the exact properties of our mapping.
auto* region = EnsureSplitRegionForMapping(virtual_addr, size);
region->is_mapped = true;
void* MapRegion(MemoryRegion* region) {
VAddr virtual_addr = region->base;
PAddr phys_addr = region->phys_base;
u64 size = region->size;
ULONG prot = region->prot;
s32 fd = region->fd;
void* ptr = nullptr;
if (phys_addr != -1) {
HANDLE backing = fd ? reinterpret_cast<HANDLE>(fd) : backing_handle;
if (fd && prot == PAGE_READONLY) {
HANDLE backing = fd != -1 ? reinterpret_cast<HANDLE>(fd) : backing_handle;
if (fd != -1 && prot == PAGE_READONLY) {
DWORD resultvar;
ptr = VirtualAlloc2(process, reinterpret_cast<PVOID>(virtual_addr), size,
MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER,
PAGE_READWRITE, nullptr, 0);
bool ret = ReadFile(backing, ptr, size, &resultvar, NULL);
// phys_addr serves as an offset for file mmaps.
// Create an OVERLAPPED with the offset, then supply that to ReadFile
OVERLAPPED param{};
// Offset is the least-significant 32 bits, OffsetHigh is the most-significant.
param.Offset = phys_addr & 0xffffffffull;
param.OffsetHigh = (phys_addr & 0xffffffff00000000ull) >> 32;
bool ret = ReadFile(backing, ptr, size, &resultvar, &param);
ASSERT_MSG(ret, "ReadFile failed. {}", Common::GetLastErrorMsg());
ret = VirtualProtect(ptr, size, prot, &resultvar);
ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg());
} else {
ptr = MapViewOfFile3(backing, process, reinterpret_cast<PVOID>(virtual_addr),
phys_addr, size, MEM_REPLACE_PLACEHOLDER,
PAGE_EXECUTE_READWRITE, nullptr, 0);
phys_addr, size, MEM_REPLACE_PLACEHOLDER, prot, nullptr, 0);
ASSERT_MSG(ptr, "MapViewOfFile3 failed. {}", Common::GetLastErrorMsg());
DWORD resultvar;
bool ret = VirtualProtect(ptr, size, prot, &resultvar);
ASSERT_MSG(ret, "VirtualProtect failed. {}", Common::GetLastErrorMsg());
}
} else {
ptr =
@ -258,135 +268,220 @@ struct AddressSpace::Impl {
return ptr;
}
void Unmap(VAddr virtual_addr, size_t size, bool has_backing) {
bool ret;
if (has_backing) {
void UnmapRegion(MemoryRegion* region) {
VAddr virtual_addr = region->base;
PAddr phys_base = region->phys_base;
u64 size = region->size;
ULONG prot = region->prot;
s32 fd = region->fd;
bool ret = false;
if ((fd != -1 && prot != PAGE_READONLY) || (fd == -1 && phys_base != -1)) {
ret = UnmapViewOfFile2(process, reinterpret_cast<PVOID>(virtual_addr),
MEM_PRESERVE_PLACEHOLDER);
} else {
ret = VirtualFreeEx(process, reinterpret_cast<PVOID>(virtual_addr), size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER);
}
ASSERT_MSG(ret, "Unmap operation on virtual_addr={:#X} failed: {}", virtual_addr,
ASSERT_MSG(ret, "Unmap on virtual_addr {:#x}, size {:#x} failed: {}", virtual_addr, size,
Common::GetLastErrorMsg());
// The unmap call will create a new placeholder region. We need to see if we can coalesce it
// with neighbors.
JoinRegionsAfterUnmap(virtual_addr, size);
}
// The following code is inspired from Dolphin's MemArena
// https://github.com/dolphin-emu/dolphin/blob/deee3ee4/Source/Core/Common/MemArenaWin.cpp#L212
MemoryRegion* EnsureSplitRegionForMapping(VAddr address, size_t size) {
// Find closest region that is <= the given address by using upper bound and decrementing
auto it = regions.upper_bound(address);
ASSERT_MSG(it != regions.begin(), "Invalid address {:#x}", address);
--it;
ASSERT_MSG(!it->second.is_mapped,
"Attempt to map {:#x} with size {:#x} which overlaps with {:#x} mapping",
address, size, it->second.base);
auto& [base, region] = *it;
void SplitRegion(VAddr virtual_addr, u64 size) {
// First, get the region this range covers
auto it = std::prev(regions.upper_bound(virtual_addr));
const VAddr mapping_address = region.base;
const size_t region_size = region.size;
if (mapping_address == address) {
// If this region is already split up correctly we don't have to do anything
if (region_size == size) {
return &region;
// All unmapped areas will coalesce, so there should be a region
// containing the full requested range. If not, then something is mapped here.
ASSERT_MSG(it->second.base + it->second.size >= virtual_addr + size,
"Cannot fit region into one placeholder");
// If the region is mapped, we need to unmap first before we can modify the placeholders.
if (it->second.is_mapped) {
ASSERT_MSG(it->second.phys_base != -1 || !it->second.is_mapped,
"Cannot split unbacked mapping");
UnmapRegion(&it->second);
}
// We need to split this region to create a matching placeholder.
if (it->second.base != virtual_addr) {
// Requested address is not the start of the containing region,
// create a new region to represent the memory before the requested range.
auto& region = it->second;
u64 base_offset = virtual_addr - region.base;
u64 next_region_size = region.size - base_offset;
PAddr next_region_phys_base = -1;
if (region.is_mapped) {
next_region_phys_base = region.phys_base + base_offset;
}
region.size = base_offset;
ASSERT_MSG(region_size >= size,
"Region with address {:#x} and size {:#x} can't fit {:#x}", mapping_address,
region_size, size);
// Split the placeholder.
if (!VirtualFreeEx(process, LPVOID(address), size,
// Use VirtualFreeEx to create the split.
if (!VirtualFreeEx(process, LPVOID(region.base), region.size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
return nullptr;
}
// Update tracked mappings and return the first of the two
// If the mapping was mapped, remap the region.
if (region.is_mapped) {
MapRegion(&region);
}
// Store a new region matching the removed area.
it = regions.emplace_hint(std::next(it), virtual_addr,
MemoryRegion(virtual_addr, next_region_phys_base,
next_region_size, region.prot, region.fd,
region.is_mapped));
}
// At this point, the region's base will match virtual_addr.
// Now check for a size difference.
if (it->second.size != size) {
// The requested size is smaller than the current region placeholder.
// Update region to match the requested region,
// then make a new region to represent the remaining space.
auto& region = it->second;
VAddr next_region_addr = region.base + size;
u64 next_region_size = region.size - size;
PAddr next_region_phys_base = -1;
if (region.is_mapped) {
next_region_phys_base = region.phys_base + size;
}
region.size = size;
const VAddr new_mapping_start = address + size;
regions.emplace_hint(std::next(it), new_mapping_start,
MemoryRegion(new_mapping_start, region_size - size, false));
return &region;
// Store the new region matching the remaining space
regions.emplace_hint(std::next(it), next_region_addr,
MemoryRegion(next_region_addr, next_region_phys_base,
next_region_size, region.prot, region.fd,
region.is_mapped));
// Use VirtualFreeEx to create the split.
if (!VirtualFreeEx(process, LPVOID(region.base), region.size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
}
// If these regions were mapped, then map the unmapped area beyond the requested range.
if (region.is_mapped) {
MapRegion(&std::next(it)->second);
}
}
ASSERT(mapping_address < address);
// Is there enough space to map this?
const size_t offset_in_region = address - mapping_address;
const size_t minimum_size = size + offset_in_region;
ASSERT(region_size >= minimum_size);
// Split the placeholder.
if (!VirtualFreeEx(process, LPVOID(address), size,
MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) {
UNREACHABLE_MSG("Region splitting failed: {}", Common::GetLastErrorMsg());
return nullptr;
}
// Do we now have two regions or three regions?
if (region_size == minimum_size) {
// Split into two; update tracked mappings and return the second one
region.size = offset_in_region;
it = regions.emplace_hint(std::next(it), address, MemoryRegion(address, size, false));
return &it->second;
} else {
// Split into three; update tracked mappings and return the middle one
region.size = offset_in_region;
const VAddr middle_mapping_start = address;
const size_t middle_mapping_size = size;
const VAddr after_mapping_start = address + size;
const size_t after_mapping_size = region_size - minimum_size;
it = regions.emplace_hint(std::next(it), after_mapping_start,
MemoryRegion(after_mapping_start, after_mapping_size, false));
it = regions.emplace_hint(
it, middle_mapping_start,
MemoryRegion(middle_mapping_start, middle_mapping_size, false));
return &it->second;
// If the requested region was mapped, remap it.
if (it->second.is_mapped) {
MapRegion(&it->second);
}
}
void JoinRegionsAfterUnmap(VAddr address, size_t size) {
// There should be a mapping that matches the request exactly, find it
auto it = regions.find(address);
ASSERT_MSG(it != regions.end() && it->second.size == size,
"Invalid address/size given to unmap.");
void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, ULONG prot, s32 fd = -1) {
// Get a pointer to the region containing virtual_addr
auto it = std::prev(regions.upper_bound(virtual_addr));
// If needed, split surrounding regions to create a placeholder
if (it->first != virtual_addr || it->second.size != size) {
SplitRegion(virtual_addr, size);
it = std::prev(regions.upper_bound(virtual_addr));
}
// Get the address and region for this range.
auto& [base, region] = *it;
region.is_mapped = false;
ASSERT_MSG(!region.is_mapped, "Cannot overwrite mapped region");
// Check if a placeholder exists right before us.
// Now we have a region matching the requested region, perform the actual mapping.
region.is_mapped = true;
region.phys_base = phys_addr;
region.prot = prot;
region.fd = fd;
return MapRegion(&region);
}
void CoalesceFreeRegions(VAddr virtual_addr) {
// First, get the region to update
auto it = std::prev(regions.upper_bound(virtual_addr));
ASSERT_MSG(!it->second.is_mapped, "Cannot coalesce mapped regions");
// Check if there are adjacent free placeholders before this area.
bool can_coalesce = false;
auto it_prev = it != regions.begin() ? std::prev(it) : regions.end();
if (it_prev != regions.end() && !it_prev->second.is_mapped) {
const size_t total_size = it_prev->second.size + size;
if (!VirtualFreeEx(process, LPVOID(it_prev->first), total_size,
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) {
UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg());
}
it_prev->second.size = total_size;
while (it_prev != regions.end() && !it_prev->second.is_mapped &&
it_prev->first + it_prev->second.size == it->first) {
// If there is an earlier region, move our iterator to that and increase size.
it_prev->second.size = it_prev->second.size + it->second.size;
regions.erase(it);
it = it_prev;
// Mark this region as coalesce-able.
can_coalesce = true;
// Get the next previous region.
it_prev = it != regions.begin() ? std::prev(it) : regions.end();
}
// Check if a placeholder exists right after us.
// Check if there are adjacent free placeholders after this area.
auto it_next = std::next(it);
if (it_next != regions.end() && !it_next->second.is_mapped) {
const size_t total_size = it->second.size + it_next->second.size;
if (!VirtualFreeEx(process, LPVOID(it->first), total_size,
while (it_next != regions.end() && !it_next->second.is_mapped &&
it->first + it->second.size == it_next->first) {
// If there is a later region, increase our current region's size
it->second.size = it->second.size + it_next->second.size;
regions.erase(it_next);
// Mark this region as coalesce-able.
can_coalesce = true;
// Get the next region
it_next = std::next(it);
}
// If there are placeholders to coalesce, then coalesce them.
if (can_coalesce) {
if (!VirtualFreeEx(process, LPVOID(it->first), it->second.size,
MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) {
UNREACHABLE_MSG("Region coalescing failed: {}", Common::GetLastErrorMsg());
}
it->second.size = total_size;
regions.erase(it_next);
}
}
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
void Unmap(VAddr virtual_addr, u64 size) {
// Loop through all regions in the requested range
u64 remaining_size = size;
VAddr current_addr = virtual_addr;
while (remaining_size > 0) {
// Get a pointer to the region containing virtual_addr
auto it = std::prev(regions.upper_bound(current_addr));
// If necessary, split regions to ensure a valid unmap.
// To prevent complication, ensure size is within the bounds of the current region.
u64 base_offset = current_addr - it->second.base;
u64 size_to_unmap = std::min<u64>(it->second.size - base_offset, remaining_size);
if (current_addr != it->second.base || size_to_unmap != it->second.size) {
SplitRegion(current_addr, size_to_unmap);
it = std::prev(regions.upper_bound(current_addr));
}
// Get the address and region corresponding to this range.
auto& [base, region] = *it;
// Unmap the region if it was previously mapped
if (region.is_mapped) {
UnmapRegion(&region);
}
// Update region data
region.is_mapped = false;
region.fd = -1;
region.phys_base = -1;
region.prot = PAGE_NOACCESS;
// Update loop variables
remaining_size -= size_to_unmap;
current_addr += size_to_unmap;
}
// Coalesce any free space produced from these unmaps.
CoalesceFreeRegions(virtual_addr);
}
void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) {
DWORD new_flags{};
if (write && !read) {
@ -416,7 +511,7 @@ struct AddressSpace::Impl {
// If no flags are assigned, then something's gone wrong.
if (new_flags == 0) {
LOG_CRITICAL(Common_Memory,
LOG_CRITICAL(Core,
"Unsupported protection flag combination for address {:#x}, size {}, "
"read={}, write={}, execute={}",
virtual_addr, size, read, write, execute);
@ -425,13 +520,14 @@ struct AddressSpace::Impl {
const VAddr virtual_end = virtual_addr + size;
auto it = --regions.upper_bound(virtual_addr);
ASSERT_MSG(it != regions.end(), "addr {:#x} out of bounds", virtual_addr);
for (; it->first < virtual_end; it++) {
if (!it->second.is_mapped) {
continue;
}
const auto& region = it->second;
const size_t range_addr = std::max(region.base, virtual_addr);
const size_t range_size = std::min(region.base + region.size, virtual_end) - range_addr;
const u64 range_addr = std::max(region.base, virtual_addr);
const u64 range_size = std::min(region.base + region.size, virtual_end) - range_addr;
DWORD old_flags{};
if (!VirtualProtectEx(process, LPVOID(range_addr), range_size, new_flags, &old_flags)) {
UNREACHABLE_MSG(
@ -454,11 +550,11 @@ struct AddressSpace::Impl {
u8* backing_base{};
u8* virtual_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
std::map<VAddr, MemoryRegion> regions;
};
#else
@ -602,7 +698,7 @@ struct AddressSpace::Impl {
}
}
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, PosixPageProtection prot,
void* Map(VAddr virtual_addr, PAddr phys_addr, u64 size, PosixPageProtection prot,
int fd = -1) {
m_free_regions.subtract({virtual_addr, virtual_addr + size});
const int handle = phys_addr != -1 ? (fd == -1 ? backing_fd : fd) : -1;
@ -614,10 +710,10 @@ struct AddressSpace::Impl {
return ret;
}
void Unmap(VAddr virtual_addr, size_t size, bool) {
void Unmap(VAddr virtual_addr, u64 size, bool) {
// Check to see if we are adjacent to any regions.
auto start_address = virtual_addr;
auto end_address = start_address + size;
VAddr start_address = virtual_addr;
VAddr end_address = start_address + size;
auto it = m_free_regions.find({start_address - 1, end_address + 1});
// If we are, join with them, ensuring we stay in bounds.
@ -635,7 +731,7 @@ struct AddressSpace::Impl {
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
}
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
void Protect(VAddr virtual_addr, u64 size, bool read, bool write, bool execute) {
int flags = PROT_NONE;
if (read) {
flags |= PROT_READ;
@ -655,11 +751,11 @@ struct AddressSpace::Impl {
int backing_fd;
u8* backing_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
boost::icl::interval_set<VAddr> m_free_regions;
};
#endif
@ -676,8 +772,7 @@ AddressSpace::AddressSpace() : impl{std::make_unique<Impl>()} {
AddressSpace::~AddressSpace() = default;
void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr phys_addr,
bool is_exec) {
void* AddressSpace::Map(VAddr virtual_addr, u64 size, PAddr phys_addr, bool is_exec) {
#if ARCH_X86_64
const auto prot = is_exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
#else
@ -688,8 +783,7 @@ void* AddressSpace::Map(VAddr virtual_addr, size_t size, u64 alignment, PAddr ph
return impl->Map(virtual_addr, phys_addr, size, prot);
}
void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot,
uintptr_t fd) {
void* AddressSpace::MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd) {
#ifdef _WIN32
return impl->Map(virtual_addr, offset, size,
ToWindowsProt(std::bit_cast<Core::MemoryProt>(prot)), fd);
@ -699,31 +793,15 @@ void* AddressSpace::MapFile(VAddr virtual_addr, size_t size, size_t offset, u32
#endif
}
void AddressSpace::Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma,
PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file) {
void AddressSpace::Unmap(VAddr virtual_addr, u64 size, bool has_backing) {
#ifdef _WIN32
// There does not appear to be comparable support for partial unmapping on Windows.
// Unfortunately, a least one title was found to require this. The workaround is to unmap
// the entire allocation and remap the portions outside of the requested unmapping range.
impl->Unmap(virtual_addr, size, has_backing && !readonly_file);
// TODO: Determine if any titles require partial unmapping support for un-backed allocations.
ASSERT_MSG(has_backing || (start_in_vma == 0 && end_in_vma == size),
"Partial unmapping of un-backed allocations is not supported");
if (start_in_vma != 0) {
Map(virtual_addr, start_in_vma, 0, phys_base, is_exec);
}
if (end_in_vma != size) {
Map(virtual_addr + end_in_vma, size - end_in_vma, 0, phys_base + end_in_vma, is_exec);
}
impl->Unmap(virtual_addr, size);
#else
impl->Unmap(virtual_addr + start_in_vma, end_in_vma - start_in_vma, has_backing);
impl->Unmap(virtual_addr, size, has_backing);
#endif
}
void AddressSpace::Protect(VAddr virtual_addr, size_t size, MemoryPermission perms) {
void AddressSpace::Protect(VAddr virtual_addr, u64 size, MemoryPermission perms) {
const bool read = True(perms & MemoryPermission::Read);
const bool write = True(perms & MemoryPermission::Write);
const bool execute = True(perms & MemoryPermission::Execute);

View File

@ -39,7 +39,7 @@ public:
[[nodiscard]] const u8* SystemManagedVirtualBase() const noexcept {
return system_managed_base;
}
[[nodiscard]] size_t SystemManagedVirtualSize() const noexcept {
[[nodiscard]] u64 SystemManagedVirtualSize() const noexcept {
return system_managed_size;
}
@ -49,7 +49,7 @@ public:
[[nodiscard]] const u8* SystemReservedVirtualBase() const noexcept {
return system_reserved_base;
}
[[nodiscard]] size_t SystemReservedVirtualSize() const noexcept {
[[nodiscard]] u64 SystemReservedVirtualSize() const noexcept {
return system_reserved_size;
}
@ -59,7 +59,7 @@ public:
[[nodiscard]] const u8* UserVirtualBase() const noexcept {
return user_base;
}
[[nodiscard]] size_t UserVirtualSize() const noexcept {
[[nodiscard]] u64 UserVirtualSize() const noexcept {
return user_size;
}
@ -73,17 +73,15 @@ public:
* If zero is provided the mapping is considered as private.
* @return A pointer to the mapped memory.
*/
void* Map(VAddr virtual_addr, size_t size, u64 alignment = 0, PAddr phys_addr = -1,
bool exec = false);
void* Map(VAddr virtual_addr, u64 size, PAddr phys_addr = -1, bool exec = false);
/// Memory maps a specified file descriptor.
void* MapFile(VAddr virtual_addr, size_t size, size_t offset, u32 prot, uintptr_t fd);
void* MapFile(VAddr virtual_addr, u64 size, u64 offset, u32 prot, uintptr_t fd);
/// Unmaps specified virtual memory area.
void Unmap(VAddr virtual_addr, size_t size, VAddr start_in_vma, VAddr end_in_vma,
PAddr phys_base, bool is_exec, bool has_backing, bool readonly_file);
void Unmap(VAddr virtual_addr, u64 size, bool has_backing);
void Protect(VAddr virtual_addr, size_t size, MemoryPermission perms);
void Protect(VAddr virtual_addr, u64 size, MemoryPermission perms);
// Returns an interval set containing all usable regions.
boost::icl::interval_set<VAddr> GetUsableRegions();
@ -93,11 +91,11 @@ private:
std::unique_ptr<Impl> impl;
u8* backing_base{};
u8* system_managed_base{};
size_t system_managed_size{};
u64 system_managed_size{};
u8* system_reserved_base{};
size_t system_reserved_size{};
u64 system_reserved_size{};
u8* user_base{};
size_t user_size{};
u64 user_size{};
};
} // namespace Core

View File

@ -32,7 +32,7 @@ bool MemoryMapViewer::Iterator::DrawLine() {
TableNextColumn();
Text("%s", magic_enum::enum_name(m.prot).data());
TableNextColumn();
if (m.is_exec) {
if (True(m.prot & MemoryProt::CpuExec)) {
Text("X");
}
TableNextColumn();
@ -44,7 +44,7 @@ bool MemoryMapViewer::Iterator::DrawLine() {
return false;
}
auto m = dmem.it->second;
if (m.dma_type == DMAType::Free) {
if (m.dma_type == PhysicalMemoryType::Free) {
++dmem.it;
return DrawLine();
}
@ -56,7 +56,8 @@ bool MemoryMapViewer::Iterator::DrawLine() {
auto type = static_cast<::Libraries::Kernel::MemoryTypes>(m.memory_type);
Text("%s", magic_enum::enum_name(type).data());
TableNextColumn();
Text("%d", m.dma_type == DMAType::Pooled || m.dma_type == DMAType::Committed);
Text("%d",
m.dma_type == PhysicalMemoryType::Pooled || m.dma_type == PhysicalMemoryType::Committed);
++dmem.it;
return true;
}

View File

@ -11,8 +11,8 @@ class MemoryMapViewer {
struct Iterator {
bool is_vma;
struct {
MemoryManager::DMemMap::iterator it;
MemoryManager::DMemMap::iterator end;
MemoryManager::PhysMap::iterator it;
MemoryManager::PhysMap::iterator end;
} dmem;
struct {
MemoryManager::VMAMap::iterator it;

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cmath>
@ -346,7 +346,7 @@ int PS4_SYSV_ABI sceAppContentInitialize(const OrbisAppContentInitParam* initPar
if (addcont_count > 0) {
SystemService::OrbisSystemServiceEvent event{};
event.event_type = SystemService::OrbisSystemServiceEventType::EntitlementUpdate;
event.service_entitlement_update.user_id = 0;
event.service_entitlement_update.userId = 0;
event.service_entitlement_update.np_service_label = 0;
SystemService::PushSystemServiceEvent(event);
}

View File

@ -29,10 +29,10 @@ s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(const s32 handle) {
return AudioOut::sceAudioOutClose(handle);
}
s32 PS4_SYSV_ABI
sceAudio3dAudioOutOpen(const OrbisAudio3dPortId port_id, const OrbisUserServiceUserId user_id,
s32 type, const s32 index, const u32 len, const u32 freq,
const AudioOut::OrbisAudioOutParamExtendedInformation param) {
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(
const OrbisAudio3dPortId port_id, const Libraries::UserService::OrbisUserServiceUserId user_id,
s32 type, const s32 index, const u32 len, const u32 freq,
const AudioOut::OrbisAudioOutParamExtendedInformation param) {
LOG_INFO(Lib_Audio3d,
"called, port_id = {}, user_id = {}, type = {}, index = {}, len = {}, freq = {}",
port_id, user_id, type, index, len, freq);
@ -422,7 +422,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetStatus() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dPortOpen(const Libraries::UserService::OrbisUserServiceUserId user_id,
const OrbisAudio3dOpenParameters* parameters,
OrbisAudio3dPortId* port_id) {
LOG_INFO(Lib_Audio3d, "called, user_id = {}, parameters = {}, id = {}", user_id,

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -15,8 +15,6 @@ class SymbolsResolver;
namespace Libraries::Audio3d {
using OrbisUserServiceUserId = s32;
enum class OrbisAudio3dRate : u32 {
ORBIS_AUDIO3D_RATE_48000 = 0,
};
@ -91,7 +89,8 @@ struct Audio3dState {
};
s32 PS4_SYSV_ABI sceAudio3dAudioOutClose(s32 handle);
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id, OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dAudioOutOpen(OrbisAudio3dPortId port_id,
Libraries::UserService::OrbisUserServiceUserId user_id,
s32 type, s32 index, u32 len, u32 freq,
AudioOut::OrbisAudioOutParamExtendedInformation param);
s32 PS4_SYSV_ABI sceAudio3dAudioOutOutput(s32 handle, void* ptr);
@ -127,7 +126,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortGetQueueLevel(OrbisAudio3dPortId port_id, u32* qu
u32* queue_available);
s32 PS4_SYSV_ABI sceAudio3dPortGetState();
s32 PS4_SYSV_ABI sceAudio3dPortGetStatus();
s32 PS4_SYSV_ABI sceAudio3dPortOpen(OrbisUserServiceUserId user_id,
s32 PS4_SYSV_ABI sceAudio3dPortOpen(Libraries::UserService::OrbisUserServiceUserId user_id,
const OrbisAudio3dOpenParameters* parameters,
OrbisAudio3dPortId* port_id);
s32 PS4_SYSV_ABI sceAudio3dPortPush(OrbisAudio3dPortId port_id, OrbisAudio3dBlocking blocking);

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
@ -16,7 +16,7 @@ s32 PS4_SYSV_ABI sceCompanionHttpdAddHeader(const char* key, const char* value,
}
s32 PS4_SYSV_ABI
sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId) {
sceCompanionHttpdGet2ndScreenStatus(Libraries::UserService::OrbisUserServiceUserId userId) {
LOG_ERROR(Lib_CompanionHttpd, "(STUBBED) called");
return ORBIS_OK;
}

View File

@ -1,9 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
#include "core/libraries/system/userservice.h"
namespace Core::Loader {
class SymbolsResolver;
@ -15,11 +16,11 @@ struct OrbisGameLiveStreamingStatus {
bool isOnAir;
u8 align[3];
u32 spectatorCounts;
s32 userId;
Libraries::UserService::OrbisUserServiceUserId userId;
u8 reserved[60];
};
struct OrbisGameLiveStreamingStatus2 {
s32 userId;
Libraries::UserService::OrbisUserServiceUserId userId;
bool isOnAir;
u8 align[3];
u32 spectatorCounts;

View File

@ -89,21 +89,31 @@ s32 PS4_SYSV_ABI sceKernelAllocateMainDirectMemory(u64 len, u64 alignment, s32 m
}
s32 PS4_SYSV_ABI sceKernelCheckedReleaseDirectMemory(u64 start, u64 len) {
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) {
LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start,
len);
return ORBIS_KERNEL_ERROR_EINVAL;
}
if (len == 0) {
return ORBIS_OK;
}
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
auto* memory = Core::Memory::Instance();
memory->Free(start, len);
return ORBIS_OK;
return memory->Free(start, len, true);
}
s32 PS4_SYSV_ABI sceKernelReleaseDirectMemory(u64 start, u64 len) {
LOG_INFO(Kernel_Vmm, "called start = {:#x}, len = {:#x}", start, len);
if (!Common::Is16KBAligned(start) || !Common::Is16KBAligned(len)) {
LOG_ERROR(Kernel_Vmm, "Misaligned start or length, start = {:#x}, length = {:#x}", start,
len);
return ORBIS_KERNEL_ERROR_EINVAL;
}
if (len == 0) {
return ORBIS_OK;
}
auto* memory = Core::Memory::Instance();
memory->Free(start, len);
memory->Free(start, len, false);
return ORBIS_OK;
}

View File

@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
#include "core/libraries/np/np_types.h"
#include "core/libraries/system/userservice.h"
namespace Core::Loader {
class SymbolsResolver;
@ -31,7 +32,7 @@ struct OrbisNpAuthGetAuthorizationCodeParameter {
struct OrbisNpAuthGetAuthorizationCodeParameterA {
u64 size;
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId user_id;
u8 padding[4];
const OrbisNpClientId* client_id;
const char* scope;
@ -47,7 +48,7 @@ struct OrbisNpAuthGetIdTokenParameter {
struct OrbisNpAuthGetIdTokenParameterA {
u64 size;
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId user_id;
u8 padding[4];
const OrbisNpClientId* client_id;
const OrbisNpClientSecret* client_secret;

View File

@ -23,8 +23,8 @@ enum class OrbisNpState : u32 {
SignedIn = 2,
};
using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)(s32 userId, OrbisNpState state,
void* userdata);
using OrbisNpStateCallbackForNpToolkit = PS4_SYSV_ABI void (*)(
Libraries::UserService::OrbisUserServiceUserId userId, OrbisNpState state, void* userdata);
enum class OrbisNpGamePresenseStatus {
Offline = 0,

View File

@ -149,7 +149,8 @@ int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, s32 user_id,
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
Libraries::UserService::OrbisUserServiceUserId user_id,
uint32_t service_label, u64 options) {
ASSERT(options == 0ull);
if (!context) {

View File

@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <core/libraries/system/userservice.h>
#include "common/types.h"
#include "core/libraries/rtc/rtc.h"
@ -132,7 +133,8 @@ int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup();
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion();
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails();
int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature();
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, s32 user_id,
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
Libraries::UserService::OrbisUserServiceUserId user_id,
u32 service_label, u64 options);
s32 PS4_SYSV_ABI sceNpTrophyCreateHandle(OrbisNpTrophyHandle* handle);
int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context);

View File

@ -162,7 +162,8 @@ int PS4_SYSV_ABI scePadGetFeatureReport() {
return ORBIS_OK;
}
int PS4_SYSV_ABI scePadGetHandle(s32 userId, s32 type, s32 index) {
int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index) {
if (!g_initialized) {
return ORBIS_PAD_ERROR_NOT_INITIALIZED;
}
@ -259,7 +260,8 @@ int PS4_SYSV_ABI scePadMbusTerm() {
return ORBIS_OK;
}
int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenParam* pParam) {
int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index, const OrbisPadOpenParam* pParam) {
if (!g_initialized) {
return ORBIS_PAD_ERROR_NOT_INITIALIZED;
}
@ -280,8 +282,8 @@ int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenP
return userId; // TODO: userId shouldn't be used as the handle too
}
int PS4_SYSV_ABI scePadOpenExt(s32 userId, s32 type, s32 index,
const OrbisPadOpenExtParam* pParam) {
int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index, const OrbisPadOpenExtParam* pParam) {
LOG_ERROR(Lib_Pad, "(STUBBED) called");
if (EmulatorSettings::GetInstance()->IsUsingSpecialPad()) {
if (type != ORBIS_PAD_PORT_TYPE_SPECIAL)

View File

@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <core/libraries/system/userservice.h>
#include "common/enum.h"
#include "common/types.h"
@ -276,7 +277,8 @@ int PS4_SYSV_ABI scePadGetExtControllerInformation(s32 handle,
OrbisPadExtendedControllerInformation* pInfo);
int PS4_SYSV_ABI scePadGetExtensionUnitInfo();
int PS4_SYSV_ABI scePadGetFeatureReport();
int PS4_SYSV_ABI scePadGetHandle(s32 userId, s32 type, s32 index);
int PS4_SYSV_ABI scePadGetHandle(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index);
int PS4_SYSV_ABI scePadGetIdleCount();
int PS4_SYSV_ABI scePadGetInfo();
int PS4_SYSV_ABI scePadGetInfoByPortType();
@ -294,8 +296,10 @@ int PS4_SYSV_ABI scePadIsMoveReproductionModel();
int PS4_SYSV_ABI scePadIsValidHandle();
int PS4_SYSV_ABI scePadMbusInit();
int PS4_SYSV_ABI scePadMbusTerm();
int PS4_SYSV_ABI scePadOpen(s32 userId, s32 type, s32 index, const OrbisPadOpenParam* pParam);
int PS4_SYSV_ABI scePadOpenExt(s32 userId, s32 type, s32 index, const OrbisPadOpenExtParam* pParam);
int PS4_SYSV_ABI scePadOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index, const OrbisPadOpenParam* pParam);
int PS4_SYSV_ABI scePadOpenExt(Libraries::UserService::OrbisUserServiceUserId userId, s32 type,
s32 index, const OrbisPadOpenExtParam* pParam);
int PS4_SYSV_ABI scePadOpenExt2();
int PS4_SYSV_ABI scePadOutputReport();
int PS4_SYSV_ABI scePadRead(s32 handle, OrbisPadData* pData, s32 num);

View File

@ -54,7 +54,8 @@ int PS4_SYSV_ABI sceRemoteplayGetConnectHistory() {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(s32 userId, int* pStatus) {
int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(
Libraries::UserService::OrbisUserServiceUserId userId, int* pStatus) {
*pStatus = ORBIS_REMOTEPLAY_CONNECTION_STATUS_DISCONNECT;
return ORBIS_OK;
}

View File

@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <core/libraries/system/userservice.h>
#include "common/types.h"
namespace Core::Loader {
@ -24,7 +25,8 @@ int PS4_SYSV_ABI sceRemoteplayDisconnect();
int PS4_SYSV_ABI sceRemoteplayGeneratePinCode();
int PS4_SYSV_ABI sceRemoteplayGetApMode();
int PS4_SYSV_ABI sceRemoteplayGetConnectHistory();
int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(s32 userId, int* pStatus);
int PS4_SYSV_ABI sceRemoteplayGetConnectionStatus(
Libraries::UserService::OrbisUserServiceUserId userId, int* pStatus);
int PS4_SYSV_ABI sceRemoteplayGetConnectUserId();
int PS4_SYSV_ABI sceRemoteplayGetMbusDeviceInfo();
int PS4_SYSV_ABI sceRemoteplayGetOperationStatus();

View File

@ -167,7 +167,7 @@ void StopThread() {
g_backup_thread_semaphore.release();
}
bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id,
bool NewRequest(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view title_id,
std::string_view dir_name, OrbisSaveDataEventType origin) {
auto save_path = SaveInstance::MakeDirSavePath(user_id, title_id, dir_name);

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -6,12 +6,11 @@
#include <filesystem>
#include <optional>
#include <core/libraries/system/userservice.h>
#include "common/types.h"
namespace Libraries::SaveData {
using OrbisUserServiceUserId = s32;
namespace Backup {
enum class WorkerStatus {
@ -32,7 +31,7 @@ enum class OrbisSaveDataEventType : u32 {
struct BackupRequest {
bool done{};
OrbisUserServiceUserId user_id{};
Libraries::UserService::OrbisUserServiceUserId user_id{};
std::string title_id{};
std::string dir_name{};
OrbisSaveDataEventType origin{};
@ -45,7 +44,7 @@ void StartThread();
void StopThread();
bool NewRequest(OrbisUserServiceUserId user_id, std::string_view title_id,
bool NewRequest(Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view title_id,
std::string_view dir_name, OrbisSaveDataEventType origin);
bool Restore(const std::filesystem::path& save_path);

View File

@ -47,7 +47,7 @@ static const std::unordered_map<int, std::string> default_title = {
namespace Libraries::SaveData {
fs::path SaveInstance::MakeTitleSavePath(OrbisUserServiceUserId user_id,
fs::path SaveInstance::MakeTitleSavePath(Libraries::UserService::OrbisUserServiceUserId user_id,
std::string_view game_serial) {
return EmulatorSettings::GetInstance()->GetHomeDir() / std::to_string(user_id) / "savedata" /
game_serial;
@ -92,8 +92,8 @@ void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name,
#undef P
}
SaveInstance::SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string _game_serial,
std::string_view _dir_name, int max_blocks)
SaveInstance::SaveInstance(int slot_num, Libraries::UserService::OrbisUserServiceUserId user_id,
std::string _game_serial, std::string_view _dir_name, int max_blocks)
: slot_num(slot_num), user_id(user_id), game_serial(std::move(_game_serial)),
dir_name(_dir_name),
max_blocks(std::clamp(max_blocks, OrbisSaveDataBlocksMin2, OrbisSaveDataBlocksMax)) {

View File

@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <core/libraries/system/userservice.h>
#include "common/io_file.h"
#include "core/file_format/psf.h"
@ -52,13 +53,13 @@ class SaveInstance {
public:
// Location of all save data for a title
static std::filesystem::path MakeTitleSavePath(OrbisUserServiceUserId user_id,
std::string_view game_serial);
static std::filesystem::path MakeTitleSavePath(
Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial);
// Location of a specific save data directory
static std::filesystem::path MakeDirSavePath(OrbisUserServiceUserId user_id,
std::string_view game_serial,
std::string_view dir_name);
static std::filesystem::path MakeDirSavePath(
Libraries::UserService::OrbisUserServiceUserId user_id, std::string_view game_serial,
std::string_view dir_name);
static uint64_t GetMaxBlockFromSFO(const PSF& psf);
@ -67,8 +68,8 @@ public:
static void SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name, std::string game_serial);
explicit SaveInstance(int slot_num, OrbisUserServiceUserId user_id, std::string game_serial,
std::string_view dir_name, int max_blocks = 0);
explicit SaveInstance(int slot_num, Libraries::UserService::OrbisUserServiceUserId user_id,
std::string game_serial, std::string_view dir_name, int max_blocks = 0);
~SaveInstance();

View File

@ -88,8 +88,8 @@ std::string GetSaveDir(u32 slot_id) {
return dir;
}
std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id,
std::string_view game_serial) {
std::filesystem::path GetSavePath(Libraries::UserService::OrbisUserServiceUserId user_id,
u32 slot_id, std::string_view game_serial) {
std::string dir(StandardDirnameSaveDataMemory);
if (slot_id > 0) {
dir += std::to_string(slot_id);
@ -97,8 +97,8 @@ std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id,
return SaveInstance::MakeDirSavePath(user_id, game_serial, dir);
}
size_t SetupSaveMemory(OrbisUserServiceUserId user_id, u32 slot_id, std::string_view game_serial,
size_t memory_size) {
size_t SetupSaveMemory(Libraries::UserService::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);

View File

@ -4,26 +4,24 @@
#pragma once
#include <vector>
#include <core/libraries/system/userservice.h>
#include "core/libraries/save_data/save_backup.h"
class PSF;
namespace Libraries::SaveData {
using OrbisUserServiceUserId = s32;
} // namespace Libraries::SaveData
namespace Libraries::SaveData::SaveMemory {
void PersistMemory(u32 slot_id, bool lock = true);
[[nodiscard]] std::string GetSaveDir(u32 slot_id);
[[nodiscard]] std::filesystem::path GetSavePath(OrbisUserServiceUserId user_id, u32 slot_id,
std::string_view game_serial);
[[nodiscard]] std::filesystem::path GetSavePath(
Libraries::UserService::OrbisUserServiceUserId user_id, u32 slot_id,
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 memory_size);
size_t SetupSaveMemory(Libraries::UserService::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);

View File

@ -44,7 +44,6 @@ enum class OrbisSaveDataSaveDataMemoryOption : u32 {
UNLOCK_LIMITATIONS = 1 << 2,
};
using OrbisUserServiceUserId = s32;
using OrbisSaveDataBlocks = u64;
constexpr u32 OrbisSaveDataBlockSize = 32768; // 32 KiB
@ -99,7 +98,7 @@ struct OrbisSaveDataFingerprint {
};
struct OrbisSaveDataBackup {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -108,7 +107,7 @@ struct OrbisSaveDataBackup {
};
struct OrbisSaveDataCheckBackupData {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -118,7 +117,7 @@ struct OrbisSaveDataCheckBackupData {
};
struct OrbisSaveDataDelete {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -155,7 +154,7 @@ struct OrbisSaveDataMemoryData {
};
struct OrbisSaveDataMemoryGet2 {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
std::array<u8, 4> _pad;
OrbisSaveDataMemoryData* data;
OrbisSaveDataParam* param;
@ -165,7 +164,7 @@ struct OrbisSaveDataMemoryGet2 {
};
struct OrbisSaveDataMemorySet2 {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
std::array<u8, 4> _pad;
const OrbisSaveDataMemoryData* data;
const OrbisSaveDataParam* param;
@ -177,7 +176,7 @@ struct OrbisSaveDataMemorySet2 {
struct OrbisSaveDataMemorySetup2 {
OrbisSaveDataSaveDataMemoryOption option;
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
size_t memorySize;
size_t iconMemorySize;
// +4.5
@ -199,14 +198,14 @@ enum OrbisSaveDataMemorySyncOption : u32 {
};
struct OrbisSaveDataMemorySync {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
u32 slotId;
OrbisSaveDataMemorySyncOption option;
std::array<u8, 28> _reserved;
};
struct OrbisSaveDataMount2 {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataDirName* dirName;
OrbisSaveDataBlocks blocks;
@ -216,7 +215,7 @@ struct OrbisSaveDataMount2 {
};
struct OrbisSaveDataMount {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -247,7 +246,7 @@ struct OrbisSaveDataMountResult {
};
struct OrbisSaveDataRestoreBackupData {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
s32 : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -258,7 +257,7 @@ struct OrbisSaveDataRestoreBackupData {
};
struct OrbisSaveDataTransferringMount {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
const OrbisSaveDataFingerprint* fingerprint;
@ -266,7 +265,7 @@ struct OrbisSaveDataTransferringMount {
};
struct OrbisSaveDataDirNameSearchCond {
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
int : 32;
const OrbisSaveDataTitleId* titleId;
const OrbisSaveDataDirName* dirName;
@ -305,7 +304,7 @@ using OrbisSaveDataEventType = Backup::OrbisSaveDataEventType;
struct OrbisSaveDataEvent {
OrbisSaveDataEventType type;
s32 errorCode;
OrbisUserServiceUserId userId;
Libraries::UserService::OrbisUserServiceUserId userId;
std::array<u8, 4> _pad;
OrbisSaveDataTitleId titleId;
OrbisSaveDataDirName dirName;
@ -1111,8 +1110,9 @@ int PS4_SYSV_ABI sceSaveDataGetSaveDataCount() {
return ORBIS_OK;
}
Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(const OrbisUserServiceUserId userId, void* buf,
const size_t bufSize, const int64_t offset) {
Error PS4_SYSV_ABI
sceSaveDataGetSaveDataMemory(const Libraries::UserService::OrbisUserServiceUserId userId, void* buf,
const size_t bufSize, const int64_t offset) {
LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataGetSaveDataMemory2");
OrbisSaveDataMemoryData data{};
data.buf = buf;
@ -1474,8 +1474,9 @@ int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser() {
return ORBIS_OK;
}
Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset) {
Error PS4_SYSV_ABI
sceSaveDataSetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset) {
LOG_DEBUG(Lib_SaveData, "Redirecting to sceSaveDataSetSaveDataMemory2");
OrbisSaveDataMemoryData data{};
data.buf = buf;
@ -1532,8 +1533,9 @@ Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2*
return Error::OK;
}
Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(OrbisUserServiceUserId userId, size_t memorySize,
OrbisSaveDataParam* param) {
Error PS4_SYSV_ABI
sceSaveDataSetupSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId,
size_t memorySize, OrbisSaveDataParam* param) {
LOG_DEBUG(Lib_SaveData, "called: userId = {}, memorySize = {}", userId, memorySize);
OrbisSaveDataMemorySetup2 setupParam{};
setupParam.userId = userId;

View File

@ -3,6 +3,7 @@
#pragma once
#include <core/libraries/system/userservice.h>
#include "common/cstring.h"
#include "common/types.h"
@ -21,8 +22,6 @@ constexpr size_t OrbisSaveDataDetailMaxsize = 1024; // Maximum detail name size
enum class Error : u32;
enum class OrbisSaveDataParamType : u32;
using OrbisUserServiceUserId = s32;
// Maximum size for a title ID (4 uppercase letters + 5 digits)
constexpr int OrbisSaveDataTitleIdDataSize = 10;
// Maximum save directory name size
@ -126,8 +125,9 @@ Error PS4_SYSV_ABI sceSaveDataGetParam(const OrbisSaveDataMountPoint* mountPoint
size_t paramBufSize, size_t* gotSize);
Error PS4_SYSV_ABI sceSaveDataGetProgress(float* progress);
int PS4_SYSV_ABI sceSaveDataGetSaveDataCount();
Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory(OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset);
Error PS4_SYSV_ABI
sceSaveDataGetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset);
Error PS4_SYSV_ABI sceSaveDataGetSaveDataMemory2(OrbisSaveDataMemoryGet2* getParam);
int PS4_SYSV_ABI sceSaveDataGetSaveDataRootDir();
int PS4_SYSV_ABI sceSaveDataGetSaveDataRootPath();
@ -163,11 +163,13 @@ Error PS4_SYSV_ABI sceSaveDataSetParam(const OrbisSaveDataMountPoint* mountPoint
OrbisSaveDataParamType paramType, const void* paramBuf,
size_t paramBufSize);
int PS4_SYSV_ABI sceSaveDataSetSaveDataLibraryUser();
Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory(OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset);
Error PS4_SYSV_ABI
sceSaveDataSetSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId, void* buf,
size_t bufSize, int64_t offset);
Error PS4_SYSV_ABI sceSaveDataSetSaveDataMemory2(const OrbisSaveDataMemorySet2* setParam);
Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory(OrbisUserServiceUserId userId, size_t memorySize,
OrbisSaveDataParam* param);
Error PS4_SYSV_ABI
sceSaveDataSetupSaveDataMemory(Libraries::UserService::OrbisUserServiceUserId userId,
size_t memorySize, OrbisSaveDataParam* param);
Error PS4_SYSV_ABI sceSaveDataSetupSaveDataMemory2(const OrbisSaveDataMemorySetup2* setupParam,
OrbisSaveDataMemorySetupResult* result);
int PS4_SYSV_ABI sceSaveDataShutdownStart();

View File

@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <core/libraries/system/userservice.h>
#include "common/types.h"
#include "core/libraries/np/np_types.h"
@ -21,8 +22,8 @@ struct OrbisSharePlayConnectionInfo {
int mode;
Libraries::Np::OrbisNpOnlineId hostOnlineId;
Libraries::Np::OrbisNpOnlineId visitorOnlineId;
s32 hostUserId;
s32 visitorUserId;
Libraries::UserService::OrbisUserServiceUserId hostUserId;
Libraries::UserService::OrbisUserServiceUserId visitorUserId;
};
int PS4_SYSV_ABI sceSharePlayCrashDaemon();

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// reference
// https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/_types/sys_service.h
@ -7,6 +7,7 @@
#include <mutex>
#include <queue>
#include "common/types.h"
#include "userservice.h"
namespace Core::Loader {
class SymbolsResolver;
@ -119,12 +120,12 @@ struct OrbisSystemServiceEvent {
char boot_argument[7169];
} join_event;
struct {
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId userId;
u32 np_service_label;
u8 reserved[8184];
} service_entitlement_update;
struct {
s32 user_id;
Libraries::UserService::OrbisUserServiceUserId userId;
u32 np_service_label;
u8 reserved[8184];
} unified_entitlement_update;

View File

@ -292,8 +292,8 @@ s32 PS4_SYSV_ABI sceVideoOutGetResolutionStatus(s32 handle, SceVideoOutResolutio
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceVideoOutOpen(SceUserServiceUserId userId, s32 busType, s32 index,
const void* param) {
s32 PS4_SYSV_ABI sceVideoOutOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 busType,
s32 index, const void* param) {
LOG_INFO(Lib_VideoOut, "called");
ASSERT(busType == SCE_VIDEO_OUT_BUS_TYPE_MAIN);

View File

@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <core/libraries/system/userservice.h>
#include "core/libraries/kernel/equeue.h"
#include "core/libraries/videoout/buffer.h"
@ -12,8 +13,6 @@ class SymbolsResolver;
namespace Libraries::VideoOut {
using SceUserServiceUserId = s32; // TODO move it to proper place
// SceVideoOutBusType
constexpr int SCE_VIDEO_OUT_BUS_TYPE_MAIN = 0; // Main output
constexpr int SCE_VIDEO_OUT_BUS_TYPE_AUX_SOCIAL_SCREEN = 5; // Aux output for social
@ -131,8 +130,8 @@ s32 PS4_SYSV_ABI sceVideoOutWaitVblank(s32 handle);
s32 PS4_SYSV_ABI sceVideoOutSubmitFlip(s32 handle, s32 bufferIndex, s32 flipMode, s64 flipArg);
s32 PS4_SYSV_ABI sceVideoOutGetFlipStatus(s32 handle, FlipStatus* status);
s32 PS4_SYSV_ABI sceVideoOutGetResolutionStatus(s32 handle, SceVideoOutResolutionStatus* status);
s32 PS4_SYSV_ABI sceVideoOutOpen(SceUserServiceUserId userId, s32 busType, s32 index,
const void* param);
s32 PS4_SYSV_ABI sceVideoOutOpen(Libraries::UserService::OrbisUserServiceUserId userId, s32 busType,
s32 index, const void* param);
s32 PS4_SYSV_ABI sceVideoOutClose(s32 handle);
s32 PS4_SYSV_ABI sceVideoOutGetEventId(const Kernel::SceKernelEvent* ev);
s32 PS4_SYSV_ABI sceVideoOutGetEventData(const Kernel::SceKernelEvent* ev, s64* data);

View File

@ -5,9 +5,12 @@
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
#include "core/libraries/web_browser_dialog/webbrowserdialog.h"
#include "magic_enum/magic_enum.hpp"
namespace Libraries::WebBrowserDialog {
static auto g_status = Libraries::CommonDialog::Status::NONE;
s32 PS4_SYSV_ABI sceWebBrowserDialogClose() {
LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called");
return ORBIS_OK;
@ -23,14 +26,19 @@ s32 PS4_SYSV_ABI sceWebBrowserDialogGetResult() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceWebBrowserDialogGetStatus() {
LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called");
return ORBIS_OK;
Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogGetStatus() {
LOG_TRACE(Lib_MsgDlg, "called status={}", magic_enum::enum_name(g_status));
return g_status;
}
s32 PS4_SYSV_ABI sceWebBrowserDialogInitialize() {
LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called");
return ORBIS_OK;
Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogInitialize() {
if (CommonDialog::g_isInitialized) {
LOG_INFO(Lib_WebBrowserDialog, "already initialized");
return Libraries::CommonDialog::Error::ALREADY_SYSTEM_INITIALIZED;
}
LOG_DEBUG(Lib_WebBrowserDialog, "initialized");
CommonDialog::g_isInitialized = true;
return Libraries::CommonDialog::Error::OK;
}
s32 PS4_SYSV_ABI sceWebBrowserDialogNavigate() {
@ -63,14 +71,22 @@ s32 PS4_SYSV_ABI sceWebBrowserDialogSetZoom() {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceWebBrowserDialogTerminate() {
LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called");
return ORBIS_OK;
Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogTerminate() {
if (g_status == Libraries::CommonDialog::Status::RUNNING) {
LOG_ERROR(Lib_WebBrowserDialog,
"CloseWebBrowser Dialog unimplemented"); // sceWebBrowserDialogClose();
}
if (g_status == Libraries::CommonDialog::Status::NONE) {
return Libraries::CommonDialog::Error::NOT_INITIALIZED;
}
g_status = Libraries::CommonDialog::Status::NONE;
CommonDialog::g_isUsed = false;
return Libraries::CommonDialog::Error::OK;
}
s32 PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus() {
LOG_ERROR(Lib_WebBrowserDialog, "(STUBBED) called");
return ORBIS_OK;
Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus() {
LOG_TRACE(Lib_MsgDlg, "called status={}", magic_enum::enum_name(g_status));
return g_status;
}
s32 PS4_SYSV_ABI Func_F2BE042771625F8C() {

View File

@ -3,6 +3,7 @@
#pragma once
#include <core/libraries/system/commondialog.h>
#include "common/types.h"
namespace Core::Loader {
@ -14,16 +15,16 @@ namespace Libraries::WebBrowserDialog {
s32 PS4_SYSV_ABI sceWebBrowserDialogClose();
s32 PS4_SYSV_ABI sceWebBrowserDialogGetEvent();
s32 PS4_SYSV_ABI sceWebBrowserDialogGetResult();
s32 PS4_SYSV_ABI sceWebBrowserDialogGetStatus();
s32 PS4_SYSV_ABI sceWebBrowserDialogInitialize();
Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogGetStatus();
Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogInitialize();
s32 PS4_SYSV_ABI sceWebBrowserDialogNavigate();
s32 PS4_SYSV_ABI sceWebBrowserDialogOpen();
s32 PS4_SYSV_ABI sceWebBrowserDialogOpenForPredeterminedContent();
s32 PS4_SYSV_ABI sceWebBrowserDialogResetCookie();
s32 PS4_SYSV_ABI sceWebBrowserDialogSetCookie();
s32 PS4_SYSV_ABI sceWebBrowserDialogSetZoom();
s32 PS4_SYSV_ABI sceWebBrowserDialogTerminate();
s32 PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus();
Libraries::CommonDialog::Error PS4_SYSV_ABI sceWebBrowserDialogTerminate();
Libraries::CommonDialog::Status PS4_SYSV_ABI sceWebBrowserDialogUpdateStatus();
s32 PS4_SYSV_ABI Func_F2BE042771625F8C();
void RegisterLib(Core::Loader::SymbolsResolver* sym);

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
#include <string>
#include <string_view>
#include "common/enum.h"
#include "common/shared_first_mutex.h"
#include "common/singleton.h"
#include "common/types.h"
#include "core/address_space.h"
@ -54,12 +55,37 @@ enum class MemoryMapFlags : u32 {
};
DECLARE_ENUM_FLAG_OPERATORS(MemoryMapFlags)
enum class DMAType : u32 {
enum class PhysicalMemoryType : u32 {
Free = 0,
Allocated = 1,
Mapped = 2,
Pooled = 3,
Committed = 4,
Flexible = 5,
};
struct PhysicalMemoryArea {
PAddr base = 0;
u64 size = 0;
s32 memory_type = 0;
PhysicalMemoryType dma_type = PhysicalMemoryType::Free;
PAddr GetEnd() const {
return base + size;
}
bool CanMergeWith(const PhysicalMemoryArea& next) const {
if (base + size != next.base) {
return false;
}
if (memory_type != next.memory_type) {
return false;
}
if (dma_type != next.dma_type) {
return false;
}
return true;
}
};
enum class VMAType : u32 {
@ -74,65 +100,24 @@ enum class VMAType : u32 {
File = 8,
};
struct DirectMemoryArea {
PAddr base = 0;
u64 size = 0;
s32 memory_type = 0;
DMAType dma_type = DMAType::Free;
PAddr GetEnd() const {
return base + size;
}
bool CanMergeWith(const DirectMemoryArea& next) const {
if (base + size != next.base) {
return false;
}
if (memory_type != next.memory_type) {
return false;
}
if (dma_type != next.dma_type) {
return false;
}
return true;
}
};
struct FlexibleMemoryArea {
PAddr base = 0;
u64 size = 0;
bool is_free = true;
PAddr GetEnd() const {
return base + size;
}
bool CanMergeWith(const FlexibleMemoryArea& next) const {
if (base + size != next.base) {
return false;
}
if (is_free != next.is_free) {
return false;
}
return true;
}
};
struct VirtualMemoryArea {
VAddr base = 0;
u64 size = 0;
PAddr phys_base = 0;
std::map<uintptr_t, PhysicalMemoryArea> phys_areas;
VMAType type = VMAType::Free;
MemoryProt prot = MemoryProt::NoAccess;
bool disallow_merge = false;
std::string name = "";
uintptr_t fd = 0;
bool is_exec = false;
s32 fd = 0;
bool disallow_merge = false;
bool Contains(VAddr addr, u64 size) const {
return addr >= base && (addr + size) <= (base + this->size);
}
bool Overlaps(VAddr addr, u64 size) const {
return addr <= (base + this->size) && (addr + size) >= base;
}
bool IsFree() const noexcept {
return type == VMAType::Free;
}
@ -141,30 +126,35 @@ struct VirtualMemoryArea {
return type != VMAType::Free && type != VMAType::Reserved && type != VMAType::PoolReserved;
}
bool CanMergeWith(const VirtualMemoryArea& next) const {
bool CanMergeWith(VirtualMemoryArea& next) {
if (disallow_merge || next.disallow_merge) {
return false;
}
if (base + size != next.base) {
return false;
}
if ((type == VMAType::Direct || type == VMAType::Flexible || type == VMAType::Pooled) &&
phys_base + size != next.phys_base) {
return false;
if (type == VMAType::Direct && next.type == VMAType::Direct) {
auto& last_phys = std::prev(phys_areas.end())->second;
auto& first_next_phys = next.phys_areas.begin()->second;
if (last_phys.base + last_phys.size != first_next_phys.base ||
last_phys.memory_type != first_next_phys.memory_type) {
return false;
}
}
if (prot != next.prot || type != next.type) {
return false;
}
if (name.compare(next.name) != 0) {
return false;
}
return true;
}
};
class MemoryManager {
using DMemMap = std::map<PAddr, DirectMemoryArea>;
using DMemHandle = DMemMap::iterator;
using FMemMap = std::map<PAddr, FlexibleMemoryArea>;
using FMemHandle = FMemMap::iterator;
using PhysMap = std::map<PAddr, PhysicalMemoryArea>;
using PhysHandle = PhysMap::iterator;
using VMAMap = std::map<VAddr, VirtualMemoryArea>;
using VMAHandle = VMAMap::iterator;
@ -220,10 +210,11 @@ public:
// Now make sure the full address range is contained in vma_map.
auto vma_handle = FindVMA(virtual_addr);
auto addr_to_check = virtual_addr;
s64 size_to_validate = size;
u64 size_to_validate = size;
while (vma_handle != vma_map.end() && size_to_validate > 0) {
const auto offset_in_vma = addr_to_check - vma_handle->second.base;
const auto size_in_vma = vma_handle->second.size - offset_in_vma;
const auto size_in_vma =
std::min<u64>(vma_handle->second.size - offset_in_vma, size_to_validate);
size_to_validate -= size_in_vma;
addr_to_check += size_in_vma;
vma_handle++;
@ -245,7 +236,7 @@ public:
void CopySparseMemory(VAddr source, u8* dest, u64 size);
bool TryWriteBacking(void* address, const void* data, u32 num_bytes);
bool TryWriteBacking(void* address, const void* data, u64 size);
void SetupMemoryRegions(u64 flexible_size, bool use_extended_mem1, bool use_extended_mem2);
@ -253,7 +244,7 @@ public:
PAddr Allocate(PAddr search_start, PAddr search_end, u64 size, u64 alignment, s32 memory_type);
void Free(PAddr phys_addr, u64 size);
s32 Free(PAddr phys_addr, u64 size, bool is_checked);
s32 PoolCommit(VAddr virtual_addr, u64 size, MemoryProt prot, s32 mtype);
@ -300,52 +291,37 @@ private:
return std::prev(vma_map.upper_bound(target));
}
DMemHandle FindDmemArea(PAddr target) {
PhysHandle FindDmemArea(PAddr target) {
return std::prev(dmem_map.upper_bound(target));
}
FMemHandle FindFmemArea(PAddr target) {
PhysHandle FindFmemArea(PAddr target) {
return std::prev(fmem_map.upper_bound(target));
}
template <typename Handle>
Handle MergeAdjacent(auto& handle_map, Handle iter) {
const auto next_vma = std::next(iter);
if (next_vma != handle_map.end() && iter->second.CanMergeWith(next_vma->second)) {
iter->second.size += next_vma->second.size;
handle_map.erase(next_vma);
}
if (iter != handle_map.begin()) {
auto prev_vma = std::prev(iter);
if (prev_vma->second.CanMergeWith(iter->second)) {
prev_vma->second.size += iter->second.size;
handle_map.erase(iter);
iter = prev_vma;
}
}
return iter;
}
bool HasPhysicalBacking(VirtualMemoryArea vma) {
return vma.type == VMAType::Direct || vma.type == VMAType::Flexible ||
vma.type == VMAType::Pooled;
}
std::pair<s32, MemoryManager::VMAHandle> CreateArea(VAddr virtual_addr, u64 size,
MemoryProt prot, MemoryMapFlags flags,
VMAType type, std::string_view name,
u64 alignment);
VAddr SearchFree(VAddr virtual_addr, u64 size, u32 alignment);
VMAHandle MergeAdjacent(VMAMap& map, VMAHandle iter);
PhysHandle MergeAdjacent(PhysMap& map, PhysHandle iter);
VMAHandle CarveVMA(VAddr virtual_addr, u64 size);
DMemHandle CarveDmemArea(PAddr addr, u64 size);
FMemHandle CarveFmemArea(PAddr addr, u64 size);
PhysHandle CarvePhysArea(PhysMap& map, PAddr addr, u64 size);
VMAHandle Split(VMAHandle vma_handle, u64 offset_in_vma);
DMemHandle Split(DMemHandle dmem_handle, u64 offset_in_area);
FMemHandle Split(FMemHandle fmem_handle, u64 offset_in_area);
PhysHandle Split(PhysMap& map, PhysHandle dmem_handle, u64 offset_in_area);
u64 UnmapBytesFromEntry(VAddr virtual_addr, VirtualMemoryArea vma_base, u64 size);
@ -353,14 +329,15 @@ private:
private:
AddressSpace impl;
DMemMap dmem_map;
FMemMap fmem_map;
PhysMap dmem_map;
PhysMap fmem_map;
VMAMap vma_map;
std::mutex mutex;
Common::SharedFirstMutex mutex{};
u64 total_direct_size{};
u64 total_flexible_size{};
u64 flexible_usage{};
u64 pool_budget{};
s32 sdk_version{};
Vulkan::Rasterizer* rasterizer{};
struct PrtArea {

View File

@ -1,16 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <iostream>
#include <memory>
#include <optional>
#include <vector>
#include <CLI/CLI.hpp>
#include <SDL3/SDL_messagebox.h>
#include "functional"
#include "iostream"
#include "string"
#include "system_error"
#include "unordered_map"
#include <core/emulator_state.h>
#include <fmt/core.h>
#include "common/config.h"
#include "common/key_manager.h"
#include "common/logging/backend.h"
#include "common/memory_patcher.h"
#include "common/path_util.h"
@ -29,6 +30,7 @@ int main(int argc, char* argv[]) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
IPC::Instance().Init();
// Init emulator state
std::shared_ptr<EmulatorState> m_emu_state = std::make_shared<EmulatorState>();
@ -37,254 +39,166 @@ int main(int argc, char* argv[]) {
std::shared_ptr<EmulatorSettings> emu_settings = std::make_shared<EmulatorSettings>();
EmulatorSettings::SetInstance(emu_settings);
emu_settings->Load();
const auto user_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
Config::load(user_dir / "config.toml");
// temp copy the trophy key from old config to key manager if exists
// ---- Trophy key migration ----
auto key_manager = KeyManager::GetInstance();
if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty()) {
if (!Config::getTrophyKey().empty()) {
key_manager->SetAllKeys(
{.TrophyKeySet = {.ReleaseTrophyKey =
KeyManager::HexStringToBytes(Config::getTrophyKey())}});
key_manager->SaveToFile();
}
if (key_manager->GetAllKeys().TrophyKeySet.ReleaseTrophyKey.empty() &&
!Config::getTrophyKey().empty()) {
key_manager->SetAllKeys({.TrophyKeySet = {.ReleaseTrophyKey = KeyManager::HexStringToBytes(
Config::getTrophyKey())}});
key_manager->SaveToFile();
}
bool has_game_argument = false;
std::string game_path;
std::vector<std::string> game_args{};
std::optional<std::filesystem::path> game_folder;
bool waitForDebugger = false;
CLI::App app{"shadPS4 Emulator CLI"};
// ---- CLI state ----
std::optional<std::string> gamePath;
std::vector<std::string> gameArgs;
std::optional<std::filesystem::path> overrideRoot;
std::optional<int> waitPid;
bool waitForDebugger = false;
// Map of argument strings to lambda functions
std::unordered_map<std::string, std::function<void(int&)>> arg_map = {
{"-h",
[&](int&) {
std::cout
<< "Usage: shadps4 [options] <elf or eboot.bin path>\n"
"Options:\n"
" -g, --game <path|ID> 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 <patch_file> Apply specified patch file\n"
" -i, --ignore-game-patch Disable automatic loading of game patch\n"
" -f, --fullscreen <true|false> Specify window initial fullscreen "
"state. Does not overwrite the config file.\n"
" --add-game-folder <folder> Adds a new game folder to the config.\n"
" --set-addon-folder <folder> Sets the addon folder to the config.\n"
" --log-append Append log output to file instead of "
"overwriting it.\n"
" --override-root <folder> Override the game root folder. Default is the "
"parent of game path\n"
" --wait-for-debugger Wait for debugger to attach\n"
" --wait-for-pid <pid> Wait for process with specified PID to stop\n"
" --config-clean Run the emulator with the default config "
"values, ignores the config file(s) entirely.\n"
" --config-global Run the emulator with the base config file "
"only, ignores game specific configs.\n"
" --show-fps Enable FPS counter display at startup\n"
" -h, --help Display this help message\n";
exit(0);
}},
{"--help", [&](int& i) { arg_map["-h"](i); }},
std::optional<std::string> fullscreenStr;
bool ignoreGamePatch = false;
bool showFps = false;
bool configClean = false;
bool configGlobal = false;
bool logAppend = false;
{"-g",
[&](int& i) {
if (i + 1 < argc) {
game_path = argv[++i];
has_game_argument = true;
} else {
std::cerr << "Error: Missing argument for -g/--game\n";
exit(1);
}
}},
{"--game", [&](int& i) { arg_map["-g"](i); }},
std::optional<std::filesystem::path> addGameFolder;
std::optional<std::filesystem::path> setAddonFolder;
std::optional<std::string> patchFile;
{"-p",
[&](int& i) {
if (i + 1 < argc) {
MemoryPatcher::patch_file = argv[++i];
} else {
std::cerr << "Error: Missing argument for -p/--patch\n";
exit(1);
}
}},
{"--patch", [&](int& i) { arg_map["-p"](i); }},
// ---- Options ----
app.add_option("-g,--game", gamePath, "Game path or ID");
app.add_option("-p,--patch", patchFile, "Patch file to apply");
app.add_flag("-i,--ignore-game-patch", ignoreGamePatch,
"Disable automatic loading of game patches");
{"-i", [&](int&) { Core::FileSys::MntPoints::ignore_game_patches = true; }},
{"--ignore-game-patch", [&](int& i) { arg_map["-i"](i); }},
{"-f",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for -f/--fullscreen\n";
exit(1);
}
std::string f_param(argv[i]);
bool is_fullscreen;
if (f_param == "true") {
is_fullscreen = true;
} else if (f_param == "false") {
is_fullscreen = false;
} else {
std::cerr
<< "Error: Invalid argument for -f/--fullscreen. Use 'true' or 'false'.\n";
exit(1);
}
// Set fullscreen mode without saving it to config file
EmulatorSettings::GetInstance()->SetFullScreen(is_fullscreen);
}},
{"--fullscreen", [&](int& i) { arg_map["-f"](i); }},
{"--add-game-folder",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --add-game-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);
}
// FULLSCREEN: behavior-identical
app.add_option("-f,--fullscreen", fullscreenStr, "Fullscreen mode (true|false)");
EmulatorSettings::GetInstance()->AddGameInstallDir(config_path);
EmulatorSettings::GetInstance()->Save();
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);
}
app.add_option("--override-root", overrideRoot)->check(CLI::ExistingDirectory);
EmulatorSettings::GetInstance()->SetAddonInstallDir(config_path);
EmulatorSettings::GetInstance()->Save();
std::cout << "Addon folder successfully saved.\n";
exit(0);
}},
{"--log-append", [&](int& i) { Common::Log::SetAppend(); }},
{"--config-clean", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Clean); }},
{"--config-global", [&](int& i) { Config::setConfigMode(Config::ConfigMode::Global); }},
{"--override-root",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --override-root\n";
exit(1);
}
std::string folder_str{argv[i]};
std::filesystem::path folder{folder_str};
if (!std::filesystem::exists(folder) || !std::filesystem::is_directory(folder)) {
std::cerr << "Error: Folder does not exist: " << folder_str << "\n";
exit(1);
}
game_folder = folder;
}},
{"--wait-for-debugger", [&](int& i) { waitForDebugger = true; }},
{"--wait-for-pid",
[&](int& i) {
if (++i >= argc) {
std::cerr << "Error: Missing argument for --wait-for-pid\n";
exit(1);
}
waitPid = std::stoi(argv[i]);
}},
{"--show-fps", [&](int& i) { EmulatorSettings::GetInstance()->SetShowFpsCounter(true); }}};
app.add_flag("--wait-for-debugger", waitForDebugger);
app.add_option("--wait-for-pid", waitPid);
app.add_flag("--show-fps", showFps);
app.add_flag("--config-clean", configClean);
app.add_flag("--config-global", configGlobal);
app.add_flag("--log-append", logAppend);
app.add_option("--add-game-folder", addGameFolder)->check(CLI::ExistingDirectory);
app.add_option("--set-addon-folder", setAddonFolder)->check(CLI::ExistingDirectory);
// ---- Capture args after `--` verbatim ----
app.allow_extras();
app.parse_complete_callback([&]() {
const auto& extras = app.remaining();
if (!extras.empty()) {
gameArgs = extras;
}
});
// ---- No-args behavior ----
if (argc == 1) {
if (!SDL_ShowSimpleMessageBox(
SDL_MESSAGEBOX_INFORMATION, "shadPS4",
"This is a CLI application. Please use the QTLauncher for a GUI: "
"https://github.com/shadps4-emu/shadps4-qtlauncher/releases",
nullptr))
std::cerr << "Could not display SDL message box! Error: " << SDL_GetError() << "\n";
int dummy = 0; // one does not simply pass 0 directly
arg_map.at("-h")(dummy);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "shadPS4",
"This is a CLI application. Please use the QTLauncher for a GUI:\n"
"https://github.com/shadps4-emu/shadps4-qtlauncher/releases",
nullptr);
std::cout << app.help();
return -1;
}
// Parse command-line arguments using the map
for (int i = 1; i < argc; ++i) {
std::string cur_arg = argv[i];
auto it = arg_map.find(cur_arg);
if (it != arg_map.end()) {
it->second(i); // Call the associated lambda function
} else if (i == argc - 1 && !has_game_argument) {
// Assume the last argument is the game file if not specified via -g/--game
game_path = argv[i];
has_game_argument = true;
} else if (std::string(argv[i]) == "--") {
if (i + 1 == argc) {
std::cerr << "Warning: -- is set, but no game arguments are added!\n";
break;
}
for (int j = i + 1; j < argc; j++) {
game_args.push_back(argv[j]);
}
break;
} else if (i + 1 < argc && std::string(argv[i + 1]) == "--") {
if (!has_game_argument) {
game_path = argv[i];
has_game_argument = true;
}
try {
app.parse(argc, argv);
} catch (const CLI::ParseError& e) {
return app.exit(e);
}
// ---- Utility commands ----
if (addGameFolder) {
Config::addGameInstallDir(*addGameFolder);
Config::save(user_dir / "config.toml");
std::cout << "Game folder successfully saved.\n";
return 0;
}
if (setAddonFolder) {
Config::setAddonInstallDir(*setAddonFolder);
Config::save(user_dir / "config.toml");
std::cout << "Addon folder successfully saved.\n";
return 0;
}
if (!gamePath.has_value()) {
if (!gameArgs.empty()) {
gamePath = gameArgs.front();
gameArgs.erase(gameArgs.begin());
} else {
std::cerr << "Unknown argument: " << cur_arg << ", see --help for info.\n";
}
}
// If no game directory is set and no command line argument, prompt for it
if (EmulatorSettings::GetInstance()->GetGameInstallDirs().empty()) {
std::cerr << "Warning: No game folder set, please set it by calling shadps4"
" with the --add-game-folder <folder_name> argument\n";
}
if (!has_game_argument) {
std::cerr << "Error: Please provide a game path or ID.\n";
exit(1);
}
// Check if the game path or ID exists
std::filesystem::path eboot_path(game_path);
// Check if the provided path is a valid file
if (!std::filesystem::exists(eboot_path)) {
// If not a file, treat it as a game ID and search in install directories recursively
bool game_found = false;
const int max_depth = 5;
for (const auto& install_dir : EmulatorSettings::GetInstance()->GetGameInstallDirs()) {
if (auto found_path = Common::FS::FindGameByID(install_dir, game_path, max_depth)) {
eboot_path = *found_path;
game_found = true;
break;
}
}
if (!game_found) {
std::cerr << "Error: Game ID or file path not found: " << game_path << std::endl;
std::cerr << "Error: Please provide a game path or ID.\n";
return 1;
}
}
if (waitPid.has_value()) {
Core::Debugger::WaitForPid(waitPid.value());
// ---- Apply flags ----
if (patchFile)
MemoryPatcher::patch_file = *patchFile;
if (ignoreGamePatch)
Core::FileSys::MntPoints::ignore_game_patches = true;
if (fullscreenStr) {
if (*fullscreenStr == "true") {
Config::setIsFullscreen(true);
} else if (*fullscreenStr == "false") {
Config::setIsFullscreen(false);
} else {
std::cerr << "Error: Invalid argument for --fullscreen (use true|false)\n";
return 1;
}
}
// Run the emulator with the resolved eboot path
Core::Emulator* emulator = Common::Singleton<Core::Emulator>::Instance();
if (showFps)
Config::setShowFpsCounter(true);
if (configClean)
Config::setConfigMode(Config::ConfigMode::Clean);
if (configGlobal)
Config::setConfigMode(Config::ConfigMode::Global);
if (logAppend)
Common::Log::SetAppend();
// ---- Resolve game path or ID ----
std::filesystem::path ebootPath(*gamePath);
if (!std::filesystem::exists(ebootPath)) {
bool found = false;
constexpr int maxDepth = 5;
for (const auto& installDir : Config::getGameInstallDirs()) {
if (auto foundPath = Common::FS::FindGameByID(installDir, *gamePath, maxDepth)) {
ebootPath = *foundPath;
found = true;
break;
}
}
if (!found) {
std::cerr << "Error: Game ID or file path not found: " << *gamePath << "\n";
return 1;
}
}
if (waitPid)
Core::Debugger::WaitForPid(*waitPid);
auto* emulator = Common::Singleton<Core::Emulator>::Instance();
emulator->executableName = argv[0];
emulator->waitForDebuggerBeforeRun = waitForDebugger;
emulator->Run(eboot_path, game_args, game_folder);
emulator->Run(ebootPath, gameArgs, overrideRoot);
return 0;
}

View File

@ -364,7 +364,7 @@ void EmitContext::DefineInputs() {
}
break;
}
case LogicalStage::Fragment:
case LogicalStage::Fragment: {
if (info.loads.GetAny(IR::Attribute::FragCoord)) {
frag_coord = DefineVariable(F32[4], spv::BuiltIn::FragCoord, spv::StorageClass::Input);
}
@ -418,7 +418,13 @@ void EmitContext::DefineInputs() {
spv::StorageClass::Input);
}
}
for (s32 i = 0; i < runtime_info.fs_info.num_inputs; i++) {
const bool has_clip_distance_inputs = runtime_info.fs_info.clip_distance_emulation;
// Clip distances attribute vector is the last in inputs array
const auto num_inputs =
runtime_info.fs_info.num_inputs - (has_clip_distance_inputs ? 1 : 0);
for (s32 i = 0; i < num_inputs; i++) {
const auto& input = runtime_info.fs_info.inputs[i];
if (input.IsDefault()) {
continue;
@ -428,12 +434,13 @@ void EmitContext::DefineInputs() {
const auto [primary, auxiliary] = info.fs_interpolation[i];
const Id type = F32[num_components];
const Id attr_id = [&] {
const auto bind_location = input.param_index + (has_clip_distance_inputs ? 1 : 0);
if (primary == Qualifier::PerVertex &&
profile.supports_fragment_shader_barycentric) {
return Name(DefineInput(TypeArray(type, ConstU32(3U)), input.param_index),
return Name(DefineInput(TypeArray(type, ConstU32(3U)), bind_location),
fmt::format("fs_in_attr{}_p", i));
}
return Name(DefineInput(type, input.param_index), fmt::format("fs_in_attr{}", i));
return Name(DefineInput(type, bind_location), fmt::format("fs_in_attr{}", i));
}();
if (primary == Qualifier::PerVertex) {
Decorate(attr_id, profile.supports_amd_shader_explicit_vertex_parameter
@ -450,7 +457,15 @@ void EmitContext::DefineInputs() {
input_params[i] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id, num_components,
false, false, primary == Qualifier::PerVertex);
}
if (has_clip_distance_inputs) {
const auto type = F32[MaxEmulatedClipDistances];
const auto attr_id = Name(DefineInput(type, 0), fmt::format("cldist_attr{}", 0));
input_params[num_inputs] = GetAttributeInfo(AmdGpu::NumberFormat::Float, attr_id,
MaxEmulatedClipDistances, false);
}
break;
}
case LogicalStage::Compute:
if (info.loads.GetAny(IR::Attribute::WorkgroupIndex) ||
info.loads.GetAny(IR::Attribute::WorkgroupId)) {
@ -546,11 +561,16 @@ void EmitContext::DefineVertexBlock() {
const std::array<Id, 8> zero{f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value,
f32_zero_value, f32_zero_value, f32_zero_value, f32_zero_value};
output_position = DefineVariable(F32[4], spv::BuiltIn::Position, spv::StorageClass::Output);
if (info.stores.GetAny(IR::Attribute::ClipDistance)) {
const Id type{TypeArray(F32[1], ConstU32(8U))};
const Id initializer{ConstantComposite(type, zero)};
clip_distances = DefineVariable(type, spv::BuiltIn::ClipDistance, spv::StorageClass::Output,
initializer);
const bool needs_clip_distance_emulation = l_stage == LogicalStage::Vertex &&
stage == Stage::Vertex &&
profile.needs_clip_distance_emulation;
if (!needs_clip_distance_emulation) {
if (info.stores.GetAny(IR::Attribute::ClipDistance)) {
const Id type{TypeArray(F32[1], ConstU32(8U))};
const Id initializer{ConstantComposite(type, zero)};
clip_distances = DefineVariable(type, spv::BuiltIn::ClipDistance,
spv::StorageClass::Output, initializer);
}
}
if (info.stores.GetAny(IR::Attribute::CullDistance)) {
const Id type{TypeArray(F32[1], ConstU32(8U))};
@ -583,16 +603,27 @@ void EmitContext::DefineOutputs() {
Name(output_attr_array, "out_attrs");
}
} else {
const auto has_clip_distance_outputs = info.stores.GetAny(IR::Attribute::ClipDistance);
u32 num_attrs = 0u;
for (u32 i = 0; i < IR::NumParams; i++) {
const IR::Attribute param{IR::Attribute::Param0 + i};
if (!info.stores.GetAny(param)) {
continue;
}
const u32 num_components = info.stores.NumComponents(param);
const Id id{DefineOutput(F32[num_components], i)};
const Id id{
DefineOutput(F32[num_components], i + (has_clip_distance_outputs ? 1 : 0))};
Name(id, fmt::format("out_attr{}", i));
output_params[i] =
GetAttributeInfo(AmdGpu::NumberFormat::Float, id, num_components, true);
++num_attrs;
}
if (has_clip_distance_outputs) {
clip_distances = Id{DefineOutput(F32[MaxEmulatedClipDistances], 0)};
output_params[num_attrs] = GetAttributeInfo(
AmdGpu::NumberFormat::Float, clip_distances, MaxEmulatedClipDistances, true);
Name(clip_distances, fmt::format("cldist_attr{}", 0));
}
}
break;

View File

@ -101,7 +101,7 @@ std::string NameOf(Attribute attribute) {
case Attribute::Param31:
return "Param31";
case Attribute::ClipDistance:
return "ClipDistanace";
return "ClipDistance";
case Attribute::CullDistance:
return "CullDistance";
case Attribute::RenderTargetIndex:

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "shader_recompiler/info.h"
#include "shader_recompiler/ir/basic_block.h"
#include "shader_recompiler/ir/ir_emitter.h"
#include "shader_recompiler/ir/program.h"
namespace Shader {
void InjectClipDistanceAttributes(IR::Program& program, RuntimeInfo& runtime_info) {
auto& info = runtime_info.fs_info;
if (!info.clip_distance_emulation || program.info.l_stage != LogicalStage::Fragment) {
return;
}
auto* first_block = *program.blocks.begin();
auto it = std::ranges::find_if(first_block->Instructions(), [](const IR::Inst& inst) {
return inst.GetOpcode() == IR::Opcode::Prologue;
});
ASSERT(it != first_block->end());
++it;
ASSERT(it != first_block->end());
++it;
IR::IREmitter ir{*first_block, it};
// We don't know how many clip distances are exported by VS as it is not processed at this point
// yet. Here is an assumption that we will have not more than 4 of them (while max is 8) to save
// one attributes export slot.
const auto attrib = IR::Attribute::Param0 + info.num_inputs;
for (u32 comp = 0; comp < MaxEmulatedClipDistances; ++comp) {
const auto attr_read = ir.GetAttribute(attrib, comp);
const auto cond_id = ir.FPLessThan(attr_read, ir.Imm32(0.0f));
ir.Discard(cond_id);
}
++info.num_inputs;
}
} // namespace Shader

View File

@ -8,7 +8,8 @@
namespace Shader {
struct Profile;
}
void InjectClipDistanceAttributes(IR::Program& program, RuntimeInfo& runtime_info);
} // namespace Shader
namespace Shader::Optimization {

View File

@ -660,6 +660,7 @@ void PatchGlobalDataShareAccess(IR::Block& block, IR::Inst& inst, Info& info,
inst.SetArg(1, ir.Imm32(binding));
} else {
// Convert shared memory opcode to storage buffer atomic to GDS buffer.
auto& buffer = info.buffers[binding];
const IR::U32 offset = IR::U32{inst.Arg(0)};
const IR::U32 address_words = ir.ShiftRightLogical(offset, ir.Imm32(1));
const IR::U32 address_dwords = ir.ShiftRightLogical(offset, ir.Imm32(2));
@ -705,27 +706,35 @@ void PatchGlobalDataShareAccess(IR::Block& block, IR::Inst& inst, Info& info,
case IR::Opcode::SharedAtomicXor32:
inst.ReplaceUsesWith(ir.BufferAtomicXor(handle, address_dwords, inst.Arg(1), {}));
break;
case IR::Opcode::LoadSharedU16:
case IR::Opcode::LoadSharedU16: {
inst.ReplaceUsesWith(ir.LoadBufferU16(handle, address_words, {}));
buffer.used_types |= IR::Type::U16;
break;
}
case IR::Opcode::LoadSharedU32:
inst.ReplaceUsesWith(ir.LoadBufferU32(1, handle, address_dwords, {}));
break;
case IR::Opcode::LoadSharedU64:
case IR::Opcode::LoadSharedU64: {
inst.ReplaceUsesWith(ir.LoadBufferU64(handle, address_qwords, {}));
buffer.used_types |= IR::Type::U64;
break;
case IR::Opcode::WriteSharedU16:
}
case IR::Opcode::WriteSharedU16: {
ir.StoreBufferU16(handle, address_words, IR::U16{inst.Arg(1)}, {});
inst.Invalidate();
buffer.used_types |= IR::Type::U16;
break;
}
case IR::Opcode::WriteSharedU32:
ir.StoreBufferU32(1, handle, address_dwords, inst.Arg(1), {});
inst.Invalidate();
break;
case IR::Opcode::WriteSharedU64:
case IR::Opcode::WriteSharedU64: {
ir.StoreBufferU64(handle, address_qwords, IR::U64{inst.Arg(1)}, {});
inst.Invalidate();
buffer.used_types |= IR::Type::U64;
break;
}
default:
UNREACHABLE();
}

View File

@ -41,7 +41,7 @@ struct Profile {
bool needs_lds_barriers{};
bool needs_buffer_offsets{};
bool needs_unorm_fixup{};
bool _pad0{};
bool needs_clip_distance_emulation{};
};
} // namespace Shader

View File

@ -13,17 +13,16 @@ namespace Shader {
IR::BlockList GenerateBlocks(const IR::AbstractSyntaxList& syntax_list) {
size_t num_syntax_blocks{};
for (const auto& node : syntax_list) {
if (node.type == IR::AbstractSyntaxNode::Type::Block) {
for (const auto& [_, type] : syntax_list) {
if (type == IR::AbstractSyntaxNode::Type::Block) {
++num_syntax_blocks;
}
}
IR::BlockList blocks;
IR::BlockList blocks{};
blocks.reserve(num_syntax_blocks);
u32 order_index{};
for (const auto& node : syntax_list) {
if (node.type == IR::AbstractSyntaxNode::Type::Block) {
blocks.push_back(node.data.block);
for (const auto& [data, type] : syntax_list) {
if (type == IR::AbstractSyntaxNode::Type::Block) {
blocks.push_back(data.block);
}
}
return blocks;
@ -60,6 +59,10 @@ IR::Program TranslateProgram(const std::span<const u32>& code, Pools& pools, Inf
program.blocks = GenerateBlocks(program.syntax_list);
program.post_order_blocks = Shader::IR::PostOrder(program.syntax_list.front());
// On NVIDIA GPUs HW interpolation of clip distance values seems broken, and we need to emulate
// it with expensive discard in PS.
Shader::InjectClipDistanceAttributes(program, runtime_info);
// Run optimization passes
if (!profile.support_float64) {
Shader::Optimization::LowerFp64ToFp32(program);

View File

@ -34,6 +34,7 @@ enum class LogicalStage : u32 {
};
constexpr u32 MaxStageTypes = static_cast<u32>(LogicalStage::NumLogicalStages);
constexpr auto MaxEmulatedClipDistances = 4u;
constexpr Stage StageFromIndex(size_t index) noexcept {
return static_cast<Stage>(index);
@ -201,14 +202,16 @@ struct FragmentRuntimeInfo {
std::array<PsInput, 32> inputs;
std::array<PsColorBuffer, MaxColorBuffers> color_buffers;
AmdGpu::ShaderExportFormat z_export_format;
u8 mrtz_mask;
bool dual_source_blending;
u8 mrtz_mask{};
bool dual_source_blending{false};
bool clip_distance_emulation{false};
bool operator==(const FragmentRuntimeInfo& other) const noexcept {
return std::ranges::equal(color_buffers, other.color_buffers) &&
en_flags == other.en_flags && addr_flags == other.addr_flags &&
num_inputs == other.num_inputs && z_export_format == other.z_export_format &&
mrtz_mask == other.mrtz_mask && dual_source_blending == other.dual_source_blending &&
clip_distance_emulation == other.clip_distance_emulation &&
std::ranges::equal(inputs.begin(), inputs.begin() + num_inputs, other.inputs.begin(),
other.inputs.begin() + num_inputs);
}

View File

@ -831,7 +831,14 @@ Liverpool::Task Liverpool::ProcessCompute(std::span<const u32> acb, u32 vqid) {
FIBER_ENTER(acb_task_name[vqid]);
auto& queue = asc_queues[{vqid}];
struct IndirectPatch {
const PM4Header* header;
VAddr indirect_addr;
};
boost::container::small_vector<IndirectPatch, 4> indirect_patches;
auto base_addr = reinterpret_cast<VAddr>(acb.data());
size_t acb_size = acb.size_bytes();
while (!acb.empty()) {
ProcessCommands();
@ -920,8 +927,18 @@ Liverpool::Task Liverpool::ProcessCompute(std::span<const u32> acb, u32 vqid) {
dma_data->src_sel == DmaDataSrc::MemoryUsingL2) &&
(dma_data->dst_sel == DmaDataDst::Memory ||
dma_data->dst_sel == DmaDataDst::MemoryUsingL2)) {
rasterizer->CopyBuffer(dma_data->DstAddress<VAddr>(), dma_data->SrcAddress<VAddr>(),
dma_data->NumBytes(), false, false);
const u32 num_bytes = dma_data->NumBytes();
const VAddr src_addr = dma_data->SrcAddress<VAddr>();
const VAddr dst_addr = dma_data->DstAddress<VAddr>();
const PM4Header* header =
reinterpret_cast<const PM4Header*>(dst_addr - sizeof(PM4Header));
if (dst_addr >= base_addr && dst_addr < base_addr + acb_size &&
num_bytes == sizeof(PM4CmdDispatchIndirect::GroupDimensions) &&
header->type == 3 && header->type3.opcode == PM4ItOpcode::DispatchDirect) {
indirect_patches.emplace_back(header, src_addr);
} else {
rasterizer->CopyBuffer(dst_addr, src_addr, num_bytes, false, false);
}
} else {
UNREACHABLE_MSG("WriteData src_sel = {}, dst_sel = {}",
u32(dma_data->src_sel.Value()), u32(dma_data->dst_sel.Value()));
@ -965,6 +982,12 @@ Liverpool::Task Liverpool::ProcessCompute(std::span<const u32> acb, u32 vqid) {
}
case PM4ItOpcode::DispatchDirect: {
const auto* dispatch_direct = reinterpret_cast<const PM4CmdDispatchDirect*>(header);
if (auto it = std::ranges::find(indirect_patches, header, &IndirectPatch::header);
it != indirect_patches.end()) {
const auto size = sizeof(PM4CmdDispatchIndirect::GroupDimensions);
rasterizer->DispatchIndirect(it->indirect_addr, 0, size);
break;
}
auto& cs_program = GetCsRegs();
cs_program.dim_x = dispatch_direct->dim_x;
cs_program.dim_y = dispatch_direct->dim_y;
@ -1035,9 +1058,13 @@ Liverpool::Task Liverpool::ProcessCompute(std::span<const u32> acb, u32 vqid) {
}
case PM4ItOpcode::ReleaseMem: {
const auto* release_mem = reinterpret_cast<const PM4CmdReleaseMem*>(header);
release_mem->SignalFence([pipe_id = queue.pipe_id] {
Platform::IrqC::Instance()->Signal(static_cast<Platform::InterruptId>(pipe_id));
});
release_mem->SignalFence(
[pipe_id = queue.pipe_id] {
Platform::IrqC::Instance()->Signal(static_cast<Platform::InterruptId>(pipe_id));
},
[this](VAddr dst, u16 gds_index, u16 num_dwords) {
rasterizer->CopyBuffer(dst, gds_index, num_dwords * sizeof(u32), false, true);
});
break;
}
case PM4ItOpcode::EventWrite: {

View File

@ -50,7 +50,7 @@ union PM4Type3Header {
}
u32 NumWords() const {
return count + 1;
return (count + 1) & 0x3fff;
}
u32 raw;
@ -327,6 +327,7 @@ enum class DataSelect : u32 {
Data64 = 2,
GpuClock64 = 3,
PerfCounter = 4,
GdsMemStore = 5,
};
enum class InterruptSelect : u32 {
@ -920,8 +921,9 @@ struct PM4CmdReleaseMem {
u32 data_hi;
template <typename T>
T* Address() const {
return reinterpret_cast<T*>(address_lo | u64(address_hi) << 32);
T Address() const {
u64 full_address = address_lo | (u64(address_hi) << 32);
return std::bit_cast<T>(full_address);
}
u32 DataDWord() const {
@ -932,22 +934,26 @@ struct PM4CmdReleaseMem {
return data_lo | u64(data_hi) << 32;
}
void SignalFence(auto&& signal_irq) const {
void SignalFence(auto&& signal_irq, auto&& gds_to_mem) const {
switch (data_sel.Value()) {
case DataSelect::Data32Low: {
*Address<u32>() = DataDWord();
*Address<u32*>() = DataDWord();
break;
}
case DataSelect::Data64: {
*Address<u64>() = DataQWord();
*Address<u64*>() = DataQWord();
break;
}
case DataSelect::GpuClock64: {
*Address<u64>() = GetGpuClock64();
*Address<u64*>() = GetGpuClock64();
break;
}
case DataSelect::PerfCounter: {
*Address<u64>() = GetGpuPerfCounter();
*Address<u64*>() = GetGpuPerfCounter();
break;
}
case DataSelect::GdsMemStore: {
gds_to_mem(Address<VAddr>(), gds_index, num_dw);
break;
}
default: {

View File

@ -102,7 +102,7 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS
switch (stage) {
case Stage::Local: {
BuildCommon(regs.ls_program);
Shader::TessellationDataConstantBuffer tess_constants;
Shader::TessellationDataConstantBuffer tess_constants{};
const auto* hull_info = infos[u32(Shader::LogicalStage::TessellationControl)];
hull_info->ReadTessConstantBuffer(tess_constants);
info.ls_info.ls_stride = tess_constants.ls_stride;
@ -200,6 +200,10 @@ const Shader::RuntimeInfo& PipelineCache::BuildRuntimeInfo(Stage stage, LogicalS
for (u32 i = 0; i < Shader::MaxColorBuffers; i++) {
info.fs_info.color_buffers[i] = graphics_key.color_buffers[i];
}
info.fs_info.clip_distance_emulation =
regs.vs_output_control.clip_distance_enable &&
!regs.stage_enable.IsStageEnabled(static_cast<u32>(Stage::Local)) &&
profile.needs_clip_distance_emulation;
break;
}
case Stage::Compute: {
@ -267,6 +271,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_,
instance.GetDriverID() == vk::DriverId::eMoltenvk,
.needs_buffer_offsets = instance.StorageMinAlignment() > 4,
.needs_unorm_fixup = instance.GetDriverID() == vk::DriverId::eMoltenvk,
.needs_clip_distance_emulation = instance.GetDriverID() == vk::DriverId::eNvidiaProprietary,
};
WarmUp();
@ -461,7 +466,13 @@ bool PipelineCache::RefreshGraphicsStages() {
infos.fill(nullptr);
modules.fill(nullptr);
bind_stage(Stage::Fragment, LogicalStage::Fragment);
const auto result = bind_stage(Stage::Fragment, LogicalStage::Fragment);
if (!result && regs.vs_output_control.clip_distance_enable &&
profile.needs_clip_distance_emulation) {
// TODO: need to implement a discard only fallback shader
LOG_WARNING(Render_Vulkan,
"Clip distance emulation is ineffective due to absense of fragment shader");
}
const auto* fs_info = infos[static_cast<u32>(LogicalStage::Fragment)];
key.mrt_mask = fs_info ? fs_info->mrt_mask : 0u;