132 Commits

Author SHA1 Message Date
Marcin Mikołajczyk
c0987ecc73 Fake Matching2 (#3959) 2026-01-30 23:42:37 +02:00
kalaposfos13
d1150ad303 this is why you don't push local changes, shadow 2026-01-30 13:56:25 +01:00
georgemoralis
8b46650fb2 Misc (#3979)
* improved trophy key save on new keymanager

* updated CLI11 and sdl3

* delete so stupid git will know

* fixed?

* fix2

* fixed sdl3

* Fix missing CLI11 submodule
2026-01-30 13:59:52 +02:00
CrazyBloo
4f11a8c979 pngenc hle (#3957)
* PngEnc hle

* format

* formatting + fix scePngEncDelete

* fix cmake + misc improvements

i think the setjmp is right according to the libpng manual, works fine from my testing

* fixes

fix an issue with how alpha was handled, and PngEncode() now properly sets the processed_height in outputInfo.

* format

* Update pngenc.cpp

* set outputInfo->processed_height during png write

i assume some games will use this for error handling
2026-01-30 10:53:09 +02:00
Stephen Miller
5bc4183e36 Kernel.Vmm: Fix potential race condition involving concurrent Allocate and Free calls (#3978)
* Avoid nullptr dereference on GetSocket

Was gonna include this in my socket PR, but that got merged before I could push this.

* Lock unmap mutex in PoolExpand and Allocate

PAYDAY 2 has a rare race condition involving dmem releases.
I'm not certain this commit will fix it, but this would cause a race condition that could cause asserts like what PAYDAY 2 can hit, so I'll just pray this does the job until I can prove it doesn't.
2026-01-30 06:22:15 +02:00
Stephen Miller
4f3aabd7af Delete unused fds in sceNetEpollDestroy and sys_socketclose (#3976)
This issue would cause memory leaks in some EA titles, also just generally makes it harder to debug stuff when the fd table is flooded with closed sockets and epolls.
2026-01-29 23:08:21 +02:00
Marcin Mikołajczyk
39a7738d04 Include NpTus stub (#3970) 2026-01-29 17:46:30 +02:00
Berk
25d175fd41 [clang-format] Added correct version and vscode formatter (#3971) 2026-01-29 14:43:15 +02:00
Stephen Miller
f14bad729c Np: libSceNpPartner001 stubs (#3963)
* Initial work

* Oops
2026-01-28 15:10:37 +02:00
kalaposfos13
cc70fa8bb5 Fix thread names not updating correctly (#3968)
Also, apply consistency to thread names
Also also, copyright 2026
2026-01-28 13:25:48 +02:00
squidbus
9314633573 externals: Update MoltenVK to fix primitive restart disable issue. (#3967) 2026-01-28 08:54:01 +02:00
Marcin Mikołajczyk
c81ebe6418 Implement V_FFBH_I32 (#3965) 2026-01-27 23:08:26 +02:00
Marcin Mikołajczyk
1473b2358a Implement V_CMP_OP_F64 (#3962) 2026-01-27 20:18:05 +02:00
Berk
b17ac0fdda Fix most of vscode problems and switch to Arch for more stability (#3964) 2026-01-27 15:59:56 +02:00
Marcin Mikołajczyk
1e059cac04 Implement V_LSHR_B64 (#3961) 2026-01-27 12:27:56 +02:00
Stephen Miller
4ba0e62670 Kernel.Vmm: Attempt to address race conditions involving ClampRangeSize, CopySparseMemory, and TryWriteBacking (#3956)
* no

no

* Adjust locking strategy

Use a separate mutex for the initial error checks + GPU unmap instead of using the reader lock. Make sure all writers lock this separate mutex, and for those that don't perform GPU unmaps, lock the writer lock immediately too.

This gets around every race condition I've envisioned so far, and hopefully does the trick?

* Clang

* Always GPU unmap

GPU unmaps have logic built-in to only run on mapped areas.
Not sure if userfaultfd would work with this, but since that's already broken anyway, I'll let reviewers decide that.

Without doing this, I'd need to do an extra pass through VMAs to find what all needs to be GPU modified before I can unmap from GPU, then perform remaining unmap work. Especially for places like MapMemory, that's a lot of code bloat.

* Fixups

* Update memory.cpp

* Rename mutex

It's really just a mutex for the sole purpose of dealing with GPU unmaps, so unmap_mutex is a bit more fitting than transition_mutex
2026-01-27 12:25:23 +02:00
Berk
514e363472 Add Docker build support and documentation (#3960)
* Docker builder support

* update licenses

* oops I forgot change this description
2026-01-27 10:09:32 +02:00
psucien
1e99c4b506 shader_recompiler: VS clip distance emulation for NVIDIA GPUs (#3958) 2026-01-26 22:17:51 +02:00
georgemoralis
fa497f6bfd added new cli parser using CLI11 (#3950)
* added new cli parser using CLI11

* pff

* fixed repo

* fix game autodetection

* clear unessecary comments

* added a check

* fixed?

* parse extras

* one more try

* readded -g

* fixed ignore_game_patches flag

* some rewrite improvements
2026-01-24 14:57:24 +02:00
Stephen Miller
c8b45e5ebc Core: More memory hotfixes (#3954)
* Update memory.cpp

* Fix CoalesceFreeRegions to account for address space gaps

Fixes a regression in Saint's Row games.
2026-01-24 08:05:56 +02:00
Stephen Miller
46a7c4e1f5 Core: Miscellaneous memory fixes and slight optimizations (#3946)
* Optimizations

Microsoft allows you to coalesce multiple free placeholders in one VirtualFreeEx call, so we can perform the VirtualFreeEx after coalescing with neighboring regions to eliminate a VirtualFreeEx call in some situations.

* Remove unnecessary VirtualProtect call

As far as I can tell, this call wastes a bunch of time, and is completely unnecessary.
With our current codebase, simply supplying prot to MapViewOfFile3 works properly.

* Properly handle file mmaps with offsets

Pretty easy fix to perform while I'm here, so I might as well include it.

* Oops

Leftover stuff from local things + clang

* Disable tracy memory tracking

Tracy's memory tracking is built around a typical malloc/free API, so each individual alloc must correspond to a free.
Moving these to address space would fix issues on Windows, but Linux/Mac would have the same issues with our current code.
Disabling VMA merging is technically a fix, but since that's hardware-accurate behavior, I'd rather not disable it.

I'm sure there's a simple solution I'm missing, but unless other devs have a better idea of how this should be handled, the best I can do is disable it so we can keep using Tracy to trace performance.

* Update address_space.cpp

* Debug logging

Should give a decent idea of how nasty these AddressSpace calls are in games that lost perf.

* test removing thread safety

Just for testing, will revert afterwards.

* Check name before merging

Fixes a regression in Apex Legends

* Revert "test removing thread safety"

This reverts commit ab897f4b1c.

* Move mutex locks before IsValidMapping calls

These aren't thread safe, this fixes a rare race condition that I ran into with Apex Legends.

* Revert "Debug logging"

This reverts commit eb2b12a46c.

* Proper VMA splitting in ProtectBytes, SetDirectMemoryType, and NameVirtualRange

Also slight optimization by eliminating AddressSpace protect calls when requested prot matches the previous prot.
Fixes a regression in God of War: Ragnarok

* Clang

* Fixes to SetDirectMemoryType logic

Fixes some regressions in Marvel's Spider-Man that occurred with my previous commits to this PR.

* Fix Genshin Impact again

* Assert on out-of-bounds protect calls

Our page tracking code is prone to causing this.

* test mutex again

This time, remember all mutex stuff

* Revert hack

I'll work on a better way to deal with mutexes in a bit, first I'm pushing up some extra fixes

* Proper logic for checked ReleaseDirectMemory, added bounds checks

Should help some games.

* Better logging for ReleaseDirectMemory errors.

* Only perform region coalescing after all unmap operations.

A small optimization for unmapping multiple regions. Since Microsoft lets you coalesce multiple placeholders at once, we can save doing any VirtualFreeEx calls for coalescing until after we unmap everything in the requested range.

* Separate VMA creation logic into a separate method, update MapFile to use it

MapFile is technically another "emulation" of MapMemory, both should follow similar logic.
To avoid duplicating code, move shared logic to a different function that both MapMemory and MapFile can call.

This fixes memory asserts in a couple of online-only apps I have.

* Clang

* Fix TryWriteBacking

This fixes a lot of regressions that got misattributed

Co-Authored-By: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com>

* Fix again

Fixes device lost crashes with some games after my last commit.

* Oops

* Mutex cleanup

Avoided changing anything in MapMemory, UnmapMemory, PoolCommit, or PoolDecommit since those all need a little extra granularity to prevent GPU deadlocking.

Everything else now uses standard library locks to make things a little simpler.

* Swap MapMemory and PoolCommit to use scoped lock

GPU maps are safe, so this is fine. Unmaps are the primary issue.

---------

Co-authored-by: TheTurtle <47210458+raphaelthegreat@users.noreply.github.com>
2026-01-24 00:17:57 +02:00
TheTurtle
fecfbb6b4a video_core: Small fixes regarding GDS (#3942)
* shader_recompiler: Add missing descriptor type for GDS buffer

* liverpool: Implement gds to memory store

* macOS fix?

---------

Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
2026-01-22 17:05:16 +02:00
georgemoralis
508bad87d5 WebDialogBrowser module (#3938)
* initial

* added sceWebBrowserDialogUpdateStatus

* sceWebBrowserDialogInitialize
2026-01-22 17:04:57 +02:00
georgemoralis
62813c0106 Unified UserId (#3949)
* added OrbisUserServiceUserId in generic way to all classes that uses it

* clang
2026-01-22 17:03:22 +02:00
TheTurtle
0d5c5f81a6 video_core: Small readback optimization (#3941)
* pm4_cmds: Handle nop packet overflow

* liverpool: Detect DispatchDirect patches and promote to DispatchIndirect

* clang..

* log removed

---------

Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
2026-01-21 22:49:35 +02:00
Stephen Miller
3e1f5a0bfb Kernel.Vmm: Handle sparse physical memory usage + other fixes (#3932)
* Initial work

* Bug fixing

deadlocks and broken unmaps

* Fix more bugs

broken memory pools

* More bug fixing

Still plenty more to fix though

* Even more bug fixing

Finally got Final Fantasy XV back to running, haven't found anymore bugs yet.

* More bugfixing

* Update memory.cpp

* Rewrite start

* Fix for oversized unmaps

* Oops

* Update address_space.cpp

* Clang

* Mac fix?

* Track VMA physical areas based on start in VMA

Allows me to simplify some logic, and should (finally) allow merging VMAs in memory code.

* Merge VMAs, fix some bugs

Finally possible thanks to address space + phys tracking changes

* Clang

* Oops

* Oops2

* Oops3

* Bugfixing

* SDK check for coalescing

Just to rule out any issues from games that wouldn't see coalescing in the first place.

* More ReleaseDirectMemory fixes

I really suck at logic some days

* Merge physical areas within VMAs

In games that perform a lot of similar mappings, you can wind up with 1000+ phys areas in one vma.
This should reduce some of the overhead that might cause.

* Hopefully fix Mac compile

Why must their uint64_t be different?

* Mac pt.2

Oops
2026-01-21 17:36:09 +02:00
georgemoralis
c898071b72 Introducing key_manager for storing encryption keys . (#3935)
* Introducing key_manager for storing encryption keys . Currently only trophy key is neccesary

* keep gcc happy?

* addded logging to keymanager

* revert file

* added npbind file format and rewrote part of trp file format
2026-01-19 18:49:57 +02:00
georgemoralis
950d390daf implemented sceImeDialogGetPanelSizeExtended (#3934) 2026-01-17 23:54:17 +02:00
georgemoralis
220e5f67e7 NetFixes : workaround for Epolls on P2P sockets (#3933)
* return error on P2P sockets

* error message improved
2026-01-17 21:33:14 +02:00
georgemoralis
dce9c04383 AvPlayer fix (#3929)
* improved path detection

* clang is pretty

* improved

* improved?

* finished?
2026-01-16 18:33:27 +02:00
baggins183
11ee79a333 shader_recompiler: some fixes for tess shaders (#3926)
When walking the users of special constants which form LDS
addresses:
-ignore when a user contributes to the wrong operand of an LDS inst, for
example the data operand of WriteShared* instead of the address operand.
This can mistakenly happen due to phi nodes.
-don't use flags to stash temp info about phis, since flags may already
be in use. Use a separate map.
2026-01-15 09:25:09 +02:00
georgemoralis
cdf3c468b6 Added libSceAudiodec to lle modules list (#3916)
* added libSceAudiodec to lle modules list

* crappy float resample , use it at your own risk

* clang

* adjustments to aac

---------

Co-authored-by: Vladislav Mikhalin <mikhalinvlad@gmail.com>
2026-01-14 18:07:44 +02:00
Vladislav Mikhalin
1a99ab7b09 ajm: fix init params initialization (#3924) 2026-01-13 11:08:46 +02:00
Stephen Miller
acb8d06636 Lib.Audio3d: sceAudio3dGetDefaultOpenParameters fix (#3923)
* OrbisAudio3dOpenParameters struct fix

Not sure why we have the extra filler, but decomp suggests it shouldn't exist.
This fixes stack_chk_fail issues in audio3d using titles.

* Bring back filler, only copy 0x20 bytes.

The library accepts variations on struct size, with the maximum size being the 0x28 size our current struct has.
This fixes the issue without potentially breaking the struct.

* Fix memcpy

Prevent OOB read
2026-01-13 08:32:17 +02:00
Stephen Miller
108cefaf53 Better message for missing game trophies directory (#3922)
Also decreased the log level from critical to warning, as all this affects is the ability to earn trophies.
2026-01-13 08:30:56 +02:00
b7k
540fdcc812 changing the mouse speed does not affect the mouse speed offset (#3917) 2026-01-12 11:21:52 +02:00
Stephen Miller
910f737079 Core: Add libSceRtc, libSceJpegDec, libSceJpegEnc, and libScePngEnc LLEs (#3918)
* Run libSceRtc LLE

The more we've used our HLE, the more issues we've had with it.
While we debug these bugs, re-enabling LLE will address any regressions the swap caused.

* libSceJpegDec LLE

Needed for Trackmania until we implement HLE for this library

* libScePngEnc LLE

Needed for Minecraft until we implement HLE for this library

* Update documentation appropriately

* libSceJpegEnc LLE

By @georgemoralis's request
2026-01-11 20:48:41 +02:00
Stephen Miller
3844a2fb54 Lib.Videodec2: Stub sceVideodec2AllocateComputeQueue to return a valid computeQueue pointer. (#3915)
* Stub compute queue output to cpuGpuMemory

* Rename namespace

This has bugged me for far too long

* Oops
2026-01-11 10:47:34 +02:00
Stephen Miller
ae47dd5d54 Handle exceptions from OutputDebugString API. (#3914)
Needed after #3900
2026-01-11 03:36:36 +02:00
Stephen Miller
12cd27d0b7 Fix socket_is_ready (#3912) 2026-01-10 21:15:06 +02:00
Stephen Miller
ed73d2b02c Libraries: libSceNpWebApi2 stubs (#3908)
* libSceNpWebApi2 stubs

Based on some brief decompilation, brings some extra Unreal Engine titles futher.

* Clang

* Suggestions and fixes
2026-01-10 17:33:28 +02:00
Stephen Miller
b84c5ed110 Corrected physical base handling for memory pools (#3909)
On real hardware, each pool commit can have multiple physical memory allocations, always allocating the lowest free physical addresses first.

Currently, since we currently only support one physical mapping per VMA, I create a separate VMA representing each physical allocation we perform.
2026-01-10 17:23:42 +02:00
marecl
f42a566cef Dirents backport (#3876)
* lseek for directories behaves correctly when final index is smaller than 0 (EINVAL)
Backported and improved dirents from QFS
Normal directory dirents update on change

* PFS moves pointer to end when last dirent is returned

* Correct entry type in PFS directory
2026-01-10 00:35:43 +02:00
georgemoralis
be99cb4d5b Emulator state (#3906)
* moved getShowFpsCounter to emu state

* more state variables

* fps counter is back

* removed fpscolor
2026-01-09 19:42:05 +02:00
Vladislav Mikhalin
f7a473b391 read compiled SDK version from eboot (#3905) 2026-01-09 18:29:09 +03:00
TheThunderTurner
233192fa95 LIbrary: libSceRudp (#3903)
* libSceRudp

* use newer glslang

* remove module start/stop

* register lib, revert glslang upgrade

* 2026

* remove default funcions
2026-01-08 21:22:21 +02:00
Ploo
240c1d6441 Update backend.cpp (#3900) 2026-01-07 18:36:07 +02:00
kalaposfos13
299159b1e0 logging: fix thread name logging for async logs (#3898)
* logging: fix thread name logging for async logs

* copyright 2026
2026-01-06 20:49:52 +02:00
Quang Ngô
42220dfbba Add vcs-browser to net.shadps4.shadPS4.metainfo.xml (#3897) 2026-01-06 11:48:56 +02:00
kalaposfos13
55e2b0f520 fix float parsing (#3896) 2026-01-05 19:56:43 +01:00
georgemoralis
256397aa3b Net fixes (#3895)
* support for flag in recv/send

* make kalapsofos happy

* on more SendMessage try

* ReceiveMessage too
2026-01-05 01:57:31 +02:00
Stephen Miller
35da0506b6 set name (#3894) 2026-01-04 22:24:37 +02:00
mercury501
975b1d312a Fixed dialog text input off by one error (#3892)
* Fixed dialog off by one error

* Fixed exclusion error

---------

Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
2026-01-04 21:30:50 +02:00
kalaposfos13
df844f8c02 Add thread names to log lines (#3893)
* the bare minimum (this won't even compile on windows yet)

* well I guess this is redundant now

* Windows GetThreadName

* Move function to common/thread and add full guest name where applicable

* the loathsome clang-formatter

* do stuff first ask for opinions later

* copyright 2026

* remove unused header

* copyright 2024-2026

---------

Co-authored-by: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com>
2026-01-04 21:10:42 +02:00
Connor Garey
954cc77110 Added a link to CLI messagebox (#3885)
* Added a link to the CLI tool message box

* Clang fix

* Another Clang fix

* Clang x3
2025-12-30 20:23:21 +01:00
Vladislav Mikhalin
9a3e4ea56b ajm: support for m4aac (#3880)
* ajm m4aac

* fix build on unix

* small tunes

* skip 2 frames if nodelay is not set, change to google repo
2025-12-28 13:24:42 +02:00
georgemoralis
aa227cae57 started 0.13.1 WIP 2025-12-24 10:46:40 +02:00
georgemoralis
c414d1f5a1 tagged 0.13.0 release 2025-12-24 10:19:09 +02:00
Vladislav Mikhalin
05f14e3682 ajm fixes (#3875) 2025-12-23 11:11:52 +02:00
Rodrigo Cioletti
138425fdf4 Network: Fixed null string crash on sceNetResolverCreate (#3872)
* Network: Fixed null string crash on sceNetResolverCreate

* Assigned an empty string for better code styling

* Fixed wrong commit
2025-12-21 15:50:49 +02:00
marecl
2bbb04ff55 ENAMETOOLONG, posix_rename fix (#3869)
* ENAMETOOLONG on paths > 255 characters
Corrected posix_rename behaviour:
* handles errors only if dst exists (shouldn't error out if doesn't)
* recursively removes "old name" (would otherwise fail if it's a not-empty dir)
* actually creates target directory

* Updated detection

* destubbed unlink()

* proper error returned
2025-12-19 14:58:07 +02:00
kalaposfos13
eae5e0ad55 Initialize VK_EXT_shader_atomic_float before VK_EXT_shader_atomic_float2 (#3867) 2025-12-15 16:50:14 -08:00
georgemoralis
9e287564ce update submodules (ffmpeg,fmt,sdl3) (#3865)
* update submodules (ffmpeg,fmt,sdl3)

* fixed linux builds?

* more linux issues

* let's see more linux...

* baby one more time
2025-12-12 23:40:17 +02:00
Stephen Miller
9e7df6ae54 Kernel.Vmm: Remove hack from #2726 (#3864)
* Remove SceKernelInternalMemory mapping

Contrary to my initial beliefs, this is very much a hack.

* Unreachable for unpatched code

This will always infinitely loop, making logs extremely large.

* Update linker.cpp
2025-12-11 12:24:45 +02:00
AlpinDale
de6c5bbb83 cli: add --show-fps to the CLI launcher (#3860)
* cli: add `--show-fps` to the CLI launcher

* fix: clang-format

* nit: PascalCase -> camelCase
2025-12-08 21:02:54 +01:00
Stephen Miller
65f0b07c34 libkernel: Implement sceKernelEnableDmemAliasing, proper mapping type checks in posix_mmap (#3859)
* Basic handling for MAP_VOID, MAP_STACK, and MAP_ANON in mmap.

* Update memory.cpp

* Update memory.cpp

* Dmem aliasing check

* Oops
2025-12-08 13:46:32 +02:00
kalaposfos13
2a5910ed51 New translations en_us.ts (OpenOrbis) (#3858) 2025-12-07 23:56:51 +02:00
Stephen Miller
391d30cbb1 cpu_patches: Patch stack canary accesses (#3857)
* Patch stack checks done using fs:[0x28]

Additionally adds support for multiple patches per instruction, since this makes two separate patches we need to conditionally perform for mov instructions.

* Missing include

* Disable patches for Apple

Mac can use their native FS segment directly, so these patches aren't needed

* Oops
2025-12-06 22:47:39 -08:00
TheTurtle
d3ad728ac0 vector_alu: Handle -1 as src1 in v_cmp_u64 (#3855) 2025-12-06 15:11:29 -08:00
Odukoya Abdullahi Ademola
5183cbe686 sceHttpUriSweepPath (#3854) 2025-12-04 10:50:24 +02:00
Odukoya Abdullahi Ademola
9e80cde60d Implement http uri escape unescape (#3853)
* Implement sceHttpUriEscape and sceHttpUriUnescape

* Implement sceHttpUriEscape and sceHttpUriUnescape

* edge case

---------

Co-authored-by: Pirky10 <odukoyaabdullah@gmail.com>
2025-12-04 10:50:01 +02:00
kalaposfos13
98fd0689ac Revert non-Linux parts of #3819 (#3852)
* Revert non-Linux parts of #3819

* More OpenOrbis stuff that I couldn't be bothered to put in a new PR
2025-12-03 15:05:19 +02:00
Lander Gallastegi
9db4642f66 video_core: Scheduler priority pending operation queue (#3848)
* Priority pending ops

* Use priority operations on image download

* clang-format

* Simplify thread

* I'm tired, it's too late :(
2025-12-02 22:27:01 +01:00
kalaposfos13
b135a056ba Remove debug logging 2025-12-02 09:50:11 +01:00
kalaposfos13
dc6013cf0e Block normal mouse inputs in mouse-to-touchpad mode
shadow sniped my PR. :(
2025-12-02 09:41:06 +01:00
Pirky
e5ea55e425 np: Add dialog state tracking for NpCommerce (#3841) 2025-12-02 10:22:41 +02:00
kalaposfos13
c3f7a4301c Add basic mouse-to-touchpad emulation (#3842) 2025-12-02 10:21:01 +02:00
Stephen Miller
a5f9280841 Return CPU mode based on param.sfo attributes (#3846)
Values are based on hardware observations.
2025-12-01 10:21:19 +02:00
kalaposfos13
cf866ab294 Don't bother trying to restart the emulator if sceSystemServiceLoadExec is called with an invalid path (#3845) 2025-11-30 22:40:58 +02:00
Connor Garey
052f3260f3 Sdl message box when no args provided (#3843)
* Added a message box when no arguments are passed.

* clang-fix

* clang-fix episode 2

* Output message box error to stderr instead of stdout
2025-11-30 19:57:14 +02:00
TheThunderTurner
78e301c3db libSceNpCommerce (#3839)
* libSceNpCommerce

* copyright notice
2025-11-29 23:47:15 +02:00
psucien
a9f8eaf778 video_core: Initial implementation of pipeline cache (#3816)
* Initial implementation

* Fix for crash caused by stale stages data; cosmetics applied

* Someone mentioned the assert

* Async blob writer

* Fix for memory leak

* Remain stuff

* Async changed to `packaged_task`
2025-11-29 11:52:08 +02:00
Quang Ngô
f9ef57f74b Fix metainfo (#3834) 2025-11-28 18:36:11 +02:00
squidbus
1394852791 renderer_vulkan: Remove primitive restart disable support check. (#3827) 2025-11-24 23:51:39 -08:00
Stephen Miller
6295c32e5c Render.Recompiler: Implement V_FLOOR_F64 (#3828)
* VectorFpRound64 decode table

Also fixed definition for V_TRUNC_F64, though I doubt that would change anything important.

* V_FLOOR_F64 implementation

Used by Just Cause 4

* Oops

Never forget your 64s
2025-11-24 23:51:06 -08:00
TheTurtle
14d71a155a video_core: Reimplement inline data as buffer fill (#3825) 2025-11-24 23:25:22 +02:00
kalaposfos13
2577dfde7e Add assert on SFO file being empty (#3815) 2025-11-23 17:30:09 -08:00
Missake
8123c44ad1 Make FSR off by default (#3801)
* Make FSR and RCAS off by default

* Update config.cpp
2025-11-23 17:28:48 -08:00
TheTurtle
f1a8b7d85e vector_alu: Fix V_CMP_U64 (#3823)
* vector_alu: Fix V_CMP_U64

* vector_alu: Also handle vcc in V_CMP_U64
2025-11-23 17:26:34 -08:00
Stephen Miller
f6ae5544fd Kernel.Vmm: Protect Fixes (#3822)
* Some mprotect fixes

The biggest thing here is preventing mprotect on memory that isn't mapped in address space. This would cause exceptions before, but succeeds on real hardware.
I've also included a couple other minor fixes, mostly based around some tests I recently performed.

Note: All changes to memory pools in this PR are assumed. I have not yet tested memory pools with any of this logic, but I do at least want to prevent mprotect on pool reserved memory to avoid crashes.

* Update memory.cpp

* clang
2025-11-22 10:32:53 +02:00
oltolm
4922d526fe msys2: fix build (#3818)
* cmake: fix mingw-w64 build

* time.cpp: fix build with Clang on Windows

* tls.h: include malloc.h for alloca
2025-11-22 10:32:29 +02:00
kalaposfos13
56109a1331 Avoid storing the Tcb pointer on the stack (#3819)
* Avoid storing the Tcb pointer on the stack

* Just return the already stored pointer in GetTcbBase

* Replace uses of GetTcbBase with g_curthread->tcb

* copyright 2025

* sir clang offnir, the all-formatting
2025-11-21 00:42:49 -08:00
Osyotr
544a22a431 emulator: crash faster (#2360)
By disabling Windows Error Reporting.
2025-11-20 22:35:35 +02:00
marecl
6612a32523 Prevent writing to directories (#3820)
* Prevent writing to directories

* Prevent writing to directories
2025-11-20 19:41:01 +02:00
TheTurtle
3f86c2e94a buffer_cache: Split DMA fault handling code from buffer cache (#3809)
Its better not to have that raw code there
2025-11-18 08:46:51 +02:00
Alexandre Bouvier
5b699090e6 cmake: fix sdl3_mixer target name (#3811) 2025-11-17 18:56:01 +02:00
TheTurtle
aa5c045555 logging: Format message after filter check (#3808) 2025-11-16 17:34:23 +02:00
rainmakerv2
ed14359c87 Re-implement custom trophy sounds using sdl3 mixer (#3805)
* re-implement custom trophy sounds using sdl3 mixer

* fix build vars

* Don't change SDL version
2025-11-16 10:36:54 +02:00
Missake
6a9f9abda0 Delete lines about Qt in building doc for Windows (#3800) 2025-11-14 21:48:04 -08:00
TheTurtle
2f55636626 vk_rasterizer: Attempt to optimize compute clears (#3795) 2025-11-15 07:44:25 +02:00
Stephen Miller
94d0f2e7ed Avoid initializing Shader::PsColorBuffer in RefreshGraphicsKey (#3799)
The bitfield in the struct is padded, which produces uninitialized memory on initialization.
To avoid modifying the struct while making our GraphicsPipelineKey struct properly hashable, set values directly instead of re-initializing.

This fixes pipeline compile spam, and the subsequent poor performance, on certain setups.
2025-11-14 19:50:14 -08:00
kalaposfos13
f557c6ac64 Update MoltenVK to the shadPS4 fork (#3797) 2025-11-14 21:47:16 +02:00
Stephen Miller
93c340c6e1 calloc libusb_interface instead of pointer (#3793)
interface->num_altsetting is oob
2025-11-12 17:26:26 +02:00
Joshua de Reeper
25344a3b89 calloc UsbDevice instead of pointer (#3790) 2025-11-12 12:40:39 +02:00
Missake
bbd985fe4b Update building-windows.md (#3792) 2025-11-11 16:56:03 +01:00
Stephen Miller
ee2bc97248 Windows: Limit address space maximum when higher addresses are not needed (#3775)
* Earlier initialization of elf info.

Everything used for elf info initialization comes from the param.sfo, so we can initialize this earlier to have this information accessible during memory init.

* Extract compiled SDK version from pubtoolinfo string

Up until now, we've been using the game's reported "firmware version" as our compiled SDK version. This behavior is inaccurate, and is something that has come up in my hardware tests before.

For the actual compiled SDK version, we should use the SDK version in the PUBTOOLINFO string of the param.sfo, only falling back on the firmware version when that the sdk_ver component isn't present.

* Store compiled SDK version in ElfInfo

* Limit address space for compiled SDK version at or above FW 3

Sony placed a hard cap at 0xfc00000000, with a slight extension for stack mappings. For now, though stack mappings aren't implemented, there's no harm in keeping a slightly extended address space (since this cap is lower than our old user max).

Limiting the max through address space is necessary for Windows due to performance issues, in the future I plan to properly implement checks in memory manager code to properly handle this behavior for all platforms.

* Use compiled SDK version for sceKernelGetCompiledSdkVersion

I think this is pretty self explanatory.

* Log SDK version

Since this value is what most internal firmware version checks are against, logging the value will help with debugging.

* Update address_space.cpp

* Update emulator.cpp

* Backwards compatible logging

Because that's apparently an issue now
2025-11-10 17:07:17 +02:00
Connor Garey
bebfee58d6 Nix shell fixes for uuid (#3784)
* added "with pkgs;" so pkgs does not need to be appended for all the buildInputs.

* Added util linux as missing uuid. Compiles successfully.
2025-11-08 19:08:18 -08:00
georgemoralis
5ddabda2b8 started 0.12.6 WIP 2025-11-07 09:16:58 +02:00
georgemoralis
3ce1ac5e86 tagged 0.12.5 release 2025-11-07 08:59:26 +02:00
Emma
b4628b80e2 ImGui: keep drawing when there's a pending change_layer (#3782) 2025-11-06 20:58:15 -03:00
Emma
2f022a462d filesystem: return st_mtim in posix_stat (fixes RB4 / CUSA02901 DLC crash) (#3781)
* filesystem: return st_mtim in posix_stat

* filesystem: stat - remove reliance on clock_cast

* filesystem: stat - remove reliance on to_time_t
2025-11-06 22:47:40 +02:00
Joshua de Reeper
f5505daaca usbd: Emulate Dimensions Toypad (#3779)
* Dimensions Toypad

* update comment
2025-11-06 17:46:43 +02:00
Joshua de Reeper
8c1ec863da Add Infinity Base Backend (#3778) 2025-11-06 16:34:52 +02:00
kalaposfos13
604f8d9398 More OpenOrbis stuff (#3776) 2025-11-06 13:22:26 +02:00
Stephen Miller
19e974bf21 Libkernel: Implement/stub some functions (#3774)
* Define latest released firmware version, use that for sceKernelGetSystemSwVersion

I feel this is less hacky and error-prone than just returning the game firmware.

* sceKernelGetAllowedSdkVersionOnSystem

* sceKernelHasNeoMode

* sceKernelGetAppInfo stub

* sceKernelGetCurrentCpu

* fixups

* sceKernelGetMainSocId

Used by libSceAvPlayer to determine if console is a pro or not.

* Update process.cpp

* Set has_param_sfo to true

* Clang
2025-11-05 16:58:15 -08:00
Stephen Miller
7031f5968e Better return stub (#3773)
Gets THE PLAYROOM (CUSA00001) further.
2025-11-05 18:32:24 +02:00
georgemoralis
ff8869262f [Libs] Font lib HLE implementation (#2761)
* dummy fontlib libs

* register font libs

* typo fix

* added font error file

* added sceFontCharacterGetBidiLevel (from RE)

* fixup

* sceFontCharacterGetTextOrder , sceFontCharacterLooksFormatCharacters , sceFontCharacterLooksWhiteSpace RE

* sceFontCharacterRefersTextBack,sceFontCharacterRefersTextNext,sceFontRenderSurfaceInit,sceFontRenderSurfaceSetScissor  RE

* sceFontRenderSurfaceSetStyleFrame ,sceFontStyleFrameGetEffectSlant RE added

* clang fix

* sceFontStyleFrameGetEffectWeight

* added sceFontStyleFrameGetScalePixel

* Update types.h

* fixup merge
2025-11-04 17:14:22 +02:00
Stephen Miller
683e5f3b04 Core: Simulate write-only file access with read-write access (#3360)
* Swap write access mode for read write

Opening with access mode w will erase the opened file. We do not want this.

* Create mode

Opening with write access was previously the only way to create a file through open, so add a separate FileAccessMode that uses the write access mode to create files.

* Update file_system.cpp

Remove a hack added to posix_rename to bypass the file clearing behaviors of FileAccessMode::Write

* Check access mode in read functions

Write-only files cause the EBADF return on the various read functions. Now that we're opening files differently, properly handling this is necessary.

* Separate appends into proper modes

Fixes a potential regression from one of my prior PRs, and ensures the Write | Append flag combo also behaves properly in read-related functions.

* Move IsWriteOnly check after device/socket reads

file->f is only valid for files, so checking this before checking for sockets/devices will cause access violations.

* Fix issues

Now that Write is identical to ReadWrite, internal uses of Write need to be swapped to my new Create mode

* Fix remaining uses of FileAccessMode write to create files

Missed these before.

* Fix rebase

* Add stubbed get_authinfo (#3722)

* mostly stubbed get_authinfo

* Return value observed on console if get_authinfo was called for the current thread, esrch otherwise

---------

Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com>
Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
2025-11-04 10:57:26 +02:00
georgemoralis
08fe66a97f [Libs] Http HLE (part2) (#2762)
* added sceHttpParseStatusLine

* added sceHttpUriMerge

* macOS fix

* more macOS fix?

* typo

* Update src/core/libraries/network/http.cpp

Co-authored-by: illusony <37698908+illusion0001@users.noreply.github.com>

* some work on sceHttpGetStatusCode

* more draft on GetStatusCode

* added some debug info

* fixup

---------

Co-authored-by: illusony <37698908+illusion0001@users.noreply.github.com>
2025-11-04 10:41:26 +02:00
Stephen Miller
bc44865cda Implement sceGnmDrawInitToDefaultContextStateInternal functions (#3770)
These are used by LLE libSceVideodec.
From decompiling the two GnmDriver libraries, it seems like sceGnmDrawInitToDefaultContextStateInternalCommand inlines a call to sceGnmDrawInitToDefaultContextState, so I've replaced that with an actual call to the function for readability.
sceGnmDrawInitToDefaultContextStateInternalSize is one to one with decomp.
2025-11-03 11:06:26 -08:00
kalaposfos13
a42ae46553 Fix game arguments not being passed under a certain condition (#3769) 2025-11-02 23:16:11 +02:00
Vinicius Rangel
a4c3c665fe add null gpu notice (#3768) 2025-11-02 19:55:43 +02:00
Kyoskii
caccc05fb2 buffer_cache: smaller regions (#3764)
* buffer_cache: smaller regions

this was a change back between v0.9.0 to v0.10.0
9f37ede336

reverting the TRACKER_HIGHER_PAGE_BITS from 24 to 22 gives a notable increase to performance.

* Update region_definitions.h

updated copyright
2025-11-01 16:20:45 +02:00
kalaposfos13
8238ecf88a Fix patches being applied multiple times redundantly (#3763) 2025-11-01 13:47:38 +02:00
Joshua de Reeper
8bbb3956a2 Skylander Portal (#3762) 2025-11-01 12:18:34 +02:00
georgemoralis
f466352dde revert controller change from #3750. Seems to cause issues in several games need to be reinvested 2025-11-01 11:40:26 +02:00
Joshua de Reeper
430f2e4700 Fix Typo in CMakeLists for MoltenVK MacOS (#3758) 2025-10-31 09:48:34 -07:00
Pavel
6c7c5eb59c get_authinfo (#3760) 2025-10-31 15:56:11 +02:00
oltolm
493cda07c0 fix divide by zero (#3759) 2025-10-31 15:36:27 +02:00
ElBread3
eda6be746f usbd: Implement usb backend system (#3737)
* initial impl

* reviews

* upstreamed deReaperJosh changes

* fixed config.cpp

---------

Co-authored-by: georgemoralis <giorgosmrls@gmail.com>
2025-10-31 11:11:14 +02:00
georgemoralis
ed9ffbfb64 Remove Qt from emulator (#3733)
* actions removal

* removed qt dir

# Conflicts:
#	src/qt_gui/check_update.cpp
#	src/qt_gui/translations/ar_SA.ts
#	src/qt_gui/translations/ca_ES.ts
#	src/qt_gui/translations/da_DK.ts
#	src/qt_gui/translations/de_DE.ts
#	src/qt_gui/translations/el_GR.ts
#	src/qt_gui/translations/en_US.ts
#	src/qt_gui/translations/es_ES.ts
#	src/qt_gui/translations/fa_IR.ts
#	src/qt_gui/translations/fi_FI.ts
#	src/qt_gui/translations/fr_FR.ts
#	src/qt_gui/translations/hu_HU.ts
#	src/qt_gui/translations/id_ID.ts
#	src/qt_gui/translations/it_IT.ts
#	src/qt_gui/translations/ja_JP.ts
#	src/qt_gui/translations/ko_KR.ts
#	src/qt_gui/translations/lt_LT.ts
#	src/qt_gui/translations/nb_NO.ts
#	src/qt_gui/translations/nl_NL.ts
#	src/qt_gui/translations/pl_PL.ts
#	src/qt_gui/translations/pt_BR.ts
#	src/qt_gui/translations/pt_PT.ts
#	src/qt_gui/translations/ro_RO.ts
#	src/qt_gui/translations/ru_RU.ts
#	src/qt_gui/translations/sl_SI.ts
#	src/qt_gui/translations/sq_AL.ts
#	src/qt_gui/translations/sr_CS.ts
#	src/qt_gui/translations/sv_SE.ts
#	src/qt_gui/translations/tr_TR.ts
#	src/qt_gui/translations/uk_UA.ts
#	src/qt_gui/translations/ur_PK.ts
#	src/qt_gui/translations/vi_VN.ts
#	src/qt_gui/translations/zh_CN.ts
#	src/qt_gui/translations/zh_TW.ts

* removed CMakePresets for qt builds

* clear cmakelists from qt

* sync config file with qtlauncher

* fixing review stuff

* Remove Qt code from memory patcher and add non-Qt fallback for automatic loading of patches
The second feature is disabled if IPC is present, to avoid conflicts with it.

* Add json submodule

* More Qt removal

* Documentation update

* fix build

* fix REUSE?

* removed qrc file

* fix clang

* Simplify Qt installation instructions for macOS

Removed instructions for installing x86_64 Qt on ARM and x86_64 Macs.

* Remove Qt installation instructions from guide

Removed instructions for downloading and configuring Qt.

---------

Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com>
2025-10-31 10:28:39 +02:00
georgemoralis
5cabd6ddd8 started 0.12.1 WIP 2025-10-31 09:28:17 +02:00
344 changed files with 15567 additions and 106549 deletions

View File

@@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: 2024 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#!/bin/bash
if [[ -z $GITHUB_WORKSPACE ]]; then
GITHUB_WORKSPACE="${PWD%/*}"
fi
export Qt6_DIR="/usr/lib/qt6"
export PATH="$Qt6_DIR/bin:$PATH"
export EXTRA_QT_PLUGINS="waylandcompositor"
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
# Prepare Tools for building the AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-checkrt/releases/download/continuous/linuxdeploy-plugin-checkrt-x86_64.sh
chmod a+x linuxdeploy-x86_64.AppImage
chmod a+x linuxdeploy-plugin-qt-x86_64.AppImage
chmod a+x linuxdeploy-plugin-checkrt-x86_64.sh
# Build AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir
./linuxdeploy-plugin-checkrt-x86_64.sh --appdir AppDir
cp -a "$GITHUB_WORKSPACE/build/translations" AppDir/usr/bin
./linuxdeploy-x86_64.AppImage --appdir AppDir -d "$GITHUB_WORKSPACE"/dist/net.shadps4.shadPS4.desktop -e "$GITHUB_WORKSPACE"/build/shadps4 -i "$GITHUB_WORKSPACE"/src/images/net.shadps4.shadPS4.svg --plugin qt
rm AppDir/usr/plugins/multimedia/libgstreamermediaplugin.so
./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage
mv shadPS4-x86_64.AppImage Shadps4-qt.AppImage

View File

@@ -95,64 +95,6 @@ jobs:
name: shadps4-win64-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: ${{github.workspace}}/build/shadPS4.exe
windows-qt:
runs-on: windows-2025
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup Qt
uses: jurplel/install-qt-action@v4
with:
version: 6.9.3
host: windows
target: desktop
arch: win64_msvc2022_64
archives: qtbase qttools
modules: qtmultimedia
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-ninja-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $env:NUMBER_OF_PROCESSORS
- name: Deploy and Package
run: |
mkdir upload
mkdir upload/qtplugins
move build/shadPS4.exe upload
cp dist/qt.conf upload/qt.conf
windeployqt --plugindir upload/qtplugins --no-compiler-runtime --no-system-d3d-compiler --no-system-dxc-compiler --dir upload upload/shadPS4.exe
Compress-Archive -Path upload/* -DestinationPath shadps4-win64-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}.zip
- name: Upload Windows Qt artifact
uses: actions/upload-artifact@v4
with:
name: shadps4-win64-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: upload/
macos-sdl:
runs-on: macos-15
needs: get-info
@@ -204,67 +146,6 @@ jobs:
name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: upload/
macos-qt:
runs-on: macos-15
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Setup latest Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- name: Setup Qt
uses: jurplel/install-qt-action@v4
with:
version: 6.9.3
host: mac
target: desktop
arch: clang_64
archives: qtbase qttools
modules: qtmultimedia
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{runner.os}}-qt-cache-cmake-build
with:
append-timestamp: false
create-symlink: true
key: ${{env.cache-name}}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
variant: sccache
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_OSX_ARCHITECTURES=x86_64 -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(sysctl -n hw.ncpu)
- name: Package and Upload macOS Qt artifact
run: |
mkdir upload
mv ${{github.workspace}}/build/shadps4.app upload
macdeployqt upload/shadps4.app
tar cf shadps4-macos-qt.tar.gz -C upload .
- uses: actions/upload-artifact@v4
with:
name: shadps4-macos-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: shadps4-macos-qt.tar.gz
linux-sdl:
runs-on: ubuntu-24.04
needs: get-info
@@ -279,7 +160,7 @@ jobs:
sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main'
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
@@ -326,58 +207,6 @@ jobs:
name: shadps4-linux-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: Shadps4-sdl.AppImage
linux-qt:
runs-on: ubuntu-24.04
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Add LLVM repository
run: |
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
sudo add-apt-repository 'deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main'
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 clang-19 mold build-essential qt6-base-dev qt6-tools-dev qt6-multimedia-dev libasound2-dev libpulse-dev libopenal-dev libudev-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold" -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=mold" -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
- name: Run AppImage packaging script
run: ./.github/linux-appimage-qt.sh
- name: Package and Upload Linux Qt artifact
run: |
tar cf shadps4-linux-qt.tar.gz -C ${{github.workspace}}/build shadps4
- uses: actions/upload-artifact@v4
with:
name: shadps4-linux-qt-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
path: Shadps4-qt.AppImage
linux-sdl-gcc:
runs-on: ubuntu-24.04
needs: get-info
@@ -387,7 +216,7 @@ jobs:
submodules: recursive
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential libasound2-dev libpulse-dev libopenal-dev libudev-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
@@ -414,45 +243,9 @@ jobs:
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
linux-qt-gcc:
runs-on: ubuntu-24.04
needs: get-info
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Install dependencies
run: sudo apt-get update && sudo apt install -y libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libglfw3-dev libgles2-mesa-dev libfuse2 gcc-14 mold build-essential qt6-base-dev qt6-tools-dev qt6-multimedia-dev libasound2-dev libpulse-dev libopenal-dev libudev-dev
- name: Cache CMake Configuration
uses: actions/cache@v4
env:
cache-name: ${{ runner.os }}-qt-gcc-cache-cmake-configuration
with:
path: |
${{github.workspace}}/build
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
restore-keys: |
${{ env.cache-name }}-
- name: Cache CMake Build
uses: hendrikmuhs/ccache-action@v1.2.19
env:
cache-name: ${{ runner.os }}-qt-gcc-cache-cmake-build
with:
append-timestamp: false
key: ${{ env.cache-name }}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }}
- name: Configure CMake
run: cmake --fresh -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold" -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=mold" -DENABLE_QT_GUI=ON -DENABLE_UPDATER=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --parallel $(nproc)
pre-release:
if: github.ref == 'refs/heads/main' && github.repository == 'shadps4-emu/shadPS4' && github.event_name == 'push'
needs: [get-info, windows-sdl, windows-qt, macos-sdl, macos-qt, linux-sdl, linux-qt]
needs: [get-info, windows-sdl, macos-sdl, linux-sdl]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts

26
.gitmodules vendored
View File

@@ -2,10 +2,6 @@
path = externals/zlib-ng
url = https://github.com/shadps4-emu/ext-zlib-ng.git
shallow = true
[submodule "externals/sdl3"]
path = externals/sdl3
url = https://github.com/shadps4-emu/ext-SDL.git
shallow = true
[submodule "externals/fmt"]
path = externals/fmt
url = https://github.com/shadps4-emu/ext-fmt.git
@@ -108,5 +104,25 @@
branch = dist
[submodule "externals/MoltenVK"]
path = externals/MoltenVK
url = https://github.com/KhronosGroup/MoltenVK.git
url = https://github.com/shadPS4-emu/ext-MoltenVK.git
shallow = true
[submodule "externals/json"]
path = externals/json
url = https://github.com/nlohmann/json.git
[submodule "externals/sdl3_mixer"]
path = externals/sdl3_mixer
url = https://github.com/libsdl-org/SDL_mixer
shallow = true
[submodule "externals/miniz"]
path = externals/miniz
url = https://github.com/richgel999/miniz
[submodule "externals/aacdec/fdk-aac"]
path = externals/aacdec/fdk-aac
url = https://android.googlesource.com/platform/external/aac
[submodule "externals/CLI11"]
path = externals/CLI11
url = https://github.com/shadexternals/CLI11.git
[submodule "externals/sdl3"]
path = externals/sdl3
url = https://github.com/shadexternals/sdl3.git

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
# Version 3.24 needed for FetchContent OVERRIDE_FIND_PACKAGE
@@ -31,7 +31,6 @@ if(UNIX AND NOT APPLE)
endif()
endif()
option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF)
option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON)
option(ENABLE_UPDATER "Enables the options to updater" ON)
@@ -203,13 +202,13 @@ execute_process(
# Set Version
set(EMULATOR_VERSION_MAJOR "0")
set(EMULATOR_VERSION_MINOR "12")
set(EMULATOR_VERSION_PATCH "0")
set(EMULATOR_VERSION_MINOR "13")
set(EMULATOR_VERSION_PATCH "1")
set_source_files_properties(src/shadps4.rc PROPERTIES COMPILE_DEFINITIONS "EMULATOR_VERSION_MAJOR=${EMULATOR_VERSION_MAJOR};EMULATOR_VERSION_MINOR=${EMULATOR_VERSION_MINOR};EMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH}")
set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH}")
set(APP_IS_RELEASE true)
set(APP_VERSION "${EMULATOR_VERSION_MAJOR}.${EMULATOR_VERSION_MINOR}.${EMULATOR_VERSION_PATCH} WIP")
set(APP_IS_RELEASE false)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/common/scm_rev.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp" @ONLY)
message("-- end git things, remote: ${GIT_REMOTE_NAME}, branch: ${GIT_BRANCH}, link: ${GIT_REMOTE_URL}")
@@ -220,10 +219,6 @@ if(NOT (GIT_REMOTE_URL_LOWER MATCHES "shadps4-emu/shadps4" AND (GIT_BRANCH STREQ
set(ENABLE_UPDATER OFF)
endif()
if(WIN32 AND ENABLE_QT_GUI AND NOT CMAKE_PREFIX_PATH)
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/DetectQtInstallation.cmake")
endif ()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(Boost 1.84.0 CONFIG)
find_package(FFmpeg 5.1.2 MODULE)
@@ -233,7 +228,10 @@ find_package(half 1.12.0 MODULE)
find_package(magic_enum 0.9.7 CONFIG)
find_package(PNG 1.6 MODULE)
find_package(RenderDoc 1.6.0 MODULE)
find_package(SDL3 3.1.2 CONFIG)
find_package(SDL3_mixer 2.8.1 CONFIG)
if (SDL3_mixer_FOUND)
find_package(SDL3 3.1.2 CONFIG)
endif()
find_package(stb MODULE)
find_package(toml11 4.2.0 CONFIG)
find_package(tsl-robin-map 1.3.0 CONFIG)
@@ -262,32 +260,10 @@ endif()
add_subdirectory(externals)
include_directories(src)
if(ENABLE_QT_GUI)
find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent LinguistTools Network Multimedia)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(QT_TRANSLATIONS "${PROJECT_SOURCE_DIR}/src/qt_gui/translations")
file(GLOB_RECURSE TRANSLATIONS_TS ${QT_TRANSLATIONS}/*.ts)
set_source_files_properties(${TRANSLATIONS_TS} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/translations")
qt_add_translation(TRANSLATIONS_QM ${TRANSLATIONS_TS})
set(TRANSLATIONS_QRC ${CMAKE_CURRENT_BINARY_DIR}/translations/translations.qrc)
file(WRITE ${TRANSLATIONS_QRC} "<RCC><qresource prefix=\"translations\">\n")
foreach (QM ${TRANSLATIONS_QM})
get_filename_component(QM_FILE ${QM} NAME)
file(APPEND ${TRANSLATIONS_QRC} "<file>${QM_FILE}</file>\n")
endforeach (QM)
file(APPEND ${TRANSLATIONS_QRC} "</qresource></RCC>")
qt_add_resources(TRANSLATIONS ${TRANSLATIONS_QRC})
endif()
set(AJM_LIB src/core/libraries/ajm/ajm.cpp
src/core/libraries/ajm/ajm.h
src/core/libraries/ajm/ajm_aac.cpp
src/core/libraries/ajm/ajm_aac.h
src/core/libraries/ajm/ajm_at9.cpp
src/core/libraries/ajm/ajm_at9.h
src/core/libraries/ajm/ajm_batch.cpp
@@ -448,6 +424,8 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp
src/core/libraries/rtc/rtc.cpp
src/core/libraries/rtc/rtc.h
src/core/libraries/rtc/rtc_error.h
src/core/libraries/rudp/rudp.cpp
src/core/libraries/rudp/rudp.h
src/core/libraries/disc_map/disc_map.cpp
src/core/libraries/disc_map/disc_map.h
src/core/libraries/disc_map/disc_map_codes.h
@@ -490,6 +468,12 @@ set(SYSTEM_LIBS src/core/libraries/system/commondialog.cpp
src/core/libraries/mouse/mouse.h
src/core/libraries/web_browser_dialog/webbrowserdialog.cpp
src/core/libraries/web_browser_dialog/webbrowserdialog.h
src/core/libraries/font/font.cpp
src/core/libraries/font/font.h
src/core/libraries/font/fontft.cpp
src/core/libraries/font/fontft.h
src/core/libraries/font/font_error.h
)
set(VIDEOOUT_LIB src/core/libraries/videoout/buffer.h
@@ -532,7 +516,7 @@ set(PAD_LIB src/core/libraries/pad/pad.cpp
src/core/libraries/pad/pad_errors.h
)
set(SYSTEM_GESTURE_LIB
set(SYSTEM_GESTURE_LIB
src/core/libraries/system_gesture/system_gesture.cpp
src/core/libraries/system_gesture/system_gesture.h
)
@@ -540,6 +524,9 @@ set(SYSTEM_GESTURE_LIB
set(PNG_LIB src/core/libraries/libpng/pngdec.cpp
src/core/libraries/libpng/pngdec.h
src/core/libraries/libpng/pngdec_error.h
src/core/libraries/libpng/pngenc.cpp
src/core/libraries/libpng/pngenc.h
src/core/libraries/libpng/pngenc_error.h
)
set(JPEG_LIB src/core/libraries/jpeg/jpeg_error.h
@@ -561,6 +548,13 @@ set(RANDOM_LIB src/core/libraries/random/random.cpp
set(USBD_LIB src/core/libraries/usbd/usbd.cpp
src/core/libraries/usbd/usbd.h
src/core/libraries/usbd/usb_backend.h
src/core/libraries/usbd/emulated/dimensions.cpp
src/core/libraries/usbd/emulated/dimensions.h
src/core/libraries/usbd/emulated/infinity.cpp
src/core/libraries/usbd/emulated/infinity.h
src/core/libraries/usbd/emulated/skylander.cpp
src/core/libraries/usbd/emulated/skylander.h
)
set(FIBER_LIB src/core/libraries/fiber/fiber_context.s
@@ -569,6 +563,8 @@ set(FIBER_LIB src/core/libraries/fiber/fiber_context.s
src/core/libraries/fiber/fiber_error.h
)
set_source_files_properties(src/core/libraries/fiber/fiber_context.s PROPERTIES COMPILE_OPTIONS -Wno-unused-command-line-argument)
set(VDEC_LIB src/core/libraries/videodec/videodec2_impl.cpp
src/core/libraries/videodec/videodec2_impl.h
src/core/libraries/videodec/videodec2.cpp
@@ -584,16 +580,24 @@ set(VDEC_LIB src/core/libraries/videodec/videodec2_impl.cpp
set(NP_LIBS src/core/libraries/np/np_error.h
src/core/libraries/np/np_common.cpp
src/core/libraries/np/np_common.h
src/core/libraries/np/np_commerce.cpp
src/core/libraries/np/np_commerce.h
src/core/libraries/np/np_manager.cpp
src/core/libraries/np/np_manager.h
src/core/libraries/np/np_matching2.cpp
src/core/libraries/np/np_matching2.h
src/core/libraries/np/np_score.cpp
src/core/libraries/np/np_score.h
src/core/libraries/np/np_trophy.cpp
src/core/libraries/np/np_trophy.h
src/core/libraries/np/np_tus.cpp
src/core/libraries/np/np_tus.h
src/core/libraries/np/trophy_ui.cpp
src/core/libraries/np/trophy_ui.h
src/core/libraries/np/np_web_api.cpp
src/core/libraries/np/np_web_api.h
src/core/libraries/np/np_web_api2.cpp
src/core/libraries/np/np_web_api2.h
src/core/libraries/np/np_party.cpp
src/core/libraries/np/np_party.h
src/core/libraries/np/np_auth.cpp
@@ -602,6 +606,8 @@ set(NP_LIBS src/core/libraries/np/np_error.h
src/core/libraries/np/np_profile_dialog.h
src/core/libraries/np/np_sns_facebook_dialog.cpp
src/core/libraries/np/np_sns_facebook_dialog.h
src/core/libraries/np/np_partner.cpp
src/core/libraries/np/np_partner.h
)
set(ZLIB_LIB src/core/libraries/zlib/zlib.cpp
@@ -643,6 +649,7 @@ set(COMPANION_LIBS src/core/libraries/companion/companion_httpd.cpp
)
set(DEV_TOOLS src/core/devtools/layer.cpp
src/core/devtools/layer.h
src/core/devtools/layer_extra.cpp
src/core/devtools/options.cpp
src/core/devtools/options.h
src/core/devtools/gcn/gcn_context_regs.cpp
@@ -703,7 +710,6 @@ set(COMMON src/common/logging/backend.cpp
src/common/lru_cache.h
src/common/error.cpp
src/common/error.h
src/common/scope_exit.h
src/common/fixed_value.h
src/common/func_traits.h
src/common/native_clock.cpp
@@ -717,6 +723,8 @@ set(COMMON src/common/logging/backend.cpp
src/common/rdtsc.h
src/common/recursive_lock.cpp
src/common/recursive_lock.h
src/common/scope_exit.h
src/common/serdes.h
src/common/sha1.h
src/common/shared_first_mutex.h
src/common/signal_context.h
@@ -745,6 +753,8 @@ set(COMMON src/common/logging/backend.cpp
src/common/memory_patcher.cpp
${CMAKE_CURRENT_BINARY_DIR}/src/common/scm_rev.cpp
src/common/scm_rev.h
src/common/key_manager.cpp
src/common/key_manager.h
)
if (ENABLE_DISCORD_RPC)
@@ -788,6 +798,8 @@ set(CORE src/core/aerolib/stubs.cpp
src/core/file_format/playgo_chunk.h
src/core/file_format/trp.cpp
src/core/file_format/trp.h
src/core/file_format/npbind.cpp
src/core/file_format/npbind.h
src/core/file_sys/fs.cpp
src/core/file_sys/fs.h
src/core/ipc/ipc.cpp
@@ -843,6 +855,8 @@ set(CORE src/core/aerolib/stubs.cpp
src/core/thread.h
src/core/tls.cpp
src/core/tls.h
src/core/emulator_state.cpp
src/core/emulator_state.h
)
if (ARCHITECTURE STREQUAL "x86_64")
@@ -911,6 +925,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
@@ -974,6 +989,8 @@ set(VIDEO_CORE src/video_core/amdgpu/cb_db_extent.h
src/video_core/buffer_cache/buffer.h
src/video_core/buffer_cache/buffer_cache.cpp
src/video_core/buffer_cache/buffer_cache.h
src/video_core/buffer_cache/fault_manager.cpp
src/video_core/buffer_cache/fault_manager.h
src/video_core/buffer_cache/memory_tracker.h
src/video_core/buffer_cache/range_set.h
src/video_core/buffer_cache/region_definitions.h
@@ -994,6 +1011,8 @@ set(VIDEO_CORE src/video_core/amdgpu/cb_db_extent.h
src/video_core/renderer_vulkan/vk_pipeline_cache.h
src/video_core/renderer_vulkan/vk_pipeline_common.cpp
src/video_core/renderer_vulkan/vk_pipeline_common.h
src/video_core/renderer_vulkan/vk_pipeline_serialization.cpp
src/video_core/renderer_vulkan/vk_pipeline_serialization.h
src/video_core/renderer_vulkan/vk_platform.cpp
src/video_core/renderer_vulkan/vk_platform.h
src/video_core/renderer_vulkan/vk_presenter.cpp
@@ -1031,6 +1050,8 @@ set(VIDEO_CORE src/video_core/amdgpu/cb_db_extent.h
src/video_core/texture_cache/tile_manager.cpp
src/video_core/texture_cache/tile_manager.h
src/video_core/texture_cache/types.h
src/video_core/cache_storage.cpp
src/video_core/cache_storage.h
src/video_core/page_manager.cpp
src/video_core/page_manager.h
src/video_core/multi_level_page_table.h
@@ -1066,112 +1087,27 @@ set(EMULATOR src/emulator.cpp
src/sdl_window.cpp
)
# The above is shared in SDL and Qt version (TODO share them all)
if(ENABLE_QT_GUI)
qt_add_resources(RESOURCE_FILES src/shadps4.qrc)
if (ENABLE_UPDATER)
set(UPDATER src/qt_gui/check_update.cpp
src/qt_gui/check_update.h
)
endif()
set(QT_GUI src/qt_gui/about_dialog.cpp
src/qt_gui/about_dialog.h
src/qt_gui/about_dialog.ui
src/qt_gui/background_music_player.cpp
src/qt_gui/background_music_player.h
src/qt_gui/cheats_patches.cpp
src/qt_gui/cheats_patches.h
src/qt_gui/compatibility_info.cpp
src/qt_gui/compatibility_info.h
src/qt_gui/control_settings.cpp
src/qt_gui/control_settings.h
src/qt_gui/control_settings.ui
src/qt_gui/kbm_gui.cpp
src/qt_gui/kbm_gui.h
src/qt_gui/kbm_gui.ui
src/qt_gui/main_window_ui.h
src/qt_gui/main_window.cpp
src/qt_gui/main_window.h
src/qt_gui/gui_context_menus.h
src/qt_gui/game_list_utils.h
src/qt_gui/game_info.cpp
src/qt_gui/game_info.h
src/qt_gui/game_list_frame.cpp
src/qt_gui/game_list_frame.h
src/qt_gui/game_grid_frame.cpp
src/qt_gui/game_grid_frame.h
src/qt_gui/game_install_dialog.cpp
src/qt_gui/game_install_dialog.h
src/qt_gui/trophy_viewer.cpp
src/qt_gui/trophy_viewer.h
src/qt_gui/elf_viewer.cpp
src/qt_gui/elf_viewer.h
src/qt_gui/kbm_config_dialog.cpp
src/qt_gui/kbm_config_dialog.h
src/qt_gui/kbm_help_dialog.cpp
src/qt_gui/kbm_help_dialog.h
src/qt_gui/main_window_themes.cpp
src/qt_gui/main_window_themes.h
src/qt_gui/log_presets_dialog.cpp
src/qt_gui/log_presets_dialog.h
src/qt_gui/settings_dialog.cpp
src/qt_gui/settings_dialog.h
src/qt_gui/settings_dialog.ui
src/qt_gui/main.cpp
src/qt_gui/gui_settings.cpp
src/qt_gui/gui_settings.h
src/qt_gui/settings.cpp
src/qt_gui/settings.h
src/qt_gui/sdl_event_wrapper.cpp
src/qt_gui/sdl_event_wrapper.h
src/qt_gui/hotkeys.h
src/qt_gui/hotkeys.cpp
src/qt_gui/hotkeys.ui
${EMULATOR}
${RESOURCE_FILES}
${TRANSLATIONS}
${UPDATER}
add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/main.cpp
src/emulator.cpp
src/emulator.h
src/sdl_window.h
src/sdl_window.cpp
)
endif()
if (ENABLE_QT_GUI)
qt_add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${QT_GUI}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/images/shadPS4.icns
)
else()
add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
${INPUT}
${COMMON}
${CORE}
${SHADER_RECOMPILER}
${VIDEO_CORE}
${EMULATOR}
src/main.cpp
src/emulator.cpp
src/emulator.h
src/sdl_window.h
src/sdl_window.cpp
)
endif()
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 pugixml::pugixml stb::headers libusb::usb lfreist-hwinfo::hwinfo)
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 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")
@@ -1191,19 +1127,8 @@ endif()
if (APPLE)
# Include MoltenVK, along with an ICD file so it can be found by the system Vulkan loader if used for loading layers.
if (ENABLE_QT_GUI)
set(MVK_BUNDLE_PATH "Resources/vulkan/icd.d")
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path/../${MVK_BUNDLE_PATH}")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR}/shadps4.app/Contents/${MVK_BUNDLE_PATH})
add_custom_command(
OUTPUT ${MVK_DST}
COMMAND ${CMAKE_COMMAND} -E make_directory ${MVK_DST})
else()
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR})
endif()
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path")
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR})
set(MVK_DYLIB_SRC ${CMAKE_CURRENT_BINARY_DIR}/externals/MoltenVK/MoltenVK/libMoltenVK.dylib)
set(MVK_DYLIB_DST ${MVK_DST}/libMoltenVK.dylib)
set(MVK_ICD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/externals/MoltenVK/MoltenVK/icd/MoltenVK_icd.json)
@@ -1230,31 +1155,23 @@ if (APPLE)
target_link_libraries(shadps4 PRIVATE date::date-tz epoll-shim)
endif()
if (ENABLE_QT_GUI)
target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network Qt6::Multimedia)
add_definitions(-DENABLE_QT_GUI)
if (ENABLE_UPDATER)
add_definitions(-DENABLE_UPDATER)
endif()
endif()
if (WIN32)
target_link_libraries(shadps4 PRIVATE mincore wepoll)
target_link_libraries(shadps4 PRIVATE mincore wepoll wbemuuid)
if (MSVC)
# MSVC likes putting opinions on what people can use, disable:
add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_DEPRECATE _SCL_SECURE_NO_WARNINGS)
endif()
add_definitions(-DNOMINMAX -DWIN32_LEAN_AND_MEAN)
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
if (MSVC)
# Needed for conflicts with time.h of windows.h
add_definitions(-D_TIMESPEC_DEFINED)
add_compile_definitions(_TIMESPEC_DEFINED)
endif()
# Target Windows 10 RS5
add_definitions(-DNTDDI_VERSION=0x0A000006 -D_WIN32_WINNT=0x0A00 -DWINVER=0x0A00)
add_compile_definitions(NTDDI_VERSION=0x0A000006 _WIN32_WINNT=0x0A00 WINVER=0x0A00)
if (MSVC)
target_link_libraries(shadps4 PRIVATE clang_rt.builtins-x86_64.lib)
@@ -1286,7 +1203,7 @@ if (WIN32)
target_sources(shadps4 PRIVATE src/shadps4.rc)
endif()
add_definitions(-DBOOST_ASIO_STANDALONE)
add_compile_definitions(BOOST_ASIO_STANDALONE)
target_include_directories(shadps4 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
@@ -1315,25 +1232,6 @@ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/imgui/renderer)
add_dependencies(shadps4 ImGui_Resources)
target_include_directories(shadps4 PRIVATE ${IMGUI_RESOURCES_INCLUDE})
if (ENABLE_QT_GUI)
set_target_properties(shadps4 PROPERTIES
# WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/dist/MacOSBundleInfo.plist.in"
MACOSX_BUNDLE_ICON_FILE "shadPS4.icns"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${APP_VERSION}"
)
set_source_files_properties(src/images/shadPS4.icns PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
endif()
if (UNIX AND NOT APPLE)
if (ENABLE_QT_GUI)
find_package(OpenSSL REQUIRED)
target_link_libraries(shadps4 PRIVATE ${OPENSSL_LIBRARIES})
endif()
endif()
# Discord RPC
if (ENABLE_DISCORD_RPC)
@@ -1342,10 +1240,3 @@ endif()
# Install rules
install(TARGETS shadps4 BUNDLE DESTINATION .)
if (ENABLE_QT_GUI AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
install(FILES "dist/net.shadps4.shadPS4.desktop" DESTINATION "share/applications")
install(FILES "dist/net.shadps4.shadPS4.metainfo.xml" DESTINATION "share/metainfo")
install(FILES ".github/shadps4.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "net.shadps4.shadPS4.png")
install(FILES "src/images/net.shadps4.shadPS4.svg" DESTINATION "share/icons/hicolor/scalable/apps")
endif()

View File

@@ -15,15 +15,6 @@
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "x64-Clang-Debug-Qt",
"displayName": "Clang x64 Debug with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_QT_GUI": "ON"
}
},
{
"name": "x64-Clang-Release",
"displayName": "Clang x64 Release",
@@ -32,15 +23,6 @@
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "x64-Clang-Release-Qt",
"displayName": "Clang x64 Release with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_QT_GUI": "ON"
}
},
{
"name": "x64-Clang-RelWithDebInfo",
"displayName": "Clang x64 RelWithDebInfo",
@@ -48,15 +30,6 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "x64-Clang-RelWithDebInfo-Qt",
"displayName": "Clang x64 RelWithDebInfo with Qt",
"inherits": ["x64-Clang-Base"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo",
"ENABLE_QT_GUI": "ON"
}
}
]
}

View File

@@ -12,18 +12,6 @@
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Release-Qt",
"generator": "Ninja",
"configurationType": "Release",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Debug",
"generator": "Ninja",
@@ -36,18 +24,6 @@
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-Debug-Qt",
"generator": "Ninja",
"configurationType": "Debug",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-RelWithDebInfo",
"generator": "Ninja",
@@ -59,18 +35,6 @@
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
},
{
"name": "x64-Clang-RelWithDebInfo-Qt",
"generator": "Ninja",
"configurationType": "RelWithDebInfo",
"buildRoot": "${projectDir}\\Build\\${name}",
"installRoot": "${projectDir}\\Install\\${name}",
"cmakeCommandArgs": "-DENABLE_QT_GUI=ON",
"buildCommandArgs": "",
"ctestCommandArgs": "",
"inheritEnvironments": [ "clang_cl_x64_x64" ],
"intelliSenseMode": "windows-clang-x64"
}
]
}

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
-->
@@ -36,6 +36,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
**shadPS4** is an early **PlayStation 4** emulator for **Windows**, **Linux** and **macOS** written in C++.
> [!IMPORTANT]
> This is the emulator core, which does not include a GUI. If you just want to use the emulator as an end user, download the [**QtLauncher**](https://github.com/shadps4-emu/shadps4-qtlauncher/releases) instead.
If you encounter problems or have doubts, do not hesitate to look at the [**Quickstart**](https://github.com/shadps4-emu/shadPS4/wiki/I.-Quick-start-%5BUsers%5D).\
To verify that a game works, you can look at [**shadPS4 Game Compatibility**](https://github.com/shadps4-compatibility/shadps4-game-compatibility).\
To discuss shadPS4 development, suggest ideas or to ask for help, join our [**Discord server**](https://discord.gg/bFJxfftGW6).\
@@ -55,8 +58,10 @@ This project began for fun. Given our limited free time, it may take some time b
# Building
> [!IMPORTANT]
> If you want to use shadPS4 to play your games, you don't have to follow the build instructions, you can simply download the emulator from either the [**release tab**](https://github.com/shadps4-emu/shadPS4/releases) or the [**action tab**](https://github.com/shadps4-emu/shadPS4/actions).
## 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
@@ -73,6 +78,22 @@ Check the build instructions for [**macOS**](https://github.com/shadps4-emu/shad
> [!IMPORTANT]
> macOS users need at least macOS 15.4 to run shadPS4. Due to GPU issues there are currently heavy bugs on Intel Macs.
# Usage examples
> [!IMPORTANT]
> For a user-friendly GUI, download the [**QtLauncher**](https://github.com/shadps4-emu/shadps4-qtlauncher/releases).
To get the list of all available commands and also a more detailed description of what each command does, please refer to the `--help` flag's output.
Below is a list of commonly used command patterns:
```sh
shadPS4 CUSA00001 # Searches for a game folder called CUSA00001 in the list of game install folders, and boots it.
shadPS4 --fullscreen true --config-clean CUSA00001 # the game argument is always the last one,
shadPS4 -g CUSA00001 --fullscreen true --config-clean # ...unless manually specified otherwise.
shadPS4 /path/to/game.elf # Boots a PS4 ELF file directly. Useful if you want to boot an executable that is not named eboot.bin.
shadPS4 CUSA00001 -- -flag1 -flag2 # Passes '-flag1' and '-flag2' to the game executable in argv.
```
# Debugging and reporting issues
For more information on how to test, debug and report issues with the emulator or games, read the [**Debugging documentation**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/Debugging/Debugging.md).
@@ -132,9 +153,9 @@ The following firmware modules are supported and must be placed in shadPS4's `sy
| Modules | Modules | Modules | Modules |
|-------------------------|-------------------------|-------------------------|-------------------------|
| libSceCesCs.sprx | libSceFont.sprx | libSceFontFt.sprx | libSceFreeTypeOt.sprx |
| libSceJson.sprx | libSceJson2.sprx | libSceLibcInternal.sprx | libSceNgs2.sprx |
| libSceUlt.sprx | | | |
| libSceJpegDec.sprx | libSceJpegEnc.sprx | libSceJson.sprx | libSceJson2.sprx |
| libSceLibcInternal.sprx | libSceNgs2.sprx | libScePngEnc.sprx | libSceRtc.sprx |
| libSceUlt.sprx | libSceAudiodec.sprx | | |
</div>
> [!Caution]
@@ -160,15 +181,6 @@ Logo is done by [**Xphalnos**](https://github.com/Xphalnos)
If you want to contribute, please read the [**CONTRIBUTING.md**](https://github.com/shadps4-emu/shadPS4/blob/main/CONTRIBUTING.md) file.\
Open a PR and we'll check it :)
# Translations
If you want to translate shadPS4 to your language we use [**Crowdin**](https://crowdin.com/project/shadps4-emulator).
# Contributors
<a href="https://github.com/shadps4-emu/shadPS4/graphs/contributors">
<img src="https://contrib.rocks/image?repo=shadps4-emu/shadPS4&max=24">
</a>
# Special Thanks

View File

@@ -74,7 +74,6 @@ path = [
"src/images/trophy.wav",
"src/images/hotkey.png",
"src/images/game_settings.png",
"src/shadps4.qrc",
"src/shadps4.rc",
]
precedence = "aggregate"
@@ -130,9 +129,4 @@ SPDX-License-Identifier = "MIT"
[[annotations]]
path = "src/video_core/host_shaders/fsr/*"
SPDX-FileCopyrightText = "Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved."
SPDX-License-Identifier = "MIT"
[[annotations]]
path = "dist/qt.conf"
SPDX-FileCopyrightText = "shadPS4 Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later"
SPDX-License-Identifier = "MIT"

View File

@@ -1,28 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(highest_version "0")
set(CANDIDATE_DRIVES A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
foreach(drive ${CANDIDATE_DRIVES})
file(GLOB kits LIST_DIRECTORIES true CONFIGURE_DEPENDS "${drive}:/Qt/*/msvc*_64")
foreach(kit IN LISTS kits)
get_filename_component(version_dir "${kit}" DIRECTORY)
get_filename_component(kit_version "${version_dir}" NAME)
message(STATUS "DetectQtInstallation.cmake: Detected Qt: ${kit}")
if (kit_version VERSION_GREATER highest_version)
set(highest_version "${kit_version}")
set(QT_PREFIX "${kit}")
endif()
endforeach()
endforeach()
if(QT_PREFIX)
set(CMAKE_PREFIX_PATH "${QT_PREFIX}" CACHE PATH "Qt prefix autodetected" FORCE)
message(STATUS "DetectQtInstallation.cmake: Choose newest Qt: ${QT_PREFIX}")
else()
message(STATUS "DetectQtInstallation.cmake: No QtDirectory found in <drive>:/Qt please set CMAKE_PREFIX_PATH manually")
endif()

View File

@@ -11,6 +11,7 @@
<project_license translate="no">GPL-2.0</project_license>
<launchable type="desktop-id" translate="no">net.shadps4.shadPS4.desktop</launchable>
<url type="homepage" translate="no">https://shadps4.net/</url>
<url type="vcs-browser" translate="no">https://github.com/shadps4-emu/shadPS4</url>
<description>
<p>shadPS4 is an early PlayStation 4 emulator for Windows, Linux and macOS written in C++.</p>
<p>The emulator is still early in development, so don't expect a flawless experience. Nonetheless, the emulator can already run a number of commercial games.</p>
@@ -18,25 +19,31 @@
<screenshots>
<screenshot type="default">
<image type="source" translate="no" >https://cdn.jsdelivr.net/gh/shadps4-emu/shadps4@main/documents/Screenshots/1.png</image>
<caption>Bloodborne</caption>
<caption>Bloodborne by From Software</caption>
</screenshot>
<screenshot>
<image type="source" translate="no">https://cdn.jsdelivr.net/gh/shadps4-emu/shadps4@main/documents/Screenshots/2.png</image>
<caption>Hatsune Miku: Project DIVA Future Tone</caption>
<caption>Hatsune Miku Project DIVA Future Tone by SEGA</caption>
</screenshot>
<screenshot>
<image type="source" translate="no">https://cdn.jsdelivr.net/gh/shadps4-emu/shadps4@main/documents/Screenshots/3.png</image>
<caption>Yakuza 0</caption>
<caption>Yakuza 0 by SEGA</caption>
</screenshot>
<screenshot>
<image type="source" translate="no">https://cdn.jsdelivr.net/gh/shadps4-emu/shadps4@main/documents/Screenshots/4.png</image>
<caption>Persona 4 Golden</caption>
<caption>DRIVECLUB™ by Evolution Studios</caption>
</screenshot>
</screenshots>
<categories>
<category translate="no">Game</category>
</categories>
<releases>
<release version="0.13.0" date="2025-12-24">
<url>https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.13.0</url>
</release>
<release version="0.12.5" date="2025-11-07">
<url>https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.5</url>
</release>
<release version="0.12.0" date="2025-10-31">
<url>https://github.com/shadps4-emu/shadPS4/releases/tag/v.0.12.0</url>
</release>

2
dist/qt.conf vendored
View File

@@ -1,2 +0,0 @@
[Paths]
plugins = "./qtplugins"

View File

@@ -0,0 +1,51 @@
// 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",
"ms-vscode.cmake-tools",
"xaver.clang-format"
],
"settings": {
"clangd.arguments": [
"--background-index",
"--clang-tidy",
"--completion-style=detailed",
"--header-insertion=never",
"--compile-commands-dir=/workspaces/shadPS4/Build/x64-Clang-Release"
],
"C_Cpp.intelliSenseEngine": "Disabled"
}
}
},
"settings": {
"cmake.configureOnOpen": false,
"cmake.generator": "Unix Makefiles",
"cmake.environment": {
"CC": "clang",
"CXX": "clang++"
},
"cmake.configureEnvironment": {
"CMAKE_CXX_STANDARD": "23",
"CMAKE_CXX_STANDARD_REQUIRED": "ON",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
},
"editor.formatOnSave": true,
"clang-format.executable": "clang-format-19"
}
}

View File

@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
FROM archlinux:latest
RUN pacman-key --init && \
pacman-key --populate archlinux && \
pacman -Syu --noconfirm
RUN pacman -S --noconfirm \
base-devel \
clang \
clang19 \
ninja \
git \
ca-certificates \
wget \
alsa-lib \
libpulse \
openal \
openssl \
zlib \
libedit \
systemd-libs \
libevdev \
sdl2 \
jack \
sndio \
libxtst \
vulkan-headers \
vulkan-validation-layers \
libpng \
clang-tools-extra \
cmake \
libx11 \
libxrandr \
libxcursor \
libxi \
libxinerama \
libxss \
&& pacman -Scc --noconfirm
RUN ln -sf /usr/lib/llvm19/bin/clang-format /usr/bin/clang-format-19
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,100 @@
<!--
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 Tools (GUI)
Generate build with CMake Tools.
1. Go `CMake Tools > Configure > '>'`
2. And `Build > '>'`
Compiled executable in `Build` folder.
## Alternative 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

@@ -17,8 +17,7 @@ First and foremost, Clang 18 is the **recommended compiler** as it is used for o
sudo apt install build-essential clang git cmake libasound2-dev \
libpulse-dev libopenal-dev libssl-dev zlib1g-dev libedit-dev \
libudev-dev libevdev-dev libsdl2-dev libjack-dev libsndio-dev \
qt6-base-dev qt6-tools-dev qt6-multimedia-dev libvulkan-dev \
vulkan-validationlayers libpng-dev
libvulkan-dev vulkan-validationlayers libpng-dev
```
#### Fedora
@@ -27,8 +26,6 @@ sudo apt install build-essential clang git cmake libasound2-dev \
sudo dnf install clang git cmake libatomic alsa-lib-devel \
pipewire-jack-audio-connection-kit-devel openal-soft-devel \
openssl-devel libevdev-devel libudev-devel libXext-devel \
qt6-qtbase-devel qt6-qtbase-private-devel \
qt6-qtmultimedia-devel qt6-qtsvg-devel qt6-qttools-devel \
vulkan-devel vulkan-validation-layers libpng-devel libuuid-devel
```
@@ -36,8 +33,7 @@ sudo dnf install clang git cmake libatomic alsa-lib-devel \
```bash
sudo pacman -S base-devel clang git cmake sndio jack2 openal \
qt6-base qt6-declarative qt6-multimedia qt6-tools sdl2 \
vulkan-validation-layers libpng
sdl2 vulkan-validation-layers libpng
```
**Note**: The `shadps4-git` AUR package is not maintained by any of the developers, and it uses the default compiler, which is often set to GCC. Use at your own discretion.
@@ -48,9 +44,7 @@ sudo pacman -S base-devel clang git cmake sndio jack2 openal \
sudo zypper install clang git cmake libasound2 libpulse-devel \
libsndio7 libjack-devel openal-soft-devel libopenssl-devel \
zlib-devel libedit-devel systemd-devel libevdev-devel \
qt6-base-devel qt6-multimedia-devel qt6-svg-devel \
qt6-linguist-devel qt6-gui-private-devel vulkan-devel \
vulkan-validationlayers libpng-devel
vulkan-devel vulkan-validationlayers libpng-devel
```
#### NixOS
@@ -90,12 +84,12 @@ There are 3 options you can choose from. Option 1 is **highly recommended**.
1. Generate the build directory in the shadPS4 directory.
```bash
cmake -S . -B build/ -DENABLE_QT_GUI=ON -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake -S . -B build/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
```
To disable the Qt GUI, remove the `-DENABLE_QT_GUI=ON` flag. To change the build type (for debugging), add `-DCMAKE_BUILD_TYPE=Debug`.
To change the build type (for debugging), add `-DCMAKE_BUILD_TYPE=Debug`.
2. Use CMake to build the project:
1. Use CMake to build the project:
```bash
cmake --build ./build --parallel$(nproc)
@@ -103,18 +97,12 @@ cmake --build ./build --parallel$(nproc)
If your computer freezes during this step, this could be caused by excessive system resource usage. In that case, remove `--parallel$(nproc)`.
Now run the emulator. If Qt was enabled at configure time:
Now run the emulator to get the list of options:
```bash
./build/shadps4
```
Otherwise, specify the path to your game's boot file:
```bash
./build/shadps4 /"PATH"/"TO"/"GAME"/"FOLDER"/eboot.bin
```
You can also specify the Game ID as an argument for which game to boot, as long as the folder containing the games is specified in config.toml (example: Bloodborne (US) is CUSA00900).
#### Option 2: Configuring with cmake-gui
@@ -142,10 +130,6 @@ Go to Settings, filter by `@ext:ms-vscode.cmake-tools configure` and disable thi
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/1.png)
If you wish to build with the Qt GUI, add `-DENABLE_QT_GUI=ON` to the configure arguments:
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/2.png)
On the CMake tab, change the options as you wish, but make sure that it looks similar to or exactly like this:
![image](https://raw.githubusercontent.com/shadps4-emu/shadPS4/refs/heads/main/documents/Screenshots/Linux/4.png)

View File

@@ -24,21 +24,6 @@ eval $(/opt/homebrew/bin/brew shellenv)
brew install clang-format cmake
```
Next, install x86_64 Qt. You can skip these steps and move on to **Cloning and compiling** if you do not intend to build the Qt GUI.
**If you are on an ARM Mac:**
```
# Installs x86_64 Homebrew to /usr/local
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Installs libraries.
arch -x86_64 /usr/local/bin/brew install qt@6
```
**If you are on an x86_64 Mac:**
```
brew install qt@6
```
### Cloning and compiling:
Clone the repository recursively:
@@ -52,8 +37,6 @@ Generate the build directory in the shadPS4 directory:
cmake -S . -B build/ -DCMAKE_OSX_ARCHITECTURES=x86_64
```
If you want to build the Qt GUI, add `-DENABLE_QT_GUI=ON` to the end of this command as well.
Enter the directory:
```
cd build/

View File

@@ -22,23 +22,6 @@ Once you are within the installer:
2. Go to "Individual Components" tab then search and select both `C++ Clang Compiler for Windows` and `MSBuild support for LLVM`
3. Continue the installation
### (Prerequisite) Download [**Qt**](https://doc.qt.io/qt-6/get-and-install-qt.html)
Beware, this requires you to create a Qt account. If you do not want to do this, please follow the MSYS2/MinGW compilation method instead.
1. Under the current, non beta version of Qt, select the option `MSVC 2022 64-bit` or similar, as well as `QT Multimedia`.
If you are on Windows on ARM / Qualcomm Snapdragon Elite X, select `MSVC 2022 ARM64` instead.
Go through the installation normally. If you know what you are doing, you may unselect individual components that eat up too much disk space.
2. Download and install [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022)
Once you are finished, you will have to configure Qt within Visual Studio:
1. Tools -> Options -> Qt -> Versions
2. Add a new Qt version and navigate it to the correct folder. Should look like so: `C:\Qt\<QtVersion>\msvc2022_64`
3. Enable the default checkmark on the new version you just created.
### (Prerequisite) Download [**Git for Windows**](https://git-scm.com/download/win)
Go through the Git for Windows installation as normal
@@ -53,16 +36,11 @@ Go through the Git for Windows installation as normal
1. Open up Visual Studio, select `Open a local folder` and select the folder with the shadPS4 source code. The folder should contain `CMakeLists.txt`
2. Change Clang x64 Debug to Clang x64 Release if you want a regular, non-debug build.
3. If you want to build shadPS4 with the Qt Gui, simply select Clang x64 Release with Qt instead.
4. Change the project to build to shadps4.exe
5. Build -> Build All
3. Change the project to build to shadps4.exe
4. Build -> Build All
Your shadps4.exe will be in `C:\path\to\source\Build\x64-Clang-Release\`
To automatically populate the necessary files to run shadPS4.exe, run in a command prompt or terminal:
`C:\Qt\<QtVersion>\msvc2022_64\bin\windeployqt6.exe "C:\path\to\shadps4.exe"`
(Change Qt path if you've installed it to non-default path)
## Option 2: MSYS2/MinGW
> [!IMPORTANT]
@@ -79,13 +57,10 @@ Normal x86-based computers, follow:
1. Open "MSYS2 MINGW64" from your new applications
2. Run `pacman -Syu`, let it complete;
3. Run `pacman -S --needed git mingw-w64-x86_64-binutils mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-rapidjson mingw-w64-x86_64-ninja mingw-w64-x86_64-ffmpeg`
1. Optional (Qt only): run `pacman -S --needed mingw-w64-x86_64-qt6-base mingw-w64-x86_64-qt6-tools mingw-w64-x86_64-qt6-multimedia`
4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4`
5. Run `cd shadPS4`
6. Run `cmake -S . -B build -DCMAKE_C_COMPILER="clang.exe" -DCMAKE_CXX_COMPILER="clang++.exe" -DCMAKE_CXX_FLAGS="-O2 -march=native"`
1. Optional (Qt only): add `-DENABLE_QT_GUI=ON`
7. Run `cmake --build build`
1. Optional (Qt only): run `windeployqt6 build/shadps4.exe`
8. To run the finished product, run `./build/shadPS4.exe`
ARM64-based computers, follow:
@@ -93,13 +68,10 @@ ARM64-based computers, follow:
1. Open "MSYS2 CLANGARM64" from your new applications
2. Run `pacman -Syu`, let it complete;
3. Run `pacman -S --needed git mingw-w64-clang-aarch64-binutils mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-rapidjson mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja mingw-w64-clang-aarch64-ffmpeg`
1. Optional (Qt only): run `pacman -S --needed mingw-w64-clang-aarch64-qt6-base mingw-w64-clang-aarch64-qt6-tools mingw-w64-clang-aarch64-qt6-multimedia`
4. Run `git clone --depth 1 --recursive https://github.com/shadps4-emu/shadPS4`
5. Run `cd shadPS4`
6. Run `cmake -S . -B build -DCMAKE_C_COMPILER="clang.exe" -DCMAKE_CXX_COMPILER="clang++.exe" -DCMAKE_CXX_FLAGS="-O2 -march=native"`
1. Optional (Qt only): add `-DENABLE_QT_GUI=ON`
7. Run `cmake --build build`
1. Optional (Qt only): run `windeployqt6 build/shadps4.exe`
8. To run the finished product, run `./build/shadPS4.exe`
## Note on MSYS2 builds

1
externals/CLI11 vendored Submodule

Submodule externals/CLI11 added at bf5a16a26a

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)
@@ -63,6 +63,18 @@ if (NOT TARGET SDL3::SDL3)
add_subdirectory(sdl3)
endif()
# SDL3_mixer
if (NOT TARGET SDL3_mixer::SDL3_mixer)
set(SDLMIXER_FLAC OFF)
set(SDLMIXER_OGG OFF)
set(SDLMIXER_MOD OFF)
set(SDLMIXER_MIDI OFF)
set(SDLMIXER_OPUS OFF)
set(SDLMIXER_WAVPACK OFF)
set(BUILD_SHARED_LIBS OFF)
add_subdirectory(sdl3_mixer)
endif()
# vulkan-headers
if (NOT TARGET Vulkan::Headers)
set(VULKAN_HEADERS_ENABLE_MODULE OFF)
@@ -140,7 +152,7 @@ endif()
# sirit
add_subdirectory(sirit)
if (WIN32)
target_compile_options(sirit PUBLIC "-Wno-error=unused-command-line-argument")
target_compile_options(sirit PRIVATE "-Wno-error=unused-command-line-argument")
endif()
# half
@@ -192,6 +204,7 @@ add_subdirectory(tracy)
# pugixml
if (NOT TARGET pugixml::pugixml)
option(PUGIXML_NO_EXCEPTIONS "" ON)
add_subdirectory(pugixml)
endif()
@@ -245,3 +258,20 @@ endif()
if (WIN32)
add_subdirectory(ext-wepoll)
endif()
if (NOT TARGET fdk-aac)
add_subdirectory(aacdec)
endif()
#nlohmann json
set(JSON_BuildTests OFF CACHE INTERNAL "")
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(CLI11)

154
externals/aacdec/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,154 @@
# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set(AACDEC_SRC
fdk-aac/libAACdec/src/FDK_delay.cpp
fdk-aac/libAACdec/src/aac_ram.cpp
fdk-aac/libAACdec/src/aac_rom.cpp
fdk-aac/libAACdec/src/aacdec_drc.cpp
fdk-aac/libAACdec/src/aacdec_hcr.cpp
fdk-aac/libAACdec/src/aacdec_hcr_bit.cpp
fdk-aac/libAACdec/src/aacdec_hcrs.cpp
fdk-aac/libAACdec/src/aacdec_pns.cpp
fdk-aac/libAACdec/src/aacdec_tns.cpp
fdk-aac/libAACdec/src/aacdecoder.cpp
fdk-aac/libAACdec/src/aacdecoder_lib.cpp
fdk-aac/libAACdec/src/block.cpp
fdk-aac/libAACdec/src/channel.cpp
fdk-aac/libAACdec/src/channelinfo.cpp
fdk-aac/libAACdec/src/conceal.cpp
fdk-aac/libAACdec/src/ldfiltbank.cpp
fdk-aac/libAACdec/src/pulsedata.cpp
fdk-aac/libAACdec/src/rvlc.cpp
fdk-aac/libAACdec/src/rvlcbit.cpp
fdk-aac/libAACdec/src/rvlcconceal.cpp
fdk-aac/libAACdec/src/stereo.cpp
fdk-aac/libAACdec/src/usacdec_ace_d4t64.cpp
fdk-aac/libAACdec/src/usacdec_ace_ltp.cpp
fdk-aac/libAACdec/src/usacdec_acelp.cpp
fdk-aac/libAACdec/src/usacdec_fac.cpp
fdk-aac/libAACdec/src/usacdec_lpc.cpp
fdk-aac/libAACdec/src/usacdec_lpd.cpp
fdk-aac/libAACdec/src/usacdec_rom.cpp
)
set(FDK_SRC
fdk-aac/libFDK/src/FDK_bitbuffer.cpp
fdk-aac/libFDK/src/FDK_core.cpp
fdk-aac/libFDK/src/FDK_crc.cpp
fdk-aac/libFDK/src/FDK_decorrelate.cpp
fdk-aac/libFDK/src/FDK_hybrid.cpp
fdk-aac/libFDK/src/FDK_lpc.cpp
fdk-aac/libFDK/src/FDK_matrixCalloc.cpp
fdk-aac/libFDK/src/FDK_qmf_domain.cpp
fdk-aac/libFDK/src/FDK_tools_rom.cpp
fdk-aac/libFDK/src/FDK_trigFcts.cpp
fdk-aac/libFDK/src/autocorr2nd.cpp
fdk-aac/libFDK/src/dct.cpp
fdk-aac/libFDK/src/fft.cpp
fdk-aac/libFDK/src/fft_rad2.cpp
fdk-aac/libFDK/src/fixpoint_math.cpp
fdk-aac/libFDK/src/huff_nodes.cpp
fdk-aac/libFDK/src/mdct.cpp
fdk-aac/libFDK/src/nlc_dec.cpp
fdk-aac/libFDK/src/qmf.cpp
fdk-aac/libFDK/src/scale.cpp
)
set(SYS_SRC
fdk-aac/libSYS/src/genericStds.cpp
fdk-aac/libSYS/src/syslib_channelMapDescr.cpp
)
set(ARITHCODING_SRC
fdk-aac/libArithCoding/src/ac_arith_coder.cpp
)
set(MPEGTPDEC_SRC
fdk-aac/libMpegTPDec/src/tpdec_adif.cpp
fdk-aac/libMpegTPDec/src/tpdec_adts.cpp
fdk-aac/libMpegTPDec/src/tpdec_asc.cpp
fdk-aac/libMpegTPDec/src/tpdec_drm.cpp
fdk-aac/libMpegTPDec/src/tpdec_latm.cpp
fdk-aac/libMpegTPDec/src/tpdec_lib.cpp
)
set(SBRDEC_SRC
fdk-aac/libSBRdec/src/HFgen_preFlat.cpp
fdk-aac/libSBRdec/src/env_calc.cpp
fdk-aac/libSBRdec/src/env_dec.cpp
fdk-aac/libSBRdec/src/env_extr.cpp
fdk-aac/libSBRdec/src/hbe.cpp
fdk-aac/libSBRdec/src/huff_dec.cpp
fdk-aac/libSBRdec/src/lpp_tran.cpp
fdk-aac/libSBRdec/src/psbitdec.cpp
fdk-aac/libSBRdec/src/psdec.cpp
fdk-aac/libSBRdec/src/psdec_drm.cpp
fdk-aac/libSBRdec/src/psdecrom_drm.cpp
fdk-aac/libSBRdec/src/pvc_dec.cpp
fdk-aac/libSBRdec/src/sbr_deb.cpp
fdk-aac/libSBRdec/src/sbr_dec.cpp
fdk-aac/libSBRdec/src/sbr_ram.cpp
fdk-aac/libSBRdec/src/sbr_rom.cpp
fdk-aac/libSBRdec/src/sbrdec_drc.cpp
fdk-aac/libSBRdec/src/sbrdec_freq_sca.cpp
fdk-aac/libSBRdec/src/sbrdecoder.cpp
)
set(PCMUTILS_SRC
fdk-aac/libPCMutils/src/limiter.cpp
fdk-aac/libPCMutils/src/pcm_utils.cpp
fdk-aac/libPCMutils/src/pcmdmx_lib.cpp
)
set(DRCDEC_SRC
fdk-aac/libDRCdec/src/FDK_drcDecLib.cpp
fdk-aac/libDRCdec/src/drcDec_gainDecoder.cpp
fdk-aac/libDRCdec/src/drcDec_reader.cpp
fdk-aac/libDRCdec/src/drcDec_rom.cpp
fdk-aac/libDRCdec/src/drcDec_selectionProcess.cpp
fdk-aac/libDRCdec/src/drcDec_tools.cpp
fdk-aac/libDRCdec/src/drcGainDec_init.cpp
fdk-aac/libDRCdec/src/drcGainDec_preprocess.cpp
fdk-aac/libDRCdec/src/drcGainDec_process.cpp
)
set(SACDEC_SRC
fdk-aac/libSACdec/src/sac_bitdec.cpp
fdk-aac/libSACdec/src/sac_calcM1andM2.cpp
fdk-aac/libSACdec/src/sac_dec.cpp
fdk-aac/libSACdec/src/sac_dec_conceal.cpp
fdk-aac/libSACdec/src/sac_dec_lib.cpp
fdk-aac/libSACdec/src/sac_process.cpp
fdk-aac/libSACdec/src/sac_qmf.cpp
fdk-aac/libSACdec/src/sac_reshapeBBEnv.cpp
fdk-aac/libSACdec/src/sac_rom.cpp
fdk-aac/libSACdec/src/sac_smoothing.cpp
fdk-aac/libSACdec/src/sac_stp.cpp
fdk-aac/libSACdec/src/sac_tsd.cpp
)
add_library(fdk-aac
${AACDEC_SRC}
${FDK_SRC}
${SYS_SRC}
${ARITHCODING_SRC}
${MPEGTPDEC_SRC}
${SBRDEC_SRC}
${PCMUTILS_SRC}
${DRCDEC_SRC}
${SACDEC_SRC}
)
target_include_directories(fdk-aac
PUBLIC
fdk-aac/libAACdec/include
fdk-aac/libFDK/include
fdk-aac/libSYS/include
fdk-aac/libArithCoding/include
fdk-aac/libMpegTPDec/include
fdk-aac/libSBRdec/include
fdk-aac/libPCMutils/include
fdk-aac/libDRCdec/include
fdk-aac/libSACdec/include
)

1
externals/aacdec/fdk-aac vendored Submodule

2
externals/fmt vendored

1
externals/json vendored Submodule

Submodule externals/json added at 55f93686c0

1
externals/miniz vendored Submodule

Submodule externals/miniz added at 174573d602

2
externals/sdl3 vendored

1
externals/sdl3_mixer vendored Submodule

Submodule externals/sdl3_mixer added at 4182794ea4

View File

@@ -6,54 +6,47 @@ with import (fetchTarball "https://github.com/nixos/nixpkgs/archive/cfd19cdc5468
pkgs.mkShell {
name = "shadps4-build-env";
nativeBuildInputs = [
pkgs.llvmPackages_18.clang
pkgs.cmake
pkgs.pkg-config
pkgs.git
nativeBuildInputs = with pkgs; [
llvmPackages_18.clang
cmake
pkg-config
git
util-linux
];
buildInputs = [
pkgs.alsa-lib
pkgs.libpulseaudio
pkgs.openal
pkgs.openssl
pkgs.zlib
pkgs.libedit
pkgs.udev
pkgs.libevdev
pkgs.SDL2
pkgs.jack2
pkgs.sndio
pkgs.qt6.qtbase
pkgs.qt6.qttools
pkgs.qt6.qtmultimedia
buildInputs = with pkgs; [
alsa-lib
libpulseaudio
openal
zlib
libedit
udev
libevdev
SDL2
jack2
sndio
pkgs.vulkan-headers
pkgs.vulkan-utility-libraries
pkgs.vulkan-tools
vulkan-headers
vulkan-utility-libraries
vulkan-tools
pkgs.ffmpeg
pkgs.fmt
pkgs.glslang
pkgs.libxkbcommon
pkgs.wayland
pkgs.xorg.libxcb
pkgs.xorg.xcbutil
pkgs.xorg.xcbutilkeysyms
pkgs.xorg.xcbutilwm
pkgs.sdl3
pkgs.stb
pkgs.qt6.qtwayland
pkgs.wayland-protocols
pkgs.libpng
ffmpeg
fmt
glslang
libxkbcommon
wayland
xorg.libxcb
xorg.xcbutil
xorg.xcbutilkeysyms
xorg.xcbutilwm
sdl3
stb
wayland-protocols
libpng
];
shellHook = ''
echo "Entering shadPS4 dev shell"
export QT_QPA_PLATFORM="wayland"
export QT_PLUGIN_PATH="${pkgs.qt6.qtwayland}/lib/qt-6/plugins:${pkgs.qt6.qtbase}/lib/qt-6/plugins"
export QML2_IMPORT_PATH="${pkgs.qt6.qtbase}/lib/qt-6/qml"
export CMAKE_PREFIX_PATH="${pkgs.vulkan-headers}:$CMAKE_PREFIX_PATH"
# OpenGL

View File

@@ -139,13 +139,10 @@ static ConfigEntry<double> trophyNotificationDuration(6.0);
static ConfigEntry<string> logFilter("");
static ConfigEntry<string> logType("sync");
static ConfigEntry<string> userName("shadPS4");
static ConfigEntry<string> chooseHomeTab("General");
static ConfigEntry<bool> isShowSplash(false);
static ConfigEntry<string> isSideTrophy("right");
static ConfigEntry<bool> isConnectedToNetwork(false);
static bool enableDiscordRPC = false;
static bool checkCompatibilityOnStartup = false;
static bool compatibilityData = false;
static std::filesystem::path sys_modules_path = {};
// Input
@@ -180,7 +177,7 @@ static ConfigEntry<bool> isFullscreen(false);
static ConfigEntry<string> fullscreenMode("Windowed");
static ConfigEntry<string> presentMode("Mailbox");
static ConfigEntry<bool> isHDRAllowed(false);
static ConfigEntry<bool> fsrEnabled(true);
static ConfigEntry<bool> fsrEnabled(false);
static ConfigEntry<bool> rcasEnabled(true);
static ConfigEntry<int> rcasAttenuation(250);
@@ -194,16 +191,17 @@ static ConfigEntry<bool> vkCrashDiagnostic(false);
static ConfigEntry<bool> vkHostMarkers(false);
static ConfigEntry<bool> vkGuestMarkers(false);
static ConfigEntry<bool> rdocEnable(false);
static ConfigEntry<bool> pipelineCacheEnable(false);
static ConfigEntry<bool> pipelineCacheArchive(false);
// Debug
static ConfigEntry<bool> isDebugDump(false);
static ConfigEntry<bool> isShaderDebug(false);
static ConfigEntry<bool> isSeparateLogFilesEnabled(false);
static ConfigEntry<bool> isFpsColor(true);
static ConfigEntry<bool> showFpsCounter(false);
static ConfigEntry<bool> logEnabled(true);
// GUI
static bool load_game_size = true;
static std::vector<GameInstallDir> settings_install_dirs = {};
std::vector<bool> install_dirs_enabled = {};
std::filesystem::path settings_addon_install_dir = {};
@@ -212,13 +210,16 @@ std::filesystem::path save_data_path = {};
// Settings
ConfigEntry<u32> m_language(1); // english
// USB Device
static ConfigEntry<int> usbDeviceBackend(UsbBackendType::Real);
// Keys
static string trophyKey = "";
// Config version, used to determine if a user's config file is outdated.
static string config_version = Common::g_scm_rev;
// These two entries aren't stored in the config
// These entries aren't stored in the config
static bool overrideControllerColor = false;
static int controllerCustomColorRGB[3] = {0, 0, 255};
@@ -278,10 +279,6 @@ void setTrophyKey(string key) {
trophyKey = key;
}
bool GetLoadGameSizeEnabled() {
return load_game_size;
}
std::filesystem::path GetSaveDataPath() {
if (save_data_path.empty()) {
return Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "savedata";
@@ -293,10 +290,6 @@ void setVolumeSlider(int volumeValue, bool is_game_specific) {
volumeSlider.set(volumeValue, is_game_specific);
}
void setLoadGameSizeEnabled(bool enable) {
load_game_size = enable;
}
bool isNeoModeConsole() {
return isNeo.get();
}
@@ -309,8 +302,10 @@ int getExtraDmemInMbytes() {
return extraDmemInMbytes.get();
}
void setExtraDmemInMbytes(int value) {
extraDmemInMbytes.base_value = 0;
void setExtraDmemInMbytes(int value, bool is_game_specific) {
// Disable setting in global config
is_game_specific ? extraDmemInMbytes.game_specific_value = value
: extraDmemInMbytes.base_value = 0;
}
bool getIsFullscreen() {
@@ -389,10 +384,6 @@ string getUserName() {
return userName.get();
}
string getChooseHomeTab() {
return chooseHomeTab.get();
}
bool getUseSpecialPad() {
return useSpecialPad.get();
}
@@ -453,8 +444,20 @@ bool isRdocEnabled() {
return rdocEnable.get();
}
bool fpsColor() {
return isFpsColor.get();
bool isPipelineCacheEnabled() {
return pipelineCacheEnable.get();
}
bool isPipelineCacheArchived() {
return pipelineCacheArchive.get();
}
bool getShowFpsCounter() {
return showFpsCounter.get();
}
void setShowFpsCounter(bool enable, bool is_game_specific) {
showFpsCounter.set(enable, is_game_specific);
}
bool isLoggingEnabled() {
@@ -508,14 +511,6 @@ void setVkGuestMarkersEnabled(bool enable, bool is_game_specific) {
vkGuestMarkers.set(enable, is_game_specific);
}
bool getCompatibilityEnabled() {
return compatibilityData;
}
bool getCheckCompatibilityOnStartup() {
return checkCompatibilityOnStartup;
}
bool getIsConnectedToNetwork() {
return isConnectedToNetwork.get();
}
@@ -600,10 +595,26 @@ void setVkSyncValidation(bool enable, bool is_game_specific) {
vkValidationSync.set(enable, is_game_specific);
}
void setVkCoreValidation(bool enable, bool is_game_specific) {
vkValidationCore.set(enable, is_game_specific);
}
void setVkGpuValidation(bool enable, bool is_game_specific) {
vkValidationGpu.set(enable, is_game_specific);
}
void setRdocEnabled(bool enable, bool is_game_specific) {
rdocEnable.set(enable, is_game_specific);
}
void setPipelineCacheEnabled(bool enable, bool is_game_specific) {
pipelineCacheEnable.set(enable, is_game_specific);
}
void setPipelineCacheArchived(bool enable, bool is_game_specific) {
pipelineCacheArchive.set(enable, is_game_specific);
}
void setVblankFreq(u32 value, bool is_game_specific) {
vblankFrequency.set(value, is_game_specific);
}
@@ -680,10 +691,6 @@ void setUserName(const string& name, bool is_game_specific) {
userName.set(name, is_game_specific);
}
void setChooseHomeTab(const string& type, bool is_game_specific) {
chooseHomeTab.set(type, is_game_specific);
}
void setUseSpecialPad(bool use) {
useSpecialPad.base_value = use;
}
@@ -696,14 +703,6 @@ void setIsMotionControlsEnabled(bool use, bool is_game_specific) {
isMotionControlsEnabled.set(use, is_game_specific);
}
void setCompatibilityEnabled(bool use) {
compatibilityData = use;
}
void setCheckCompatibilityOnStartup(bool use) {
checkCompatibilityOnStartup = use;
}
bool addGameInstallDir(const std::filesystem::path& dir, bool enabled) {
for (const auto& install_dir : settings_install_dirs) {
if (install_dir.path == dir) {
@@ -833,6 +832,14 @@ void setRcasAttenuation(int value, bool is_game_specific) {
rcasAttenuation.set(value, is_game_specific);
}
int getUsbDeviceBackend() {
return usbDeviceBackend.get();
}
void setUsbDeviceBackend(int value, bool is_game_specific) {
usbDeviceBackend.set(value, is_game_specific);
}
void load(const std::filesystem::path& path, bool is_game_specific) {
// If the configuration file does not exist, create it and return, unless it is game specific
std::error_code error;
@@ -874,12 +881,8 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
userName.setFromToml(general, "userName", is_game_specific);
isShowSplash.setFromToml(general, "showSplash", is_game_specific);
isSideTrophy.setFromToml(general, "sideTrophy", is_game_specific);
compatibilityData = toml::find_or<bool>(general, "compatibilityEnabled", compatibilityData);
checkCompatibilityOnStartup = toml::find_or<bool>(general, "checkCompatibilityOnStartup",
checkCompatibilityOnStartup);
isConnectedToNetwork.setFromToml(general, "isConnectedToNetwork", is_game_specific);
chooseHomeTab.setFromToml(general, "chooseHomeTab", is_game_specific);
defaultControllerID.setFromToml(general, "defaultControllerID", is_game_specific);
sys_modules_path = toml::find_fs_path_or(general, "sysModulesPath", sys_modules_path);
}
@@ -894,6 +897,7 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
isMotionControlsEnabled.setFromToml(input, "isMotionControlsEnabled", is_game_specific);
useUnifiedInputConfig.setFromToml(input, "useUnifiedInputConfig", is_game_specific);
backgroundControllerInput.setFromToml(input, "backgroundControllerInput", is_game_specific);
usbDeviceBackend.setFromToml(input, "usbDeviceBackend", is_game_specific);
}
if (data.contains("Audio")) {
@@ -940,6 +944,8 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
vkHostMarkers.setFromToml(vk, "hostMarkers", is_game_specific);
vkGuestMarkers.setFromToml(vk, "guestMarkers", is_game_specific);
rdocEnable.setFromToml(vk, "rdocEnable", is_game_specific);
pipelineCacheEnable.setFromToml(vk, "pipelineCacheEnable", is_game_specific);
pipelineCacheArchive.setFromToml(vk, "pipelineCacheArchive", is_game_specific);
}
string current_version = {};
@@ -949,7 +955,7 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
isDebugDump.setFromToml(debug, "DebugDump", is_game_specific);
isSeparateLogFilesEnabled.setFromToml(debug, "isSeparateLogFilesEnabled", is_game_specific);
isShaderDebug.setFromToml(debug, "CollectShader", is_game_specific);
isFpsColor.setFromToml(debug, "FPSColor", is_game_specific);
showFpsCounter.setFromToml(debug, "showFpsCounter", is_game_specific);
logEnabled.setFromToml(debug, "logEnabled", is_game_specific);
current_version = toml::find_or<std::string>(debug, "ConfigVersion", current_version);
}
@@ -957,8 +963,6 @@ void load(const std::filesystem::path& path, bool is_game_specific) {
if (data.contains("GUI")) {
const toml::value& gui = data.at("GUI");
load_game_size = toml::find_or<bool>(gui, "loadGameSizeEnabled", load_game_size);
const auto install_dir_array =
toml::find_or<std::vector<std::u8string>>(gui, "installDirs", {});
@@ -1062,7 +1066,6 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
logFilter.setTomlValue(data, "General", "logFilter", is_game_specific);
logType.setTomlValue(data, "General", "logType", is_game_specific);
userName.setTomlValue(data, "General", "userName", is_game_specific);
chooseHomeTab.setTomlValue(data, "General", "chooseHomeTab", is_game_specific);
isShowSplash.setTomlValue(data, "General", "showSplash", is_game_specific);
isSideTrophy.setTomlValue(data, "General", "sideTrophy", is_game_specific);
isNeo.setTomlValue(data, "General", "isPS4Pro", is_game_specific);
@@ -1079,6 +1082,7 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
is_game_specific);
backgroundControllerInput.setTomlValue(data, "Input", "backgroundControllerInput",
is_game_specific);
usbDeviceBackend.setTomlValue(data, "Input", "usbDeviceBackend", is_game_specific);
micDevice.setTomlValue(data, "Audio", "micDevice", is_game_specific);
mainOutputDevice.setTomlValue(data, "Audio", "mainOutputDevice", is_game_specific);
@@ -1104,10 +1108,14 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
gpuId.setTomlValue(data, "Vulkan", "gpuId", is_game_specific);
vkValidation.setTomlValue(data, "Vulkan", "validation", is_game_specific);
vkValidationSync.setTomlValue(data, "Vulkan", "validation_sync", is_game_specific);
vkValidationCore.setTomlValue(data, "Vulkan", "validation_core", is_game_specific);
vkValidationGpu.setTomlValue(data, "Vulkan", "validation_gpu", is_game_specific);
vkCrashDiagnostic.setTomlValue(data, "Vulkan", "crashDiagnostic", is_game_specific);
vkHostMarkers.setTomlValue(data, "Vulkan", "hostMarkers", is_game_specific);
vkGuestMarkers.setTomlValue(data, "Vulkan", "guestMarkers", is_game_specific);
rdocEnable.setTomlValue(data, "Vulkan", "rdocEnable", is_game_specific);
pipelineCacheEnable.setTomlValue(data, "Vulkan", "pipelineCacheEnable", is_game_specific);
pipelineCacheArchive.setTomlValue(data, "Vulkan", "pipelineCacheArchive", is_game_specific);
isDebugDump.setTomlValue(data, "Debug", "DebugDump", is_game_specific);
isShaderDebug.setTomlValue(data, "Debug", "CollectShader", is_game_specific);
@@ -1149,13 +1157,10 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
// Non game-specific entries
data["General"]["enableDiscordRPC"] = enableDiscordRPC;
data["General"]["compatibilityEnabled"] = compatibilityData;
data["General"]["checkCompatibilityOnStartup"] = checkCompatibilityOnStartup;
data["General"]["sysModulesPath"] = string{fmt::UTF(sys_modules_path.u8string()).data};
data["GUI"]["installDirs"] = install_dirs;
data["GUI"]["installDirsEnabled"] = install_dirs_enabled;
data["GUI"]["saveDataPath"] = string{fmt::UTF(save_data_path.u8string()).data};
data["GUI"]["loadGameSizeEnabled"] = load_game_size;
data["GUI"]["addonInstallDir"] =
string{fmt::UTF(settings_addon_install_dir.u8string()).data};
data["Debug"]["ConfigVersion"] = config_version;
@@ -1169,9 +1174,7 @@ void save(const std::filesystem::path& path, bool is_game_specific) {
data["GPU"]["internalScreenWidth"] = internalScreenWidth.base_value;
data["GPU"]["internalScreenHeight"] = internalScreenHeight.base_value;
data["GPU"]["patchShaders"] = shouldPatchShaders.base_value;
data["Vulkan"]["validation_core"] = vkValidationCore.base_value;
data["Vulkan"]["validation_gpu"] = vkValidationGpu.base_value;
data["Debug"]["FPSColor"] = isFpsColor.base_value;
data["Debug"]["showFpsCounter"] = showFpsCounter.base_value;
}
// Sorting of TOML sections
@@ -1205,7 +1208,6 @@ void setDefaultValues(bool is_game_specific) {
logFilter.set("", is_game_specific);
logType.set("sync", is_game_specific);
userName.set("shadPS4", is_game_specific);
chooseHomeTab.set("General", is_game_specific);
isShowSplash.set(false, is_game_specific);
isSideTrophy.set("right", is_game_specific);
@@ -1214,6 +1216,7 @@ void setDefaultValues(bool is_game_specific) {
cursorHideTimeout.set(5, is_game_specific);
isMotionControlsEnabled.set(true, is_game_specific);
backgroundControllerInput.set(false, is_game_specific);
usbDeviceBackend.set(UsbBackendType::Real, is_game_specific);
// GS - Audio
micDevice.set("Default Device", is_game_specific);
@@ -1243,6 +1246,8 @@ void setDefaultValues(bool is_game_specific) {
vkHostMarkers.set(false, is_game_specific);
vkGuestMarkers.set(false, is_game_specific);
rdocEnable.set(false, is_game_specific);
pipelineCacheEnable.set(false, is_game_specific);
pipelineCacheArchive.set(false, is_game_specific);
// GS - Debug
isDebugDump.set(false, is_game_specific);
@@ -1258,8 +1263,6 @@ void setDefaultValues(bool is_game_specific) {
// General
enableDiscordRPC = false;
compatibilityData = false;
checkCompatibilityOnStartup = false;
// Input
useSpecialPad.base_value = false;
@@ -1278,11 +1281,8 @@ void setDefaultValues(bool is_game_specific) {
internalScreenWidth.base_value = 1280;
internalScreenHeight.base_value = 720;
// GUI
load_game_size = true;
// Debug
isFpsColor.base_value = true;
showFpsCounter.base_value = false;
}
}
@@ -1297,6 +1297,7 @@ hotkey_pause = f9
hotkey_reload_inputs = f8
hotkey_toggle_mouse_to_joystick = f7
hotkey_toggle_mouse_to_gyro = f6
hotkey_toggle_mouse_to_touchpad = delete
hotkey_quit = lctrl, lshift, end
)";
}

View File

@@ -27,6 +27,8 @@ void load(const std::filesystem::path& path, bool is_game_specific = false);
void save(const std::filesystem::path& path, bool is_game_specific = false);
void resetGameSpecificValue(std::string entry);
bool getGameRunning();
void setGameRunning(bool running);
int getVolumeSlider();
void setVolumeSlider(int volumeValue, bool is_game_specific = false);
std::string getTrophyKey();
@@ -79,6 +81,10 @@ bool vkValidationEnabled();
void setVkValidation(bool enable, bool is_game_specific = false);
bool vkValidationSyncEnabled();
void setVkSyncValidation(bool enable, bool is_game_specific = false);
bool vkValidationGpuEnabled();
void setVkGpuValidation(bool enable, bool is_game_specific = false);
bool vkValidationCoreEnabled();
void setVkCoreValidation(bool enable, bool is_game_specific = false);
bool getVkCrashDiagnosticEnabled();
void setVkCrashDiagnosticEnabled(bool enable, bool is_game_specific = false);
bool getVkHostMarkersEnabled();
@@ -88,7 +94,11 @@ void setVkGuestMarkersEnabled(bool enable, bool is_game_specific = false);
bool getEnableDiscordRPC();
void setEnableDiscordRPC(bool enable);
bool isRdocEnabled();
bool isPipelineCacheEnabled();
bool isPipelineCacheArchived();
void setRdocEnabled(bool enable, bool is_game_specific = false);
void setPipelineCacheEnabled(bool enable, bool is_game_specific = false);
void setPipelineCacheArchived(bool enable, bool is_game_specific = false);
std::string getLogType();
void setLogType(const std::string& type, bool is_game_specific = false);
std::string getLogFilter();
@@ -97,11 +107,10 @@ double getTrophyNotificationDuration();
void setTrophyNotificationDuration(double newTrophyNotificationDuration,
bool is_game_specific = false);
int getCursorHideTimeout();
void setCursorHideTimeout(int newcursorHideTimeout);
std::string getMainOutputDevice();
void setMainOutputDevice(std::string device);
void setMainOutputDevice(std::string device, bool is_game_specific = false);
std::string getPadSpkOutputDevice();
void setPadSpkOutputDevice(std::string device);
void setPadSpkOutputDevice(std::string device, bool is_game_specific = false);
std::string getMicDevice();
void setCursorHideTimeout(int newcursorHideTimeout, bool is_game_specific = false);
void setMicDevice(std::string device, bool is_game_specific = false);
@@ -116,16 +125,15 @@ int getSpecialPadClass();
bool getPSNSignedIn();
void setPSNSignedIn(bool sign, bool is_game_specific = false);
bool patchShaders(); // no set
bool fpsColor(); // no set
bool getShowFpsCounter();
void setShowFpsCounter(bool enable, bool is_game_specific = false);
bool isNeoModeConsole();
void setNeoMode(bool enable, bool is_game_specific = false);
bool isDevKitConsole();
void setDevKitConsole(bool enable, bool is_game_specific = false);
bool vkValidationCoreEnabled(); // no set
bool vkValidationGpuEnabled(); // no set
int getExtraDmemInMbytes();
void setExtraDmemInMbytes(int value);
void setExtraDmemInMbytes(int value, bool is_game_specific = false);
bool getIsMotionControlsEnabled();
void setIsMotionControlsEnabled(bool use, bool is_game_specific = false);
std::string getDefaultControllerID();
@@ -143,18 +151,16 @@ void setRcasAttenuation(int value, bool is_game_specific = false);
bool getIsConnectedToNetwork();
void setConnectedToNetwork(bool enable, bool is_game_specific = false);
void setUserName(const std::string& name, bool is_game_specific = false);
void setChooseHomeTab(const std::string& type, bool is_game_specific = false);
std::filesystem::path getSysModulesPath();
void setSysModulesPath(const std::filesystem::path& path);
enum UsbBackendType : int { Real, SkylandersPortal, InfinityBase, DimensionsToypad };
int getUsbDeviceBackend();
void setUsbDeviceBackend(int value, bool is_game_specific = false);
// TODO
bool GetLoadGameSizeEnabled();
std::filesystem::path GetSaveDataPath();
void setLoadGameSizeEnabled(bool enable);
bool getCompatibilityEnabled();
bool getCheckCompatibilityOnStartup();
std::string getUserName();
std::string getChooseHomeTab();
bool GetUseUnifiedInputConfig();
void SetUseUnifiedInputConfig(bool use);
bool GetOverrideControllerColor();
@@ -164,8 +170,6 @@ void SetControllerCustomColor(int r, int b, int g);
void setGameInstallDirs(const std::vector<std::filesystem::path>& dirs_config);
void setAllGameInstallDirs(const std::vector<GameInstallDir>& dirs_config);
void setSaveDataPath(const std::filesystem::path& path);
void setCompatibilityEnabled(bool use);
void setCheckCompatibilityOnStartup(bool use);
// Gui
bool addGameInstallDir(const std::filesystem::path& dir, bool enabled = true);
void removeGameInstallDir(const std::filesystem::path& dir);

View File

@@ -68,6 +68,7 @@ class ElfInfo {
std::string app_ver{};
u32 firmware_ver = 0;
u32 raw_firmware_ver = 0;
u32 sdk_ver = 0;
PSFAttributes psf_attributes{};
std::filesystem::path splash_path{};
@@ -117,6 +118,11 @@ public:
return raw_firmware_ver;
}
[[nodiscard]] u32 CompiledSdkVer() const {
ASSERT(initialized);
return sdk_ver;
}
[[nodiscard]] const PSFAttributes& GetPSFAttributes() const {
ASSERT(initialized);
return psf_attributes;

View File

@@ -40,28 +40,30 @@ namespace {
switch (mode) {
case FileAccessMode::Read:
return L"rb";
case FileAccessMode::Write:
return L"wb";
case FileAccessMode::Append:
return L"ab";
case FileAccessMode::Write:
case FileAccessMode::ReadWrite:
return L"r+b";
case FileAccessMode::ReadAppend:
return L"a+b";
case FileAccessMode::Create:
return L"wb";
}
break;
case FileType::TextFile:
switch (mode) {
case FileAccessMode::Read:
return L"r";
case FileAccessMode::Write:
return L"w";
case FileAccessMode::Append:
return L"a";
case FileAccessMode::Write:
case FileAccessMode::ReadWrite:
return L"r+";
case FileAccessMode::ReadAppend:
return L"a+";
case FileAccessMode::Create:
return L"w";
}
break;
}
@@ -91,28 +93,30 @@ namespace {
switch (mode) {
case FileAccessMode::Read:
return "rb";
case FileAccessMode::Write:
return "wb";
case FileAccessMode::Append:
return "ab";
case FileAccessMode::Write:
case FileAccessMode::ReadWrite:
return "r+b";
case FileAccessMode::ReadAppend:
return "a+b";
case FileAccessMode::Create:
return "wb";
}
break;
case FileType::TextFile:
switch (mode) {
case FileAccessMode::Read:
return "r";
case FileAccessMode::Write:
return "w";
case FileAccessMode::Append:
return "a";
case FileAccessMode::Write:
case FileAccessMode::ReadWrite:
return "r+";
case FileAccessMode::ReadAppend:
return "a+";
case FileAccessMode::Create:
return "w";
}
break;
}

View File

@@ -21,9 +21,8 @@ enum class FileAccessMode {
*/
Read = 1 << 0,
/**
* If the file at path exists, the existing contents of the file are erased.
* The empty file is then opened for writing.
* If the file at path does not exist, it creates and opens a new empty file for writing.
* If the file at path exists, it opens the file for writing.
* If the file at path does not exist, it fails to open the file.
*/
Write = 1 << 1,
/**
@@ -42,6 +41,12 @@ enum class FileAccessMode {
* reading and appending.
*/
ReadAppend = Read | Append,
/**
* If the file at path exists, the existing contents of the file are erased.
* The empty file is then opened for writing.
* If the file at path does not exist, it creates and opens a new empty file for writing.
*/
Create = 1 << 3,
};
DECLARE_ENUM_FLAG_OPERATORS(FileAccessMode);
@@ -102,6 +107,11 @@ public:
return file != nullptr;
}
bool IsWriteOnly() const {
return file_access_mode == FileAccessMode::Append ||
file_access_mode == FileAccessMode::Write;
}
uintptr_t GetFileMapping();
int Open(const std::filesystem::path& path, FileAccessMode mode,
@@ -210,7 +220,7 @@ public:
}
static size_t WriteBytes(const std::filesystem::path path, const auto& data) {
IOFile out(path, FileAccessMode::Write);
IOFile out(path, FileAccessMode::Create);
return out.Write(data);
}
std::FILE* file = nullptr;

161
src/common/key_manager.cpp Normal file
View File

@@ -0,0 +1,161 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fstream>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include "common/logging/log.h"
#include "key_manager.h"
#include "path_util.h"
std::shared_ptr<KeyManager> KeyManager::s_instance = nullptr;
std::mutex KeyManager::s_mutex;
// ------------------- Constructor & Singleton -------------------
KeyManager::KeyManager() {
SetDefaultKeys();
}
KeyManager::~KeyManager() {
SaveToFile();
}
std::shared_ptr<KeyManager> KeyManager::GetInstance() {
std::lock_guard<std::mutex> lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<KeyManager>();
return s_instance;
}
void KeyManager::SetInstance(std::shared_ptr<KeyManager> instance) {
std::lock_guard<std::mutex> lock(s_mutex);
s_instance = instance;
}
// ------------------- Load / Save -------------------
bool KeyManager::LoadFromFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
if (!std::filesystem::exists(keysPath)) {
SetDefaultKeys();
SaveToFile();
LOG_DEBUG(KeyManager, "Created default key file: {}", keysPath.string());
return true;
}
std::ifstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file: {}", keysPath.string());
return false;
}
json j;
file >> j;
SetDefaultKeys(); // start from defaults
if (j.contains("TrophyKeySet"))
j.at("TrophyKeySet").get_to(m_keys.TrophyKeySet);
LOG_DEBUG(KeyManager, "Successfully loaded keys from: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error loading keys, using defaults: {}", e.what());
SetDefaultKeys();
return false;
}
}
bool KeyManager::SaveToFile() {
try {
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
const auto keysPath = userDir / "keys.json";
json j;
KeysToJson(j);
std::ofstream file(keysPath);
if (!file.is_open()) {
LOG_ERROR(KeyManager, "Could not open key file for writing: {}", keysPath.string());
return false;
}
file << std::setw(4) << j;
file.flush();
if (file.fail()) {
LOG_ERROR(KeyManager, "Failed to write keys to: {}", keysPath.string());
return false;
}
LOG_DEBUG(KeyManager, "Successfully saved keys to: {}", keysPath.string());
return true;
} catch (const std::exception& e) {
LOG_ERROR(KeyManager, "Error saving keys: {}", e.what());
return false;
}
}
// ------------------- JSON conversion -------------------
void KeyManager::KeysToJson(json& j) const {
j = m_keys;
}
void KeyManager::JsonToKeys(const json& j) {
json current = m_keys; // serialize current defaults
current.update(j); // merge only fields present in file
m_keys = current.get<AllKeys>(); // deserialize back
}
// ------------------- Defaults / Checks -------------------
void KeyManager::SetDefaultKeys() {
m_keys = AllKeys{};
}
bool KeyManager::HasKeys() const {
return !m_keys.TrophyKeySet.ReleaseTrophyKey.empty();
}
// ------------------- Hex conversion -------------------
std::vector<u8> KeyManager::HexStringToBytes(const std::string& hexStr) {
std::vector<u8> bytes;
if (hexStr.empty())
return bytes;
if (hexStr.size() % 2 != 0)
throw std::runtime_error("Invalid hex string length");
bytes.reserve(hexStr.size() / 2);
auto hexCharToInt = [](char c) -> u8 {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
throw std::runtime_error("Invalid hex character");
};
for (size_t i = 0; i < hexStr.size(); i += 2) {
u8 high = hexCharToInt(hexStr[i]);
u8 low = hexCharToInt(hexStr[i + 1]);
bytes.push_back((high << 4) | low);
}
return bytes;
}
std::string KeyManager::BytesToHexString(const std::vector<u8>& bytes) {
static const char hexDigits[] = "0123456789ABCDEF";
std::string hexStr;
hexStr.reserve(bytes.size() * 2);
for (u8 b : bytes) {
hexStr.push_back(hexDigits[(b >> 4) & 0xF]);
hexStr.push_back(hexDigits[b & 0xF]);
}
return hexStr;
}

79
src/common/key_manager.h Normal file
View File

@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "common/types.h"
#include "nlohmann/json.hpp"
using json = nlohmann::json;
class KeyManager {
public:
// ------------------- Nested keysets -------------------
struct TrophyKeySet {
std::vector<u8> ReleaseTrophyKey;
};
struct AllKeys {
KeyManager::TrophyKeySet TrophyKeySet;
};
// ------------------- Construction -------------------
KeyManager();
~KeyManager();
// ------------------- Singleton -------------------
static std::shared_ptr<KeyManager> GetInstance();
static void SetInstance(std::shared_ptr<KeyManager> instance);
// ------------------- File operations -------------------
bool LoadFromFile();
bool SaveToFile();
// ------------------- Key operations -------------------
void SetDefaultKeys();
bool HasKeys() const;
// ------------------- Getters / Setters -------------------
const AllKeys& GetAllKeys() const {
return m_keys;
}
void SetAllKeys(const AllKeys& keys) {
m_keys = keys;
}
static std::vector<u8> HexStringToBytes(const std::string& hexStr);
static std::string BytesToHexString(const std::vector<u8>& bytes);
private:
void KeysToJson(json& j) const;
void JsonToKeys(const json& j);
AllKeys m_keys{};
static std::shared_ptr<KeyManager> s_instance;
static std::mutex s_mutex;
};
// ------------------- NLOHMANN macros -------------------
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::TrophyKeySet, ReleaseTrophyKey)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(KeyManager::AllKeys, TrophyKeySet)
namespace nlohmann {
template <>
struct adl_serializer<std::vector<u8>> {
static void to_json(json& j, const std::vector<u8>& vec) {
j = KeyManager::BytesToHexString(vec);
}
static void from_json(const json& j, std::vector<u8>& vec) {
vec = KeyManager::HexStringToBytes(j.get<std::string>());
}
};
} // namespace nlohmann

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
@@ -62,7 +63,7 @@ private:
class FileBackend {
public:
explicit FileBackend(const std::filesystem::path& filename, bool should_append = false)
: file{filename, should_append ? FS::FileAccessMode::Append : FS::FileAccessMode::Write,
: file{filename, should_append ? FS::FileAccessMode::Append : FS::FileAccessMode::Create,
FS::FileType::TextFile} {}
~FileBackend() = default;
@@ -97,6 +98,7 @@ private:
std::size_t bytes_written = 0;
};
#ifdef _WIN32
/**
* Backend that writes to Visual Studio's output window
*/
@@ -107,15 +109,14 @@ public:
~DebuggerBackend() = default;
void Write(const Entry& entry) {
#ifdef _WIN32
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
#endif
}
void Flush() {}
void EnableForStacktrace() {}
};
#endif
bool initialization_in_progress_suppress_logging = true;
@@ -182,7 +183,13 @@ public:
}
void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num,
const char* function, std::string message) {
const char* function, const char* format, const fmt::format_args& args) {
if (!filter.CheckMessage(log_class, log_level) || !Config::getLoggingEnabled()) {
return;
}
const auto message = fmt::vformat(format, args);
// Propagate important log messages to the profiler
if (IsProfilerConnected()) {
const auto& msg_str = fmt::format("[{}] {}", GetLogClassName(log_class), message);
@@ -201,10 +208,6 @@ public:
}
}
if (!filter.CheckMessage(log_class, log_level) || !Config::getLoggingEnabled()) {
return;
}
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
@@ -217,6 +220,7 @@ public:
.line_num = line_num,
.function = function,
.message = std::move(message),
.thread = Common::GetCurrentThreadName(),
};
if (Config::getLogType() == "async") {
message_queue.EmplaceWait(entry);
@@ -264,7 +268,9 @@ private:
}
void ForEachBackend(auto lambda) {
// lambda(debugger_backend);
#ifdef _WIN32
lambda(debugger_backend);
#endif
lambda(color_console_backend);
lambda(file_backend);
}
@@ -277,7 +283,9 @@ private:
static inline bool should_append{false};
Filter filter;
#ifdef _WIN32
DebuggerBackend debugger_backend{};
#endif
ColorConsoleBackend color_console_backend{};
FileBackend file_backend;
@@ -324,8 +332,8 @@ void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
unsigned int line_num, const char* function, const char* format,
const fmt::format_args& args) {
if (!initialization_in_progress_suppress_logging) [[likely]] {
Impl::Instance().PushEntry(log_class, log_level, filename, line_num, function,
fmt::vformat(format, args));
Impl::Instance().PushEntry(log_class, log_level, filename, line_num, function, format,
args);
}
}
} // namespace Common::Log

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@@ -104,16 +105,22 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Lib, Move) \
SUB(Lib, NpAuth) \
SUB(Lib, NpCommon) \
SUB(Lib, NpCommerce) \
SUB(Lib, NpManager) \
SUB(Lib, NpMatching2) \
SUB(Lib, NpScore) \
SUB(Lib, NpTrophy) \
SUB(Lib, NpTus) \
SUB(Lib, NpWebApi) \
SUB(Lib, NpWebApi2) \
SUB(Lib, NpProfileDialog) \
SUB(Lib, NpSnsFacebookDialog) \
SUB(Lib, NpPartner) \
SUB(Lib, Screenshot) \
SUB(Lib, LibCInternal) \
SUB(Lib, AppContent) \
SUB(Lib, Rtc) \
SUB(Lib, Rudp) \
SUB(Lib, DiscMap) \
SUB(Lib, Png) \
SUB(Lib, Jpeg) \
@@ -140,6 +147,8 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Lib, NpParty) \
SUB(Lib, Zlib) \
SUB(Lib, Hmd) \
SUB(Lib, Font) \
SUB(Lib, FontFt) \
SUB(Lib, HmdSetupDialog) \
SUB(Lib, SigninDialog) \
SUB(Lib, Camera) \
@@ -154,6 +163,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
CLS(ImGui) \
CLS(Input) \
CLS(Tty) \
CLS(KeyManager) \
CLS(Loader)
// GetClassName is a macro defined by Windows.h, grrr...

View File

@@ -21,6 +21,7 @@ struct Entry {
u32 line_num = 0;
std::string function;
std::string message;
std::string thread;
};
} // namespace Common::Log

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
@@ -23,8 +24,8 @@ std::string FormatLogMessage(const Entry& entry) {
const char* class_name = GetLogClassName(entry.log_class);
const char* level_name = GetLevelName(entry.log_level);
return fmt::format("[{}] <{}> {}:{} {}: {}", class_name, level_name, entry.filename,
entry.line_num, entry.function, entry.message);
return fmt::format("[{}] <{}> ({}) {}:{} {}: {}", class_name, level_name, entry.thread,
entry.filename, entry.line_num, entry.function, entry.message);
}
void PrintMessage(const Entry& entry) {

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -70,17 +71,22 @@ enum class Class : u8 {
Lib_Http2, ///< The LibSceHttp2 implementation.
Lib_SysModule, ///< The LibSceSysModule implementation
Lib_NpCommon, ///< The LibSceNpCommon implementation
Lib_NpCommerce, ///< The LibSceNpCommerce implementation
Lib_NpAuth, ///< The LibSceNpAuth implementation
Lib_NpManager, ///< The LibSceNpManager implementation
Lib_NpMatching2, ///< The LibSceNpMatching2 implementation
Lib_NpScore, ///< The LibSceNpScore implementation
Lib_NpTrophy, ///< The LibSceNpTrophy implementation
Lib_NpTus, ///< The LibSceNpTus implementation
Lib_NpWebApi, ///< The LibSceWebApi implementation
Lib_NpWebApi2, ///< The LibSceWebApi2 implementation
Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation
Lib_NpSnsFacebookDialog, ///< The LibSceNpSnsFacebookDialog implementation
Lib_Screenshot, ///< The LibSceScreenshot implementation
Lib_LibCInternal, ///< The LibCInternal implementation.
Lib_AppContent, ///< The LibSceAppContent implementation.
Lib_Rtc, ///< The LibSceRtc implementation.
Lib_Rudp, ///< The LibSceRudp implementation.
Lib_DiscMap, ///< The LibSceDiscMap implementation.
Lib_Png, ///< The LibScePng implementation.
Lib_Jpeg, ///< The LibSceJpeg implementation.
@@ -106,6 +112,7 @@ enum class Class : u8 {
Lib_Mouse, ///< The LibSceMouse implementation
Lib_WebBrowserDialog, ///< The LibSceWebBrowserDialog implementation
Lib_NpParty, ///< The LibSceNpParty implementation
Lib_NpPartner, ///< The LibSceNpPartner implementation
Lib_Zlib, ///< The LibSceZlib implementation.
Lib_Hmd, ///< The LibSceHmd implementation.
Lib_HmdSetupDialog, ///< The LibSceHmdSetupDialog implementation.
@@ -114,6 +121,8 @@ enum class Class : u8 {
Lib_CompanionHttpd, ///< The LibCompanionHttpd implementation.
Lib_CompanionUtil, ///< The LibCompanionUtil implementation.
Lib_VrTracker, ///< The LibSceVrTracker implementation.
Lib_Font, ///< The libSceFont implementation.
Lib_FontFt, ///< The libSceFontFt implementation.
Frontend, ///< Emulator UI
Render, ///< Video Core
Render_Vulkan, ///< Vulkan backend
@@ -122,6 +131,7 @@ enum class Class : u8 {
Loader, ///< ROM loader
Input, ///< Input emulation
Tty, ///< Debug output from emu
KeyManager, ///< Key management system
Count ///< Total number of logging classes
};

View File

@@ -1,25 +1,18 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <codecvt>
#include <fstream>
#include <sstream>
#include <string>
#include <nlohmann/json.hpp>
#include <pugixml.hpp>
#ifdef ENABLE_QT_GUI
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QListView>
#include <QMessageBox>
#include <QString>
#include <QXmlStreamReader>
#endif
#include "common/config.h"
#include "common/elf_info.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/emulator_state.h"
#include "core/file_format/psf.h"
#include "memory_patcher.h"
@@ -28,7 +21,7 @@ namespace MemoryPatcher {
EXPORT uintptr_t g_eboot_address;
uint64_t g_eboot_image_size;
std::string g_game_serial;
std::string patchFile;
std::string patch_file;
bool patches_applied = false;
std::vector<patchInfo> pending_patches;
@@ -59,14 +52,14 @@ std::string convertValueToHex(const std::string type, const std::string valueStr
uint32_t i;
} floatUnion;
floatUnion.f = std::stof(valueStr);
result = toHex(floatUnion.i, sizeof(floatUnion.i));
result = toHex(std::byteswap(floatUnion.i), sizeof(floatUnion.i));
} else if (type == "float64") {
union {
double d;
uint64_t i;
} doubleUnion;
doubleUnion.d = std::stod(valueStr);
result = toHex(doubleUnion.i, sizeof(doubleUnion.i));
result = toHex(std::byteswap(doubleUnion.i), sizeof(doubleUnion.i));
} else if (type == "utf8") {
std::vector<unsigned char> byteArray =
std::vector<unsigned char>(valueStr.begin(), valueStr.end());
@@ -122,256 +115,104 @@ std::string convertValueToHex(const std::string type, const std::string valueStr
void ApplyPendingPatches();
void OnGameLoaded() {
if (!patchFile.empty()) {
std::filesystem::path patchDir = Common::FS::GetUserPath(Common::FS::PathType::PatchesDir);
void ApplyPatchesFromXML(std::filesystem::path path) {
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(path.c_str());
auto filePath = (patchDir / patchFile).native();
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(filePath.c_str());
if (result) {
auto patchXML = doc.child("Patch");
for (pugi::xml_node_iterator it = patchXML.children().begin();
it != patchXML.children().end(); ++it) {
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
if (std::string(it->name()) == "Metadata") {
if (std::string(it->attribute("isEnabled").value()) == "true") {
std::string currentPatchName = it->attribute("Name").value();
std::string metadataAppVer = it->attribute("AppVer").value();
bool versionMatches = metadataAppVer == app_version;
if (result) {
auto patchXML = doc.child("Patch");
for (pugi::xml_node_iterator it = patchXML.children().begin();
it != patchXML.children().end(); ++it) {
auto patchList = it->first_child();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
if (std::string(it->name()) == "Metadata") {
if (std::string(it->attribute("isEnabled").value()) == "true") {
std::string currentPatchName = it->attribute("Name").value();
std::string metadataAppVer = it->attribute("AppVer").value();
bool versionMatches = metadataAppVer == app_version;
std::string type = patchLineIt->attribute("Type").value();
if (!versionMatches && type != "mask" && type != "mask_jump32")
continue;
auto patchList = it->first_child();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
std::string type = patchLineIt->attribute("Type").value();
if (!versionMatches && type != "mask" && type != "mask_jump32")
continue;
std::string currentPatchName = it->attribute("Name").value();
for (pugi::xml_node_iterator patchLineIt = patchList.children().begin();
patchLineIt != patchList.children().end(); ++patchLineIt) {
std::string type = patchLineIt->attribute("Type").value();
std::string address = patchLineIt->attribute("Address").value();
std::string patchValue = patchLineIt->attribute("Value").value();
std::string maskOffsetStr =
patchLineIt->attribute("Offset").value();
std::string targetStr = "";
std::string sizeStr = "";
if (type == "mask_jump32") {
targetStr = patchLineIt->attribute("Target").value();
sizeStr = patchLineIt->attribute("Size").value();
} else {
patchValue = convertValueToHex(type, patchValue);
}
bool littleEndian = false;
if (type == "bytes16" || type == "bytes32" || type == "bytes64") {
littleEndian = true;
}
MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
int maskOffsetValue = 0;
if (type == "mask")
patchMask = MemoryPatcher::PatchMask::Mask;
if (type == "mask_jump32")
patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
if ((type == "mask" || type == "mask_jump32") &&
!maskOffsetStr.empty()) {
maskOffsetValue = std::stoi(maskOffsetStr, 0, 10);
}
MemoryPatcher::PatchMemory(currentPatchName, address, patchValue,
targetStr, sizeStr, false, littleEndian,
patchMask, maskOffsetValue);
}
std::string address = patchLineIt->attribute("Address").value();
std::string patchValue = patchLineIt->attribute("Value").value();
std::string maskOffsetStr = patchLineIt->attribute("Offset").value();
std::string targetStr = "";
std::string sizeStr = "";
if (type == "mask_jump32") {
targetStr = patchLineIt->attribute("Target").value();
sizeStr = patchLineIt->attribute("Size").value();
} else {
patchValue = convertValueToHex(type, patchValue);
}
bool littleEndian = false;
if (type == "bytes16" || type == "bytes32" || type == "bytes64") {
littleEndian = true;
}
MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
int maskOffsetValue = 0;
if (type == "mask")
patchMask = MemoryPatcher::PatchMask::Mask;
if (type == "mask_jump32")
patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
if ((type == "mask" || type == "mask_jump32") && !maskOffsetStr.empty()) {
maskOffsetValue = std::stoi(maskOffsetStr, 0, 10);
}
MemoryPatcher::PatchMemory(currentPatchName, address, patchValue, targetStr,
sizeStr, false, littleEndian, patchMask,
maskOffsetValue);
}
}
}
}
} else {
LOG_ERROR(Loader, "Could not parse patch XML: {}", result.description());
}
}
void OnGameLoaded() {
std::filesystem::path patch_dir = Common::FS::GetUserPath(Common::FS::PathType::PatchesDir);
if (!patch_file.empty()) {
auto file_path = (patch_dir / patch_file).native();
if (std::filesystem::exists(patch_file)) {
ApplyPatchesFromXML(patch_file);
} else {
LOG_ERROR(Loader, "couldnt patch parse xml : {}", result.description());
ApplyPatchesFromXML(file_path);
}
} else if (EmulatorState::GetInstance()->IsAutoPatchesLoadEnabled()) {
for (auto const& repo : std::filesystem::directory_iterator(patch_dir)) {
if (!repo.is_directory()) {
continue;
}
std::ifstream json_file{repo.path() / "files.json"};
nlohmann::json available_patches = nlohmann::json::parse(json_file);
std::filesystem::path game_patch_file;
for (auto const& [filename, serials] : available_patches.items()) {
if (std::find(serials.begin(), serials.end(), g_game_serial) != serials.end()) {
game_patch_file = repo.path() / filename;
break;
}
}
if (std::filesystem::exists(game_patch_file)) {
ApplyPatchesFromXML(game_patch_file);
}
}
}
ApplyPendingPatches();
#ifdef ENABLE_QT_GUI
// We use the QT headers for the xml and json parsing, this define is only true on QT builds
QString patchDir;
Common::FS::PathToQString(patchDir, Common::FS::GetUserPath(Common::FS::PathType::PatchesDir));
QDir dir(patchDir);
QStringList folders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString& folder : folders) {
QString filesJsonPath = patchDir + "/" + folder + "/files.json";
QFile jsonFile(filesJsonPath);
if (!jsonFile.open(QIODevice::ReadOnly)) {
LOG_ERROR(Loader, "Unable to open files.json for reading in repository {}",
folder.toStdString());
continue;
}
QByteArray jsonData = jsonFile.readAll();
jsonFile.close();
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
QJsonObject jsonObject = jsonDoc.object();
QString selectedFileName;
QString serial = QString::fromStdString(g_game_serial);
for (auto it = jsonObject.constBegin(); it != jsonObject.constEnd(); ++it) {
QString filePath = it.key();
QJsonArray idsArray = it.value().toArray();
if (idsArray.contains(QJsonValue(serial))) {
selectedFileName = filePath;
break;
}
}
if (selectedFileName.isEmpty()) {
LOG_ERROR(Loader, "No patch file found for the current serial in repository {}",
folder.toStdString());
continue;
}
QString filePath = patchDir + "/" + folder + "/" + selectedFileName;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
LOG_ERROR(Loader, "Unable to open the file for reading.");
continue;
}
QByteArray xmlData = file.readAll();
file.close();
QString newXmlData;
QXmlStreamReader xmlReader(xmlData);
bool isEnabled = false;
std::string currentPatchName;
auto* param_sfo = Common::Singleton<PSF>::Instance();
auto app_version = param_sfo->GetString("APP_VER").value_or("Unknown version");
bool versionMatches = true;
while (!xmlReader.atEnd()) {
xmlReader.readNext();
if (xmlReader.isStartElement()) {
QJsonArray patchLines;
if (xmlReader.name() == QStringLiteral("Metadata")) {
QString name = xmlReader.attributes().value("Name").toString();
currentPatchName = name.toStdString();
QString appVer = xmlReader.attributes().value("AppVer").toString();
// Check and update the isEnabled attribute
isEnabled = false;
for (const QXmlStreamAttribute& attr : xmlReader.attributes()) {
if (attr.name() == QStringLiteral("isEnabled")) {
isEnabled = (attr.value().toString() == "true");
}
}
versionMatches = (appVer.toStdString() == app_version);
} else if (xmlReader.name() == QStringLiteral("PatchList")) {
QJsonArray linesArray;
while (!xmlReader.atEnd() &&
!(xmlReader.tokenType() == QXmlStreamReader::EndElement &&
xmlReader.name() == QStringLiteral("PatchList"))) {
xmlReader.readNext();
if (xmlReader.tokenType() == QXmlStreamReader::StartElement &&
xmlReader.name() == QStringLiteral("Line")) {
QXmlStreamAttributes attributes = xmlReader.attributes();
QJsonObject lineObject;
lineObject["Type"] = attributes.value("Type").toString();
lineObject["Address"] = attributes.value("Address").toString();
lineObject["Value"] = attributes.value("Value").toString();
lineObject["Offset"] = attributes.value("Offset").toString();
if (lineObject["Type"].toString() == "mask_jump32") {
lineObject["Target"] = attributes.value("Target").toString();
lineObject["Size"] = attributes.value("Size").toString();
}
linesArray.append(lineObject);
}
}
patchLines = linesArray;
if (isEnabled) {
foreach (const QJsonValue& value, patchLines) {
QJsonObject lineObject = value.toObject();
QString type = lineObject["Type"].toString();
if ((type != "mask" && type != "mask_jump32") && !versionMatches)
continue;
QString address = lineObject["Address"].toString();
QString patchValue = lineObject["Value"].toString();
QString maskOffsetStr = lineObject["Offset"].toString();
QString targetStr;
QString sizeStr;
if (type == "mask_jump32") {
targetStr = lineObject["Target"].toString();
sizeStr = lineObject["Size"].toString();
} else {
patchValue = QString::fromStdString(convertValueToHex(
type.toStdString(), patchValue.toStdString()));
}
bool littleEndian = false;
if (type == "bytes16" || type == "bytes32" || type == "bytes64")
littleEndian = true;
MemoryPatcher::PatchMask patchMask = MemoryPatcher::PatchMask::None;
int maskOffsetValue = 0;
if (type == "mask")
patchMask = MemoryPatcher::PatchMask::Mask;
if (type == "mask_jump32")
patchMask = MemoryPatcher::PatchMask::Mask_Jump32;
if ((type == "mask" || type == "mask_jump32") &&
!maskOffsetStr.toStdString().empty()) {
maskOffsetValue = std::stoi(maskOffsetStr.toStdString(), 0, 10);
}
MemoryPatcher::PatchMemory(
currentPatchName, address.toStdString(), patchValue.toStdString(),
targetStr.toStdString(), sizeStr.toStdString(), false, littleEndian,
patchMask, maskOffsetValue);
}
}
}
}
}
if (xmlReader.hasError()) {
LOG_ERROR(Loader, "Failed to parse XML for {}", g_game_serial);
} else {
LOG_INFO(Loader, "Patches loaded successfully, repository: {}", folder.toStdString());
}
ApplyPendingPatches();
}
#endif
}
void AddPatchToQueue(patchInfo patchToAdd) {

View File

@@ -17,7 +17,7 @@ namespace MemoryPatcher {
extern EXPORT uintptr_t g_eboot_address;
extern uint64_t g_eboot_image_size;
extern std::string g_game_serial;
extern std::string patchFile;
extern std::string patch_file;
enum PatchMask : uint8_t {
None,

View File

@@ -25,10 +25,6 @@
#endif
#endif
#ifdef ENABLE_QT_GUI
#include <QString>
#endif
namespace Common::FS {
namespace fs = std::filesystem;
@@ -88,13 +84,6 @@ static std::optional<std::filesystem::path> GetBundleParentDirectory() {
#endif
static auto UserPaths = [] {
#if defined(__APPLE__) && defined(ENABLE_QT_GUI)
// Set the current path to the directory containing the app bundle.
if (const auto bundle_dir = GetBundleParentDirectory()) {
std::filesystem::current_path(*bundle_dir);
}
#endif
// Try the portable user directory first.
auto user_dir = std::filesystem::current_path() / PORTABLE_DIR;
if (!std::filesystem::exists(user_dir)) {
@@ -138,6 +127,7 @@ static auto UserPaths = [] {
create_path(PathType::MetaDataDir, user_dir / METADATA_DIR);
create_path(PathType::CustomTrophy, user_dir / CUSTOM_TROPHY);
create_path(PathType::CustomConfigs, user_dir / CUSTOM_CONFIGS);
create_path(PathType::CacheDir, user_dir / CACHE_DIR);
std::ofstream notice_file(user_dir / CUSTOM_TROPHY / "Notice.txt");
if (notice_file.is_open()) {
@@ -229,22 +219,4 @@ std::optional<fs::path> FindGameByID(const fs::path& dir, const std::string& gam
return std::nullopt;
}
#ifdef ENABLE_QT_GUI
void PathToQString(QString& result, const std::filesystem::path& path) {
#ifdef _WIN32
result = QString::fromStdWString(path.wstring());
#else
result = QString::fromStdString(path.string());
#endif
}
std::filesystem::path PathFromQString(const QString& path) {
#ifdef _WIN32
return std::filesystem::path(path.toStdWString());
#else
return std::filesystem::path(path.toStdString());
#endif
}
#endif
} // namespace Common::FS

View File

@@ -7,10 +7,6 @@
#include <optional>
#include <vector>
#ifdef ENABLE_QT_GUI
class QString; // to avoid including <QString> in this header
#endif
namespace Common::FS {
enum class PathType {
@@ -28,6 +24,7 @@ enum class PathType {
MetaDataDir, // Where game metadata (e.g. trophies and menu backgrounds) is stored.
CustomTrophy, // Where custom files for trophies are stored.
CustomConfigs, // Where custom files for different games are stored.
CacheDir, // Where pipeline and shader cache is stored.
};
constexpr auto PORTABLE_DIR = "user";
@@ -46,6 +43,7 @@ constexpr auto PATCHES_DIR = "patches";
constexpr auto METADATA_DIR = "game_data";
constexpr auto CUSTOM_TROPHY = "custom_trophy";
constexpr auto CUSTOM_CONFIGS = "custom_configs";
constexpr auto CACHE_DIR = "cache";
// Filenames
constexpr auto LOG_FILE = "shad_log.txt";
@@ -99,25 +97,6 @@ constexpr auto LOG_FILE = "shad_log.txt";
*/
void SetUserPath(PathType user_path, const std::filesystem::path& new_path);
#ifdef ENABLE_QT_GUI
/**
* Converts an std::filesystem::path to a QString.
* The native underlying string of a path is wstring on Windows and string on POSIX.
*
* @param result The resulting QString
* @param path The path to convert
*/
void PathToQString(QString& result, const std::filesystem::path& path);
/**
* Converts a QString to an std::filesystem::path.
* The native underlying string of a path is wstring on Windows and string on POSIX.
*
* @param path The path to convert
*/
[[nodiscard]] std::filesystem::path PathFromQString(const QString& path);
#endif
/**
* Recursively searches for a game directory by its ID.
* Limits search depth to prevent excessive filesystem traversal.

140
src/common/serdes.h Normal file
View File

@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/assert.h"
#include "common/types.h"
#include <cstddef>
namespace Serialization {
template <typename T>
concept Container = requires(T t) {
typename T::iterator;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
{ t.size() } -> std::convertible_to<std::size_t>;
};
struct Archive {
void Alloc(size_t size) {
container.resize(size);
}
void Grow(size_t size) {
container.resize(container.size() + size);
}
void Merge(const Archive& ar) {
container.insert(container.end(), ar.container.cbegin(), ar.container.cend());
offset = container.size();
}
[[nodiscard]] size_t SizeBytes() const {
return container.size();
}
u8* CurrPtr() {
return container.data() + offset;
}
void Advance(size_t size) {
ASSERT(offset + size <= container.size());
offset += size;
}
std::vector<u8>&& TakeOff() {
offset = 0;
return std::move(container);
}
[[nodiscard]] bool IsEoS() const {
return offset >= container.size();
}
Archive() = default;
explicit Archive(std::vector<u8>&& v) : container{v} {}
private:
u32 offset{};
std::vector<u8> container{};
friend struct Writer;
friend struct Reader;
};
struct Writer {
template <typename T>
void Write(const T* ptr, size_t size) {
if (ar.offset + size >= ar.container.size()) {
ar.Grow(size);
}
std::memcpy(ar.CurrPtr(), reinterpret_cast<const void*>(ptr), size);
ar.Advance(size);
}
template <typename T>
requires(!Container<T>)
void Write(const T& value) {
const auto size = sizeof(value);
Write(&value, size);
}
void Write(const auto& v) {
Write(v.size());
for (const auto& elem : v) {
Write(elem);
}
}
void Write(const std::string& s) {
Write(s.size());
Write(s.c_str(), s.size());
}
Writer() = delete;
explicit Writer(Archive& ar_) : ar{ar_} {}
Archive& ar;
};
struct Reader {
template <typename T>
void Read(T* ptr, size_t size) {
ASSERT(ar.offset + size <= ar.container.size());
std::memcpy(reinterpret_cast<void*>(ptr), ar.CurrPtr(), size);
ar.Advance(size);
}
template <typename T>
requires(!Container<T>)
void Read(T& value) {
const auto size = sizeof(value);
Read(&value, size);
}
void Read(auto& v) {
size_t num_elements{};
Read(num_elements);
for (int i = 0; i < num_elements; ++i) {
v.emplace_back();
Read(v.back());
}
}
void Read(std::string& s) {
size_t length{};
Read(length);
s.resize(length);
Read(s.data(), length);
}
Reader() = delete;
explicit Reader(Archive& ar_) : ar{ar_} {}
Archive& ar;
};
} // namespace Serialization

View File

@@ -17,6 +17,15 @@ public:
writer_active = true;
}
bool try_lock() {
std::lock_guard<std::mutex> lock(mtx);
if (writer_active || readers > 0) {
return false;
}
writer_active = true;
return true;
}
void unlock() {
std::lock_guard<std::mutex> lock(mtx);
writer_active = false;

View File

@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <ctime>
#include <string>
#include <thread>
#include "core/libraries/kernel/threads/pthread.h"
#include "common/error.h"
#include "common/logging/log.h"
#include "common/thread.h"
@@ -171,6 +174,9 @@ bool AccurateSleep(const std::chrono::nanoseconds duration, std::chrono::nanosec
// Sets the debugger-visible name of the current thread.
void SetCurrentThreadName(const char* name) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = std::string{name};
}
SetThreadDescription(GetCurrentThread(), UTF8ToUTF16W(name).data());
}
@@ -183,6 +189,9 @@ void SetThreadName(void* thread, const char* name) {
// MinGW with the POSIX threading model does not support pthread_setname_np
#if !defined(_WIN32) || defined(_MSC_VER)
void SetCurrentThreadName(const char* name) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = std::string{name};
}
#ifdef __APPLE__
pthread_setname_np(name);
#elif defined(__Bitrig__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__)
@@ -209,6 +218,9 @@ void SetThreadName(void* thread, const char* name) {
#if defined(_WIN32)
void SetCurrentThreadName(const char*) {
if (Libraries::Kernel::g_curthread) {
Libraries::Kernel::g_curthread->name = std::string{name};
}
// Do Nothing on MinGW
}
@@ -237,4 +249,22 @@ void AccurateTimer::End() {
target_interval - std::chrono::duration_cast<std::chrono::nanoseconds>(now - start_time);
}
std::string GetCurrentThreadName() {
using namespace Libraries::Kernel;
if (g_curthread && !g_curthread->name.empty()) {
return g_curthread->name;
}
#ifdef _WIN32
PWSTR name;
GetThreadDescription(GetCurrentThread(), &name);
return Common::UTF16ToUTF8(name);
#else
char name[256];
if (pthread_getname_np(pthread_self(), name, sizeof(name)) != 0) {
return "<unknown name>";
}
return std::string{name};
#endif
}
} // namespace Common

View File

@@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -46,4 +47,6 @@ public:
}
};
std::string GetCurrentThreadName();
} // namespace Common

View File

@@ -6,6 +6,7 @@
#include "common/arch.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "common/error.h"
#include "core/address_space.h"
#include "core/libraries/kernel/memory.h"
@@ -92,7 +93,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;
};
@@ -103,8 +107,8 @@ struct AddressSpace::Impl {
GetSystemInfo(&sys_info);
u64 alignment = sys_info.dwAllocationGranularity;
// Determine the host OS build number
// Retrieve module handle for ntdll
// Older Windows builds have a severe performance issue with VirtualAlloc2.
// We need to get the host's Windows version, then determine if it needs a workaround.
auto ntdll_handle = GetModuleHandleW(L"ntdll.dll");
ASSERT_MSG(ntdll_handle, "Failed to retrieve ntdll handle");
@@ -120,12 +124,20 @@ struct AddressSpace::Impl {
u64 supported_user_max = USER_MAX;
// This is the build number for Windows 11 22H2
static constexpr s32 AffectedBuildNumber = 22621;
if (os_version_info.dwBuildNumber <= AffectedBuildNumber) {
// Older Windows builds have an issue with VirtualAlloc2 on higher addresses.
// To prevent regressions, limit the maximum address we reserve for this platform.
supported_user_max = 0x11000000000ULL;
LOG_WARNING(Core, "Windows 10 detected, reducing user max to {:#x} to avoid problems",
supported_user_max);
// Higher PS4 firmware versions prevent higher address mappings too.
s32 sdk_ver = Common::ElfInfo::Instance().CompiledSdkVer();
if (os_version_info.dwBuildNumber <= AffectedBuildNumber ||
sdk_ver >= Common::ElfInfo::FW_30) {
supported_user_max = 0x10000000000ULL;
// Only log the message if we're restricting the user max due to operating system.
// Since higher compiled SDK versions also get reduced max, we don't need to log there.
if (sdk_ver < Common::ElfInfo::FW_30) {
LOG_WARNING(
Core,
"Older Windows version detected, reducing user max to {:#x} to avoid problems",
supported_user_max);
}
}
// Determine the free address ranges we can access.
@@ -150,7 +162,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});
}
}
@@ -198,46 +211,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 =
@@ -248,135 +267,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) {
@@ -406,7 +510,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);
@@ -415,13 +519,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(
@@ -444,11 +549,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
@@ -592,7 +697,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;
@@ -604,10 +709,10 @@ struct AddressSpace::Impl {
return ret;
}
void Unmap(VAddr virtual_addr, size_t size, bool) {
void Unmap(VAddr virtual_addr, u64 size) {
// 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.
@@ -625,7 +730,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;
@@ -645,11 +750,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
@@ -666,8 +771,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
@@ -678,8 +782,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);
@@ -689,31 +792,11 @@ 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) {
#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);
}
#else
impl->Unmap(virtual_addr + start_in_vma, end_in_vma - start_in_vma, has_backing);
#endif
void AddressSpace::Unmap(VAddr virtual_addr, u64 size) {
impl->Unmap(virtual_addr, size);
}
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,16 @@ 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);
void Protect(VAddr virtual_addr, size_t size, MemoryPermission perms);
/// Protects requested region.
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 +92,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

@@ -5,6 +5,7 @@
#include <memory>
#include <mutex>
#include <set>
#include <vector>
#include <Zydis/Zydis.h>
#include <xbyak/xbyak.h>
#include <xbyak/xbyak_util.h>
@@ -122,6 +123,30 @@ static void GenerateTcbAccess(void* /* address */, const ZydisDecodedOperand* op
#endif
}
static bool FilterStackCheck(const ZydisDecodedOperand* operands) {
const auto& dst_op = operands[0];
const auto& src_op = operands[1];
// Some compilers emit stack checks by starting a function with
// 'mov (64-bit register), fs:[0x28]', then checking with `xor (64-bit register), fs:[0x28]`
return src_op.type == ZYDIS_OPERAND_TYPE_MEMORY && src_op.mem.segment == ZYDIS_REGISTER_FS &&
src_op.mem.base == ZYDIS_REGISTER_NONE && src_op.mem.index == ZYDIS_REGISTER_NONE &&
src_op.mem.disp.value == 0x28 && dst_op.reg.value >= ZYDIS_REGISTER_RAX &&
dst_op.reg.value <= ZYDIS_REGISTER_R15;
}
static void GenerateStackCheck(void* /* address */, const ZydisDecodedOperand* operands,
Xbyak::CodeGenerator& c) {
const auto dst = ZydisToXbyakRegisterOperand(operands[0]);
c.xor_(dst, 0);
}
static void GenerateStackCanary(void* /* address */, const ZydisDecodedOperand* operands,
Xbyak::CodeGenerator& c) {
const auto dst = ZydisToXbyakRegisterOperand(operands[0]);
c.mov(dst, 0);
}
static bool FilterNoSSE4a(const ZydisDecodedOperand*) {
Cpu cpu;
return !cpu.has(Cpu::tSSE4a);
@@ -440,18 +465,26 @@ struct PatchInfo {
bool trampoline;
};
static const std::unordered_map<ZydisMnemonic, PatchInfo> Patches = {
static const std::unordered_map<ZydisMnemonic, std::vector<PatchInfo>> Patches = {
// SSE4a
{ZYDIS_MNEMONIC_EXTRQ, {FilterNoSSE4a, GenerateEXTRQ, true}},
{ZYDIS_MNEMONIC_INSERTQ, {FilterNoSSE4a, GenerateINSERTQ, true}},
{ZYDIS_MNEMONIC_MOVNTSS, {FilterNoSSE4a, ReplaceMOVNTSS, false}},
{ZYDIS_MNEMONIC_MOVNTSD, {FilterNoSSE4a, ReplaceMOVNTSD, false}},
{ZYDIS_MNEMONIC_EXTRQ, {{FilterNoSSE4a, GenerateEXTRQ, true}}},
{ZYDIS_MNEMONIC_INSERTQ, {{FilterNoSSE4a, GenerateINSERTQ, true}}},
{ZYDIS_MNEMONIC_MOVNTSS, {{FilterNoSSE4a, ReplaceMOVNTSS, false}}},
{ZYDIS_MNEMONIC_MOVNTSD, {{FilterNoSSE4a, ReplaceMOVNTSD, false}}},
#if !defined(__APPLE__)
// FS segment patches
// These first two patches are for accesses to the stack canary, fs:[0x28]
{ZYDIS_MNEMONIC_XOR, {{FilterStackCheck, GenerateStackCheck, false}}},
{ZYDIS_MNEMONIC_MOV,
{{FilterStackCheck, GenerateStackCanary, false},
#if defined(_WIN32)
// Windows needs a trampoline.
{ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, true}},
#elif !defined(__APPLE__)
{ZYDIS_MNEMONIC_MOV, {FilterTcbAccess, GenerateTcbAccess, false}},
// Windows needs a trampoline for Tcb accesses.
{FilterTcbAccess, GenerateTcbAccess, true}
#else
{FilterTcbAccess, GenerateTcbAccess, false}
#endif
}},
#endif
};
@@ -503,51 +536,53 @@ static std::pair<bool, u64> TryPatch(u8* code, PatchModule* module) {
}
if (Patches.contains(instruction.mnemonic)) {
const auto& patch_info = Patches.at(instruction.mnemonic);
bool needs_trampoline = patch_info.trampoline;
if (patch_info.filter(operands)) {
auto& patch_gen = module->patch_gen;
const auto& patches = Patches.at(instruction.mnemonic);
for (const auto& patch_info : patches) {
bool needs_trampoline = patch_info.trampoline;
if (patch_info.filter(operands)) {
auto& patch_gen = module->patch_gen;
if (needs_trampoline && instruction.length < 5) {
// Trampoline is needed but instruction is too short to patch.
// Return false and length to signal to AOT compilation that this instruction
// should be skipped and handled at runtime.
return std::make_pair(false, instruction.length);
}
if (needs_trampoline && instruction.length < 5) {
// Trampoline is needed but instruction is too short to patch.
// Return false and length to signal to AOT compilation that this instruction
// should be skipped and handled at runtime.
return std::make_pair(false, instruction.length);
}
// Reset state and move to current code position.
patch_gen.reset();
patch_gen.setSize(code - patch_gen.getCode());
// Reset state and move to current code position.
patch_gen.reset();
patch_gen.setSize(code - patch_gen.getCode());
if (needs_trampoline) {
auto& trampoline_gen = module->trampoline_gen;
const auto trampoline_ptr = trampoline_gen.getCurr();
if (needs_trampoline) {
auto& trampoline_gen = module->trampoline_gen;
const auto trampoline_ptr = trampoline_gen.getCurr();
patch_info.generator(code, operands, trampoline_gen);
patch_info.generator(code, operands, trampoline_gen);
// Return to the following instruction at the end of the trampoline.
trampoline_gen.jmp(code + instruction.length);
// Return to the following instruction at the end of the trampoline.
trampoline_gen.jmp(code + instruction.length);
// Replace instruction with near jump to the trampoline.
patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR);
} else {
patch_info.generator(code, operands, patch_gen);
}
// Replace instruction with near jump to the trampoline.
patch_gen.jmp(trampoline_ptr, Xbyak::CodeGenerator::LabelType::T_NEAR);
} else {
patch_info.generator(code, operands, patch_gen);
}
const auto patch_size = patch_gen.getCurr() - code;
if (patch_size > 0) {
ASSERT_MSG(instruction.length >= patch_size,
"Instruction {} with length {} is too short to replace at: {}",
ZydisMnemonicGetString(instruction.mnemonic), instruction.length,
fmt::ptr(code));
const auto patch_size = patch_gen.getCurr() - code;
if (patch_size > 0) {
ASSERT_MSG(instruction.length >= patch_size,
"Instruction {} with length {} is too short to replace at: {}",
ZydisMnemonicGetString(instruction.mnemonic), instruction.length,
fmt::ptr(code));
// Fill remaining space with nops.
patch_gen.nop(instruction.length - patch_size);
// Fill remaining space with nops.
patch_gen.nop(instruction.length - patch_size);
module->patched.insert(code);
LOG_DEBUG(Core, "Patched instruction '{}' at: {}",
ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code));
return std::make_pair(true, instruction.length);
module->patched.insert(code);
LOG_DEBUG(Core, "Patched instruction '{}' at: {}",
ZydisMnemonicGetString(instruction.mnemonic), fmt::ptr(code));
return std::make_pair(true, instruction.length);
}
}
}
}
@@ -755,11 +790,12 @@ static bool PatchesIllegalInstructionHandler(void* context) {
Common::Decoder::Instance()->decodeInstruction(instruction, operands, code_address);
if (ZYAN_SUCCESS(status) && instruction.mnemonic == ZydisMnemonic::ZYDIS_MNEMONIC_UD2)
[[unlikely]] {
UNREACHABLE_MSG("ud2 at code address {:#x}", (u64)code_address);
UNREACHABLE_MSG("ud2 at code address {:#x}", reinterpret_cast<u64>(code_address));
}
LOG_ERROR(Core, "Failed to patch address {:x} -- mnemonic: {}", (u64)code_address,
ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic)
: "Failed to decode");
UNREACHABLE_MSG("Failed to patch address {:x} -- mnemonic: {}",
reinterpret_cast<u64>(code_address),
ZYAN_SUCCESS(status) ? ZydisMnemonicGetString(instruction.mnemonic)
: "Failed to decode");
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "layer.h"
@@ -11,6 +11,7 @@
#include "common/singleton.h"
#include "common/types.h"
#include "core/debug_state.h"
#include "core/emulator_state.h"
#include "imgui/imgui_std.h"
#include "imgui_internal.h"
#include "options.h"
@@ -273,14 +274,10 @@ void L::DrawAdvanced() {
void L::DrawSimple() {
const float frameRate = DebugState.Framerate;
if (Config::fpsColor()) {
if (frameRate < 10) {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red
} else if (frameRate >= 10 && frameRate < 20) {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange
} else {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White
}
if (frameRate < 10) {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); // Red
} else if (frameRate >= 10 && frameRate < 20) {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.0f, 1.0f)); // Orange
} else {
PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); // White
}
@@ -311,6 +308,7 @@ static void LoadSettings(const char* line) {
void L::SetupSettings() {
frame_graph.is_open = true;
show_simple_fps = Config::getShowFpsCounter();
using SettingLoader = void (*)(const char*);
@@ -460,12 +458,12 @@ void L::Draw() {
}
void L::TextCentered(const std::string& text) {
float window_width = ImGui::GetWindowSize().x;
float text_width = ImGui::CalcTextSize(text.c_str()).x;
float window_width = GetWindowSize().x;
float text_width = CalcTextSize(text.c_str()).x;
float text_indentation = (window_width - text_width) * 0.5f;
ImGui::SameLine(text_indentation);
ImGui::Text("%s", text.c_str());
SameLine(text_indentation);
Text("%s", text.c_str());
}
namespace Overlay {
@@ -475,6 +473,11 @@ void ToggleSimpleFps() {
visibility_toggled = true;
}
void SetSimpleFps(bool enabled) {
show_simple_fps = enabled;
visibility_toggled = true;
}
void ToggleQuitWindow() {
show_quit_window = !show_quit_window;
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -9,18 +9,20 @@
namespace Core::Devtools {
class Layer final : public ImGui::Layer {
static void DrawMenuBar();
static void DrawAdvanced();
static void DrawSimple();
public:
static void SetupSettings();
void Draw() override;
void TextCentered(const std::string& text);
// Must be inside a window
static void DrawNullGpuNotice();
private:
static void DrawMenuBar();
static void DrawAdvanced();
static void DrawSimple();
static void TextCentered(const std::string& text);
};
} // namespace Core::Devtools
@@ -28,6 +30,7 @@ public:
namespace Overlay {
void ToggleSimpleFps();
void SetSimpleFps(bool enabled);
void ToggleQuitWindow();
} // namespace Overlay

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "layer.h"
#include "imgui/imgui_std.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <cmath>
#include <numbers>
namespace Core::Devtools {
void Layer::DrawNullGpuNotice() {
auto* window = ImGui::GetCurrentWindow();
if (window->SkipItems) {
return;
}
constexpr std::string_view mainNotice = "Null GPU is enabled";
constexpr std::string_view detailsNotice =
"Disable the nullGpu config to show the game display";
auto displaySize = window->Size;
ImVec2 targetSize = displaySize * 0.7f;
float minFontSize = 1.0f;
float maxFontSize = 200.0f;
float optimalFontSize = minFontSize;
static auto lastSize = ImVec2(-1, -1);
static float lastFontSize = -1.0f;
auto* font = ImGui::GetIO().Fonts->Fonts[IMGUI_FONT_TEXT_BIG];
if (lastSize != targetSize) {
while (maxFontSize - minFontSize > 0.1f) {
float testFontSize = (minFontSize + maxFontSize) / 2.0f;
ImVec2 textSize = font->CalcTextSizeA(testFontSize, FLT_MAX, 0.0f, &mainNotice.front(),
&mainNotice.back() + 1);
if (textSize.x <= targetSize.x && textSize.y <= targetSize.y) {
optimalFontSize = testFontSize;
minFontSize = testFontSize;
} else {
maxFontSize = testFontSize;
}
}
lastSize = targetSize;
lastFontSize = optimalFontSize;
} else {
optimalFontSize = lastFontSize;
}
ImVec2 textSize = font->CalcTextSizeA(optimalFontSize, FLT_MAX, 0.0f, &mainNotice.front(),
&mainNotice.back() + 1);
ImVec2 textPos = (displaySize - textSize) * 0.5f + window->Pos;
const float scale = optimalFontSize / font->FontSize;
double timeAnim = -std::numbers::pi * ImGui::GetTime();
int i = 0;
for (auto ch : mainNotice) {
double colorTime = sin(timeAnim + i * std::numbers::pi / 6.0) / 2.0 + 0.5;
int color = (int)(200 * colorTime) + 55;
double posTime = sin(timeAnim + i * std::numbers::pi / 15.0) / 2.0 + 0.5;
auto pos = textPos;
pos.y += 10.0 * (posTime < 0.5 ? std::pow(2.0, 20.0 * posTime - 10.0) / 2.0
: (2.0 - std::pow(2.0, -20.0 * posTime + 10.0)) / 2.0);
window->DrawList->AddText(font, optimalFontSize, pos, IM_COL32(color, color, color, 255),
&ch, &ch + 1);
textPos.x += font->FindGlyph(ch)->AdvanceX * scale;
i++;
}
font = ImGui::GetIO().Fonts->Fonts[IMGUI_FONT_TEXT];
textPos.y += textSize.y + 1.0;
optimalFontSize *= 0.2;
textSize = font->CalcTextSizeA(optimalFontSize, FLT_MAX, 0.0f, &detailsNotice.front(),
&detailsNotice.back() + 1);
textPos.x = window->Pos.x + (window->Size.x - textSize.x) * 0.5f;
window->DrawList->AddText(font, optimalFontSize, textPos, IM_COL32(200, 200, 200, 255),
&detailsNotice.front(), &detailsNotice.back() + 1);
}
} // namespace Core::Devtools

View File

@@ -152,7 +152,7 @@ inline std::string RunDisassembler(const std::string& disassembler_cli, const T&
}
} else {
cli.replace(pos, src_arg.size(), "\"" + bin_path.string() + "\"");
Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Write);
Common::FS::IOFile file(bin_path, Common::FS::FileAccessMode::Create);
file.Write(shader_code);
file.Close();

View File

@@ -123,7 +123,7 @@ void FrameDumpViewer::Draw() {
const auto fname = fmt::format("{:%F %H-%M-%S} {}_{}_{}.bin", now_time,
magic_enum::enum_name(selected_queue_type),
selected_submit_num, selected_queue_num2);
Common::FS::IOFile file(fname, Common::FS::FileAccessMode::Write);
Common::FS::IOFile file(fname, Common::FS::FileAccessMode::Create);
const auto& data = frame_dump->queues[selected_cmd].data;
if (file.IsOpen()) {
DebugState.ShowDebugMessage(fmt::format("Dumping cmd as {}", fname));

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

@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "emulator_state.h"
std::shared_ptr<EmulatorState> EmulatorState::s_instance = nullptr;
std::mutex EmulatorState::s_mutex;
EmulatorState::EmulatorState() {}
EmulatorState::~EmulatorState() {}
std::shared_ptr<EmulatorState> EmulatorState::GetInstance() {
std::lock_guard<std::mutex> lock(s_mutex);
if (!s_instance)
s_instance = std::make_shared<EmulatorState>();
return s_instance;
}
void EmulatorState::SetInstance(std::shared_ptr<EmulatorState> instance) {
std::lock_guard<std::mutex> lock(s_mutex);
s_instance = instance;
}
bool EmulatorState::IsGameRunning() const {
return m_running;
}
void EmulatorState::SetGameRunning(bool running) {
m_running = running;
}
bool EmulatorState::IsAutoPatchesLoadEnabled() const {
return m_load_patches_auto;
}
void EmulatorState::SetAutoPatchesLoadEnabled(bool enable) {
m_load_patches_auto = enable;
}

29
src/core/emulator_state.h Normal file
View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <mutex>
class EmulatorState {
public:
EmulatorState();
~EmulatorState();
static std::shared_ptr<EmulatorState> GetInstance();
static void SetInstance(std::shared_ptr<EmulatorState> instance);
bool IsGameRunning() const;
void SetGameRunning(bool running);
bool IsAutoPatchesLoadEnabled() const;
void SetAutoPatchesLoadEnabled(bool enable);
private:
static std::shared_ptr<EmulatorState> s_instance;
static std::mutex s_mutex;
// state variables
bool m_running = false;
bool m_load_patches_auto = true;
};

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <vector>
#include "npbind.h"
bool NPBindFile::Load(const std::string& path) {
Clear(); // Clear any existing data
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f)
return false;
std::streamsize sz = f.tellg();
if (sz <= 0)
return false;
f.seekg(0, std::ios::beg);
std::vector<u8> buf(static_cast<size_t>(sz));
if (!f.read(reinterpret_cast<char*>(buf.data()), sz))
return false;
const u64 size = buf.size();
if (size < sizeof(NpBindHeader))
return false;
// Read header
memcpy(&m_header, buf.data(), sizeof(NpBindHeader));
if (m_header.magic != NPBIND_MAGIC)
return false;
// offset start of bodies
size_t offset = sizeof(NpBindHeader);
m_bodies.reserve(static_cast<size_t>(m_header.num_entries));
// For each body: read 4 TLV entries then skip padding (0x98 = 152 bytes)
const u64 body_padding = 0x98; // 152
for (u64 bi = 0; bi < m_header.num_entries; ++bi) {
// Ensure we have room for 4 entries' headers at least
if (offset + 4 * 4 > size)
return false; // 4 entries x (type+size)
NPBindBody body;
// helper lambda to read one entry
auto read_entry = [&](NPBindEntryRaw& e) -> bool {
if (offset + 4 > size)
return false;
memcpy(&e.type, &buf[offset], 2);
memcpy(&e.size, &buf[offset + 2], 2);
offset += 4;
if (offset + e.size > size)
return false;
e.data.assign(buf.begin() + offset, buf.begin() + offset + e.size);
offset += e.size;
return true;
};
// read 4 entries in order
if (!read_entry(body.npcommid))
return false;
if (!read_entry(body.trophy))
return false;
if (!read_entry(body.unk1))
return false;
if (!read_entry(body.unk2))
return false;
// skip fixed padding after body if present (but don't overrun)
if (offset + body_padding <= size) {
offset += body_padding;
} else {
// If padding not fully present, allow file to end (some variants may omit)
offset = size;
}
m_bodies.push_back(std::move(body));
}
// Read digest if available
if (size >= 20) {
// Digest is typically the last 20 bytes, independent of offset
memcpy(m_digest, &buf[size - 20], 20);
} else {
memset(m_digest, 0, 20);
}
return true;
}
std::vector<std::string> NPBindFile::GetNpCommIds() const {
std::vector<std::string> npcommids;
npcommids.reserve(m_bodies.size());
for (const auto& body : m_bodies) {
// Convert binary data to string directly
if (!body.npcommid.data.empty()) {
std::string raw_string(reinterpret_cast<const char*>(body.npcommid.data.data()),
body.npcommid.data.size());
npcommids.push_back(raw_string);
}
}
return npcommids;
}

View File

@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadLauncher4 Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "common/endian.h"
#include "common/types.h"
#define NPBIND_MAGIC 0xD294A018u
#pragma pack(push, 1)
struct NpBindHeader {
u32_be magic;
u32_be version;
u64_be file_size;
u64_be entry_size;
u64_be num_entries;
char padding[0x60]; // 96 bytes
};
#pragma pack(pop)
struct NPBindEntryRaw {
u16_be type;
u16_be size; // includes internal padding
std::vector<u8> data;
};
struct NPBindBody {
NPBindEntryRaw npcommid; // expected type 0x0010, size 12
NPBindEntryRaw trophy; // expected type 0x0011, size 12
NPBindEntryRaw unk1; // expected type 0x0012, size 176
NPBindEntryRaw unk2; // expected type 0x0013, size 16
// The 0x98 padding after these entries is skipped while parsing
};
class NPBindFile {
private:
NpBindHeader m_header;
std::vector<NPBindBody> m_bodies;
u8 m_digest[20]; // zeroed if absent
public:
NPBindFile() {
memset(m_digest, 0, sizeof(m_digest));
}
// Load from file
bool Load(const std::string& path);
// Accessors
const NpBindHeader& Header() const {
return m_header;
}
const std::vector<NPBindBody>& Bodies() const {
return m_bodies;
}
const u8* Digest() const {
return m_digest;
}
// Get npcommid data
std::vector<std::string> GetNpCommIds() const;
// Get specific body
const NPBindBody& GetBody(size_t index) const {
return m_bodies.at(index);
}
// Get number of bodies
u64 BodyCount() const {
return m_bodies.size();
}
// Check if file was loaded successfully
bool IsValid() const {
return m_header.magic == NPBIND_MAGIC;
}
// Clear all data
void Clear() {
m_header = NpBindHeader{};
m_bodies.clear();
memset(m_digest, 0, sizeof(m_digest));
}
};

View File

@@ -36,6 +36,7 @@ bool PSF::Open(const std::filesystem::path& filepath) {
}
const u64 psfSize = file.GetSize();
ASSERT_MSG(psfSize != 0, "SFO file at {} is empty!", filepath.string());
std::vector<u8> psf(psfSize);
file.Seek(0);
file.Read(psf);
@@ -99,7 +100,7 @@ bool PSF::Open(const std::vector<u8>& psf_buffer) {
}
bool PSF::Encode(const std::filesystem::path& filepath) const {
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Write);
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Create);
if (!file.IsOpen()) {
return false;
}

View File

@@ -1,44 +1,31 @@
// 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/aes.h"
#include "common/config.h"
#include "common/key_manager.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/file_format/npbind.h"
#include "core/file_format/trp.h"
static void DecryptEFSM(std::span<u8, 16> trophyKey, std::span<u8, 16> NPcommID,
std::span<u8, 16> efsmIv, std::span<u8> ciphertext,
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
std::span<const u8, 16> efsmIv, std::span<const u8> ciphertext,
std::span<u8> decrypted) {
// Step 1: Encrypt NPcommID
std::array<u8, 16> trophyIv{};
std::array<u8, 16> trpKey;
// Convert spans to pointers for the aes functions
aes::encrypt_cbc(NPcommID.data(), NPcommID.size(), trophyKey.data(), trophyKey.size(),
trophyIv.data(), trpKey.data(), trpKey.size(), false);
// Step 2: Decrypt EFSM
aes::decrypt_cbc(ciphertext.data(), ciphertext.size(), trpKey.data(), trpKey.size(),
efsmIv.data(), decrypted.data(), decrypted.size(), nullptr);
const_cast<u8*>(efsmIv.data()), decrypted.data(), decrypted.size(), nullptr);
}
TRP::TRP() = default;
TRP::~TRP() = default;
void TRP::GetNPcommID(const std::filesystem::path& trophyPath, int index) {
std::filesystem::path trpPath = trophyPath / "sce_sys/npbind.dat";
Common::FS::IOFile npbindFile(trpPath, Common::FS::FileAccessMode::Read);
if (!npbindFile.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Failed to open npbind.dat file");
return;
}
if (!npbindFile.Seek(0x84 + (index * 0x180))) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to NPbind offset");
return;
}
npbindFile.ReadRaw<u8>(np_comm_id.data(), 12);
std::fill(np_comm_id.begin() + 12, np_comm_id.end(), 0); // fill with 0, we need 16 bytes.
}
static void removePadding(std::vector<u8>& vec) {
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
if (*it == '>') {
@@ -59,95 +46,236 @@ static void hexToBytes(const char* hex, unsigned char* dst) {
bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) {
std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/";
if (!std::filesystem::exists(gameSysDir)) {
LOG_CRITICAL(Common_Filesystem, "Game sce_sys directory doesn't exist");
LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist");
return false;
}
const auto user_key_str = Config::getTrophyKey();
if (user_key_str.size() != 32) {
const auto& user_key_vec =
KeyManager::GetInstance()->GetAllKeys().TrophyKeySet.ReleaseTrophyKey;
if (user_key_vec.size() != 16) {
LOG_INFO(Common_Filesystem, "Trophy decryption key is not specified");
return false;
}
std::array<u8, 16> user_key{};
hexToBytes(user_key_str.c_str(), user_key.data());
std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin());
for (int index = 0; const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (it.is_regular_file()) {
GetNPcommID(trophyPath, index);
// Load npbind.dat using the new class
std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat";
NPBindFile npbind;
if (!npbind.Load(npbindPath.string())) {
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
}
auto npCommIds = npbind.GetNpCommIds();
if (npCommIds.empty()) {
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
}
bool success = true;
int trpFileIndex = 0;
try {
// Process each TRP file in the trophy directory
for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
if (!it.is_regular_file() || it.path().extension() != ".trp") {
continue; // Skip non-TRP files
}
// Get NPCommID for this TRP file (if available)
std::string npCommId;
if (trpFileIndex < static_cast<int>(npCommIds.size())) {
npCommId = npCommIds[trpFileIndex];
LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId,
it.path().filename().string());
} else {
LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}",
trpFileIndex);
}
Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
LOG_CRITICAL(Common_Filesystem, "Unable to open trophy file for read");
return false;
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string());
success = false;
continue;
}
TrpHeader header;
file.Read(header);
if (header.magic != 0xDCA24D00) {
LOG_CRITICAL(Common_Filesystem, "Wrong trophy magic number");
return false;
if (!file.Read(header)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}",
it.path().string());
success = false;
continue;
}
if (header.magic != TRP_MAGIC) {
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string());
success = false;
continue;
}
s64 seekPos = sizeof(TrpHeader);
std::filesystem::path trpFilesPath(
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId /
"TrophyFiles" / it.path().stem());
std::filesystem::create_directories(trpFilesPath / "Icons");
std::filesystem::create_directory(trpFilesPath / "Xml");
// Create output directories
if (!std::filesystem::create_directories(trpFilesPath / "Icons") ||
!std::filesystem::create_directories(trpFilesPath / "Xml")) {
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId);
success = false;
continue;
}
// Process each entry in the TRP file
for (int i = 0; i < header.entry_num; i++) {
if (!file.Seek(seekPos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
success = false;
break;
}
seekPos += (s64)header.entry_size;
seekPos += static_cast<s64>(header.entry_size);
TrpEntry entry;
file.Read(entry);
std::string_view name(entry.entry_name);
if (entry.flag == 0) { // PNG
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
file.Read(icon);
Common::FS::IOFile::WriteBytes(trpFilesPath / "Icons" / name, icon);
if (!file.Read(entry)) {
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
success = false;
break;
}
if (entry.flag == 3 && np_comm_id[0] == 'N' &&
np_comm_id[1] == 'P') { // ESFM, encrypted.
if (!file.Seek(entry.entry_pos)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry offset");
return false;
std::string_view name(entry.entry_name);
if (entry.flag == ENTRY_FLAG_PNG) {
if (!ProcessPngEntry(file, entry, trpFilesPath, name)) {
success = false;
// Continue with next entry
}
file.Read(esfmIv); // get iv key.
// Skip the first 16 bytes which are the iv key on every entry as we want a
// clean xml file.
std::vector<u8> ESFM(entry.entry_len - iv_len);
std::vector<u8> XML(entry.entry_len - iv_len);
if (!file.Seek(entry.entry_pos + iv_len)) {
LOG_CRITICAL(Common_Filesystem, "Failed to seek to TRP entry + iv offset");
return false;
}
file.Read(ESFM);
DecryptEFSM(user_key, np_comm_id, esfmIv, ESFM, XML); // decrypt
removePadding(XML);
std::string xml_name = entry.entry_name;
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos)
xml_name.replace(pos, xml_name.length(), "XML");
std::filesystem::path path = trpFilesPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(path, XML);
if (written != XML.size()) {
LOG_CRITICAL(
Common_Filesystem,
"Trophy XML {} write failed, wanted to write {} bytes, wrote {}",
fmt::UTF(path.u8string()), XML.size(), written);
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
// Check if we have a valid NPCommID for decryption
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key,
npCommId)) {
success = false;
// Continue with next entry
}
} else {
LOG_WARNING(Common_Filesystem,
"Skipping encrypted XML entry - invalid NPCommID");
// Skip this entry but continue
}
} else {
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
static_cast<unsigned int>(entry.flag), name);
}
}
trpFileIndex++;
}
index++;
} catch (const std::filesystem::filesystem_error& e) {
LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what());
return false;
} catch (const std::exception& e) {
LOG_CRITICAL(Common_Filesystem, "Error during trophy extraction: {}", e.what());
return false;
}
if (success) {
LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex,
titleId);
}
return success;
}
bool TRP::ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name) {
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to PNG entry offset");
return false;
}
std::vector<u8> icon(entry.entry_len);
if (!file.Read(icon)) {
LOG_ERROR(Common_Filesystem, "Failed to read PNG data");
return false;
}
auto outputFile = outputPath / "Icons" / name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, icon);
if (written != icon.size()) {
LOG_ERROR(Common_Filesystem, "PNG write failed: wanted {} bytes, wrote {}", icon.size(),
written);
return false;
}
return true;
}
bool TRP::ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key,
const std::string& npCommId) {
constexpr size_t IV_LEN = 16;
if (!file.Seek(entry.entry_pos)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted XML entry offset");
return false;
}
std::array<u8, IV_LEN> esfmIv;
if (!file.Read(esfmIv)) {
LOG_ERROR(Common_Filesystem, "Failed to read IV for encrypted XML");
return false;
}
if (entry.entry_len <= IV_LEN) {
LOG_ERROR(Common_Filesystem, "Encrypted XML entry too small");
return false;
}
// Skip to the encrypted data (after IV)
if (!file.Seek(entry.entry_pos + IV_LEN)) {
LOG_ERROR(Common_Filesystem, "Failed to seek to encrypted data");
return false;
}
std::vector<u8> ESFM(entry.entry_len - IV_LEN);
std::vector<u8> XML(entry.entry_len - IV_LEN);
if (!file.Read(ESFM)) {
LOG_ERROR(Common_Filesystem, "Failed to read encrypted XML data");
return false;
}
// Decrypt the data - FIX: Don't check return value since DecryptEFSM returns void
std::span<const u8, 16> key_span(user_key);
// Convert npCommId string to span (pad or truncate to 16 bytes)
std::array<u8, 16> npcommid_array{};
size_t copy_len = std::min(npCommId.size(), npcommid_array.size());
std::memcpy(npcommid_array.data(), npCommId.data(), copy_len);
std::span<const u8, 16> npcommid_span(npcommid_array);
DecryptEFSM(key_span, npcommid_span, esfmIv, ESFM, XML);
// Remove padding
removePadding(XML);
// Create output filename (replace ESFM with XML)
std::string xml_name(entry.entry_name);
size_t pos = xml_name.find("ESFM");
if (pos != std::string::npos) {
xml_name.replace(pos, 4, "XML");
}
auto outputFile = outputPath / "Xml" / xml_name;
size_t written = Common::FS::IOFile::WriteBytes(outputFile, XML);
if (written != XML.size()) {
LOG_ERROR(Common_Filesystem, "XML write failed: wanted {} bytes, wrote {}", XML.size(),
written);
return false;
}
return true;
}

View File

@@ -8,6 +8,10 @@
#include "common/io_file.h"
#include "common/types.h"
static constexpr u32 TRP_MAGIC = 0xDCA24D00;
static constexpr u8 ENTRY_FLAG_PNG = 0;
static constexpr u8 ENTRY_FLAG_ENCRYPTED_XML = 3;
struct TrpHeader {
u32_be magic; // (0xDCA24D00)
u32_be version;
@@ -33,9 +37,14 @@ public:
TRP();
~TRP();
bool Extract(const std::filesystem::path& trophyPath, const std::string titleId);
void GetNPcommID(const std::filesystem::path& trophyPath, int index);
private:
bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name);
bool ProcessEncryptedXmlEntry(Common::FS::IOFile& file, const TrpEntry& entry,
const std::filesystem::path& outputPath, std::string_view name,
const std::array<u8, 16>& user_key, const std::string& npCommId);
std::vector<u8> NPcommID = std::vector<u8>(12);
std::array<u8, 16> np_comm_id{};
std::array<u8, 16> esfmIv{};

View File

@@ -5,6 +5,7 @@
#include "common/singleton.h"
#include "core/file_sys/directories/base_directory.h"
#include "core/file_sys/fs.h"
#include "core/libraries/kernel/orbis_error.h"
namespace Core::Directories {
@@ -12,4 +13,35 @@ BaseDirectory::BaseDirectory() = default;
BaseDirectory::~BaseDirectory() = default;
s64 BaseDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
s64 bytes_read = 0;
for (s32 i = 0; i < iovcnt; i++) {
const s64 result = read(iov[i].iov_base, iov[i].iov_len);
if (result < 0) {
return result;
}
bytes_read += result;
}
return bytes_read;
}
s64 BaseDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
const u64 old_file_pointer = file_offset;
file_offset = offset;
const s64 bytes_read = readv(iov, iovcnt);
file_offset = old_file_pointer;
return bytes_read;
}
s64 BaseDirectory::lseek(s64 offset, s32 whence) {
s64 file_offset_new = ((0 == whence) * offset) + ((1 == whence) * (file_offset + offset)) +
((2 == whence) * (directory_size + offset));
if (file_offset_new < 0)
return ORBIS_KERNEL_ERROR_EINVAL;
file_offset = file_offset_new;
return file_offset;
}
} // namespace Core::Directories

View File

@@ -19,6 +19,17 @@ struct OrbisKernelDirent;
namespace Core::Directories {
class BaseDirectory {
protected:
static inline u32 fileno_pool{10};
static u32 next_fileno() {
return ++fileno_pool;
}
s64 file_offset = 0;
u64 directory_size = 0;
std::vector<u8> dirent_cache_bin{};
public:
explicit BaseDirectory();
@@ -28,18 +39,23 @@ public:
return ORBIS_KERNEL_ERROR_EBADF;
}
virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt);
virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset);
virtual s64 write(const void* buf, u64 nbytes) {
return ORBIS_KERNEL_ERROR_EBADF;
}
virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
virtual s64 writev(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
return ORBIS_KERNEL_ERROR_EBADF;
}
virtual s64 lseek(s64 offset, s32 whence) {
virtual s64 pwritev(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
return ORBIS_KERNEL_ERROR_EBADF;
}
virtual s64 lseek(s64 offset, s32 whence);
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) {
return ORBIS_KERNEL_ERROR_EBADF;
}

View File

@@ -15,111 +15,30 @@ std::shared_ptr<BaseDirectory> NormalDirectory::Create(std::string_view guest_di
std::make_shared<NormalDirectory>(guest_directory));
}
NormalDirectory::NormalDirectory(std::string_view guest_directory) {
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
static s32 fileno = 0;
mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) {
auto& dirent = dirents.emplace_back();
dirent.d_fileno = ++fileno;
dirent.d_type = (ent_is_file ? 8 : 4);
strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1);
dirent.d_namlen = ent_path.filename().string().size();
// Calculate the appropriate length for this dirent.
// Account for the null terminator in d_name too.
dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) +
sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) +
(dirent.d_namlen + 1),
4);
directory_size += dirent.d_reclen;
});
// The last entry of a normal directory should have d_reclen covering the remaining data.
// Since the dirents of a folder are constant by this point, we can modify the last dirent
// before creating the emulated file buffer.
const u64 filler_count = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT) - directory_size;
dirents[dirents.size() - 1].d_reclen += filler_count;
// Reading from standard directories seems to be based around file pointer logic.
// Keep an internal buffer representing the raw contents of this file descriptor,
// then emulate the various read functions with that.
directory_size = Common::AlignUp(directory_size, DIRECTORY_ALIGNMENT);
data_buffer.reserve(directory_size);
memset(data_buffer.data(), 0, directory_size);
u8* current_dirent = data_buffer.data();
for (const NormalDirectoryDirent& dirent : dirents) {
NormalDirectoryDirent* dirent_to_write =
reinterpret_cast<NormalDirectoryDirent*>(current_dirent);
dirent_to_write->d_fileno = dirent.d_fileno;
// Using size d_namlen + 1 to account for null terminator.
strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
dirent_to_write->d_namlen = dirent.d_namlen;
dirent_to_write->d_reclen = dirent.d_reclen;
dirent_to_write->d_type = dirent.d_type;
current_dirent += dirent.d_reclen;
}
NormalDirectory::NormalDirectory(std::string_view guest_directory)
: guest_directory(guest_directory) {
RebuildDirents();
}
s64 NormalDirectory::read(void* buf, u64 nbytes) {
// Nothing left to read.
if (file_offset >= directory_size) {
return ORBIS_OK;
}
RebuildDirents();
const s64 remaining_data = directory_size - file_offset;
const s64 bytes = nbytes > remaining_data ? remaining_data : nbytes;
// data is contiguous. read goes like any regular file would: start at offset, read n bytes
// output is always aligned up to 512 bytes with 0s
// offset - classic. however at the end of read any unused (exceeding dirent buffer size) buffer
// space will be left untouched
// reclen always sums up to end of current alignment
std::memcpy(buf, data_buffer.data() + file_offset, bytes);
s64 bytes_available = this->dirent_cache_bin.size() - file_offset;
if (bytes_available <= 0)
return 0;
bytes_available = std::min<s64>(bytes_available, static_cast<s64>(nbytes));
file_offset += bytes;
return bytes;
}
// data
memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available);
s64 NormalDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
s64 bytes_read = 0;
for (s32 i = 0; i < iovcnt; i++) {
const s64 result = read(iov[i].iov_base, iov[i].iov_len);
if (result < 0) {
return result;
}
bytes_read += result;
}
return bytes_read;
}
s64 NormalDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
s64 offset) {
const u64 old_file_pointer = file_offset;
file_offset = offset;
const s64 bytes_read = readv(iov, iovcnt);
file_offset = old_file_pointer;
return bytes_read;
}
s64 NormalDirectory::lseek(s64 offset, s32 whence) {
switch (whence) {
case 0: {
file_offset = offset;
break;
}
case 1: {
file_offset += offset;
break;
}
case 2: {
file_offset = directory_size + offset;
break;
}
default: {
UNREACHABLE_MSG("lseek with unknown whence {}", whence);
}
}
return file_offset;
file_offset += bytes_available;
return bytes_available;
}
s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
@@ -131,10 +50,110 @@ s32 NormalDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
}
s64 NormalDirectory::getdents(void* buf, u64 nbytes, s64* basep) {
if (basep != nullptr) {
RebuildDirents();
if (basep)
*basep = file_offset;
// same as others, we just don't need a variable
if (file_offset >= directory_size)
return 0;
s64 bytes_written = 0;
s64 working_offset = file_offset;
s64 dirent_buffer_offset = 0;
s64 aligned_count = Common::AlignDown(nbytes, 512);
const u8* dirent_buffer = this->dirent_cache_bin.data();
while (dirent_buffer_offset < this->dirent_cache_bin.size()) {
const u8* normal_dirent_ptr = dirent_buffer + dirent_buffer_offset;
const NormalDirectoryDirent* normal_dirent =
reinterpret_cast<const NormalDirectoryDirent*>(normal_dirent_ptr);
auto d_reclen = normal_dirent->d_reclen;
// bad, incomplete or OOB entry
if (normal_dirent->d_namlen == 0)
break;
if (working_offset >= d_reclen) {
dirent_buffer_offset += d_reclen;
working_offset -= d_reclen;
continue;
}
if ((bytes_written + d_reclen) > aligned_count)
// dirents are aligned to the last full one
break;
memcpy(static_cast<u8*>(buf) + bytes_written, normal_dirent_ptr + working_offset,
d_reclen - working_offset);
bytes_written += d_reclen - working_offset;
dirent_buffer_offset += d_reclen;
working_offset = 0;
}
// read behaves identically to getdents for normal directories.
return read(buf, nbytes);
file_offset += bytes_written;
return bytes_written;
}
void NormalDirectory::RebuildDirents() {
// regenerate only when target wants to read contents again
// no reason for testing - read is always raw and dirents get processed on the go
if (previous_file_offset == file_offset)
return;
previous_file_offset = file_offset;
constexpr u32 dirent_meta_size =
sizeof(NormalDirectoryDirent::d_fileno) + sizeof(NormalDirectoryDirent::d_type) +
sizeof(NormalDirectoryDirent::d_namlen) + sizeof(NormalDirectoryDirent::d_reclen);
u64 next_ceiling = 0;
u64 dirent_offset = 0;
u64 last_reclen_offset = 4;
dirent_cache_bin.clear();
dirent_cache_bin.reserve(512);
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
mnt->IterateDirectory(
guest_directory, [this, &next_ceiling, &dirent_offset, &last_reclen_offset](
const std::filesystem::path& ent_path, const bool ent_is_file) {
NormalDirectoryDirent tmp{};
std::string leaf(ent_path.filename().string());
// prepare dirent
tmp.d_fileno = BaseDirectory::next_fileno();
tmp.d_namlen = leaf.size();
strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1);
tmp.d_type = (ent_is_file ? 0100000 : 0040000) >> 12;
tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 4);
// next element may break 512 byte alignment
if (tmp.d_reclen + dirent_offset > next_ceiling) {
// align previous dirent's size to the current ceiling
*reinterpret_cast<u16*>(static_cast<u8*>(dirent_cache_bin.data()) +
last_reclen_offset) += next_ceiling - dirent_offset;
// set writing pointer to the aligned start position (current ceiling)
dirent_offset = next_ceiling;
// move the ceiling up and zero-out the buffer
next_ceiling += 512;
dirent_cache_bin.resize(next_ceiling);
std::fill(dirent_cache_bin.begin() + dirent_offset,
dirent_cache_bin.begin() + next_ceiling, 0);
}
// current dirent's reclen position
last_reclen_offset = dirent_offset + 4;
memcpy(dirent_cache_bin.data() + dirent_offset, &tmp, tmp.d_reclen);
dirent_offset += tmp.d_reclen;
});
// last reclen, as before
*reinterpret_cast<u16*>(static_cast<u8*>(dirent_cache_bin.data()) + last_reclen_offset) +=
next_ceiling - dirent_offset;
// i have no idea if this is the case, but lseek returns size aligned to 512
directory_size = next_ceiling;
}
} // namespace Core::Directories

View File

@@ -19,27 +19,23 @@ public:
~NormalDirectory() override = default;
virtual s64 read(void* buf, u64 nbytes) override;
virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override;
virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
s64 offset) override;
virtual s64 lseek(s64 offset, s32 whence) override;
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override;
private:
static constexpr s32 MAX_LENGTH = 255;
static constexpr s64 DIRECTORY_ALIGNMENT = 0x200;
#pragma pack(push, 1)
struct NormalDirectoryDirent {
u32 d_fileno;
u16 d_reclen;
u8 d_type;
u8 d_namlen;
char d_name[MAX_LENGTH + 1];
char d_name[256];
};
#pragma pack(pop)
u64 directory_size = 0;
s64 file_offset = 0;
std::vector<u8> data_buffer;
std::vector<NormalDirectoryDirent> dirents;
std::string_view guest_directory{};
s64 previous_file_offset = -1;
void RebuildDirents(void);
};
} // namespace Core::Directories

View File

@@ -15,77 +15,49 @@ std::shared_ptr<BaseDirectory> PfsDirectory::Create(std::string_view guest_direc
}
PfsDirectory::PfsDirectory(std::string_view guest_directory) {
constexpr u32 dirent_meta_size =
sizeof(PfsDirectoryDirent::d_fileno) + sizeof(PfsDirectoryDirent::d_type) +
sizeof(PfsDirectoryDirent::d_namlen) + sizeof(PfsDirectoryDirent::d_reclen);
dirent_cache_bin.reserve(512);
auto* mnt = Common::Singleton<Core::FileSys::MntPoints>::Instance();
static s32 fileno = 0;
mnt->IterateDirectory(guest_directory, [this](const auto& ent_path, const auto ent_is_file) {
auto& dirent = dirents.emplace_back();
dirent.d_fileno = ++fileno;
dirent.d_type = (ent_is_file ? 8 : 4);
strncpy(dirent.d_name, ent_path.filename().string().data(), MAX_LENGTH + 1);
dirent.d_namlen = ent_path.filename().string().size();
mnt->IterateDirectory(
guest_directory, [this](const std::filesystem::path& ent_path, const bool ent_is_file) {
PfsDirectoryDirent tmp{};
std::string leaf(ent_path.filename().string());
// Calculate the appropriate length for this dirent.
// Account for the null terminator in d_name too.
dirent.d_reclen = Common::AlignUp(sizeof(dirent.d_fileno) + sizeof(dirent.d_type) +
sizeof(dirent.d_namlen) + sizeof(dirent.d_reclen) +
(dirent.d_namlen + 1),
8);
tmp.d_fileno = BaseDirectory::next_fileno();
tmp.d_namlen = leaf.size();
strncpy(tmp.d_name, leaf.data(), tmp.d_namlen + 1);
tmp.d_type = ent_is_file ? 2 : 4;
tmp.d_reclen = Common::AlignUp(dirent_meta_size + tmp.d_namlen + 1, 8);
auto dirent_ptr = reinterpret_cast<const u8*>(&tmp);
// To handle some obscure dirents_index behavior,
// keep track of the "actual" length of this directory.
directory_content_size += dirent.d_reclen;
});
dirent_cache_bin.insert(dirent_cache_bin.end(), dirent_ptr, dirent_ptr + tmp.d_reclen);
});
directory_size = Common::AlignUp(directory_content_size, DIRECTORY_ALIGNMENT);
directory_size = Common::AlignUp(dirent_cache_bin.size(), 0x10000);
}
s64 PfsDirectory::read(void* buf, u64 nbytes) {
if (dirents_index >= dirents.size()) {
if (dirents_index < directory_content_size) {
// We need to find the appropriate dirents_index to start from.
s64 data_to_skip = dirents_index;
u64 corrected_index = 0;
while (data_to_skip > 0) {
const auto dirent = dirents[corrected_index++];
data_to_skip -= dirent.d_reclen;
}
dirents_index = corrected_index;
} else {
// Nothing left to read.
return ORBIS_OK;
}
s64 bytes_available = this->dirent_cache_bin.size() - file_offset;
if (bytes_available <= 0)
return 0;
bytes_available = std::min<s64>(bytes_available, static_cast<s64>(nbytes));
memcpy(buf, this->dirent_cache_bin.data() + file_offset, bytes_available);
s64 to_fill =
(std::min<s64>(directory_size, static_cast<s64>(nbytes))) - bytes_available - file_offset;
if (to_fill < 0) {
LOG_ERROR(Kernel_Fs, "Dirent may have leaked {} bytes", -to_fill);
return -to_fill + bytes_available;
}
s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes;
// read on PfsDirectories will always return the maximum possible value.
const u64 bytes_written = bytes_remaining;
memset(buf, 0, bytes_remaining);
char* current_dirent = static_cast<char*>(buf);
PfsDirectoryDirent dirent = dirents[dirents_index];
while (bytes_remaining > dirent.d_reclen) {
PfsDirectoryDirent* dirent_to_write = reinterpret_cast<PfsDirectoryDirent*>(current_dirent);
dirent_to_write->d_fileno = dirent.d_fileno;
// Using size d_namlen + 1 to account for null terminator.
strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
dirent_to_write->d_namlen = dirent.d_namlen;
dirent_to_write->d_reclen = dirent.d_reclen;
dirent_to_write->d_type = dirent.d_type;
current_dirent += dirent.d_reclen;
bytes_remaining -= dirent.d_reclen;
if (dirents_index == dirents.size() - 1) {
// Currently at the last dirent, so break out of the loop.
dirents_index++;
break;
}
dirent = dirents[++dirents_index];
}
return bytes_written;
memset(static_cast<u8*>(buf) + bytes_available, 0, to_fill);
file_offset += to_fill + bytes_available;
return to_fill + bytes_available;
}
s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) {
@@ -101,62 +73,13 @@ s64 PfsDirectory::readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovc
}
s64 PfsDirectory::preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt, s64 offset) {
const u64 old_dirent_index = dirents_index;
dirents_index = 0;
s64 data_to_skip = offset;
// If offset is part-way through one dirent, that dirent is skipped.
while (data_to_skip > 0) {
const auto dirent = dirents[dirents_index++];
data_to_skip -= dirent.d_reclen;
if (dirents_index == dirents.size()) {
// We've reached the end of the dirents, nothing more can be skipped.
break;
}
}
const u64 old_file_pointer = file_offset;
file_offset = offset;
const s64 bytes_read = readv(iov, iovcnt);
dirents_index = old_dirent_index;
file_offset = old_file_pointer;
return bytes_read;
}
s64 PfsDirectory::lseek(s64 offset, s32 whence) {
switch (whence) {
// Seek start
case 0: {
dirents_index = 0;
}
case 1: {
// There aren't any dirents left to pass through.
if (dirents_index >= dirents.size()) {
dirents_index = dirents_index + offset;
break;
}
s64 data_to_skip = offset;
while (data_to_skip > 0) {
const auto dirent = dirents[dirents_index++];
data_to_skip -= dirent.d_reclen;
if (dirents_index == dirents.size()) {
// We've passed through all file dirents.
// Set dirents_index to directory_size + remaining_offset instead.
dirents_index = directory_content_size + data_to_skip;
break;
}
}
break;
}
case 2: {
// Seems like real hardware gives up on tracking dirents_index if you go this route.
dirents_index = directory_size + offset;
break;
}
default: {
UNREACHABLE_MSG("lseek with unknown whence {}", whence);
}
}
return dirents_index;
}
s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
stat->st_mode = 0000777u | 0040000u;
stat->st_size = directory_size;
@@ -166,55 +89,58 @@ s32 PfsDirectory::fstat(Libraries::Kernel::OrbisKernelStat* stat) {
}
s64 PfsDirectory::getdents(void* buf, u64 nbytes, s64* basep) {
// basep is set at the start of the function.
if (basep != nullptr) {
*basep = dirents_index;
}
if (basep)
*basep = file_offset;
if (dirents_index >= dirents.size()) {
if (dirents_index < directory_content_size) {
// We need to find the appropriate dirents_index to start from.
s64 data_to_skip = dirents_index;
u64 corrected_index = 0;
while (data_to_skip > 0) {
const auto dirent = dirents[corrected_index++];
data_to_skip -= dirent.d_reclen;
}
dirents_index = corrected_index;
} else {
// Nothing left to read.
return ORBIS_OK;
}
}
s64 bytes_remaining = nbytes > directory_size ? directory_size : nbytes;
memset(buf, 0, bytes_remaining);
// same as others, we just don't need a variable
if (file_offset >= directory_size)
return 0;
u64 bytes_written = 0;
char* current_dirent = static_cast<char*>(buf);
// getdents has to convert pfs dirents to normal dirents
PfsDirectoryDirent dirent = dirents[dirents_index];
while (bytes_remaining > dirent.d_reclen) {
NormalDirectoryDirent* dirent_to_write =
reinterpret_cast<NormalDirectoryDirent*>(current_dirent);
dirent_to_write->d_fileno = dirent.d_fileno;
strncpy(dirent_to_write->d_name, dirent.d_name, dirent.d_namlen + 1);
dirent_to_write->d_namlen = dirent.d_namlen;
dirent_to_write->d_reclen = dirent.d_reclen;
dirent_to_write->d_type = dirent.d_type;
u64 starting_offset = 0;
u64 buffer_position = 0;
while (buffer_position < this->dirent_cache_bin.size()) {
const PfsDirectoryDirent* pfs_dirent =
reinterpret_cast<PfsDirectoryDirent*>(this->dirent_cache_bin.data() + buffer_position);
current_dirent += dirent.d_reclen;
bytes_remaining -= dirent.d_reclen;
bytes_written += dirent.d_reclen;
if (dirents_index == dirents.size() - 1) {
// Currently at the last dirent, so set dirents_index appropriately and break.
dirents_index = directory_size;
// bad, incomplete or OOB entry
if (pfs_dirent->d_namlen == 0)
break;
if (starting_offset < file_offset) {
// reading starts from the nearest full dirent
starting_offset += pfs_dirent->d_reclen;
buffer_position = bytes_written + starting_offset;
continue;
}
dirent = dirents[++dirents_index];
if ((bytes_written + pfs_dirent->d_reclen) > nbytes)
// dirents are aligned to the last full one
break;
// if this dirent breaks alignment, skip
// dirents are count-aligned here, excess data is simply not written
// if (Common::AlignUp(buffer_position, count) !=
// Common::AlignUp(buffer_position + pfs_dirent->d_reclen, count))
// break;
// reclen for both is the same despite difference in var sizes, extra 0s are padded after
// the name
NormalDirectoryDirent normal_dirent{};
normal_dirent.d_fileno = pfs_dirent->d_fileno;
normal_dirent.d_reclen = pfs_dirent->d_reclen;
normal_dirent.d_type = (pfs_dirent->d_type == 2) ? 8 : 4;
normal_dirent.d_namlen = pfs_dirent->d_namlen;
memcpy(normal_dirent.d_name, pfs_dirent->d_name, pfs_dirent->d_namlen);
memcpy(static_cast<u8*>(buf) + bytes_written, &normal_dirent, normal_dirent.d_reclen);
bytes_written += normal_dirent.d_reclen;
buffer_position = bytes_written + starting_offset;
}
file_offset = (buffer_position >= this->dirent_cache_bin.size())
? directory_size
: (file_offset + bytes_written);
return bytes_written;
}
} // namespace Core::Directories

View File

@@ -22,32 +22,28 @@ public:
virtual s64 readv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt) override;
virtual s64 preadv(const Libraries::Kernel::OrbisKernelIovec* iov, s32 iovcnt,
s64 offset) override;
virtual s64 lseek(s64 offset, s32 whence) override;
virtual s32 fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
virtual s64 getdents(void* buf, u64 nbytes, s64* basep) override;
private:
static constexpr s32 MAX_LENGTH = 255;
static constexpr s32 DIRECTORY_ALIGNMENT = 0x10000;
#pragma pack(push, 1)
struct PfsDirectoryDirent {
u32 d_fileno;
u32 d_type;
u32 d_namlen;
u32 d_reclen;
char d_name[MAX_LENGTH + 1];
char d_name[256];
};
#pragma pack(pop)
#pragma pack(push, 1)
struct NormalDirectoryDirent {
u32 d_fileno;
u16 d_reclen;
u8 d_type;
u8 d_namlen;
char d_name[MAX_LENGTH + 1];
char d_name[256];
};
u64 directory_size = 0;
u64 directory_content_size = 0;
s64 dirents_index = 0;
std::vector<PfsDirectoryDirent> dirents;
#pragma pack(pop)
};
} // namespace Core::Directories

View File

@@ -52,6 +52,9 @@ std::filesystem::path MntPoints::GetHostPath(std::string_view path, bool* is_rea
pos = corrected_path.find("//", pos + 1);
}
if (path.length() > 255)
return "";
const MntPair* mount = GetMount(corrected_path);
if (!mount) {
return "";
@@ -229,6 +232,9 @@ File* HandleTable::GetSocket(int d) {
return nullptr;
}
auto file = m_files.at(d);
if (!file) {
return nullptr;
}
if (file->type != Core::FileSys::FileType::Socket) {
return nullptr;
}

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
#include "ipc.h"
@@ -14,9 +14,11 @@
#include "common/types.h"
#include "core/debug_state.h"
#include "core/debugger.h"
#include "core/emulator_state.h"
#include "core/libraries/audio/audioout.h"
#include "input/input_handler.h"
#include "sdl_window.h"
#include "src/core/libraries/usbd/usbd.h"
#include "video_core/renderer_vulkan/vk_presenter.h"
extern std::unique_ptr<Vulkan::Presenter> presenter;
@@ -70,6 +72,8 @@ void IPC::Init() {
return;
}
EmulatorState::GetInstance()->SetAutoPatchesLoadEnabled(false);
input_thread = std::jthread([this] {
Common::SetCurrentThreadName("IPC Read thread");
this->InputLoop();
@@ -167,6 +171,37 @@ void IPC::InputLoop() {
presenter->GetFsrSettingsRef().rcas_attenuation =
static_cast<float>(value / 1000.0f);
}
} else if (cmd == "USB_LOAD_FIGURE") {
const auto ref = Libraries::Usbd::usb_backend->GetImplRef();
if (ref) {
ref->LoadFigure(next_str(), next_u64(), next_u64());
}
} else if (cmd == "USB_REMOVE_FIGURE") {
const auto ref = Libraries::Usbd::usb_backend->GetImplRef();
if (ref) {
ref->RemoveFigure(next_u64(), next_u64(), next_u64() != 0);
}
} else if (cmd == "USB_MOVE_FIGURE") {
const auto ref = Libraries::Usbd::usb_backend->GetImplRef();
if (ref) {
const u8 new_pad = next_u64();
const u8 new_index = next_u64();
const u8 old_pad = next_u64();
const u8 old_index = next_u64();
ref->MoveFigure(new_pad, new_index, old_pad, old_index);
}
} else if (cmd == "USB_TEMP_REMOVE_FIGURE") {
const auto ref = Libraries::Usbd::usb_backend->GetImplRef();
if (ref) {
const u8 index = next_u64();
ref->TempRemoveFigure(index);
}
} else if (cmd == "USB_CANCEL_REMOVE_FIGURE") {
const auto ref = Libraries::Usbd::usb_backend->GetImplRef();
if (ref) {
const u8 index = next_u64();
ref->CancelRemoveFigure(index);
}
} else if (cmd == "RELOAD_INPUTS") {
std::string config = next_str();
Input::ParseInputConfig(config);

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"
@@ -34,7 +34,7 @@ u32 GetChannelMask(u32 num_channels) {
case 8:
return ORBIS_AJM_CHANNELMASK_7POINT1;
default:
UNREACHABLE();
UNREACHABLE_MSG("Unexpected number of channels: {}", num_channels);
}
}
@@ -144,9 +144,8 @@ int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) {
return ORBIS_OK;
}
int PS4_SYSV_ABI sceAjmInstanceCodecType() {
LOG_ERROR(Lib_Ajm, "(STUBBED) called");
return ORBIS_OK;
AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id) {
return static_cast<AjmCodecType>((instance_id >> 14) & 0x1F);
}
int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type,

View File

@@ -82,8 +82,6 @@ enum class AjmStatisticsFlags : u64 {
DECLARE_ENUM_FLAG_OPERATORS(AjmStatisticsFlags)
union AjmStatisticsJobFlags {
AjmStatisticsJobFlags(AjmJobFlags job_flags) : raw(job_flags.raw) {}
u64 raw;
struct {
u64 version : 3;
@@ -133,7 +131,7 @@ struct AjmSidebandGaplessDecode {
struct AjmSidebandResampleParameters {
float ratio;
uint32_t flags;
u32 flags;
};
struct AjmDecAt9InitializeParameters {
@@ -217,7 +215,7 @@ int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* stream, u32 stream_size, int p
AjmDecMp3ParseFrame* frame);
int PS4_SYSV_ABI sceAjmFinalize();
int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context);
int PS4_SYSV_ABI sceAjmInstanceCodecType();
AjmCodecType PS4_SYSV_ABI sceAjmInstanceCodecType(u32 instance_id);
int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags,
u32* instance);
int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance);

View File

@@ -0,0 +1,218 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "ajm.h"
#include "ajm_aac.h"
#include "ajm_result.h"
#include <aacdecoder_lib.h>
// using this internal header to manually configure the decoder in RAW mode
#include "externals/aacdec/fdk-aac/libAACdec/src/aacdecoder.h"
#include <algorithm> // std::transform
#include <iterator> // std::back_inserter
#include <limits>
namespace Libraries::Ajm {
std::span<const s16> AjmAacDecoder::GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const {
const auto pcm_data = std::span(m_pcm_buffer).subspan(skipped_pcm);
return pcm_data.subspan(0, std::min<u32>(pcm_data.size(), max_pcm));
}
template <>
size_t AjmAacDecoder::WriteOutputSamples<float>(SparseOutputBuffer& out, std::span<const s16> pcm) {
if (pcm.empty()) {
return 0;
}
m_resample_buffer.clear();
constexpr float inv_scale = 1.0f / std::numeric_limits<s16>::max();
std::transform(pcm.begin(), pcm.end(), std::back_inserter(m_resample_buffer),
[](auto sample) { return float(sample) * inv_scale; });
return out.Write(std::span(m_resample_buffer));
}
AjmAacDecoder::AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels)
: m_format(format), m_flags(flags), m_channels(channels), m_pcm_buffer(1024 * 8),
m_skip_frames(True(flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2) {
m_resample_buffer.reserve(m_pcm_buffer.size());
}
AjmAacDecoder::~AjmAacDecoder() {
if (m_decoder) {
aacDecoder_Close(m_decoder);
}
}
TRANSPORT_TYPE TransportTypeFromConfigType(ConfigType config_type) {
switch (config_type) {
case ConfigType::ADTS:
return TT_MP4_ADTS;
case ConfigType::RAW:
return TT_MP4_RAW;
default:
UNREACHABLE();
}
}
static UINT g_freq[] = {
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000,
};
void AjmAacDecoder::Reset() {
if (m_decoder) {
aacDecoder_Close(m_decoder);
}
m_decoder = aacDecoder_Open(TransportTypeFromConfigType(m_init_params.config_type), 1);
if (m_init_params.config_type == ConfigType::RAW) {
// Manually configure the decoder
// Things may be incorrect due to limited documentation
CSAudioSpecificConfig asc{};
asc.m_aot = AOT_AAC_LC;
asc.m_samplingFrequency = g_freq[m_init_params.sampling_freq_type];
asc.m_samplingFrequencyIndex = m_init_params.sampling_freq_type;
asc.m_samplesPerFrame = 1024;
asc.m_epConfig = -1;
switch (m_channels) {
case 0:
asc.m_channelConfiguration = 2;
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
asc.m_channelConfiguration = m_channels;
break;
case 7:
asc.m_channelConfiguration = 11;
break;
case 8:
asc.m_channelConfiguration = 12; // 7, 12 or 14 ?
break;
default:
UNREACHABLE();
}
UCHAR changed = 1;
CAacDecoder_Init(m_decoder, &asc, AC_CM_ALLOC_MEM, &changed);
}
m_skip_frames = True(m_flags & AjmAacCodecFlags::EnableNondelayOutput) ? 0 : 2;
}
void AjmAacDecoder::Initialize(const void* buffer, u32 buffer_size) {
ASSERT(buffer_size == 8);
m_init_params = *reinterpret_cast<const InitializeParameters*>(buffer);
Reset();
}
void AjmAacDecoder::GetInfo(void* out_info) const {
auto* codec_info = reinterpret_cast<AjmSidebandDecM4aacCodecInfo*>(out_info);
*codec_info = {
.heaac = True(m_flags & AjmAacCodecFlags::EnableSbrDecode),
};
}
AjmSidebandFormat AjmAacDecoder::GetFormat() const {
const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
return {
.num_channels = static_cast<u32>(info->numChannels),
.channel_mask = GetChannelMask(info->numChannels),
.sampl_freq = static_cast<u32>(info->sampleRate),
.sample_encoding = m_format,
.bitrate = static_cast<u32>(info->bitRate),
};
}
u32 AjmAacDecoder::GetMinimumInputSize() const {
return 0;
}
u32 AjmAacDecoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const {
const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
if (info->aacSamplesPerFrame <= 0) {
return 0;
}
const auto skip_samples = std::min<u32>(gapless.current.skip_samples, info->frameSize);
const auto samples =
gapless.init.total_samples != 0
? std::min<u32>(gapless.current.total_samples, info->frameSize - skip_samples)
: info->frameSize - skip_samples;
return samples * info->numChannels * GetPCMSize(m_format);
}
DecoderResult AjmAacDecoder::ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) {
DecoderResult result{};
// Discard the previous contents of the internal buffer and replace them with new ones
aacDecoder_SetParam(m_decoder, AAC_TPDEC_CLEAR_BUFFER, 1);
UCHAR* buffers[] = {input.data()};
const UINT sizes[] = {static_cast<UINT>(input.size())};
UINT valid = sizes[0];
aacDecoder_Fill(m_decoder, buffers, sizes, &valid);
auto ret = aacDecoder_DecodeFrame(m_decoder, m_pcm_buffer.data(), m_pcm_buffer.size(), 0);
switch (ret) {
case AAC_DEC_OK:
break;
case AAC_DEC_NOT_ENOUGH_BITS:
result.result = ORBIS_AJM_RESULT_PARTIAL_INPUT;
return result;
default:
LOG_ERROR(Lib_Ajm, "aacDecoder_DecodeFrame failed ret = {:#x}", static_cast<u32>(ret));
result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL;
result.internal_result = ret;
return result;
}
const auto* const info = aacDecoder_GetStreamInfo(m_decoder);
auto bytes_used = info->numTotalBytes;
result.frames_decoded += 1;
input = input.subspan(bytes_used);
if (m_skip_frames > 0) {
--m_skip_frames;
return result;
}
u32 skip_samples = 0;
if (gapless.current.skip_samples > 0) {
skip_samples = std::min<u16>(info->frameSize, gapless.current.skip_samples);
gapless.current.skip_samples -= skip_samples;
}
const auto max_samples =
gapless.init.total_samples != 0 ? gapless.current.total_samples : info->aacSamplesPerFrame;
size_t pcm_written = 0;
auto pcm = GetOuputPcm(skip_samples * info->numChannels, max_samples * info->numChannels);
switch (m_format) {
case AjmFormatEncoding::S16:
pcm_written = output.Write(pcm);
break;
case AjmFormatEncoding::S32:
UNREACHABLE_MSG("NOT IMPLEMENTED");
break;
case AjmFormatEncoding::Float:
pcm_written = WriteOutputSamples<float>(output, pcm);
break;
default:
UNREACHABLE();
}
result.samples_written = pcm_written / info->numChannels;
gapless.current.skipped_samples += info->frameSize - result.samples_written;
if (gapless.init.total_samples != 0) {
gapless.current.total_samples -= result.samples_written;
}
return result;
}
} // namespace Libraries::Ajm

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/enum.h"
#include "common/types.h"
#include "core/libraries/ajm/ajm_instance.h"
#include <span>
#include <vector>
struct AAC_DECODER_INSTANCE;
namespace Libraries::Ajm {
enum ConfigType : u32 {
ADTS = 1,
RAW = 2,
};
enum AjmAacCodecFlags : u32 {
EnableSbrDecode = 1 << 0,
EnableNondelayOutput = 1 << 1,
SurroundChannelInterleaveOrderExtlExtrLsRs = 1 << 2,
SurroundChannelInterleaveOrderLsRsExtlExtr = 1 << 3,
};
DECLARE_ENUM_FLAG_OPERATORS(AjmAacCodecFlags)
struct AjmSidebandDecM4aacCodecInfo {
u32 heaac;
u32 reserved;
};
struct AjmAacDecoder final : AjmCodec {
explicit AjmAacDecoder(AjmFormatEncoding format, AjmAacCodecFlags flags, u32 channels);
~AjmAacDecoder() override;
void Reset() override;
void Initialize(const void* buffer, u32 buffer_size) override;
void GetInfo(void* out_info) const override;
AjmSidebandFormat GetFormat() const override;
u32 GetMinimumInputSize() const override;
u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override;
DecoderResult ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) override;
private:
struct InitializeParameters {
ConfigType config_type;
u32 sampling_freq_type;
};
template <class T>
size_t WriteOutputSamples(SparseOutputBuffer& output, std::span<const s16> pcm);
std::span<const s16> GetOuputPcm(u32 skipped_pcm, u32 max_pcm) const;
const AjmFormatEncoding m_format;
const AjmAacCodecFlags m_flags;
const u32 m_channels;
std::vector<s16> m_pcm_buffer;
std::vector<float> m_resample_buffer;
u32 m_skip_frames = 0;
InitializeParameters m_init_params = {};
AAC_DECODER_INSTANCE* m_decoder = nullptr;
};
} // namespace Libraries::Ajm

View File

@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "ajm_result.h"
#include "common/assert.h"
#include "core/libraries/ajm/ajm_at9.h"
#include "error_codes.h"
@@ -53,7 +54,7 @@ struct RIFFHeader {
};
static_assert(sizeof(RIFFHeader) == 12);
AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags)
AjmAt9Decoder::AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32)
: m_format(format), m_flags(flags), m_handle(Atrac9GetHandle()) {}
AjmAt9Decoder::~AjmAt9Decoder() {
@@ -85,8 +86,8 @@ void AjmAt9Decoder::GetInfo(void* out_info) const {
auto* info = reinterpret_cast<AjmSidebandDecAt9CodecInfo*>(out_info);
info->super_frame_size = m_codec_info.superframeSize;
info->frames_in_super_frame = m_codec_info.framesInSuperframe;
info->next_frame_size = m_superframe_bytes_remain;
info->frame_samples = m_codec_info.frameSamples;
info->next_frame_size = static_cast<Atrac9Handle*>(m_handle)->Config.FrameBytes;
}
u8 g_at9_guid[] = {0xD2, 0x42, 0xE1, 0x47, 0xBA, 0x36, 0x8D, 0x4D,
@@ -133,18 +134,22 @@ void AjmAt9Decoder::ParseRIFFHeader(std::span<u8>& in_buf, AjmInstanceGapless& g
}
}
std::tuple<u32, u32, bool> AjmAt9Decoder::ProcessData(std::span<u8>& in_buf,
SparseOutputBuffer& output,
AjmInstanceGapless& gapless) {
bool is_reset = false;
u32 AjmAt9Decoder::GetMinimumInputSize() const {
return m_superframe_bytes_remain;
}
DecoderResult AjmAt9Decoder::ProcessData(std::span<u8>& in_buf, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) {
DecoderResult result{};
if (True(m_flags & AjmAt9CodecFlags::ParseRiffHeader) &&
*reinterpret_cast<u32*>(in_buf.data()) == 'FFIR') {
ParseRIFFHeader(in_buf, gapless);
is_reset = true;
result.is_reset = true;
}
if (!m_is_initialized) {
return {0, 0, is_reset};
result.result = ORBIS_AJM_RESULT_NOT_INITIALIZED;
return result;
}
int ret = 0;
@@ -166,7 +171,14 @@ std::tuple<u32, u32, bool> AjmAt9Decoder::ProcessData(std::span<u8>& in_buf,
default:
UNREACHABLE();
}
ASSERT_MSG(ret == At9Status::ERR_SUCCESS, "Atrac9Decode failed ret = {:#x}", ret);
if (ret != At9Status::ERR_SUCCESS) {
LOG_ERROR(Lib_Ajm, "Atrac9Decode failed ret = {:#x}", ret);
result.result = ORBIS_AJM_RESULT_CODEC_ERROR | ORBIS_AJM_RESULT_FATAL;
result.internal_result = ret;
return result;
}
result.frames_decoded += 1;
in_buf = in_buf.subspan(bytes_used);
m_superframe_bytes_remain -= bytes_used;
@@ -196,10 +208,10 @@ std::tuple<u32, u32, bool> AjmAt9Decoder::ProcessData(std::span<u8>& in_buf,
UNREACHABLE();
}
const auto samples_written = pcm_written / m_codec_info.channels;
gapless.current.skipped_samples += m_codec_info.frameSamples - samples_written;
result.samples_written = pcm_written / m_codec_info.channels;
gapless.current.skipped_samples += m_codec_info.frameSamples - result.samples_written;
if (gapless.init.total_samples != 0) {
gapless.current.total_samples -= samples_written;
gapless.current.total_samples -= result.samples_written;
}
m_num_frames += 1;
@@ -209,9 +221,23 @@ std::tuple<u32, u32, bool> AjmAt9Decoder::ProcessData(std::span<u8>& in_buf,
}
m_superframe_bytes_remain = m_codec_info.superframeSize;
m_num_frames = 0;
} else if (gapless.IsEnd()) {
// Drain the remaining superframe
std::vector<s16> buf(m_codec_info.frameSamples * m_codec_info.channels, 0);
while ((m_num_frames % m_codec_info.framesInSuperframe) != 0) {
ret = Atrac9Decode(m_handle, in_buf.data(), buf.data(), &bytes_used,
True(m_flags & AjmAt9CodecFlags::NonInterleavedOutput));
in_buf = in_buf.subspan(bytes_used);
m_superframe_bytes_remain -= bytes_used;
result.frames_decoded += 1;
m_num_frames += 1;
}
in_buf = in_buf.subspan(m_superframe_bytes_remain);
m_superframe_bytes_remain = m_codec_info.superframeSize;
m_num_frames = 0;
}
return {1, m_codec_info.frameSamples, is_reset};
return result;
}
AjmSidebandFormat AjmAt9Decoder::GetFormat() const {
@@ -232,7 +258,7 @@ u32 AjmAt9Decoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const {
const auto samples =
gapless.init.total_samples != 0
? std::min<u32>(gapless.current.total_samples, m_codec_info.frameSamples - skip_samples)
: m_codec_info.frameSamples;
: m_codec_info.frameSamples - skip_samples;
return samples * m_codec_info.channels * GetPCMSize(m_format);
}

View File

@@ -3,6 +3,7 @@
#pragma once
#include "common/enum.h"
#include "common/types.h"
#include "core/libraries/ajm/ajm_instance.h"
@@ -13,8 +14,6 @@
namespace Libraries::Ajm {
constexpr s32 ORBIS_AJM_DEC_AT9_MAX_CHANNELS = 8;
enum AjmAt9CodecFlags : u32 {
ParseRiffHeader = 1 << 0,
NonInterleavedOutput = 1 << 8,
@@ -29,16 +28,17 @@ struct AjmSidebandDecAt9CodecInfo {
};
struct AjmAt9Decoder final : AjmCodec {
explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags);
explicit AjmAt9Decoder(AjmFormatEncoding format, AjmAt9CodecFlags flags, u32 channels);
~AjmAt9Decoder() override;
void Reset() override;
void Initialize(const void* buffer, u32 buffer_size) override;
void GetInfo(void* out_info) const override;
AjmSidebandFormat GetFormat() const override;
u32 GetMinimumInputSize() const override;
u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override;
std::tuple<u32, u32, bool> ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) override;
DecoderResult ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) override;
private:
template <class T>

View File

@@ -165,7 +165,7 @@ AjmJob AjmStatisticsJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buf
ASSERT(job_flags.has_value());
job.flags = job_flags.value();
AjmStatisticsJobFlags flags(job.flags);
AjmStatisticsJobFlags flags{.raw = job.flags.raw};
if (input_control_buffer.has_value()) {
AjmBatchBuffer input_batch(input_control_buffer.value());
if (True(flags.statistics_flags & AjmStatisticsFlags::Engine)) {
@@ -280,9 +280,7 @@ AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) {
job.input.resample_parameters = input_batch.Consume<AjmSidebandResampleParameters>();
}
if (True(control_flags & AjmJobControlFlags::Initialize)) {
job.input.init_params = AjmDecAt9InitializeParameters{};
std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(),
input_batch.BytesRemaining());
job.input.init_params = input_batch.Consume<AjmSidebandInitParameters>();
}
}

View File

@@ -21,7 +21,7 @@ namespace Libraries::Ajm {
struct AjmJob {
struct Input {
std::optional<AjmDecAt9InitializeParameters> init_params;
std::optional<AjmSidebandInitParameters> init_params;
std::optional<AjmSidebandResampleParameters> resample_parameters;
std::optional<AjmSidebandStatisticsEngineParameters> statistics_engine_parameters;
std::optional<AjmSidebandFormat> format;
@@ -52,6 +52,7 @@ struct AjmBatch {
u32 id{};
std::atomic_bool waiting{};
std::atomic_bool canceled{};
std::atomic_bool processed{};
std::binary_semaphore finished{0};
boost::container::small_vector<AjmJob, 16> jobs;

View File

@@ -18,7 +18,8 @@
namespace Libraries::Ajm {
static constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1;
constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1;
constexpr int INSTANCE_ID_MASK = 0x3FFF;
AjmContext::AjmContext() {
worker_thread = std::jthread([this](std::stop_token stop) { this->WorkerThread(stop); });
@@ -39,7 +40,12 @@ s32 AjmContext::BatchCancel(const u32 batch_id) {
batch = *p_batch;
}
batch->canceled = true;
if (batch->processed) {
return ORBIS_OK;
}
bool expected = false;
batch->canceled.compare_exchange_strong(expected, true);
return ORBIS_OK;
}
@@ -58,7 +64,9 @@ void AjmContext::WorkerThread(std::stop_token stop) {
Common::SetCurrentThreadName("shadPS4:AjmWorker");
while (!stop.stop_requested()) {
auto batch = batch_queue.PopWait(stop);
if (batch != nullptr) {
if (batch != nullptr && !batch->canceled) {
bool expected = false;
batch->processed.compare_exchange_strong(expected, true);
ProcessBatch(batch->id, batch->jobs);
batch->finished.release();
}
@@ -77,7 +85,7 @@ void AjmContext::ProcessBatch(u32 id, std::span<AjmJob> jobs) {
std::shared_ptr<AjmInstance> instance;
{
std::shared_lock lock(instances_mutex);
auto* p_instance = instances.Get(job.instance_id);
auto* p_instance = instances.Get(job.instance_id & INSTANCE_ID_MASK);
ASSERT_MSG(p_instance != nullptr, "Attempting to execute job on null instance");
instance = *p_instance;
}
@@ -169,15 +177,15 @@ s32 AjmContext::InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags,
if (!opt_index.has_value()) {
return ORBIS_AJM_ERROR_OUT_OF_RESOURCES;
}
*out_instance = opt_index.value();
*out_instance = opt_index.value() | (static_cast<u32>(codec_type) << 14);
LOG_INFO(Lib_Ajm, "instance = {}", *out_instance);
return ORBIS_OK;
}
s32 AjmContext::InstanceDestroy(u32 instance) {
s32 AjmContext::InstanceDestroy(u32 instance_id) {
std::unique_lock lock(instances_mutex);
if (!instances.Destroy(instance)) {
if (!instances.Destroy(instance_id & INSTANCE_ID_MASK)) {
return ORBIS_AJM_ERROR_INVALID_INSTANCE;
}
return ORBIS_OK;

View File

@@ -1,27 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/libraries/ajm/ajm_at9.h"
#include "core/libraries/ajm/ajm_instance.h"
#include "core/libraries/ajm/ajm_mp3.h"
#include "ajm_aac.h"
#include "ajm_at9.h"
#include "ajm_instance.h"
#include "ajm_mp3.h"
#include "ajm_result.h"
#include <magic_enum/magic_enum.hpp>
namespace Libraries::Ajm {
constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001;
constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002;
constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004;
constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008;
constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010;
constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020;
constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040;
constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080;
constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100;
constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200;
constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000;
constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000;
u8 GetPCMSize(AjmFormatEncoding format) {
switch (format) {
case AjmFormatEncoding::S16:
@@ -38,13 +27,18 @@ u8 GetPCMSize(AjmFormatEncoding format) {
AjmInstance::AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags) : m_flags(flags) {
switch (codec_type) {
case AjmCodecType::At9Dec: {
m_codec = std::make_unique<AjmAt9Decoder>(AjmFormatEncoding(flags.format),
AjmAt9CodecFlags(flags.codec));
m_codec = std::make_unique<AjmAt9Decoder>(
AjmFormatEncoding(flags.format), AjmAt9CodecFlags(flags.codec), u32(flags.channels));
break;
}
case AjmCodecType::Mp3Dec: {
m_codec = std::make_unique<AjmMp3Decoder>(AjmFormatEncoding(flags.format),
AjmMp3CodecFlags(flags.codec));
m_codec = std::make_unique<AjmMp3Decoder>(
AjmFormatEncoding(flags.format), AjmMp3CodecFlags(flags.codec), u32(flags.channels));
break;
}
case AjmCodecType::M4aacDec: {
m_codec = std::make_unique<AjmAacDecoder>(
AjmFormatEncoding(flags.format), AjmAacCodecFlags(flags.codec), u32(flags.channels));
break;
}
default:
@@ -60,6 +54,7 @@ void AjmInstance::Reset() {
void AjmInstance::ExecuteJob(AjmJob& job) {
const auto control_flags = job.flags.control_flags;
job.output.p_result->result = 0;
if (True(control_flags & AjmJobControlFlags::Reset)) {
LOG_TRACE(Lib_Ajm, "Resetting instance {}", job.instance_id);
Reset();
@@ -91,8 +86,7 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
m_gapless.current.total_samples -= sample_difference;
} else {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER");
job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER;
return;
job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER;
}
}
@@ -106,61 +100,59 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
m_gapless.current.skip_samples -= sample_difference;
} else {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_INVALID_PARAMETER");
job.output.p_result->result = ORBIS_AJM_RESULT_INVALID_PARAMETER;
return;
job.output.p_result->result |= ORBIS_AJM_RESULT_INVALID_PARAMETER;
}
}
}
if (!job.input.buffer.empty() && !job.output.buffers.empty()) {
std::span<u8> in_buf(job.input.buffer);
SparseOutputBuffer out_buf(job.output.buffers);
std::span<u8> in_buf(job.input.buffer);
SparseOutputBuffer out_buf(job.output.buffers);
auto in_size = in_buf.size();
auto out_size = out_buf.Size();
u32 frames_decoded = 0;
u32 frames_decoded = 0;
auto in_size = in_buf.size();
auto out_size = out_buf.Size();
while (!in_buf.empty() && !out_buf.IsEmpty() && !m_gapless.IsEnd()) {
if (!HasEnoughSpace(out_buf)) {
if (job.output.p_mframe == nullptr || frames_decoded == 0) {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})",
out_buf.Size(), m_codec->GetNextFrameSize(m_gapless));
job.output.p_result->result = ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
}
const auto [nframes, nsamples, reset] =
m_codec->ProcessData(in_buf, out_buf, m_gapless);
if (reset) {
if (!job.input.buffer.empty()) {
for (;;) {
if (m_flags.gapless_loop && m_gapless.IsEnd()) {
m_gapless.Reset();
m_total_samples = 0;
}
if (!nframes) {
LOG_WARNING(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_INITIALIZED");
job.output.p_result->result = ORBIS_AJM_RESULT_NOT_INITIALIZED;
if (!HasEnoughSpace(out_buf)) {
LOG_TRACE(Lib_Ajm, "ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM ({} < {})", out_buf.Size(),
m_codec->GetNextFrameSize(m_gapless));
job.output.p_result->result |= ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM;
}
if (in_buf.size() < m_codec->GetMinimumInputSize()) {
job.output.p_result->result |= ORBIS_AJM_RESULT_PARTIAL_INPUT;
}
if (job.output.p_result->result != 0) {
break;
}
const auto result = m_codec->ProcessData(in_buf, out_buf, m_gapless);
if (result.is_reset) {
m_total_samples = 0;
} else {
m_total_samples += result.samples_written;
}
frames_decoded += result.frames_decoded;
if (result.result != 0) {
job.output.p_result->result |= result.result;
job.output.p_result->internal_result = result.internal_result;
break;
}
frames_decoded += nframes;
m_total_samples += nsamples;
if (False(job.flags.run_flags & AjmJobRunFlags::MultipleFrames)) {
break;
}
}
}
const auto total_decoded_samples = m_total_samples;
if (m_flags.gapless_loop && m_gapless.IsEnd()) {
in_buf = in_buf.subspan(in_buf.size());
m_gapless.Reset();
m_codec->Reset();
}
if (job.output.p_mframe) {
job.output.p_mframe->num_frames = frames_decoded;
}
if (job.output.p_stream) {
job.output.p_stream->input_consumed = in_size - in_buf.size();
job.output.p_stream->output_written = out_size - out_buf.Size();
job.output.p_stream->total_decoded_samples = total_decoded_samples;
}
if (job.output.p_mframe) {
job.output.p_mframe->num_frames = frames_decoded;
}
if (job.output.p_stream) {
job.output.p_stream->input_consumed = in_size - in_buf.size();
job.output.p_stream->output_written = out_size - out_buf.Size();
job.output.p_stream->total_decoded_samples = m_total_samples;
}
if (job.output.p_format != nullptr) {
@@ -175,6 +167,9 @@ void AjmInstance::ExecuteJob(AjmJob& job) {
}
bool AjmInstance::HasEnoughSpace(const SparseOutputBuffer& output) const {
if (m_gapless.IsEnd()) {
return true;
}
return output.Size() >= m_codec->GetNextFrameSize(m_gapless);
}

View File

@@ -73,6 +73,14 @@ struct AjmInstanceGapless {
}
};
struct DecoderResult {
s32 result = 0;
s32 internal_result = 0;
u32 frames_decoded = 0;
u32 samples_written = 0;
bool is_reset = false;
};
class AjmCodec {
public:
virtual ~AjmCodec() = default;
@@ -81,9 +89,10 @@ public:
virtual void Reset() = 0;
virtual void GetInfo(void* out_info) const = 0;
virtual AjmSidebandFormat GetFormat() const = 0;
virtual u32 GetMinimumInputSize() const = 0;
virtual u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const = 0;
virtual std::tuple<u32, u32, bool> ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) = 0;
virtual DecoderResult ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) = 0;
};
class AjmInstance {
@@ -94,7 +103,6 @@ public:
private:
bool HasEnoughSpace(const SparseOutputBuffer& output) const;
std::optional<u32> GetNumRemainingSamples() const;
void Reset();
AjmInstanceFlags m_flags{};

View File

@@ -8,7 +8,7 @@ namespace Libraries::Ajm {
void AjmInstanceStatistics::ExecuteJob(AjmJob& job) {
if (job.output.p_engine) {
job.output.p_engine->usage_batch = 0.01;
job.output.p_engine->usage_batch = 0.05;
const auto ic = job.input.statistics_engine_parameters->interval_count;
for (u32 idx = 0; idx < ic; ++idx) {
job.output.p_engine->usage_interval[idx] = 0.01;
@@ -25,10 +25,12 @@ void AjmInstanceStatistics::ExecuteJob(AjmJob& job) {
job.output.p_memory->batch_size = 0x4200;
job.output.p_memory->input_size = 0x2000;
job.output.p_memory->output_size = 0x2000;
job.output.p_memory->small_size = 0x200;
job.output.p_memory->small_size = 0x400;
}
}
void AjmInstanceStatistics::Reset() {}
AjmInstanceStatistics& AjmInstanceStatistics::Getinstance() {
static AjmInstanceStatistics instance;
return instance;

View File

@@ -10,6 +10,7 @@ namespace Libraries::Ajm {
class AjmInstanceStatistics {
public:
void ExecuteJob(AjmJob& job);
void Reset();
static AjmInstanceStatistics& Getinstance();
};

View File

@@ -105,7 +105,7 @@ AVFrame* AjmMp3Decoder::ConvertAudioFrame(AVFrame* frame) {
return new_frame;
}
AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags)
AjmMp3Decoder::AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags, u32)
: m_format(format), m_flags(flags), m_codec(avcodec_find_decoder(AV_CODEC_ID_MP3)),
m_codec_context(avcodec_alloc_context3(m_codec)), m_parser(av_parser_init(m_codec->id)) {
int ret = avcodec_open2(m_codec_context, m_codec, nullptr);
@@ -122,6 +122,7 @@ void AjmMp3Decoder::Reset() {
avcodec_flush_buffers(m_codec_context);
m_header.reset();
m_frame_samples = 0;
m_frame_size = 0;
}
void AjmMp3Decoder::GetInfo(void* out_info) const {
@@ -138,16 +139,28 @@ void AjmMp3Decoder::GetInfo(void* out_info) const {
}
}
std::tuple<u32, u32, bool> AjmMp3Decoder::ProcessData(std::span<u8>& in_buf,
SparseOutputBuffer& output,
AjmInstanceGapless& gapless) {
u32 AjmMp3Decoder::GetMinimumInputSize() const {
// 4 bytes is for mp3 header that contains frame_size
return std::max<u32>(m_frame_size, 4);
}
DecoderResult AjmMp3Decoder::ProcessData(std::span<u8>& in_buf, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) {
DecoderResult result{};
AVPacket* pkt = av_packet_alloc();
if ((!m_header.has_value() || m_frame_samples == 0) && in_buf.size() >= 4) {
m_header = std::byteswap(*reinterpret_cast<u32*>(in_buf.data()));
AjmDecMp3ParseFrame info{};
ParseMp3Header(in_buf.data(), in_buf.size(), false, &info);
ParseMp3Header(in_buf.data(), in_buf.size(), true, &info);
m_frame_samples = info.samples_per_channel;
m_frame_size = info.frame_size;
gapless.init = {
.total_samples = info.total_samples,
.skip_samples = static_cast<u16>(info.encoder_delay),
.skipped_samples = 0,
};
gapless.current = gapless.init;
}
int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(),
@@ -155,9 +168,6 @@ std::tuple<u32, u32, bool> AjmMp3Decoder::ProcessData(std::span<u8>& in_buf,
ASSERT_MSG(ret >= 0, "Error while parsing {}", ret);
in_buf = in_buf.subspan(ret);
u32 frames_decoded = 0;
u32 samples_decoded = 0;
if (pkt->size) {
// Send the packet with the compressed data to the decoder
pkt->pts = m_parser->pts;
@@ -177,9 +187,8 @@ std::tuple<u32, u32, bool> AjmMp3Decoder::ProcessData(std::span<u8>& in_buf,
UNREACHABLE_MSG("Error during decoding");
}
frame = ConvertAudioFrame(frame);
samples_decoded += u32(frame->nb_samples);
frames_decoded += 1;
result.frames_decoded += 1;
u32 skip_samples = 0;
if (gapless.current.skip_samples > 0) {
skip_samples = std::min(u16(frame->nb_samples), gapless.current.skip_samples);
@@ -211,6 +220,7 @@ std::tuple<u32, u32, bool> AjmMp3Decoder::ProcessData(std::span<u8>& in_buf,
if (gapless.init.total_samples != 0) {
gapless.current.total_samples -= samples;
}
result.samples_written += samples;
av_frame_free(&frame);
}
@@ -218,16 +228,16 @@ std::tuple<u32, u32, bool> AjmMp3Decoder::ProcessData(std::span<u8>& in_buf,
av_packet_free(&pkt);
return {frames_decoded, samples_decoded, false};
return result;
}
u32 AjmMp3Decoder::GetNextFrameSize(const AjmInstanceGapless& gapless) const {
const auto max_samples = gapless.init.total_samples != 0
? std::min(gapless.current.total_samples, m_frame_samples)
: m_frame_samples;
const auto skip_samples = std::min(u32(gapless.current.skip_samples), max_samples);
return (max_samples - skip_samples) * m_codec_context->ch_layout.nb_channels *
GetPCMSize(m_format);
const auto skip_samples = std::min<u32>(gapless.current.skip_samples, m_frame_samples);
const auto samples =
gapless.init.total_samples != 0
? std::min<u32>(gapless.current.total_samples, m_frame_samples - skip_samples)
: m_frame_samples - skip_samples;
return samples * m_codec_context->ch_layout.nb_channels * GetPCMSize(m_format);
}
class BitReader {
@@ -264,7 +274,7 @@ private:
int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_ofl,
AjmDecMp3ParseFrame* frame) {
LOG_INFO(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, parse_ofl);
LOG_TRACE(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, parse_ofl);
if (p_begin == nullptr || stream_size < 4 || frame == nullptr) {
return ORBIS_AJM_ERROR_INVALID_PARAMETER;
@@ -301,7 +311,8 @@ int AjmMp3Decoder::ParseMp3Header(const u8* p_begin, u32 stream_size, int parse_
BitReader reader(p_current);
if (header->protection_type == 0) {
reader.Skip(16); // crc = reader.Read<u16>(16);
// crc = reader.Read<u16>(16);
reader.Skip(16);
}
if (header->version == Mp3AudioVersion::V1) {

View File

@@ -3,6 +3,7 @@
#pragma once
#include "common/enum.h"
#include "common/types.h"
#include "core/libraries/ajm/ajm_instance.h"
@@ -63,16 +64,17 @@ struct AjmSidebandDecMp3CodecInfo {
class AjmMp3Decoder : public AjmCodec {
public:
explicit AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags);
explicit AjmMp3Decoder(AjmFormatEncoding format, AjmMp3CodecFlags flags, u32 channels);
~AjmMp3Decoder() override;
void Reset() override;
void Initialize(const void* buffer, u32 buffer_size) override {}
void GetInfo(void* out_info) const override;
AjmSidebandFormat GetFormat() const override;
u32 GetMinimumInputSize() const override;
u32 GetNextFrameSize(const AjmInstanceGapless& gapless) const override;
std::tuple<u32, u32, bool> ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) override;
DecoderResult ProcessData(std::span<u8>& input, SparseOutputBuffer& output,
AjmInstanceGapless& gapless) override;
static int ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl,
AjmDecMp3ParseFrame* frame);
@@ -97,6 +99,7 @@ private:
SwrContext* m_swr_context = nullptr;
std::optional<u32> m_header;
u32 m_frame_samples = 0;
u32 m_frame_size = 0;
};
} // namespace Libraries::Ajm

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001;
constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002;
constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004;
constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008;
constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010;
constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020;
constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040;
constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080;
constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100;
constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200;
constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000;
constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000;

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 <cmath>
@@ -345,7 +345,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);
@@ -189,7 +189,7 @@ s32 PS4_SYSV_ABI sceAudio3dDeleteSpeakerArray() {
s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters* params) {
LOG_DEBUG(Lib_Audio3d, "called");
if (params) {
*params = OrbisAudio3dOpenParameters{
auto default_params = OrbisAudio3dOpenParameters{
.size_this = 0x20,
.granularity = 0x100,
.rate = OrbisAudio3dRate::ORBIS_AUDIO3D_RATE_48000,
@@ -197,6 +197,7 @@ s32 PS4_SYSV_ABI sceAudio3dGetDefaultOpenParameters(OrbisAudio3dOpenParameters*
.queue_depth = 2,
.buffer_mode = OrbisAudio3dBufferMode::ORBIS_AUDIO3D_BUFFER_ADVANCE_AND_PUSH,
};
memcpy(params, &default_params, 0x20);
}
return ORBIS_OK;
}
@@ -421,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,
@@ -445,7 +446,7 @@ s32 PS4_SYSV_ABI sceAudio3dPortOpen(const OrbisUserServiceUserId user_id,
}
*port_id = id;
std::memcpy(&state->ports[id].parameters, parameters, sizeof(OrbisAudio3dOpenParameters));
std::memcpy(&state->ports[id].parameters, parameters, parameters->size_this);
return ORBIS_OK;
}

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

@@ -30,7 +30,15 @@ AvPlayerSourceType GetSourceType(std::string_view path) {
}
// schema://server.domain/path/to/file.ext/and/beyond -> .ext/and/beyond
auto ext = name.substr(name.rfind('.'));
// Find extension dot
auto dot_pos = name.rfind('.');
if (dot_pos == std::string_view::npos) {
return AvPlayerSourceType::Unknown;
}
// Extract extension (".ext/anything" or ".ext")
auto ext = name.substr(dot_pos);
if (ext.empty()) {
return AvPlayerSourceType::Unknown;
}

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,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "fiber.h"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::Font {
struct OrbisFontTextCharacter {
// Other fields...
struct OrbisFontTextCharacter* next; // Pointer to the next node 0x00
struct OrbisFontTextCharacter* prev; // Pointer to the next node 0x08
void* textOrder; // Field at offset 0x10 (pointer to text order info)
u32 characterCode; // Field assumed at offset 0x28
u8 unkn_0x31; // Offset 0x31
u8 unkn_0x33; // Offset 0x33
u8 charType; // Field assumed at offset 0x39
u8 bidiLevel; // Field assumed at offset 0x3B stores the Bidi level
u8 formatFlags; // Field at offset 0x3D (stores format-related flags)
};
struct OrbisFontRenderSurface {
void* buffer;
s32 widthByte;
s8 pixelSizeByte;
u8 unkn_0xd;
u8 styleFlag;
u8 unkn_0xf;
s32 width, height;
u32 sc_x0;
u32 sc_y0;
u32 sc_x1;
u32 sc_y1;
void* unkn_28[3];
};
struct OrbisFontStyleFrame {
/*0x00*/ u16 magic; // Expected to be 0xF09
/*0x02*/ u16 flags;
/*0x04*/ s32 dpiX; // DPI scaling factor for width
/*0x08*/ s32 dpiY; // DPI scaling factor for height
/*0x0c*/ s32 scalingFlag; // Indicates whether scaling is enabled
/*0x10*/
/*0x14*/ float scaleWidth; // Width scaling factor
/*0x18*/ float scaleHeight; // Height scaling factor
/*0x1c*/ float weightXScale;
/*0x20*/ float weightYScale;
/*0x24*/ float slantRatio;
};
s32 PS4_SYSV_ABI sceFontAttachDeviceCacheBuffer();
s32 PS4_SYSV_ABI sceFontBindRenderer();
s32 PS4_SYSV_ABI sceFontCharacterGetBidiLevel(OrbisFontTextCharacter* textCharacter,
int* bidiLevel);
s32 PS4_SYSV_ABI sceFontCharacterGetSyllableStringState();
s32 PS4_SYSV_ABI sceFontCharacterGetTextFontCode();
s32 PS4_SYSV_ABI sceFontCharacterGetTextOrder(OrbisFontTextCharacter* textCharacter,
void** pTextOrder);
u32 PS4_SYSV_ABI sceFontCharacterLooksFormatCharacters(OrbisFontTextCharacter* textCharacter);
u32 PS4_SYSV_ABI sceFontCharacterLooksWhiteSpace(OrbisFontTextCharacter* textCharacter);
OrbisFontTextCharacter* PS4_SYSV_ABI
sceFontCharacterRefersTextBack(OrbisFontTextCharacter* textCharacter);
OrbisFontTextCharacter* PS4_SYSV_ABI
sceFontCharacterRefersTextNext(OrbisFontTextCharacter* textCharacter);
s32 PS4_SYSV_ABI sceFontCharactersRefersTextCodes();
s32 PS4_SYSV_ABI sceFontClearDeviceCache();
s32 PS4_SYSV_ABI sceFontCloseFont();
s32 PS4_SYSV_ABI sceFontControl();
s32 PS4_SYSV_ABI sceFontCreateGraphicsDevice();
s32 PS4_SYSV_ABI sceFontCreateGraphicsService();
s32 PS4_SYSV_ABI sceFontCreateGraphicsServiceWithEdition();
s32 PS4_SYSV_ABI sceFontCreateLibrary();
s32 PS4_SYSV_ABI sceFontCreateLibraryWithEdition();
s32 PS4_SYSV_ABI sceFontCreateRenderer();
s32 PS4_SYSV_ABI sceFontCreateRendererWithEdition();
s32 PS4_SYSV_ABI sceFontCreateString();
s32 PS4_SYSV_ABI sceFontCreateWords();
s32 PS4_SYSV_ABI sceFontCreateWritingLine();
s32 PS4_SYSV_ABI sceFontDefineAttribute();
s32 PS4_SYSV_ABI sceFontDeleteGlyph();
s32 PS4_SYSV_ABI sceFontDestroyGraphicsDevice();
s32 PS4_SYSV_ABI sceFontDestroyGraphicsService();
s32 PS4_SYSV_ABI sceFontDestroyLibrary();
s32 PS4_SYSV_ABI sceFontDestroyRenderer();
s32 PS4_SYSV_ABI sceFontDestroyString();
s32 PS4_SYSV_ABI sceFontDestroyWords();
s32 PS4_SYSV_ABI sceFontDestroyWritingLine();
s32 PS4_SYSV_ABI sceFontDettachDeviceCacheBuffer();
s32 PS4_SYSV_ABI sceFontGenerateCharGlyph();
s32 PS4_SYSV_ABI sceFontGetAttribute();
s32 PS4_SYSV_ABI sceFontGetCharGlyphCode();
s32 PS4_SYSV_ABI sceFontGetCharGlyphMetrics();
s32 PS4_SYSV_ABI sceFontGetEffectSlant();
s32 PS4_SYSV_ABI sceFontGetEffectWeight();
s32 PS4_SYSV_ABI sceFontGetFontGlyphsCount();
s32 PS4_SYSV_ABI sceFontGetFontGlyphsOutlineProfile();
s32 PS4_SYSV_ABI sceFontGetFontMetrics();
s32 PS4_SYSV_ABI sceFontGetFontResolution();
s32 PS4_SYSV_ABI sceFontGetFontStyleInformation();
s32 PS4_SYSV_ABI sceFontGetGlyphExpandBufferState();
s32 PS4_SYSV_ABI sceFontGetHorizontalLayout();
s32 PS4_SYSV_ABI sceFontGetKerning();
s32 PS4_SYSV_ABI sceFontGetLibrary();
s32 PS4_SYSV_ABI sceFontGetPixelResolution();
s32 PS4_SYSV_ABI sceFontGetRenderCharGlyphMetrics();
s32 PS4_SYSV_ABI sceFontGetRenderEffectSlant();
s32 PS4_SYSV_ABI sceFontGetRenderEffectWeight();
s32 PS4_SYSV_ABI sceFontGetRenderScaledKerning();
s32 PS4_SYSV_ABI sceFontGetRenderScalePixel();
s32 PS4_SYSV_ABI sceFontGetRenderScalePoint();
s32 PS4_SYSV_ABI sceFontGetResolutionDpi();
s32 PS4_SYSV_ABI sceFontGetScalePixel();
s32 PS4_SYSV_ABI sceFontGetScalePoint();
s32 PS4_SYSV_ABI sceFontGetScriptLanguage();
s32 PS4_SYSV_ABI sceFontGetTypographicDesign();
s32 PS4_SYSV_ABI sceFontGetVerticalLayout();
s32 PS4_SYSV_ABI sceFontGlyphDefineAttribute();
s32 PS4_SYSV_ABI sceFontGlyphGetAttribute();
s32 PS4_SYSV_ABI sceFontGlyphGetGlyphForm();
s32 PS4_SYSV_ABI sceFontGlyphGetMetricsForm();
s32 PS4_SYSV_ABI sceFontGlyphGetScalePixel();
s32 PS4_SYSV_ABI sceFontGlyphRefersMetrics();
s32 PS4_SYSV_ABI sceFontGlyphRefersMetricsHorizontal();
s32 PS4_SYSV_ABI sceFontGlyphRefersMetricsHorizontalAdvance();
s32 PS4_SYSV_ABI sceFontGlyphRefersMetricsHorizontalX();
s32 PS4_SYSV_ABI sceFontGlyphRefersOutline();
s32 PS4_SYSV_ABI sceFontGlyphRenderImage();
s32 PS4_SYSV_ABI sceFontGlyphRenderImageHorizontal();
s32 PS4_SYSV_ABI sceFontGlyphRenderImageVertical();
s32 PS4_SYSV_ABI sceFontGraphicsBeginFrame();
s32 PS4_SYSV_ABI sceFontGraphicsDrawingCancel();
s32 PS4_SYSV_ABI sceFontGraphicsDrawingFinish();
s32 PS4_SYSV_ABI sceFontGraphicsEndFrame();
s32 PS4_SYSV_ABI sceFontGraphicsExchangeResource();
s32 PS4_SYSV_ABI sceFontGraphicsFillMethodInit();
s32 PS4_SYSV_ABI sceFontGraphicsFillPlotInit();
s32 PS4_SYSV_ABI sceFontGraphicsFillPlotSetLayout();
s32 PS4_SYSV_ABI sceFontGraphicsFillPlotSetMapping();
s32 PS4_SYSV_ABI sceFontGraphicsFillRatesInit();
s32 PS4_SYSV_ABI sceFontGraphicsFillRatesSetFillEffect();
s32 PS4_SYSV_ABI sceFontGraphicsFillRatesSetLayout();
s32 PS4_SYSV_ABI sceFontGraphicsFillRatesSetMapping();
s32 PS4_SYSV_ABI sceFontGraphicsGetDeviceUsage();
s32 PS4_SYSV_ABI sceFontGraphicsRegionInit();
s32 PS4_SYSV_ABI sceFontGraphicsRegionInitCircular();
s32 PS4_SYSV_ABI sceFontGraphicsRegionInitRoundish();
s32 PS4_SYSV_ABI sceFontGraphicsRelease();
s32 PS4_SYSV_ABI sceFontGraphicsRenderResource();
s32 PS4_SYSV_ABI sceFontGraphicsSetFramePolicy();
s32 PS4_SYSV_ABI sceFontGraphicsSetupClipping();
s32 PS4_SYSV_ABI sceFontGraphicsSetupColorRates();
s32 PS4_SYSV_ABI sceFontGraphicsSetupFillMethod();
s32 PS4_SYSV_ABI sceFontGraphicsSetupFillRates();
s32 PS4_SYSV_ABI sceFontGraphicsSetupGlyphFill();
s32 PS4_SYSV_ABI sceFontGraphicsSetupGlyphFillPlot();
s32 PS4_SYSV_ABI sceFontGraphicsSetupHandleDefault();
s32 PS4_SYSV_ABI sceFontGraphicsSetupLocation();
s32 PS4_SYSV_ABI sceFontGraphicsSetupPositioning();
s32 PS4_SYSV_ABI sceFontGraphicsSetupRotation();
s32 PS4_SYSV_ABI sceFontGraphicsSetupScaling();
s32 PS4_SYSV_ABI sceFontGraphicsSetupShapeFill();
s32 PS4_SYSV_ABI sceFontGraphicsSetupShapeFillPlot();
s32 PS4_SYSV_ABI sceFontGraphicsStructureCanvas();
s32 PS4_SYSV_ABI sceFontGraphicsStructureCanvasSequence();
s32 PS4_SYSV_ABI sceFontGraphicsStructureDesign();
s32 PS4_SYSV_ABI sceFontGraphicsStructureDesignResource();
s32 PS4_SYSV_ABI sceFontGraphicsStructureSurfaceTexture();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateClipping();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateColorRates();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateFillMethod();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateFillRates();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateGlyphFill();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateGlyphFillPlot();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateLocation();
s32 PS4_SYSV_ABI sceFontGraphicsUpdatePositioning();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateRotation();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateScaling();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateShapeFill();
s32 PS4_SYSV_ABI sceFontGraphicsUpdateShapeFillPlot();
s32 PS4_SYSV_ABI sceFontMemoryInit();
s32 PS4_SYSV_ABI sceFontMemoryTerm();
s32 PS4_SYSV_ABI sceFontOpenFontFile();
s32 PS4_SYSV_ABI sceFontOpenFontInstance();
s32 PS4_SYSV_ABI sceFontOpenFontMemory();
s32 PS4_SYSV_ABI sceFontOpenFontSet();
s32 PS4_SYSV_ABI sceFontRebindRenderer();
s32 PS4_SYSV_ABI sceFontRenderCharGlyphImage();
s32 PS4_SYSV_ABI sceFontRenderCharGlyphImageHorizontal();
s32 PS4_SYSV_ABI sceFontRenderCharGlyphImageVertical();
s32 PS4_SYSV_ABI sceFontRendererGetOutlineBufferSize();
s32 PS4_SYSV_ABI sceFontRendererResetOutlineBuffer();
s32 PS4_SYSV_ABI sceFontRendererSetOutlineBufferPolicy();
void PS4_SYSV_ABI sceFontRenderSurfaceInit(OrbisFontRenderSurface* renderSurface, void* buffer,
int bufWidthByte, int pixelSizeByte, int widthPixel,
int heightPixel);
void PS4_SYSV_ABI sceFontRenderSurfaceSetScissor(OrbisFontRenderSurface* renderSurface, int x0,
int y0, int w, int h);
s32 PS4_SYSV_ABI sceFontRenderSurfaceSetStyleFrame(OrbisFontRenderSurface* renderSurface,
OrbisFontStyleFrame* styleFrame);
s32 PS4_SYSV_ABI sceFontSetEffectSlant();
s32 PS4_SYSV_ABI sceFontSetEffectWeight();
s32 PS4_SYSV_ABI sceFontSetFontsOpenMode();
s32 PS4_SYSV_ABI sceFontSetResolutionDpi();
s32 PS4_SYSV_ABI sceFontSetScalePixel();
s32 PS4_SYSV_ABI sceFontSetScalePoint();
s32 PS4_SYSV_ABI sceFontSetScriptLanguage();
s32 PS4_SYSV_ABI sceFontSetTypographicDesign();
s32 PS4_SYSV_ABI sceFontSetupRenderEffectSlant();
s32 PS4_SYSV_ABI sceFontSetupRenderEffectWeight();
s32 PS4_SYSV_ABI sceFontSetupRenderScalePixel();
s32 PS4_SYSV_ABI sceFontSetupRenderScalePoint();
s32 PS4_SYSV_ABI sceFontStringGetTerminateCode();
s32 PS4_SYSV_ABI sceFontStringGetTerminateOrder();
s32 PS4_SYSV_ABI sceFontStringGetWritingForm();
s32 PS4_SYSV_ABI sceFontStringRefersRenderCharacters();
s32 PS4_SYSV_ABI sceFontStringRefersTextCharacters();
s32 PS4_SYSV_ABI sceFontStyleFrameGetEffectSlant(OrbisFontStyleFrame* styleFrame,
float* slantRatio);
s32 PS4_SYSV_ABI sceFontStyleFrameGetEffectWeight(OrbisFontStyleFrame* fontStyleFrame,
float* weightXScale, float* weightYScale,
uint32_t* mode);
s32 PS4_SYSV_ABI sceFontStyleFrameGetResolutionDpi();
s32 PS4_SYSV_ABI sceFontStyleFrameGetScalePixel(OrbisFontStyleFrame* styleFrame, float* w,
float* h);
s32 PS4_SYSV_ABI sceFontStyleFrameGetScalePoint();
s32 PS4_SYSV_ABI sceFontStyleFrameInit();
s32 PS4_SYSV_ABI sceFontStyleFrameSetEffectSlant();
s32 PS4_SYSV_ABI sceFontStyleFrameSetEffectWeight();
s32 PS4_SYSV_ABI sceFontStyleFrameSetResolutionDpi();
s32 PS4_SYSV_ABI sceFontStyleFrameSetScalePixel();
s32 PS4_SYSV_ABI sceFontStyleFrameSetScalePoint();
s32 PS4_SYSV_ABI sceFontStyleFrameUnsetEffectSlant();
s32 PS4_SYSV_ABI sceFontStyleFrameUnsetEffectWeight();
s32 PS4_SYSV_ABI sceFontStyleFrameUnsetScale();
s32 PS4_SYSV_ABI sceFontSupportExternalFonts();
s32 PS4_SYSV_ABI sceFontSupportGlyphs();
s32 PS4_SYSV_ABI sceFontSupportSystemFonts();
s32 PS4_SYSV_ABI sceFontTextCodesStepBack();
s32 PS4_SYSV_ABI sceFontTextCodesStepNext();
s32 PS4_SYSV_ABI sceFontTextSourceInit();
s32 PS4_SYSV_ABI sceFontTextSourceRewind();
s32 PS4_SYSV_ABI sceFontTextSourceSetDefaultFont();
s32 PS4_SYSV_ABI sceFontTextSourceSetWritingForm();
s32 PS4_SYSV_ABI sceFontUnbindRenderer();
s32 PS4_SYSV_ABI sceFontWordsFindWordCharacters();
s32 PS4_SYSV_ABI sceFontWritingGetRenderMetrics();
s32 PS4_SYSV_ABI sceFontWritingInit();
s32 PS4_SYSV_ABI sceFontWritingLineClear();
s32 PS4_SYSV_ABI sceFontWritingLineGetOrderingSpace();
s32 PS4_SYSV_ABI sceFontWritingLineGetRenderMetrics();
s32 PS4_SYSV_ABI sceFontWritingLineRefersRenderStep();
s32 PS4_SYSV_ABI sceFontWritingLineWritesOrder();
s32 PS4_SYSV_ABI sceFontWritingRefersRenderStep();
s32 PS4_SYSV_ABI sceFontWritingRefersRenderStepCharacter();
s32 PS4_SYSV_ABI sceFontWritingSetMaskInvisible();
s32 PS4_SYSV_ABI Func_00F4D778F1C88CB3();
s32 PS4_SYSV_ABI Func_03C650025FBB0DE7();
s32 PS4_SYSV_ABI Func_07EAB8A163B27E1A();
s32 PS4_SYSV_ABI Func_09408E88E4F97CE3();
s32 PS4_SYSV_ABI Func_09F92905ED82A814();
s32 PS4_SYSV_ABI Func_0D142CEE1AB21ABE();
s32 PS4_SYSV_ABI Func_14BD2E9E119C16F2();
s32 PS4_SYSV_ABI Func_1AC53C9EDEAE8D75();
s32 PS4_SYSV_ABI Func_1D401185D5E24C3D();
s32 PS4_SYSV_ABI Func_1E83CD20C2CC996F();
s32 PS4_SYSV_ABI Func_314B1F765B9FE78A();
s32 PS4_SYSV_ABI Func_350E6725FEDE29E1();
s32 PS4_SYSV_ABI Func_3DB773F0A604BF39();
s32 PS4_SYSV_ABI Func_4FF49DD21E311B1C();
s32 PS4_SYSV_ABI Func_526287664A493981();
s32 PS4_SYSV_ABI Func_55CA718DBC84A6E9();
s32 PS4_SYSV_ABI Func_563FC5F0706A8B4D();
s32 PS4_SYSV_ABI Func_569E2ECD34290F45();
s32 PS4_SYSV_ABI Func_5A04775B6BE47685();
s32 PS4_SYSV_ABI Func_5FD93BCAB6F79750();
s32 PS4_SYSV_ABI Func_62B5398F864BD3B4();
s32 PS4_SYSV_ABI Func_6F9010294D822367();
s32 PS4_SYSV_ABI Func_7757E947423A7A67();
s32 PS4_SYSV_ABI Func_7E06BA52077F54FA();
s32 PS4_SYSV_ABI Func_93B36DEA021311D6();
s32 PS4_SYSV_ABI Func_94B0891E7111598A();
s32 PS4_SYSV_ABI Func_9785C9128C2FE7CD();
s32 PS4_SYSV_ABI Func_97DFBC9B65FBC0E1();
s32 PS4_SYSV_ABI Func_ACD9717405D7D3CA();
s32 PS4_SYSV_ABI Func_B19A8AEC3FD4F16F();
s32 PS4_SYSV_ABI Func_C10F488AD7CF103D();
s32 PS4_SYSV_ABI Func_D0C8B5FF4A6826C7();
s32 PS4_SYSV_ABI Func_E48D3CD01C342A33();
s32 PS4_SYSV_ABI Func_EAC96B2186B71E14();
s32 PS4_SYSV_ABI Func_FE4788A96EF46256();
s32 PS4_SYSV_ABI Func_FE7E5AE95D3058F5();
void RegisterlibSceFont(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Font

View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/libraries/error_codes.h"
constexpr int ORBIS_FONT_ERROR_FATAL = 0x80460001;
constexpr int ORBIS_FONT_ERROR_INVALID_PARAMETER = 0x80460002;
constexpr int ORBIS_FONT_ERROR_INVALID_MEMORY = 0x80460003;
constexpr int ORBIS_FONT_ERROR_INVALID_LIBRARY = 0x80460004;
constexpr int ORBIS_FONT_ERROR_INVALID_FONT_HANDLE = 0x80460005;
constexpr int ORBIS_FONT_ERROR_INVALID_GLYPH = 0x80460006;
constexpr int ORBIS_FONT_ERROR_INVALID_RENDERER = 0x80460007;
constexpr int ORBIS_FONT_ERROR_INVALID_TEXT_SOURCE = 0x80460008;
constexpr int ORBIS_FONT_ERROR_INVALID_STRING = 0x80460009;
constexpr int ORBIS_FONT_ERROR_INVALID_WRITING = 0x8046000A;
constexpr int ORBIS_FONT_ERROR_INVALID_WORDS = 0x8046000B;
constexpr int ORBIS_FONT_ERROR_ALLOCATION_FAILED = 0x80460010;
constexpr int ORBIS_FONT_ERROR_FS_OPEN_FAILED = 0x80460011;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_LIBRARY = 0x80460018;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_FORMAT = 0x80460019;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_FUNCTION = 0x80460020;
constexpr int ORBIS_FONT_ERROR_ALREADY_SPECIFIED = 0x80460021;
constexpr int ORBIS_FONT_ERROR_ALREADY_ATTACHED = 0x80460022;
constexpr int ORBIS_FONT_ERROR_ALREADY_OPENED = 0x80460023;
constexpr int ORBIS_FONT_ERROR_NOT_ATTACHED_CACHE_BUFFER = 0x80460025;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_FONTSET = 0x80460031;
constexpr int ORBIS_FONT_ERROR_FONT_OPEN_MAX = 0x80460033;
constexpr int ORBIS_FONT_ERROR_FONT_OPEN_FAILED = 0x80460036;
constexpr int ORBIS_FONT_ERROR_FONT_CLOSE_FAILED = 0x80460037;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_TYPOGRAPHY = 0x80460040;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_CODE = 0x80460041;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_GLYPH = 0x80460042;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_SCRIPT = 0x80460043;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_LANGUAGE = 0x80460044;
constexpr int ORBIS_FONT_ERROR_NO_SUPPORT_SURFACE = 0x80460050;
constexpr int ORBIS_FONT_ERROR_UNSET_PARAMETER = 0x80460058;
constexpr int ORBIS_FONT_ERROR_FUNCTIONAL_LIMIT = 0x8046005C;
constexpr int ORBIS_FONT_ERROR_ALREADY_BOUND_RENDERER = 0x80460060;
constexpr int ORBIS_FONT_ERROR_NOT_BOUND_RENDERER = 0x80460061;
constexpr int ORBIS_FONT_ERROR_RENDERER_ALLOCATION_FAILED = 0x80460063;
constexpr int ORBIS_FONT_ERROR_RENDERER_ALLOCATION_LIMITED = 0x80460064;
constexpr int ORBIS_FONT_ERROR_RENDERER_RENDER_FAILED = 0x80460065;

Some files were not shown because too many files have changed in this diff Show More