Compare commits

..

69 Commits

Author SHA1 Message Date
xbzk 95485b01a0 header update
Signed-off-by: xbzk <xbzk@eden-emu.dev>
2026-02-23 12:33:11 +01:00
xbzk 991512af4c Update src/video_core/dma_pusher.cpp
Upon investigating ender magnolia crashes i need to add lots of instrumentation in the "if (header.size > 0) {...}" block (between headers(...) and ProcessCommands(...)) for both safe and unsafe paths. The old way i need to add same instrumentation twice, always ending up maintaining only one path, and when i realize i need to test the other, i have to update/copy/move instrumentation between methods.

This new writing preserves exactly the same logic, but gathers both paths into a single one, and by no means should have performance impact.
And it's more elegant too. You're welcome ^^.

We must let the compiler do the hard work for us, right?

Feel free to blame!

Signed-off-by: xbzk <xbzk@eden-emu.dev>
2026-02-23 12:33:11 +01:00
xbzk 963f9981ce [chore] Resolve more "warning C4127: conditional expression is constant" while building under MSVC (#3595)
NOISY warning on NINJA MSVC build log.
Complement for PR 3567. Now in another place.

D:\dev\eden\src\dynarmic\src\dynarmic\backend\x64\emit_x64_vector_floating_point.cpp(207): warning C4127: conditional expression is constant
D:\dev\eden\src\dynarmic\src\dynarmic\backend\x64\emit_x64_vector_floating_point.cpp(207): note: consider using 'if constexpr' statement instead

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

Co-authored-by: DraVee <dravee@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3595
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-23 06:40:42 +01:00
crueter 89dd600790 [cmake] Comment out YUZU_USE_QT_MULTIMEDIA usages (#3616)
This option is currently dead code. Let's not use it. In the future we
should definitely update it. (or not, not a single soul used this beforehand).

See: #3468

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3616
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-23 03:47:25 +01:00
crueter eb57e11f42 [cmake] Delete YUZU_USE_CPM forever (#3615)
This option is really confusing, and has long since outlived its
originally intended purpose.

Goodbye forever!

Closes #3467

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3615
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-23 03:47:19 +01:00
crueter 45eb9cfcee [desktop] Reset speed mode on game shutdown (#3613)
Closes #3602

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3613
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-23 02:52:12 +01:00
lizzie 0a687b82d4 [crypto] Atomize all traces of MbedTLS, and require OpenSSL 3+ (#3606)
Closes #3137
Closes #3465

- Replace all mbedtls usage with OpenSSL
- require OpenSSL
- Up OpenSSL version to 3, cuz that's what we actually need...

CAVEATS:
- httplib also now required
- other ssl backends for svc are unused, maybe remove later
  * To be fair, our CI never used them anyways. And we never tested those

TESTERS PLEASE TEST:
- All games and applets boot
- Boot, load, exit, etc. times

Co-authored-by: crueter <crueter@eden-emu.dev>
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Co-authored-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3606
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-23 02:50:13 +01:00
crueter 80d6172084 [desktop] Display currently applied update in tooltip alongside playtime (#3611)
Also nicely formats both it and play time, alongside fallbacks if they
have nothing

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3611
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-02-23 02:03:30 +01:00
crueter d8b2971a2e [desktop] Fix game list name column starting out super small by default (#3610)
Unsure why but geometry is at 100x30 when this func is called. For now
just use sane ish default of 840

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3610
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-23 00:20:14 +01:00
crueter 12157214b6 [externals] Update FFmpeg to c7b5f1537d (#3604)
Fixes some Vulkan VP9 thing

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3604
Reviewed-by: DraVee <dravee@eden-emu.dev>
2026-02-22 23:28:46 +01:00
crueter 0574d7d2c9 [desktop] Fix mod import for romfs mods (#3609)
Forgot a comma :(

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3609
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-22 22:03:32 +01:00
lizzie d64f7dfe5c [dynarmic] Transition IR::Block to use stable_vector<Inst>, remove inline pool + pooled vector, remove unused mutable get/set block functors (#3543)
theoretically, better

in practice, uh

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3543
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-22 21:01:04 +01:00
lizzie 2d27359074 [core/memory] remove indirection handling for unaligned access (#3584)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3584
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-22 06:05:08 +01:00
xbzk 978ba3ed6f [android,ui] added toggle to swap confirm/back buttons (#3601)
Most android joypads has xbox layout, so while when in UI CONFIRM buttom (A) is the bottom one, in games it is the right one. And the opposite for BACK (B) button.
And that kinda sucks. And some users complained, so i had this idea.
Disabled by default. Toggle in the lonely App Settings menu. No impact at all.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3601
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-22 06:04:41 +01:00
DraVee f1e9e846f1 [docs] Add Gentoo and Arch Linux packages to GDB instructions (#3521)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3521
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: DraVee <dravee@eden-emu.dev>
Co-committed-by: DraVee <dravee@eden-emu.dev>
2026-02-22 05:29:57 +01:00
DraVee 097ccf830d [dynarmic] Fix building of tests (#3599)
Proposed-by: lizzie lizzie@eden-emu.dev
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3599
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: DraVee <dravee@eden-emu.dev>
Co-committed-by: DraVee <dravee@eden-emu.dev>
2026-02-22 04:22:50 +01:00
crueter fecf763a51 [desktop] Fix crash on restart (#3600)
Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3600
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-02-22 04:15:26 +01:00
lizzie a12eb5fef6 [docs] add qt6-charts to deps.md, add nixOS note (#3597)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3597
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-22 03:28:00 +01:00
DraVee 4014b45cb6 [cmake] Remove not working QT_VERSION check (#3553)
* fix copy of files to bin dir on MSVC

* does we even support Qt5? I dont think so

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3553
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: DraVee <dravee@eden-emu.dev>
Co-committed-by: DraVee <dravee@eden-emu.dev>
2026-02-22 03:09:23 +01:00
lizzie 2082ba7ec7 [cmake, common] allow build with -fno-rtti and /GR-, to disable gen of rtti and save some bytes in vtables (#359)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/359
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-22 03:09:03 +01:00
lizzie bfc720152a [vfs] remove usage of 'dynamic_cast', use ankerl::map for files (#3594)
Do I need to writeout everything wrong with `dynamic_cast`?

Also the usual std::map -> `ankerl::unordered_dense::map` conversion, seems to work just fine - but of course test that updates/DLCs/mods/cheats/etc are still being applied :)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3594
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-21 18:38:32 +01:00
lizzie b6238d6df7 [vfs] fix crash at dtor() time (#3593)
Not very happy with the "solution"

Either way, this affected FBSD where opening then closing the emu would result in a crash, always.

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3593
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-21 13:47:48 +01:00
lizzie f94bae10f2 [dynarmic] fix SignedDiv64 crashing if immediate constprop took effect; also fix result for 0x8000_0000/0xFFFF_FFFF not returning properly for SignedDiv32 (#3517)
a program could crash the emulator by just using div immediates and/or dividing by
0x8000_0000_0000_0000/0xFFFF_FFFF_FFFF_FFFF

X0 = 0x8000_0000_0000_0000
X1 = 0xFFFF_FFFF_FFFF_FFFF

SDIV X2, X0, X1
SDIV X2, X1, X0 <- crashes emu

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3517
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-21 03:01:41 +01:00
xbzk d19303883e [chore] Resolve "warning C4127: conditional expression is constant" while building under MSVC (#3567)
NINJA REPORTS THIS SHIT ALMOST EVERY STEP!

[161/863] Building CXX object src\hid_core\CMakeFiles\hid_core.dir\frontend\input_interpreter.cpp.obj
D:\dev\eden\src\.\core/hle/kernel/k_auto_object.h(100): warning C4127: conditional expression is constant
D:\dev\eden\src\.\core/hle/kernel/k_auto_object.h(100): note: consider using 'if constexpr' statement instead
D:\dev\eden\src\.\core/hle/kernel/k_memory_manager.h(246): warning C4127: conditional expression is constant
D:\dev\eden\src\.\core/hle/kernel/k_memory_manager.h(246): note: consider using 'if constexpr' statement instead

The reason, any mention to UNIMPLEMENTED() macro reaches this point where a constant expression (macro argument) is not declared so.
There must be several other ways to suppress, like via cmake C4127 suppression or making every source constexpr, but lazy.
Since it's an unimplemented() feature assert call, i`ve just made it non constant. Covers everything. No charges.

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3567
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-21 03:01:19 +01:00
xbzk 5ee81a4932 [settings] changed debug knobs category to allow per-game usage (#3564)
as the name says, plus changed android type for shortsetting, and added communication good pratices to handbook (.md) file.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3564
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-21 02:35:05 +01:00
lizzie 4f29128e50 [dynarmic] Fix ISB with new JIT schema (#3576)
If an ISB is issued, the changes on the memory wouldn't be reflected
Now we emulate "instruction cache" properly by just telling the mechanism to re-fetch the entire page again (to make ANY changes visible immediately)

This mainly fixes a potential issue where an ISB is emitted before a branch on the same codepage.

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3576
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-21 01:53:48 +01:00
lizzie f76dc401c3 [dynarmic] remove dead-code interpreter (#3547)
interpreter was never called in practice and doesn't do anything other than just crash

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3547
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-21 01:53:34 +01:00
xbzk 732b7eb560 [file_sys] added packed language entries support (#3585)
Normally,  language_entries contained plain UTF-8 names.
In BotW v1.9.0, that title block is no longer directly readable as old UTF-8, so we were parsing binary data as text and going nuts.
...
In patch_manager.cpp (ParseControlNCA), I added a validity check: If update title text is unreadable, we keep update metadata but copy only the base language_entries block (0x0000..0x2FFF) so the game name is valid again.

UPDATE:
managed to decode the new language entries is a raw headerless zlib deflate.
added support for proper detection and inflation.

Co-authored-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3585
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-20 23:28:24 +01:00
lizzie 93eecca894 [xbyak] fix unordered map that wasn't actually wanting to be unordered (#3589)
It seems to be related to the fact multimap on xbyak doesn't properly work if it's unordered
It also seems to NOT play nice at all with unstable iterators (like those found on ankerl) - either we use boost or just let xbyak use normal std

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3589
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-20 19:28:36 +01:00
lizzie d5aa10d572 [dynarmic] make Xbyak::CodeGen use Ankerl for backing unordered maps (#3438)
standard std::unordered_map and std::unordered_set sucks, hadn't found drop-in replacement for multimap, but this change should bring a nice perf boost for x86_64 :)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3438
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-20 16:38:17 +01:00
crueter 308a995275 [desktop] Revert FPS counter to 500ms (#3586)
This broke things, needs more investigation.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3586
2026-02-20 15:59:53 +01:00
crueter f8145152c7 [cmake] Fix MoltenVK Fetch and Dynarmic tests build (#3559)
Originally was a test for Qt 6.9.3. Still broken, I have absolutely zero idea how to fix it.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3559
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-02-20 15:59:45 +01:00
lizzie c9c136bea7 [texture_cache, buffer_cache] Added TLS handling + changed command queue for GPU threading. (#3579)
(Merge of #3495 + #3108)

This PR works around to simplify math operations on hot pointers inside the access and requests to the cache of buffers and texture cache, removing previous logic of indirection and replaced by a PoD approach.

This will ensure less CPU times spended on the same request and flow directly into another chain of the render, in the same way, command queue currently uses an internal mutex that constraints the flow of data within the GPU threads, we're moving over a single command, I verified to keep using mutexes instead of internal mutex + mutex per operation, which are resolved by themselves.

In simplier words, this aims to improve performance on those games and devices where the waits for next orders on GPU commands were heavier than a single verification.

Co-Authored-by: @CamilleLaVey
Co-Authored-by: @Lizzie
Co-authored-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3579
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-20 00:52:07 +01:00
lizzie 6f9d025ad2 [host_memory] avoid doing madvise(2) syscall for a simple memset op (#3108)
Signed-off-by: lizzie lizzie@eden-emu.dev
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3108
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-19 09:16:19 +01:00
lizzie ab129cf314 Revert "[vk] Simplified layouts per driver configuration (#3271)" (#3571)
causes evil VUID bullshit

This reverts commit 75fda70db2.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3571
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-19 04:07:45 +01:00
lizzie 72b3f3a8a5 [dynarmic] remove dead-code arm/thumb disassembler (#3546)
llvm already does this, no need to reinvent the wheel

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3546
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-17 19:15:59 +01:00
John 5fb3ae487c [windows] Return x86 microsleep for Windows (#3563)
Microsleep removal may have regressed AOC in msvc

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3563
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: John <john@eden-emu.dev>
Co-committed-by: John <john@eden-emu.dev>
2026-02-17 06:15:45 +01:00
lizzie 86e02aceba [video_core] remove static texture scratch buffer and just use something allocated on vec (#3560)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3560
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-16 21:51:46 +01:00
lizzie 50ec0892c6 [meta] restore normal icon, valentine is over (#3556)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3556
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-15 23:11:46 +01:00
crueter f4e7fc91ba [cmake, frontend] Fix update checker and move to self-hosted Git (#3558)
httplib doesn't like when you include the protocol, as it would seem

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3558
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-02-15 20:51:05 +01:00
crueter 45c9f9bbb3 [desktop] Add basic Frametime/FPS overlay (#3537)
Just displays min, max, avg frametime/fps, alongside a chart of FPS in
the last 30 seconds.

Notes:
- Qt Charts is now required
- FPS/frametime collector now runs 2x as often. TODO: keep status bar at
  500ms, but put perf overlay at 250ms

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3537
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-15 19:22:19 +01:00
MaranBr 8e373eb714 [vulkan] Improve frame pacing (#3535)
This improves frame pacing control, fixes accumulated drift, and adds the ability to change it while the game is running.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3535
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-02-15 04:12:01 +01:00
lizzie 75ebfaa090 [common] autogenerate unique console serial for every install (#3550)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3550
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-15 04:11:39 +01:00
DraVee e3035ae8f2 [cmake] Use https over http (#3554)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3554
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: DraVee <dravee@eden-emu.dev>
Co-committed-by: DraVee <dravee@eden-emu.dev>
2026-02-15 03:42:05 +01:00
lizzie bcd22d9f8b [windows] return x86 microsleep for mingw only (#3544)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3544
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-15 03:41:25 +01:00
lizzie 4e7c036c7e [hle] fix spl being registered as 'spl' instead of 'spl:' (#3549)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3549
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-15 02:39:27 +01:00
crueter 19e2dba35a [cmake] CPMUtil: Don't consider system packages if FORCE_BUNDLED is on (#3539)
Thanks, Debian.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3539
2026-02-14 03:33:03 +01:00
crueter 0634b4a278 [externals] Update SDL2 CI (#3538)
https://github.com/eden-emulator/Issue-Reports/issues/327

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3538
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2026-02-14 03:24:36 +01:00
John ee428deb1e [settings] Set EDS to 2 as default on Windows/FreeBSD (#3532)
<video src="/attachments/9a1b2e43-0acf-4a40-9be5-db93b1164111" title="Regressions" controls></video>

This PR fixes Sonic's broken graphics.

- EDS3 hard coded as _WIN32 default broke graphics and was the issue. Manually adjusting EDS[0-3] per-game/global settings can not fix graphics.
- EDS2 hard coded as _WIN32 default fixes graphical issues. Manually adjusting EDS[0-3] per-game/global settings works as intended.
- EDS0 as _WIN32 default was also tested and works. Old logic behavior before #292 regression.

Co-authored-by: DraVee <dravee@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3532
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: John <john@eden-emu.dev>
Co-committed-by: John <john@eden-emu.dev>
2026-02-13 18:37:42 +01:00
lizzie 07bc77c7e7 [dynarmic] Fix Sonic X shadow regression due to aggresive const prop discarding NZCV side ffects (#3534)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3534
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-13 16:10:09 +01:00
lizzie 72973fe582 [dynarmic] fix NSBU regressions (#3533)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3533
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-13 11:51:34 +01:00
lizzie c263b6af6f [dynarmic] add current code page cache (#3459)
should make JIT translation a bit faster - especially for non-fastmem
test if thumb still werks

previously:
we read 32-bits one by one, and do translation **for each u32 we read**

now:
one big read of 4096 bytes (aligned of course), only 1 VAddr translation

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3459
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 02:14:50 +01:00
PavelBARABANOV e46576b4c3 [android] add graphics artifacts warning to bloom fix description (#3471)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3471
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
Co-committed-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
2026-02-12 01:56:16 +01:00
lizzie 89bcb73d47 [dynarmic] x86 direct immediate call for host calls within 2G (#3508)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3508
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 01:45:22 +01:00
MaranBr 7d81a724ef [shader_recompiler] Fix Shuffle handling for Position attributes (#3406)
This fixes the rescaling path to properly convert to F32, matching native 1x behavior for any resolution.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3406
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-02-12 01:34:25 +01:00
lizzie 634686d59b [dynarmic] add back A64 identity pass (#3519)
results in way better codegen (less noise for movs and all dat)
i think it was removed by accident by me, oops

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3519
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 01:33:51 +01:00
lizzie d0af14632e [dynarmic] UseScratchGpr for path with immediate additions; lea materialisation with in-range s32 immediate (+immediate carry) (#2958)
Allows regalloc to more aggressively take on registers for immediate codepath; also uses lea for stc/adc (since carry can equate +1 in some cases)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2958
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 01:33:22 +01:00
lizzie c87986b276 [dynarmic] reuse IR::Block memory instead of creating new block each time (#3501)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3501
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 01:32:55 +01:00
lizzie b600b90d53 [dynarmic] Make SetX/GetW pairs optimized by getSet pass (#3518)
this results in removed reloads for any code that uses those heavily :)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3518
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-12 01:32:26 +01:00
crueter 2b979024cb [frontend] Slow and Turbo modes (#3525)
Closes #3344

Adds slow and turbo modes with configurable speeds that can then be
toggled by the user. Behavior is:
- Standard/slow limit, toggle turbo = turbo
- Turbo limit, toggle turbo = standard
- Standard/turbo limit, toggle slow = slow
- Slow limit, toggle slow = standard

Enabling the turbo/slow mode enables the frame limiter unconditionally.

This has some conflicts with VSync. For example when I set my refresh
rate to 60hz and enable vsync, turbo mode does nothing. Not sure how to
go about fixing this, @MaranBr probably knows better the proper
solution.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3525
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-02-12 01:31:55 +01:00
MaranBr 5f676a6a55 [vulkan] Add support for target FPS frame pacing (#3494)
This allows users to choose how the emulator manages frame pacing to reduce stuttering and provide a smoother and more consistent frame rate.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3494
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-02-12 00:23:19 +01:00
xbzk 2ab5b37137 [android, ui] unswizzle combo picker core (#3516)
Combo picker for unswizzle. Attempt to combine settings + Enable toggle added.
WARNING! The toggle won't have effect! It just controls GPU_UNSWIZZLE_ENABLED boolean setting, and will need @PavelBARABANOV unswizzle enable/disable integration.

Co-authored-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3516
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-02-12 00:11:54 +01:00
lizzie f6547fac8c [dynarmic] Define LSW on the same argument/use scratch gpr (#3520)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3520
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 23:13:40 +01:00
lizzie 9c685c1449 [dynarmic] inline SM4 ARM operation (#3474)
most useless function call ever
why even call? just look it up! it's right there!

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3474
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 19:41:36 +01:00
crueter aaf1a597ef [externals] Don't force xbyak to be bundled (#3526)
I explicitly asked for it to be removed, and it was added back again.
Why?

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3526
2026-02-11 19:37:20 +01:00
lizzie 850fc372b7 [dynarmic] AVX512CD impl for lzcnt16 (#3499)
dont have AVX512CD to verify the change; but should[tm] work

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3499
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 19:36:23 +01:00
lizzie bad9d8043b [dynarmic, externals] update to xbyak v7.33.2 (#3505)
PLEASE test, & provide gdb logs kthx!

mainly because there is a bug with pop/push for r8-r15:
https://github.com/herumi/xbyak/commit/94f6fe6941103a826db63d1e98cc27ba19024668
(may have been linked as to why Engage had SCREECH when doing xmm spills!)

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

Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3505
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 19:35:29 +01:00
lizzie 8e8e66f880 [vk] load VK_KHR_GET_SURFACE_CAPABILITIES_2 on drivers (not a hard requirement) for maintainance 1 (#3514)
fixes VVL, but doesnt make it a hard requirement so drivers which cant load it for some reason just, dont

Validation Error: [ VUID-vkCreateInstance-ppEnabledExtensionNames-01388 ] | MessageID = 0xe5e52180
vkCreateInstance(): pCreateInfo->ppEnabledExtensionNames[3] Missing extension required by the instance extension VK_EXT_surface_maintenance1: VK_KHR_get_surface_capabilities2.
The Vulkan spec states: All required extensions for each extension in the VkInstanceCreateInfo::ppEnabledExtensionNames list must also be present in that list (https://docs.vulkan.org/spec/latest/chapters/initialization.html#VUID-vkCreateInstance-ppEnabledExtensionNames-01388)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3514
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 18:54:24 +01:00
lizzie fec5ac2298 [meta] add San Valentine's 2026 icon variation (#3476)
Pinkish theme as well, very fruity, very nice

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3476
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 05:04:39 +01:00
248 changed files with 4757 additions and 7437 deletions
-35
View File
@@ -1,35 +0,0 @@
diff --git a/library/aesni.h b/library/aesni.h
index 754c984c79..59e27afd3e 100644
--- a/library/aesni.h
+++ b/library/aesni.h
@@ -35,7 +35,7 @@
/* GCC-like compilers: currently, we only support intrinsics if the requisite
* target flag is enabled when building the library (e.g. `gcc -mpclmul -msse2`
* or `clang -maes -mpclmul`). */
-#if (defined(__GNUC__) || defined(__clang__)) && defined(__AES__) && defined(__PCLMUL__)
+#if defined(__GNUC__) || defined(__clang__)
#define MBEDTLS_AESNI_HAVE_INTRINSICS
#endif
/* For 32-bit, we only support intrinsics */
diff --git a/library/aesni.c b/library/aesni.c
index 2857068..3e104ab 100644
--- a/library/aesni.c
+++ b/library/aesni.c
@@ -31,16 +31,14 @@
#include <immintrin.h>
#endif
-#if defined(MBEDTLS_ARCH_IS_X86)
#if defined(MBEDTLS_COMPILER_IS_GCC)
#pragma GCC push_options
#pragma GCC target ("pclmul,sse2,aes")
#define MBEDTLS_POP_TARGET_PRAGMA
-#elif defined(__clang__) && (__clang_major__ >= 5)
+#elif defined(__clang__)
#pragma clang attribute push (__attribute__((target("pclmul,sse2,aes"))), apply_to=function)
#define MBEDTLS_POP_TARGET_PRAGMA
#endif
-#endif
#if !defined(MBEDTLS_AES_USE_HARDWARE_ONLY)
/*
-20
View File
@@ -1,20 +0,0 @@
diff --git a/library/common.h b/library/common.h
index 50f2a29..c60d9dc 100644
--- a/library/common.h
+++ b/library/common.h
@@ -19,11 +19,11 @@
#include <stdint.h>
#include <stddef.h>
-#if defined(__ARM_NEON)
-#include <arm_neon.h>
+#if defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64)
+#include <arm64_neon.h>
#define MBEDTLS_HAVE_NEON_INTRINSICS
-#elif defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64)
-#include <arm64_neon.h>
+#elif (defined(__ANDROID__) && defined(__ARM_FP)) || defined(__ARM_NEON)
+#include <arm_neon.h>
#define MBEDTLS_HAVE_NEON_INTRINSICS
#endif
+86 -147
View File
@@ -55,29 +55,23 @@ if (YUZU_STATIC_ROOM)
set(ENABLE_UPDATE_CHECKER OFF)
set(USE_DISCORD_PRESENCE OFF)
set(BUILD_TESTING OFF)
set(ENABLE_OPENSSL OFF)
set(ENABLE_WEB_SERVICE OFF)
set(ENABLE_LIBUSB OFF)
# allow static libs for boost and mbedtls though
# allow static libs for boost though
set(Boost_USE_STATIC_LIBS ON)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(MBEDTLS_LIB_SUFFIX "_static")
set(YUZU_USE_CPM ON)
set(OPENSSL_USE_STATIC_LIBS ON)
set(zstd_FORCE_BUNDLED ON)
set(fmt_FORCE_BUNDLED ON)
endif()
# common network mbedtls
# common: xbyak? booost fmt lz4 zstd
# network: enet boost
# qt stuff
option(ENABLE_QT "Enable the Qt frontend" ON)
option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and Android)" OFF)
option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
# option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
cmake_dependent_option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF "NOT YUZU_USE_BUNDLED_QT" OFF)
set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries")
cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF)
@@ -130,12 +124,7 @@ if (YUZU_STATIC_BUILD)
set(YUZU_USE_BUNDLED_OPENSSL ON)
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF)
## some libraries define a Library::Name_static alternative ##
set(MBEDTLS_LIB_SUFFIX _static)
elseif(APPLE)
set(YUZU_USE_CPM ON)
set(YUZU_USE_BUNDLED_FFMPEG ON)
set(YUZU_USE_BUNDLED_SDL2 ON)
set(YUZU_USE_BUNDLED_OPENSSL ON)
@@ -145,7 +134,6 @@ if (YUZU_STATIC_BUILD)
set(SPIRV-Tools_FORCE_BUNDLED ON)
set(SPIRV-Headers_FORCE_BUNDLED ON)
set(zstd_FORCE_BUNDLED ON)
set(MbedTLS_FORCE_BUNDLED ON)
endif()
endif()
@@ -198,8 +186,6 @@ if (MSVC OR ANDROID)
set(EXT_DEFAULT ON)
endif()
option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
# ffmpeg
option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from external source" "${PLATFORM_SUN}" "NOT WIN32 AND NOT ANDROID" OFF)
@@ -245,30 +231,12 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
set(DEFAULT_ENABLE_OPENSSL ON)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
# - Windows defaults to the Schannel backend.
# - macOS defaults to the SecureTransport backend.
# - Android currently has no SSL backend as the NDK doesn't include any SSL
# library; a proper 'native' backend would have to go through Java.
# - Solaris and OpenBSD have too old backends
# But you can force builds for those platforms to use OpenSSL if you have
# your own copy of it.
set(DEFAULT_ENABLE_OPENSSL OFF)
endif()
if (ENABLE_WEB_SERVICE OR USE_DISCORD_PRESENCE)
set(DEFAULT_ENABLE_OPENSSL ON)
endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
endif()
cmake_dependent_option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" ${DEFAULT_YUZU_USE_BUNDLED_OPENSSL} "ENABLE_OPENSSL" OFF)
option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" ${DEFAULT_YUZU_USE_BUNDLED_OPENSSL})
if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL)
AddJsonPackage(vulkan-validation-layers)
@@ -396,123 +364,94 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
# openssl funniness
if (ENABLE_OPENSSL)
if (YUZU_USE_BUNDLED_OPENSSL)
set(BUILD_SHARED_LIBS OFF)
AddJsonPackage(openssl)
if (OpenSSL_ADDED)
add_compile_definitions(YUZU_BUNDLED_OPENSSL)
endif()
endif()
find_package(RenderDoc MODULE)
find_package(OpenSSL 1.1.1 REQUIRED)
# openssl funniness
if (YUZU_USE_BUNDLED_OPENSSL)
set(BUILD_SHARED_LIBS OFF)
AddJsonPackage(openssl)
if (OpenSSL_ADDED)
add_compile_definitions(YUZU_BUNDLED_OPENSSL)
endif()
endif()
if (YUZU_USE_CPM)
message(STATUS "Fetching needed dependencies with CPM")
find_package(OpenSSL 3 REQUIRED)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
set(ENABLE_TESTING OFF)
message(STATUS "Fetching needed dependencies with CPM")
# TODO(crueter): renderdoc?
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
set(ENABLE_TESTING OFF)
# boost
set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant)
# boost
set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant)
AddJsonPackage(boost)
AddJsonPackage(boost)
# really annoying thing where boost::headers doesn't work with cpm
# TODO(crueter) investigate
set(BOOST_NO_HEADERS ${Boost_ADDED})
# really annoying thing where boost::headers doesn't work with cpm
# TODO(crueter) investigate
set(BOOST_NO_HEADERS ${Boost_ADDED})
if (Boost_ADDED)
if (MSVC OR ANDROID)
add_compile_definitions(YUZU_BOOST_v1)
if (Boost_ADDED)
if (MSVC OR ANDROID)
add_compile_definitions(YUZU_BOOST_v1)
endif()
if (NOT MSVC OR CXX_CLANG)
# boost sucks
if (PLATFORM_SUN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthreads")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthreads")
endif()
if (NOT MSVC OR CXX_CLANG)
# boost sucks
if (PLATFORM_SUN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthreads")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthreads")
endif()
target_compile_options(boost_heap INTERFACE -Wno-shadow)
target_compile_options(boost_icl INTERFACE -Wno-shadow)
target_compile_options(boost_asio INTERFACE -Wno-conversion -Wno-implicit-fallthrough)
endif()
endif()
target_compile_options(boost_heap INTERFACE -Wno-shadow)
target_compile_options(boost_icl INTERFACE -Wno-shadow)
target_compile_options(boost_asio INTERFACE -Wno-conversion -Wno-implicit-fallthrough)
# fmt
AddJsonPackage(fmt)
# lz4
AddJsonPackage(lz4)
if (lz4_ADDED)
add_library(lz4::lz4 ALIAS lz4_static)
endif()
# zstd
AddJsonPackage(zstd)
if (zstd_ADDED)
add_library(zstd::zstd ALIAS libzstd_static)
add_library(zstd::libzstd ALIAS libzstd_static)
endif()
if (NOT YUZU_STATIC_ROOM)
# nlohmann
AddJsonPackage(nlohmann)
# zlib
AddJsonPackage(zlib)
if (ZLIB_ADDED)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
endif()
# Opus
AddJsonPackage(opus)
if (Opus_ADDED)
if (MSVC AND CXX_CLANG)
target_compile_options(opus PRIVATE
-Wno-implicit-function-declaration
)
endif()
endif()
# fmt
AddJsonPackage(fmt)
# lz4
AddJsonPackage(lz4)
if (lz4_ADDED)
add_library(lz4::lz4 ALIAS lz4_static)
endif()
# zstd
AddJsonPackage(zstd)
if (zstd_ADDED)
add_library(zstd::zstd ALIAS libzstd_static)
add_library(zstd::libzstd ALIAS libzstd_static)
endif()
if (NOT YUZU_STATIC_ROOM)
# nlohmann
AddJsonPackage(nlohmann)
# zlib
AddJsonPackage(zlib)
if (ZLIB_ADDED)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
endif()
# Opus
AddJsonPackage(opus)
if (Opus_ADDED)
if (MSVC AND CXX_CLANG)
target_compile_options(opus PRIVATE
-Wno-implicit-function-declaration
)
endif()
endif()
if (NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS opus)
endif()
endif()
else()
# TODO: we can probably just use CPM for this... right?
# Enforce the search mode of non-required packages for better and shorter failure messages
find_package(fmt 8 REQUIRED)
if (NOT YUZU_DISABLE_LLVM)
find_package(LLVM MODULE COMPONENTS Demangle)
endif()
find_package(nlohmann_json 3.8 REQUIRED)
find_package(lz4 REQUIRED)
find_package(RenderDoc MODULE)
find_package(stb MODULE)
find_package(Opus 1.3 MODULE REQUIRED)
find_package(ZLIB 1.2 REQUIRED)
find_package(zstd 1.5 REQUIRED MODULE)
# wow
find_package(Boost 1.57.0 CONFIG REQUIRED OPTIONAL_COMPONENTS headers context system fiber filesystem)
if (ENABLE_OPENSSL)
find_package(OpenSSL 1.1.1 REQUIRED)
if (NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS opus)
endif()
endif()
@@ -574,8 +513,8 @@ message(STATUS "Platform Libraries: ${PLATFORM_LIBRARIES}")
add_subdirectory(externals)
# pass targets from externals
# TODO(crueter): CPMUtil Propagate func?
find_package(enet)
find_package(MbedTLS)
find_package(unordered_dense REQUIRED)
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
@@ -641,11 +580,11 @@ if (ENABLE_QT)
list(APPEND CMAKE_PREFIX_PATH "${Qt6_DIR}")
endif()
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Concurrent)
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Charts Concurrent)
if (YUZU_USE_QT_MULTIMEDIA)
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
endif()
# if (YUZU_USE_QT_MULTIMEDIA)
# find_package(Qt6 REQUIRED COMPONENTS Multimedia)
# endif()
if (PLATFORM_LINUX OR PLATFORM_FREEBSD)
# yes Qt, we get it
@@ -680,13 +619,13 @@ if (ENABLE_QT)
## Components ##
# Best practice is to ask for all components at once, so they are from the same version
set(YUZU_QT_COMPONENTS Core Widgets Concurrent)
set(YUZU_QT_COMPONENTS Core Widgets Charts Concurrent)
if (PLATFORM_LINUX)
list(APPEND YUZU_QT_COMPONENTS DBus)
endif()
if (YUZU_USE_QT_MULTIMEDIA)
list(APPEND YUZU_QT_COMPONENTS Multimedia)
endif()
# if (YUZU_USE_QT_MULTIMEDIA)
# list(APPEND YUZU_QT_COMPONENTS Multimedia)
# endif()
if (YUZU_USE_QT_WEB_ENGINE)
list(APPEND YUZU_QT_COMPONENTS WebEngineCore WebEngineWidgets)
endif()
+5 -1
View File
@@ -84,7 +84,11 @@ function(SystemPackageViable JSON_NAME)
parse_object(${object})
string(REPLACE " " ";" find_args "${find_args}")
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
if (${package}_FORCE_BUNDLED)
set(${package}_FOUND OFF)
else()
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
endif()
set(${pkg}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
set(${pkg}_PACKAGE ${package} PARENT_SCOPE)
+7 -4
View File
@@ -36,14 +36,17 @@ set(GIT_DESC ${BUILD_VERSION})
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
# Auto-updater metadata! Must somewhat mirror GitHub API endpoint
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "http://api.github.com")
if (NIGHTLY_BUILD)
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "api.github.com")
set(BUILD_AUTO_UPDATE_API_PATH "/repos/")
set(BUILD_AUTO_UPDATE_REPO "Eden-CI/Nightly")
set(REPO_NAME "Eden Nightly")
else()
set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases")
set(BUILD_AUTO_UPDATE_WEBSITE "https://git.eden-emu.dev")
set(BUILD_AUTO_UPDATE_API "git.eden-emu.dev")
set(BUILD_AUTO_UPDATE_API_PATH "/api/v1/repos/")
set(BUILD_AUTO_UPDATE_REPO "eden-emu/eden")
set(REPO_NAME "Eden")
endif()
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "openssl",
"repo": "crueter-ci/OpenSSL",
"version": "3.6.0-1cb0d36b39",
"min_version": "1.1.1"
"min_version": "3"
},
"boost": {
"package": "Boost",
+211
View File
@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
fill="none"
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="EdenLogoLoveWhiteV3.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xml:space="preserve"
inkscape:export-filename="dev.eden_emu.eden.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:creator>
<cc:Agent>
<dc:title>Madeline_Dev</dc:title>
<dc:identifier>mailto:madelvidel@gmail.com</dc:identifier>
</cc:Agent>
</dc:creator>
<dc:date>2025</dc:date>
<dc:license
rdf:resource="https://www.gnu.org/licenses/gpl-3.0.html" />
<dc:rights>2025 Eden Emulator Project</dc:rights>
<dc:source>https://git.eden-emu.dev</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7"><linearGradient
id="swatch14"
inkscape:swatch="solid"><stop
style="stop-color:#66003b;stop-opacity:1;"
offset="0"
id="stop14" /></linearGradient><linearGradient
id="linearGradient1"
inkscape:collect="always"><stop
style="stop-color:#ffffff;stop-opacity:0.50980395;"
offset="0"
id="stop1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop2" /></linearGradient><linearGradient
id="linearGradient11"
inkscape:collect="always"><stop
style="stop-color:#9523a7;stop-opacity:0.50980395;"
offset="0"
id="stop11" /><stop
style="stop-color:#ffabad;stop-opacity:1;"
offset="0.99898213"
id="stop20" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="0.99898213"
id="stop12" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient12"
x1="109.74531"
y1="106.54533"
x2="431.05463"
y2="427.85461"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
gradientTransform="matrix(1.0945321,0,0,1.0945321,-39.661525,-35.159057)" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="125.40197"
y1="271.834"
x2="431.02424"
y2="271.834"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0964243,0,0,1.0253208,-40.187969,-20.060025)" /><filter
inkscape:label="Light Contour"
inkscape:menu="Image Paint and Draw"
inkscape:menu-tooltip="Uses vertical specular light to draw lines"
style="color-interpolation-filters:sRGB"
id="filter11"
x="-0.01907517"
y="-0.054959154"
width="1.0379885"
height="1.1092314"><feGaussianBlur
in="SourceGraphic"
stdDeviation="0.38250006"
result="result3"
id="feGaussianBlur9" /><feComponentTransfer
result="result1"
in="result3"
id="feComponentTransfer9"><feFuncR
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncR9" /><feFuncG
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncG9" /><feFuncB
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncB9" /></feComponentTransfer><feGaussianBlur
result="result5"
stdDeviation="0.01"
id="feGaussianBlur10" /><feBlend
in2="result5"
result="result6"
mode="lighten"
in="result5"
id="feBlend10" /><feColorMatrix
in="result6"
type="luminanceToAlpha"
result="result2"
id="feColorMatrix10" /><feSpecularLighting
surfaceScale="5"
result="result9"
specularExponent="20"
in="result2"
specularConstant="1"
id="feSpecularLighting10"><feDistantLight
azimuth="180"
elevation="90"
id="feDistantLight10" /></feSpecularLighting><feComposite
in2="result6"
operator="arithmetic"
in="result9"
k1="0.4"
k3="0.7"
result="result3"
id="feComposite10"
k2="0"
k4="0" /><feBlend
in2="result1"
in="result3"
mode="normal"
result="result8"
id="feBlend11" /><feComposite
in2="SourceGraphic"
in="result8"
operator="in"
result="result7"
id="feComposite11" /></filter></defs><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.35"
inkscape:cx="254.81481"
inkscape:cy="260.37037"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg7"
showguides="false" /><circle
style="fill:url(#linearGradient12);fill-opacity:1;stroke:#e4e4e4;stroke-width:14.0448;stroke-opacity:1;paint-order:stroke fill markers"
id="path8"
cx="256.2999"
cy="257.2999"
r="248.67769" /><path
id="path15"
style="fill:url(#linearGradient2);fill-opacity:1;stroke:#ffffff;stroke-width:16.9642;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.52156866;paint-order:stroke fill markers"
d="m 306.72111,24.233031 c 0,0 -11.98397,40.08696 -18.0546,60.431848 -12.96613,9.503601 -21.49377,18.397701 -21.49377,18.397701 0,0 -23.41313,-31.029398 -45.74145,-43.934598 -22.32833,-12.905201 -52.42065,-11.242483 -52.42065,-11.242483 0,0 -12.50052,4e-4 -27.63117,5.537132 -15.13066,5.536732 -21.27107,9.227888 -21.27107,9.227888 0,0 15.35165,-0.410529 37.93799,6.766716 22.58635,7.177243 32.45374,11.484796 32.45374,11.484796 l 31.02752,-6.562453 -8.44161,11.074275 c 0,0 14.80259,8.920284 22.80648,16.917787 8.0039,7.9975 11.73088,12.50812 11.73088,12.50812 0,0 -13.92373,-5.43341 -56.68427,-1.74226 -42.76055,3.69116 -84.86368,56.39265 -84.86368,56.39265 0,0 41.22428,-15.9958 65.5649,-21.32747 24.34062,-5.33166 47.58524,-7.9983 47.58524,-7.9983 0,0 -18.41865,7.3827 -38.15428,38.3474 -19.73564,30.96468 -14.0351,80.18128 -14.0351,80.18128 0,0 31.35774,-59.05848 61.39977,-78.94969 30.04203,-19.89124 36.182,-20.50642 36.182,-20.50642 0,0 -24.1209,48.39514 -33.55015,126.11445 -9.42924,77.71931 30.26089,207.72959 30.26089,207.72959 l 47.80367,-6.97298 c 0,0 -45.61041,-42.24281 -49.55753,-151.33695 -3.94713,-109.09413 16.66479,-174.30453 16.66479,-174.30453 0,0 17.10508,1.23039 42.54213,32.81027 25.43704,31.57987 40.34713,57.41795 40.34713,57.41795 0,0 9.65024,-51.26683 -11.4011,-74.64415 -21.05135,-23.3773 -53.50637,-33.62931 -53.50637,-33.62931 0,0 13.70573,-6.66419 42.10313,-4.71609 28.39738,1.94813 87.30278,27.12897 87.30278,27.12896 0,0 -15.8158,-25.5214 -52.87463,-43.21167 -37.05881,-17.69028 -81.35597,0.29238 -81.35597,0.29238 0,0 4.13589,-15.37277 29.10021,-32.628037 24.96432,-17.25527 56.907,-34.36427 56.907,-34.36427 0,0 -26.3595,1.160335 -55.20025,12.035504 -7.176,2.7059 -14.064,6.299542 -20.36309,10.173104 l 17.40574,-51.051762 z" /><path
id="path4-2-6"
style="fill:#ffffff;fill-opacity:0.509804;stroke:#ffffff;stroke-width:8.49614;stroke-dasharray:none;stroke-opacity:0.521569"
d="m 105.64857,363.95957 a 27.851731,25.309928 0 0 0 -7.115581,35.20471 27.851731,25.309928 0 0 0 5.656921,5.48617 c 7.16052,5.83605 21.58543,14.65841 45.88391,17.847 37.57172,4.93039 41.7105,10.38005 41.71122,10.381 -4.5e-4,-6e-4 -4.13529,-5.45309 4.14459,-39.11953 5.35491,-21.77281 1.40624,-37.16078 -2.05217,-45.31698 a 27.851731,25.309928 0 0 0 -3.63281,-6.75471 27.851731,25.309928 0 0 0 -38.74027,-6.4662 27.851731,25.309928 0 0 0 -7.11556,35.20472 27.851731,25.309928 0 0 0 -38.74025,-6.46618 z" /><path
id="path4-2-6-1"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 351.29524,378.32915 a 20.862787,22.283365 87.929759 0 0 -28.94663,11.7882 20.862787,22.283365 87.929759 0 0 -1.50914,6.04125 c -1.09835,7.13663 -1.16134,20.16231 7.28339,36.72931 13.0577,25.61689 10.90177,30.69793 10.90138,30.69882 2.4e-4,-5.6e-4 2.16,-5.08045 30.49632,-15.43688 18.32576,-6.69765 27.84648,-16.23659 32.287,-22.14109 a 20.862787,22.283365 87.929759 0 0 3.33646,-5.36755 20.862787,22.283365 87.929759 0 0 -12.45108,-27.05014 20.862787,22.283365 87.929759 0 0 -28.94664,11.78822 20.862787,22.283365 87.929759 0 0 -12.45106,-27.05014 z" /><path
id="path4-2-6-1-5"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.52156866"
d="m 83.024352,249.4105 a 22.283365,20.862787 0 0 0 -29.353581,10.73482 22.283365,20.862787 0 0 0 -1.72639,5.98278 c -1.35544,7.0923 -1.88894,20.10721 5.951801,36.96846 12.12378,26.07187 9.7857,31.07171 9.78528,31.07259 2.6e-4,-5.5e-4 2.34212,-4.99911 31.03406,-14.32513 18.555748,-6.03127 28.414858,-15.22005 33.065768,-20.96029 a 22.283365,20.862787 0 0 0 3.52818,-5.24352 22.283365,20.862787 0 0 0 -11.46577,-27.48227 22.283365,20.862787 0 0 0 -29.353588,10.73483 22.283365,20.862787 0 0 0 -11.46576,-27.48227 z" /><path
id="path4-2-6-1-9"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:5.01078;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 320.97602,291.36362 a 16.625347,14.748181 0 0 0 -21.90036,7.58859 16.625347,14.748181 0 0 0 -1.28804,4.2293 c -1.01128,5.01364 -1.40932,14.21406 4.44057,26.1335 9.0454,18.43055 7.30098,21.965 7.30067,21.96562 2e-4,-3.9e-4 1.74743,-3.53394 23.15415,-10.12663 13.84423,-4.26358 21.19999,-10.75925 24.66998,-14.8171 a 16.625347,14.748181 0 0 0 2.63233,-3.70671 16.625347,14.748181 0 0 0 -8.55447,-19.42758 16.625347,14.748181 0 0 0 -21.90037,7.58859 16.625347,14.748181 0 0 0 -8.55446,-19.42758 z" /><path
id="path4-2-6-1-9-8"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:4.22313;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 416.90426,193.13811 a 12.322476,14.134158 82.30909 0 0 -17.56469,8.83612 12.322476,14.134158 82.30909 0 0 -0.59553,3.65007 c -0.27175,4.26676 0.45709,11.92642 6.76182,21.10665 9.74867,14.19503 8.68847,17.32338 8.68827,17.3239 1.2e-4,-3.3e-4 1.06282,-3.12827 18.32699,-11.08266 11.16516,-5.14424 16.60821,-11.37802 19.06101,-15.14093 a 12.322476,14.134158 82.30909 0 0 1.788,-3.37453 12.322476,14.134158 82.30909 0 0 -9.45059,-15.07737 12.322476,14.134158 82.30909 0 0 -17.5647,8.83612 12.322476,14.134158 82.30909 0 0 -9.45058,-15.07737 z" /><path
id="path4-2-6-1-9-8-4"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.48342;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 96.169666,99.754795 a 10.296362,11.508677 62.365085 0 0 -7.330189,13.841365 10.296362,11.508677 62.365085 0 0 1.473186,2.6805 c 2.008889,2.93989 6.437543,7.66269 15.415927,10.84633 13.88288,4.92276 14.77391,7.47086 14.77407,7.4713 -1e-4,-2.8e-4 -0.88925,-2.54921 6.69094,-15.67164 4.90232,-8.48655 5.37758,-15.08279 5.10162,-18.6847 a 10.296362,11.508677 62.365085 0 0 -0.5257,-3.042454 10.296362,11.508677 62.365085 0 0 -14.1348,-5.641051 10.296362,11.508677 62.365085 0 0 -7.33021,13.841385 10.296362,11.508677 62.365085 0 0 -14.134844,-5.641035 z" /><path
id="path4-2-6-1-9-8-4-9-2"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.05139;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 296.64657,233.26347 a 8.8114666,10.319195 80.445391 0 0 -11.58972,8.06989 8.8114666,10.319195 80.445391 0 0 0.0366,2.62543 c 0.35019,3.02392 1.86017,8.32306 7.60386,14.09672 8.8812,8.92759 8.51432,11.23624 8.51426,11.23663 5e-5,-2.5e-4 0.36876,-2.30885 11.85385,-9.72788 7.4277,-4.79799 10.57103,-9.75243 11.86523,-12.65455 a 8.8114666,10.319195 80.445391 0 0 0.86244,-2.55882 8.8114666,10.319195 80.445391 0 0 -8.77837,-9.57865 8.8114666,10.319195 80.445391 0 0 -11.58975,8.06989 8.8114666,10.319195 80.445391 0 0 -8.77842,-9.57866 z" /><path
id="path4-2-6-1-9-8-4-9-2-6"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:2.49738;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 403.95446,91.44744 a 7.0950375,8.5844443 82.142743 0 0 -9.64888,6.492875 7.0950375,8.5844443 82.142743 0 0 0.0305,2.112375 c 0.29155,2.43298 1.54866,6.69657 6.33049,11.34195 7.39392,7.18298 7.08848,9.04046 7.08843,9.04078 4e-5,-2.1e-4 0.307,-1.85765 9.86876,-7.82686 6.18383,-3.86038 8.80077,-7.84663 9.87824,-10.18161 a 7.0950375,8.5844443 82.142743 0 0 0.718,-2.05879 7.0950375,8.5844443 82.142743 0 0 -7.30831,-7.706798 7.0950375,8.5844443 82.142743 0 0 -9.64889,6.492875 7.0950375,8.5844443 82.142743 0 0 -7.30834,-7.706797 z" /><path
id="path4-2-6-1-9-8-4-9-5"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:7.54176;stroke-dasharray:none;stroke-opacity:0.521569"
d="m 410.46352,265.86621 a 24.802737,22.39469 2.0523303 0 0 -30.17074,15.95063 24.802737,22.39469 2.0523303 0 0 -0.74427,6.61415 c -0.12337,7.7333 1.79614,21.62064 13.65297,38.28529 18.33368,25.76802 16.72835,31.4347 16.72811,31.43572 1.4e-4,-6.3e-4 1.60998,-5.66652 31.32518,-20.02007 19.21759,-9.28266 28.27643,-20.56192 32.27921,-27.37361 a 24.802737,22.39469 2.0523303 0 0 2.8654,-6.11021 24.802737,22.39469 2.0523303 0 0 -17.88248,-27.36631 24.802737,22.39469 2.0523303 0 0 -30.17081,15.95066 24.802737,22.39469 2.0523303 0 0 -17.88257,-27.36625 z" /><path
id="path4-2-6-1-9-8-5"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.6405;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 114.43099,189.33754 a 10.333135,12.525309 79.004057 0 0 -14.00848,9.75499 10.333135,12.525309 79.004057 0 0 0.0547,3.08382 c 0.43602,3.54457 2.28588,9.73496 9.26636,16.38295 10.79359,10.27954 10.35819,13.00063 10.3581,13.00111 6e-5,-3e-4 0.4377,-2.72141 14.32197,-11.70923 8.9793,-5.8126 12.76786,-11.70785 14.32433,-15.14812 a 10.333135,12.525309 79.004057 0 0 1.03481,-3.02677 10.333135,12.525309 79.004057 0 0 -10.67161,-11.04688 10.333135,12.525309 79.004057 0 0 -14.00848,9.75501 10.333135,12.525309 79.004057 0 0 -10.6716,-11.04688 z" /><path
id="path4-2-6-1-9-8-4-9-2-2"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.62674;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 178.56055,273.32338 a 10.656883,12.053184 66.089768 0 0 -7.97821,14.16205 10.656883,12.053184 66.089768 0 0 1.49012,2.79504 c 2.04675,3.07265 6.5954,8.03049 15.94965,11.48124 14.46412,5.33577 15.34612,7.98353 15.34628,7.98398 -8e-5,-2.8e-4 -0.88012,-2.64887 7.3455,-16.0639 5.31973,-8.67576 5.95524,-15.47956 5.74045,-19.20452 a 10.656883,12.053184 66.089768 0 0 -0.48843,-3.15169 10.656883,12.053184 66.089768 0 0 -14.7135,-6.08217 10.656883,12.053184 66.089768 0 0 -7.97826,14.1621 10.656883,12.053184 66.089768 0 0 -14.71355,-6.08215 z" /></svg>

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -0,0 +1 @@
#e48cc9ff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

+12 -4
View File
@@ -22,17 +22,25 @@ Debug logs can be found in General -> Debug -> Open Log Location on desktop, and
Ignoring SIGSEGV when debugging in host:
- **gdb**: `handle all nostop pass`.
- **gdb**: `handle SIGSEGV nostop pass`.
- **lldb**: `pro hand -p true -s false -n false SIGSEGV`.
## Debugging (guest code)
### gdb
Run `./build/bin/eden-cli -c <path to your config file (see logs where you run eden normally to see where it is)> -d -g <path to game>`
You must have GDB installed for aarch64 to debug the target. Install it through your package manager, e.g.:
* On Arch:
* `sudo pacman -Syu aarch64-linux-gnu-gdb`
* On Gentoo:
* `sudo emerge --ask crossdev`
* `sudo crossdev -t aarch64-unknown-linux-gnu --ex-gdb`
Then hook up an aarch64-gdb (use `yay aarch64-gdb` or `sudo pkg in arch64-gdb` to install)
Then type `target remote localhost:1234` and type `c` (for continue) - and then if it crashes just do a `bt` (backtrace) and `layout asm`.
Run `./build/bin/eden-cli -c <path to your config file (see logs where you run eden normally to see where it is)> -d -g <path to game>`
Or `Enable GDB Stub` at General > Debug, then hook up an aarch64-gdb:
* `target remote localhost:6543`
Type `c` (for continue) and then if it crashes just do a `bt` (backtrace) and `layout asm`
### gdb cheatsheet
+14 -16
View File
@@ -51,12 +51,11 @@ All other dependencies will be downloaded and built by [CPM](https://github.com/
* [fmt](https://fmt.dev/) 8.0.1+
* [lz4](http://www.lz4.org)
* [nlohmann\_json](https://github.com/nlohmann/json) 3.8+
* [OpenSSL](https://www.openssl.org/source/) 1.1.1+
* [OpenSSL](https://www.openssl.org/source/) 3+
* [ZLIB](https://www.zlib.net/) 1.2+
* [zstd](https://facebook.github.io/zstd/) 1.5+
* [enet](http://enet.bespin.org/) 1.3+
* [Opus](https://opus-codec.org/) 1.3+
* [MbedTLS](https://github.com/Mbed-TLS/mbedtls) 3+
Vulkan 1.3.274+ is also needed:
@@ -121,7 +120,7 @@ sudo emerge -a \
dev-util/vulkan-utility-libraries dev-util/glslang \
media-gfx/renderdoc media-libs/libva media-libs/opus media-video/ffmpeg \
media-libs/VulkanMemoryAllocator media-libs/libsdl2 media-libs/cubeb \
net-libs/enet net-libs/mbedtls \
net-libs/enet \
sys-libs/zlib \
dev-cpp/nlohmann_json dev-cpp/simpleini dev-cpp/cpp-httplib dev-cpp/cpp-jwt \
games-util/gamemode \
@@ -139,7 +138,6 @@ Required USE flags:
* `dev-qt/qtbase network concurrent dbus gui widgets`
* `dev-libs/quazip qt6`
* `net-libs/mbedtls cmac`
* `media-libs/libsdl2 haptic joystick sound video`
* `dev-cpp/cpp-httplib ssl`
@@ -151,7 +149,7 @@ Required USE flags:
<summary>Arch Linux</summary>
```sh
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 mbedtls ninja nlohmann-json openssl opus qt6-base qt6-multimedia sdl2 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja nlohmann-json openssl opus qt6-base qt6-multimedia qt6-charts sdl2 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
```
* Building with QT Web Engine requires `qt6-webengine` as well.
@@ -164,7 +162,7 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl
<summary>Ubuntu, Debian, Mint Linux</summary>
```sh
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev libmbedtls-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl2-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl2-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev
```
* Ubuntu 22.04, Linux Mint 20, or Debian 12 or later is required.
@@ -178,7 +176,7 @@ sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev l
Fedora:
```sh
sudo dnf install autoconf cmake fmt-devel gcc{,-c++} glslang hidapi-devel json-devel libtool libusb1-devel libzstd-devel lz4-devel nasm ninja-build openssl-devel pulseaudio-libs-devel qt6-linguist qt6-qtbase{-private,}-devel qt6-qtwebengine-devel qt6-qtmultimedia-devel speexdsp-devel wayland-devel zlib-devel ffmpeg-devel libXext-devel boost jq
sudo dnf install autoconf cmake fmt-devel gcc{,-c++} glslang hidapi-devel json-devel libtool libusb1-devel libzstd-devel lz4-devel nasm ninja-build openssl-devel pulseaudio-libs-devel qt6-linguist qt6-qtbase{-private,}-devel qt6-qtwebengine-devel qt6-qtmultimedia-devel qt6-charts-devel speexdsp-devel wayland-devel zlib-devel ffmpeg-devel libXext-devel boost jq
```
AlmaLinux (use `YUZU_USE_CPM=ON`):
@@ -211,17 +209,15 @@ First, enable the community repository; [see here](https://wiki.alpinelinux.org/
# Enable the community repository
setup-apkrepos -c
# Install
apk add g++ git cmake make mbedtls-dev mbedtls-static mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev libusb-dev libtool boost-dev sdl2-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch
apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl2-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch
```
`mbedtls-static` has to be specified otherwise `libeverest.a` and `libp256m.a` will fail to be found.
</details>
<details>
<summary>Void Linux</summary>
```sh
xbps-install -Su git make cmake clang pkg-config patch mbedtls-devel SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL2-devel quazip-qt6-devel qt6-base-devel qt6-qt5compat-devel fmt-devel json-c++ libenet-devel libusb-devel
xbps-install -Su git make cmake clang pkg-config patch SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL2-devel quazip-qt6-devel qt6-base-devel qt6-qt5compat-devel qt6-charts-devel fmt-devel json-c++ libenet-devel libusb-devel
```
Yes, `nlohmann-json` is just named `json-c++`. Why?
@@ -233,6 +229,8 @@ Yes, `nlohmann-json` is just named `json-c++`. Why?
A convenience script is provided on the root of this project [shell.nix](../shell.nix). Run the usual `nix-shell`.
If you're going for a pure build (i.e no downloaded deps), use `-DYUZU_USE_CPM=ON -DCPMUTIL_FORCE_SYSTEM=ON`.
</details>
<details>
<summary>macOS</summary>
@@ -257,7 +255,7 @@ brew install molten-vk
<details>
<summary>FreeBSD</summary>
As root run: `pkg install devel/cmake devel/sdl20 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib devel/unordered-dense mbedtls3 vulkan-headers quazip-qt6`
As root run: `pkg install devel/cmake devel/sdl20 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base devel/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib devel/unordered-dense vulkan-headers quazip-qt6`
If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
@@ -267,7 +265,7 @@ If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
<details>
<summary>NetBSD</summary>
For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6 mbedtls3 cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx`.
For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6 cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx`.
[Caveats](./Caveats.md#netbsd).
@@ -287,7 +285,7 @@ pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gm
<summary>DragonFlyBSD</summary>
```sh
pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq nlohmann-json enet spirv-tools sdl2 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs cpp-httplib qt6-base quazip-qt6 unordered-dense libva-vdpau-driver libva-utils libva-intel-driver
pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq nlohmann-json enet spirv-tools sdl2 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs cpp-httplib qt6-base qt6-charts quazip-qt6 unordered-dense libva-vdpau-driver libva-utils libva-intel-driver
```
[Caveats](./Caveats.md#dragonflybsd).
@@ -311,7 +309,7 @@ sudo pkg install qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json ope
```sh
BASE="git make autoconf libtool automake-wrapper jq patch"
MINGW="qt6-base qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus mbedtls libusb unordered_dense openssl SDL2"
MINGW="qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus libusb unordered_dense openssl SDL2"
# Either x86_64 or clang-aarch64 (Windows on ARM)
packages="$BASE"
for pkg in $MINGW; do
@@ -337,7 +335,7 @@ pacman -Syuu --needed --noconfirm $packages
<summary>HaikuOS</summary>
```sh
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.89_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl2_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel mbedtls3_devel glslang qt6_devel
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.89_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl2_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel
```
[Caveats](./Caveats.md#haikuos).
-9
View File
@@ -286,15 +286,6 @@ Exclusive OR (i.e.: XOR)
Memory access.
### Terminal: Interpret
```c++
SetTerm(IR::Term::Interpret{next})
```
This terminal instruction calls the interpreter, starting at `next`.
The interpreter must interpret exactly one instruction.
### Terminal: ReturnToDispatch
```c++
-5
View File
@@ -117,11 +117,6 @@ public:
MemoryWrite32(vaddr + 4, u32(value >> 32));
}
void InterpreterFallback(u32 pc, size_t num_instructions) override {
// This is never called in practice.
std::terminate();
}
void CallSVC(u32 swi) override {
// Do something.
}
+41 -2
View File
@@ -12,12 +12,13 @@ The setting ranges from 0 to 65535 (0x0000 to 0xFFFF), where each bit represents
* [Accessing Debug Knobs (dev side)](#accessing-debug-knobs-dev-side)
* [Setting Debug Knobs (user side)](#setting-debug-knobs-user-side)
* [Bit Manipulation Examples](#bit-manipulation-examples)
3. [Examples](#examples)
3. [Terminology and user communication](#terminology-and-user-communication)
4. [Examples](#examples)
* [Example 1: Conditional Debug Logging](#example-1-conditional-debug-logging)
* [Example 2: Performance Tuning](#example-2-performance-tuning)
* [Example 3: Feature Gating](#example-3-feature-gating)
4. [Best Practices](#best-practices)
5. [Best Practices](#best-practices)
---
@@ -77,6 +78,44 @@ To enable specific features, calculate the decimal value by setting the appropri
* **Enable bits 0 and 1**: Value = 3 (2^0 + 2^1)
* **Enable bit 15**: Value = 32768 (2^15)
## Terminology and user communication
There are two main confusions when talking about knobs:
### Whether it's zero-based or one-based
Sometimes when an user reports: knobs 1 and 2 gets better performance, dev may get confuse whether he means the knobs 1 and 2 literally, or the 1st and 2nd knobs (knobs 0 and 1).
Debug knobs are **zero-based**, which means:
* The first knob is the knob(0) (or knob0 henceforth), and the last one is the 15 (knob15, likewise)
* You can talk: "knob0 is enabled/disabled", "In this video i was using only knobs 0 and 2", etc.
### Whether one is talking about the knob itself or about the entire parameter value (which represents all knobs)
Sometimes when an user reports: knob 3 results, it's unclear whether he's referring to knob setting with value 3 (which means both knob 0 and 1 are enabled), or to knob(3) specifically.
Whenever you're instructing tests or reporting results, be precise about whether one you're talking to avoid confusion:
### Setting based terminology
ALWAYS use the word in PLURAL (knobs), without mentioning which one, to refer to the setting, aka multiple knobs at once:
Examples:
- **knobs=0**: no knobs enabled
- **knobs=1**: knob0 enabled, others disabled
- **knobs=2**: knob1 enabled, others disabled
- **knobs=3**: knobs 0 and 1 enabled, others disabled
...
### Knob based terminology
Use the word in SINGULAR (knob), or in plural but referring which ones, when meaning multiple knobs at once:
Examples:
- **knob0**: knob 0 enabled, others disabled
- **knob1**: knob 1 enabled, others disabled
- **knobs 0 and 1**: knobs 0 and 1 enabled, others disabled
...
## Examples
### Example 1: Conditional Debug Logging
+1 -4
View File
@@ -6,7 +6,4 @@ Debugging on physical hardware can get tedious and time consuming. Users are emp
**Standard key prefix**: Allows to redirect the key manager to a file other than `prod.keys` (for example `other` would redirect to `other.keys`). This is useful for testing multiple keysets. Default is `prod`.
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as it will be writen in-place (that is, it will be filled with the default serial and then overwrite the serial from the beginning).
- Battery serial: `YUZU0EMULATOR14022024`
- Board serial: `YUZ10000000001`
If the user were to set their board serial as `ABC`, then it will be written in-place and the resulting serial would be `ABC10000000001`. There are no underlying checks to ensure correctness of serials other than a hard limit of 16-characters for both.
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as only the first digits (excluding the last) will be accoutned for. Region settings will affect the generated serial. The serial corresponds to a non-OLED/Lite console.
+3 -14
View File
@@ -27,11 +27,7 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
# Xbyak
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
if (PLATFORM_SUN OR PLATFORM_OPENBSD OR PLATFORM_NETBSD OR PLATFORM_DRAGONFLY)
AddJsonPackage(xbyak_sun)
else()
AddJsonPackage(xbyak)
endif()
AddJsonPackage(xbyak)
endif()
# enet
@@ -45,9 +41,6 @@ if (NOT TARGET enet::enet)
add_library(enet::enet ALIAS enet)
endif()
# mbedtls
AddJsonPackage(mbedtls)
# stb
add_library(stb stb/stb_dxt.cpp)
target_include_directories(stb PUBLIC ./stb)
@@ -238,9 +231,7 @@ if (VulkanMemoryAllocator_ADDED)
endif()
# httplib
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER OR USE_DISCORD_PRESENCE OR ENABLE_OPENSSL)
AddJsonPackage(httplib)
endif()
AddJsonPackage(httplib)
# cpp-jwt
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
@@ -411,12 +402,10 @@ if (APPLE)
# moltenvk
if (NOT YUZU_USE_BUNDLED_MOLTENVK)
find_library(MOLTENVK_LIBRARY MoltenVK)
else()
unset(MOLTENVK_LIBRARY)
endif()
# TODO: kosmickrisp?
if (NOT MOLTENVK_LIBRARY)
if (NOT MOLTENVK_LIBRARY OR YUZU_USE_BUNDLED_MOLTENVK)
AddJsonPackage(moltenvk)
set(MOLTENVK_LIBRARY "${moltenvk_SOURCE_DIR}/MoltenVK/dylib/macOS/libMoltenVK.dylib" CACHE STRING "" FORCE)
+9 -33
View File
@@ -33,6 +33,9 @@
"find_args": "MODULE GLOBAL",
"patches": [
"0001-mingw.patch"
],
"options": [
"HTTPLIB_REQUIRE_OPENSSL ON"
]
},
"cpp-jwt": {
@@ -48,23 +51,12 @@
"0001-fix-missing-decl.patch"
]
},
"xbyak_sun": {
"package": "xbyak",
"repo": "herumi/xbyak",
"tag": "v%VERSION%",
"hash": "b40dade90fb0e46a2bd52934f7ce461e37be931b571e58cbe2203bc08ed5b54c7ff1a29026c74c7f9805e4e3f6c9636deca528e6b4a8093ce7eae145218599f1",
"git_version": "7.29",
"bundled": true,
"skip_updates": true
},
"xbyak": {
"package": "xbyak",
"repo": "herumi/xbyak",
"tag": "v%VERSION%",
"hash": "1042090405c426e339506c179d53e91d4d545ce9c9f53d8f797caa092d589f913a9bcb9c8f31c4c60870acb954c556e305fb6732c66bc3c8f1cd924f9172def9",
"git_version": "7.22",
"bundled": true,
"skip_updates": true
"hash": "ac333d7bea1d61865bebebb116201a58db431946aa2f11aa042ef5795c390ff30af4d6c90ed3b3d24443a1d430703b08f14fc13b2fa405c155a241456ed78a47",
"git_version": "7.33.2"
},
"oaknut": {
"repo": "eden-emulator/oaknut",
@@ -99,20 +91,6 @@
"0001-avoid-memset-when-clearing-an-empty-table.patch"
]
},
"mbedtls": {
"package": "MbedTLS",
"repo": "Mbed-TLS/mbedtls",
"tag": "mbedtls-%VERSION%",
"hash": "6671fb8fcaa832e5b115dfdce8f78baa6a4aea71f5c89a640583634cdee27aefe3bf4be075744da91f7c3ae5ea4e0c765c8fc3937b5cfd9ea73d87ef496524da",
"version": "3",
"git_version": "3.6.4",
"artifact": "%TAG%.tar.bz2",
"skip_updates": true,
"patches": [
"0001-aesni-fix.patch",
"0002-arm64-aes-fix.patch"
]
},
"enet": {
"repo": "lsalzman/enet",
"tag": "v%VERSION%",
@@ -161,7 +139,7 @@
"package": "SDL2",
"name": "SDL2",
"repo": "crueter-ci/SDL2",
"version": "2.32.10-a65111bd2d",
"version": "2.32.10-cf5dabd6ea",
"min_version": "2.26.4"
},
"catch2": {
@@ -195,7 +173,6 @@
"repo": "libsdl-org/SDL",
"tag": "release-%VERSION%",
"hash": "d5622d6bb7266f7942a7b8ad43e8a22524893bf0c2ea1af91204838d9b78d32768843f6faa248757427b8404b8c6443776d4afa6b672cd8571a4e0c03a829383",
"key": "generic",
"bundled": true,
"git_version": "2.32.10",
"skip_updates": true
@@ -205,7 +182,6 @@
"repo": "libsdl-org/SDL",
"sha": "cc016b0046",
"hash": "b8d9873446cdb922387471df9968e078714683046674ef0d0edddf8e25da65a539a3bae83d635496b970237f90b07b36a69f8d7855d450de59311d6d6e8c3dbc",
"key": "steamdeck",
"bundled": true,
"skip_updates": "true"
},
@@ -255,8 +231,8 @@
},
"ffmpeg": {
"repo": "FFmpeg/FFmpeg",
"sha": "5e56937b74",
"hash": "9ab0457dcd6ce6359b5053c1662f57910d332f68ca0cca9d4134d858464840917027374de3d97e0863c3a7daaea2fe4f4cd17d1c6d8e7f740f4ad91e71c2932b",
"sha": "c7b5f1537d",
"hash": "ed177621176b3961bdcaa339187d3a7688c1c8b060b79c4bb0257cbc67ad7021ae5d5adca5303b45625abbbe3d9aafdd87ce777b8690ac295290d744c875489a",
"bundled": true
},
"ffmpeg-ci": {
@@ -264,7 +240,7 @@
"package": "FFmpeg",
"name": "ffmpeg",
"repo": "crueter-ci/FFmpeg",
"version": "8.0.1-5e56937b74",
"version": "8.0.1-c7b5f1537d",
"min_version": "4.1"
},
"tzdb": {
+6 -3
View File
@@ -94,6 +94,10 @@ if (MSVC AND NOT CXX_CLANG)
/wd4324 # 'struct_name': structure was padded due to __declspec(align())
/wd4201 # nonstandard extension used : nameless struct/union
/wd4702 # unreachable code (when used with LTO)
$<$<CONFIG:Release>:/GS-> # No stack buffer overflow checks
/Gy # Enable function level linking
/GR- # Disable run time type information
)
if (NOT CXX_CLANG)
@@ -108,15 +112,14 @@ if (MSVC AND NOT CXX_CLANG)
add_compile_options(/QIntel-jcc-erratum)
endif()
# /GS- - No stack buffer overflow checks
add_compile_options("$<$<CONFIG:Release>:/GS->")
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
else()
if (NOT MSVC)
add_compile_options(
-fwrapv
-fno-rtti # Disable RTTI
-pipe
)
endif()
+26 -45
View File
@@ -5,6 +5,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// import android.annotation.SuppressLint
import com.android.build.gradle.api.ApplicationVariant
import kotlin.collections.setOf
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import org.gradle.api.tasks.Copy
@@ -13,8 +15,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
id("org.jetbrains.kotlin.plugin.compose")
kotlin("plugin.serialization") version "2.3.0"
kotlin("plugin.serialization") version "1.9.20"
id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
id("com.github.triplet.play") version "3.8.6"
@@ -42,7 +43,6 @@ android {
buildFeatures {
viewBinding = true
compose = true
}
compileOptions {
@@ -50,10 +50,8 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
kotlinOptions {
jvmTarget = "17"
}
packaging {
@@ -67,7 +65,7 @@ android {
defaultConfig {
applicationId = "dev.eden.eden_emulator"
minSdk = 26
minSdk = 24
targetSdk = 36
versionName = getGitVersion()
versionCode = autoVersion
@@ -269,10 +267,6 @@ android {
}
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
}
idea {
module {
// Inclusion to exclude build/ dir from non-Android
@@ -298,7 +292,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
ktlint {
version.set("0.50.0")
version.set("0.47.1")
android.set(true)
ignoreFailures.set(false)
disabledRules.set(
@@ -323,42 +317,29 @@ play {
}
dependencies {
implementation("androidx.compose:compose-bom:2026.01.01")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.4.0")
implementation("androidx.activity:activity-compose:1.12.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
implementation("androidx.navigation:navigation-compose:2.9.7")
implementation("io.coil-kt:coil-compose:2.7.0")
implementation("io.coil-kt:coil-svg:2.7.0")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.documentfile:documentfile:1.1.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.fragment:fragment-ktx:1.8.6")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("io.coil-kt:coil:2.7.0")
implementation("androidx.core:core-splashscreen:1.2.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.21.0")
implementation("androidx.window:window:1.5.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0")
implementation("org.commonmark:commonmark:0.27.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.9.7")
implementation("androidx.navigation:navigation-ui-ktx:2.9.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
implementation("androidx.window:window:1.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("org.commonmark:commonmark:0.22.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation("androidx.compose.ui:ui-graphics-android:1.10.2")
implementation("androidx.compose.ui:ui-text-android:1.10.2")
implementation("net.swiftzer.semver:semver:2.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("androidx.compose.ui:ui-graphics-android:1.7.8")
implementation("androidx.compose.ui:ui-text-android:1.7.8")
implementation("net.swiftzer.semver:semver:2.0.0")
}
fun runGitCommand(command: List<String>): String {
-230
View File
@@ -1,230 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
fill="none"
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:0.5;"
offset="0"
id="stop3" />
<stop
style="stop-color:#bf42f6;stop-opacity:0.5;"
offset="0.44631511"
id="stop4" />
<stop
style="stop-color:#5da5ed;stop-opacity:0.5;"
offset="0.90088946"
id="stop2" />
</linearGradient>
<linearGradient
id="linearGradient138"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop152" />
<stop
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop137" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop138" />
</linearGradient>
<linearGradient
id="swatch37"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop37" />
</linearGradient>
<linearGradient
id="swatch28"
inkscape:swatch="solid">
<stop
style="stop-color:#252525;stop-opacity:1;"
offset="0"
id="stop28" />
</linearGradient>
<linearGradient
id="swatch27"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop27" />
</linearGradient>
<linearGradient
id="swatch15"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop16" />
</linearGradient>
<linearGradient
id="linearGradient14"
inkscape:swatch="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop14" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop15" />
</linearGradient>
<linearGradient
id="swatch9"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop10" />
</linearGradient>
<linearGradient
id="swatch8"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop9" />
</linearGradient>
<rect
x="22.627417"
y="402.76802"
width="521.34025"
height="248.94868"
id="rect24" />
<linearGradient
id="linearGradient11"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop11" />
<stop
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop154" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop12" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient138"
id="linearGradient6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.118028,0,0,1.116699,-46.314723,-42.388667)"
x1="270.39996"
y1="40.000019"
x2="270.39996"
y2="494.39996"
spreadMethod="pad" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle18"
cx="-246.8315"
cy="246.8338"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle22"
cx="256"
cy="256"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient27"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-6.9401139e-5,-2.8678628)"
x1="256.00012"
y1="102.94693"
x2="256.00012"
y2="409.05307" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath128">
<circle
style="fill:none;fill-opacity:1;stroke:#03ffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
id="circle128"
cx="256"
cy="256"
r="192" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="256"
y1="64"
x2="256"
y2="448"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3229974,0,0,1.3214002,-82.687336,-82.290326)" />
</defs>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.4142136"
inkscape:cx="261.62951"
inkscape:cy="230.87036"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="1080"
inkscape:window-y="351"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<path
id="path8-7"
style="display:inline;mix-blend-mode:multiply;fill:url(#linearGradient6);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:3.9666;stroke-dasharray:none;stroke-opacity:0.566238;paint-order:stroke fill markers"
inkscape:label="Circle"
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
<path
id="path27"
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient27);stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
inkscape:label="MainOutline"
clip-path="url(#clipPath128)"
transform="matrix(1.3229974,0,0,1.3214002,-82.687282,-82.278451)" />
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,184 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.background
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import dev.eden.emu.ui.theme.EdenTheme
import kotlin.math.pow
@Composable
fun RetroGridBackground(
modifier: Modifier = Modifier,
lineColor: Color = EdenTheme.colors.primary.copy(alpha = 0.2f),
spacing: Dp = 40.dp,
lineWidth: Dp = 2.dp,
speedDpPerSecond: Float = 12f,
fadeColor: Color = EdenTheme.colors.background,
logoScale: Float = 0.35f,
logoAlpha: Float = 0.25f,
horizonRatio: Float = 0.45f,
logoAssetPath: String = "file:///android_asset/base.svg", // correct way?
logoWidth: Dp? = null,
logoHeight: Dp? = null,
logoCropBottomRatio: Float = 0.33f,
) {
val context = LocalContext.current
val density = LocalDensity.current
val durationMs = remember(spacing, speedDpPerSecond, density) {
val spacingPx = spacing.value * density.density
val speedPxPerSecond = speedDpPerSecond * density.density
((spacingPx / speedPxPerSecond) * 1000f).toInt().coerceAtLeast(300)
}
val transition = rememberInfiniteTransition(label = "grid")
val phase by transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = durationMs, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "gridPhase",
)
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val baseSize = maxWidth * logoScale
val resolvedWidth = logoWidth ?: baseSize
val resolvedHeight = logoHeight ?: baseSize
val cropRatio = logoCropBottomRatio.coerceIn(0f, 0.9f)
val visibleHeight = resolvedHeight * (1f - cropRatio)
val visibleHeightPx = with(density) { visibleHeight.toPx() }
val horizonY = maxHeight * horizonRatio
val logoOffsetY = horizonY - (resolvedHeight * 0.5f) - 50.dp
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.drawWithCache {
val spacingPx = spacing.toPx().coerceAtLeast(8f)
val strokePx = lineWidth.toPx().coerceAtLeast(1f)
val horizonScale = 0.18f
val overscan = 5.0f
val perspectivePower = 2.2f
onDrawBehind {
val width = size.width
val height = size.height
val centerX = width * 0.5f
val horizonYpx = height * horizonRatio
val depth = (height - horizonYpx).coerceAtLeast(1f)
val offset = (1f - phase) * spacingPx
val baseHalf = centerX * overscan
val horizonHalf = baseHalf * horizonScale
val lineCount = (depth / (spacingPx * 0.65f))
.toInt()
.coerceAtLeast(20)
var x = centerX - baseHalf - spacingPx * 6f
while (x <= centerX + baseHalf + spacingPx * 6f) {
val endX = centerX + (x - centerX) * horizonScale
drawLine(
color = lineColor,
start = Offset(x, height),
end = Offset(endX, horizonYpx),
strokeWidth = strokePx,
)
x += spacingPx
}
var i = 0
while (i <= lineCount) {
val z = ((i + (offset / spacingPx)) / lineCount.toFloat())
.coerceIn(0f, 1f)
val zCurve = z.toDouble().pow(perspectivePower.toDouble()).toFloat()
val y = horizonYpx + depth * zCurve
val halfWidth = horizonHalf + (baseHalf - horizonHalf) * z
drawLine(
color = lineColor,
start = Offset(centerX - halfWidth, y),
end = Offset(centerX + halfWidth, y),
strokeWidth = strokePx,
)
i += 1
}
val fadeHeight = height * 0.12f
val solidHeight = (horizonYpx - fadeHeight).coerceAtLeast(0f)
if (solidHeight > 0f) {
drawRect(
color = fadeColor,
topLeft = Offset(0f, 0f),
size = Size(width, solidHeight),
)
}
drawRect(
brush = Brush.verticalGradient(
colors = listOf(fadeColor, Color.Transparent),
startY = horizonYpx - fadeHeight,
endY = horizonYpx + fadeHeight,
),
topLeft = Offset(0f, horizonYpx - fadeHeight),
size = Size(width, fadeHeight * 2f),
)
}
},
)
AsyncImage(
model = ImageRequest.Builder(context)
.data(logoAssetPath)
.decoderFactory(SvgDecoder.Factory())
.build(),
contentDescription = null,
modifier = Modifier
.align(Alignment.TopCenter)
.requiredWidth(resolvedWidth)
.requiredHeight(resolvedHeight)
.offset(y = logoOffsetY)
.drawWithContent {
clipRect(
left = 0f,
top = 0f,
right = size.width,
bottom = visibleHeightPx,
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer(alpha = logoAlpha),
)
}
}
}
@@ -1,157 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.GameIconUtils
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.ConfirmKeys
import dev.eden.emu.ui.utils.MenuKeys
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun EdenTile(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
imageUri: String? = null,
iconSize: Dp = 140.dp,
tileHeight: Dp = 168.dp,
useLargerFont: Boolean = false,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused = interactionSource.collectIsFocusedAsState().value
val borderColor = if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface
Column(
modifier = modifier
.width(iconSize)
.height(tileHeight)
.semantics { role = Role.Button }
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp) {
when {
e.key in ConfirmKeys -> { onClick(); true }
e.key in MenuKeys -> { onLongClick?.invoke(); true }
else -> false
}
} else false
}
.focusable(interactionSource = interactionSource)
.combinedClickable(
interactionSource = interactionSource,
indication = null,
onClick = onClick,
onLongClick = onLongClick,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// Game icon
Box(
modifier = Modifier
.size(iconSize)
.background(EdenTheme.colors.surface, Shapes.medium)
.border(BorderStroke(3.dp, borderColor), Shapes.medium)
.clip(Shapes.medium),
contentAlignment = Alignment.Center,
) {
if (imageUri.isNullOrBlank()) {
BasicText(
text = title.take(1).uppercase(Locale.getDefault()),
style = EdenTheme.typography.title.copy(color = EdenTheme.colors.onSurface),
)
} else {
AndroidView(
factory = { ctx ->
android.widget.ImageView(ctx).apply {
layoutParams = android.view.ViewGroup.LayoutParams(-1, -1)
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
setImageResource(R.drawable.default_icon)
GameIconUtils.loadGameIcon(Game(title, imageUri, "0", "", "", false), this)
}
},
modifier = Modifier.fillMaxSize()
)
}
}
// Game title with marquee
MarqueeText(title, isFocused, 32.dp, useLargerFont, Modifier.width(iconSize))
}
}
@Composable
private fun MarqueeText(
text: String,
isAnimating: Boolean,
height: Dp,
useLargerFont: Boolean,
modifier: Modifier = Modifier,
) {
val style = if (useLargerFont) {
EdenTheme.typography.body.copy(Color.White, 18.sp, fontWeight = FontWeight.Bold)
} else {
EdenTheme.typography.label.copy(Color.White)
}
Box(modifier.height(height).clipToBounds(), Alignment.Center) {
BasicText(
text = text,
style = style,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = if (isAnimating) {
Modifier.basicMarquee(
iterations = Int.MAX_VALUE,
animationMode = MarqueeAnimationMode.Immediately,
initialDelayMillis = 150, // does not seem to take effect
repeatDelayMillis = 1000,
velocity = 30.dp,
)
} else Modifier,
)
}
}
@@ -1,259 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.utils.ConfirmKeys
/**
* Expandable search bar for controller-based navigation.
* ________________________________________________________________
* Note: Focus Manager on this doesn't work good. Need a hide impl.
* when it looses focus for better Controller Experience
* ________________________________________________________________
*/
@Composable
fun ExpandableSearchBar(
query: String,
onQueryChange: (String) -> Unit,
isExpanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val searchHint = context.getString(R.string.home_search_games)
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val iconInteractionSource = remember { MutableInteractionSource() }
val isIconFocused by iconInteractionSource.collectIsFocusedAsState()
// Track if we have an active filter (submitted search, if keyboard just hides, it kinda breaks)
val hasActiveFilter = query.isNotEmpty() && !isExpanded
val textFieldFocusRequester = remember { FocusRequester() }
// Focus text field when expanded
LaunchedEffect(isExpanded) {
if (isExpanded) {
delay(100)
try {
textFieldFocusRequester.requestFocus()
keyboardController?.show()
} catch (_: Exception) {}
}
}
// Use a fixed width container to prevent layout shift.
Box(
modifier = modifier.width(if (isExpanded) 300.dp else 48.dp),
contentAlignment = Alignment.CenterEnd,
) {
if (isExpanded) {
Row(
modifier = Modifier
.width(300.dp)
.clip(RoundedCornerShape(24.dp))
.background(EdenTheme.colors.surface)
.border(
width = 2.dp,
color = EdenTheme.colors.primary,
shape = RoundedCornerShape(24.dp)
),
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.width(16.dp))
// Text input - NO onFocusChanged to prevent auto-close issues
// (Needs to be "submitted", hiding keyboard breaks focus flow)
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.weight(1f)
.focusRequester(textFieldFocusRequester)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyUp) {
when (keyEvent.key) {
Key.Escape, Key.Back -> {
onQueryChange("")
onExpandedChange(false)
keyboardController?.hide()
focusManager.clearFocus()
true
}
Key.Enter, Key.NumPadEnter -> {
keyboardController?.hide()
onExpandedChange(false)
focusManager.clearFocus()
true
}
else -> false
}
} else false
},
textStyle = TextStyle(
color = Color.White,
fontSize = 16.sp,
),
cursorBrush = SolidColor(EdenTheme.colors.primary),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
keyboardController?.hide()
onExpandedChange(false)
focusManager.clearFocus()
}
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.padding(vertical = 14.dp),
contentAlignment = Alignment.CenterStart,
) {
if (query.isEmpty()) {
androidx.compose.foundation.text.BasicText(
text = searchHint,
style = TextStyle(
color = Color.White.copy(alpha = 0.5f),
fontSize = 16.sp,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
innerTextField()
}
}
)
// Clear/Close button (X icon)
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.clickable {
onQueryChange("")
onExpandedChange(false)
keyboardController?.hide()
focusManager.clearFocus()
}
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_clear),
contentDescription = context.getString(R.string.home_clear),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp),
)
}
Spacer(modifier = Modifier.width(4.dp))
}
} else {
// Collapsed state: search icon
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(
if (isIconFocused) EdenTheme.colors.primary
else EdenTheme.colors.surface
)
.focusRequester(focusRequester)
.focusable(interactionSource = iconInteractionSource)
.clickable(
interactionSource = iconInteractionSource,
indication = null,
) {
if (hasActiveFilter) {
onQueryChange("")
} else {
onExpandedChange(true)
}
}
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key in ConfirmKeys) {
if (hasActiveFilter) {
onQueryChange("")
} else {
onExpandedChange(true)
}
true
} else false
},
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
contentDescription = context.getString(R.string.home_search),
tint = if (hasActiveFilter) EdenTheme.colors.primary else Color.White,
modifier = Modifier.size(24.dp),
)
// Active filter indicator dot
if (hasActiveFilter) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(6.dp)
.size(10.dp)
.clip(CircleShape)
.background(EdenTheme.colors.primary)
)
}
}
}
}
}
@@ -1,319 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlin.math.abs
import kotlin.math.cos
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun GameCarousel(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit = {},
modifier: Modifier = Modifier,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onShowGameInfo: (GameTile) -> Unit = {},
) {
if (gameTiles.isEmpty()) return
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = safeInitialIndex,
initialFirstVisibleItemScrollOffset = 0
)
val snapFlingBehavior = rememberSnapFlingBehavior(
lazyListState = listState,
snapPosition = SnapPosition.Center
)
val coroutineScope = rememberCoroutineScope()
var centerItemIndex by remember { mutableIntStateOf(safeInitialIndex) }
LaunchedEffect(centerItemIndex) {
onFocusedIndexChanged(centerItemIndex)
}
suspend fun centerOnIndex(index: Int, animate: Boolean = true) {
if (gameTiles.isEmpty()) return
if (listState.layoutInfo.visibleItemsInfo.none { it.index == index }) {
if (animate) {
listState.animateScrollToItem(index)
} else {
listState.scrollToItem(index)
}
}
delay(10)
val layoutInfo = listState.layoutInfo
val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return
val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2
val desiredStart = viewportCenter - itemInfo.size / 2
if (itemInfo.offset != desiredStart) {
val scrollOffset = -(viewportCenter - itemInfo.size / 2)
if (animate) {
listState.animateScrollToItem(index, scrollOffset = scrollOffset)
} else {
listState.scrollToItem(index, scrollOffset = scrollOffset)
}
}
}
LaunchedEffect(Unit) {
listState.scrollToItem(safeInitialIndex)
delay(50)
centerOnIndex(safeInitialIndex, animate = false)
}
val focusRequesters = remember(gameTiles.size) {
List(gameTiles.size) { FocusRequester() }
}
var isFocusTriggeredScroll by remember { mutableStateOf(false) }
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { isScrolling ->
if (!isScrolling && !isFocusTriggeredScroll && gameTiles.isNotEmpty()) {
val firstVisible = listState.firstVisibleItemIndex
val scrollOffset = listState.firstVisibleItemScrollOffset
val layoutInfo = listState.layoutInfo
val firstVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull()
val itemSize = firstVisibleItem?.size ?: 1
val newCenterIndex = if (scrollOffset > itemSize / 2) {
(firstVisible + 1).coerceAtMost(gameTiles.size - 1)
} else {
firstVisible
}
if (newCenterIndex != centerItemIndex && newCenterIndex in focusRequesters.indices) {
centerItemIndex = newCenterIndex
try {
focusRequesters[newCenterIndex].requestFocus()
} catch (_: Exception) { }
}
}
// Reset the flag when scroll stops
if (!isScrolling) {
isFocusTriggeredScroll = false
}
}
}
// Carousel settings (matching original CarouselRecyclerView)
val borderScale = 0.6f
val borderAlpha = 0.35f
val overlapFactor = 0.15f
BoxWithConstraints(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
val density = LocalDensity.current
val screenWidthPx = with(density) { maxWidth.toPx() }
val screenHeightPx = with(density) { maxHeight.toPx() }
val tileTextHeight = 40.dp
val tileSpacing = 6.dp
// Card size
val totalTextAreaPx = with(density) { (tileTextHeight + tileSpacing).toPx() }
val cardSizePx = (screenHeightPx - totalTextAreaPx) * 0.7f
val cardSize = with(density) { cardSizePx.toDp() }
val tileHeight = cardSize + tileSpacing + tileTextHeight
// Horizontal padding to center first/last card
val horizontalPaddingPx = (screenWidthPx - cardSizePx) / 2f
val horizontalPadding = with(density) { horizontalPaddingPx.toDp() }
// Negative spacing for overlap, no overlap currently tho
val overlapPx = cardSizePx * overlapFactor
val itemSpacing = with(density) { (-overlapPx).toDp() }
val centerPushPx = cardSizePx * 0.12f
val screenCenterPx = screenWidthPx / 2f
val itemWidthWithSpacing = cardSizePx - overlapPx
LazyRow(
state = listState,
modifier = Modifier.fillMaxSize(),
flingBehavior = snapFlingBehavior,
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(start = horizontalPadding, end = horizontalPadding, top = 30.dp),
verticalAlignment = Alignment.CenterVertically,
) {
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
val distanceFromCenter by remember {
derivedStateOf {
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
if (itemInfo == null) {
1f
} else {
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
val distance = abs(itemCenterInScreen - screenCenterPx)
(distance / screenCenterPx).coerceIn(0f, 1f)
}
}
}
val shapedScale = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
val scale = borderScale + (1f - borderScale) * shapedScale
val shapedAlpha = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
val alpha = borderAlpha + (1f - borderAlpha) * shapedAlpha
val zIndex = 100f * (1f - distanceFromCenter)
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
val isLeftOfCenter = if (itemInfo != null) {
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
itemCenterInScreen < screenCenterPx
} else {
index < (gameTiles.size / 2)
}
val pushFactor = (1f - distanceFromCenter) * distanceFromCenter * 4f
val horizontalPush = centerPushPx * pushFactor * (if (isLeftOfCenter) -1f else 1f)
val tileModifier = if (index == centerItemIndex) {
Modifier
.focusRequester(focusRequester)
.focusRequester(focusRequesters[index])
} else {
Modifier.focusRequester(focusRequesters[index])
}
Box(
modifier = Modifier
.size(width = cardSize, height = tileHeight)
.zIndex(zIndex)
.onFocusChanged { focusState ->
if (focusState.hasFocus && centerItemIndex != index) {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
}
}
}
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown) {
when (keyEvent.key) {
Key.ButtonX -> {
onShowGameInfo(tile)
true
}
Key.DirectionUp, Key.DirectionUpLeft, Key.DirectionUpRight -> {
onNavigateUp()
true
}
Key.DirectionLeft -> {
if (index > 0) {
focusRequesters[index - 1].requestFocus()
}
true
}
Key.DirectionRight -> {
if (index < gameTiles.size - 1) {
focusRequesters[index + 1].requestFocus()
}
true
}
else -> false
}
} else {
false
}
}
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
translationX = horizontalPush
},
contentAlignment = Alignment.Center,
) {
EdenTile(
title = tile.title,
iconSize = cardSize,
tileHeight = tileHeight,
onClick = {
if (index == centerItemIndex) {
onGameClick(tile)
} else {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
}
focusRequesters[index].requestFocus()
}
},
onLongClick = {
if (index == centerItemIndex) {
onGameLongClick(tile)
} else {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
onGameLongClick(tile)
}
focusRequesters[index].requestFocus()
}
},
modifier = tileModifier,
imageUri = tile.iconUri,
useLargerFont = true,
)
}
}
}
}
}
@@ -1,304 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.EdenTheme
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun GameGrid(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
onNavigateToSettings: () -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit = {},
rowCount: Int = 2,
tileIconSize: Dp = 100.dp,
paddingTop: Dp = 0.dp,
modifier: Modifier = Modifier,
isLoading: Boolean = false,
emptyMessage: String? = null,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onShowGameInfo: (GameTile) -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxSize()
) {
when {
isLoading -> LoadingMessage()
emptyMessage != null -> EmptyMessage(emptyMessage)
else -> {
GameGridContent(
gameTiles = gameTiles,
onGameClick = onGameClick,
onGameLongClick = onGameLongClick,
focusRequester = focusRequester,
onNavigateUp = onNavigateUp,
rowCount = rowCount,
tileIconSize = tileIconSize,
paddingTop = paddingTop,
initialFocusedIndex = initialFocusedIndex,
onFocusedIndexChanged = onFocusedIndexChanged,
onXPress = { index ->
gameTiles.getOrNull(index)?.let { tile ->
onShowGameInfo(tile)
}
},
)
}
}
}
}
@Composable
private fun GameGridContent(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit,
rowCount: Int,
tileIconSize: Dp,
paddingTop: Dp,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onXPress: (Int) -> Unit = {},
) {
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
var focusedIndex by remember { mutableIntStateOf(safeInitialIndex) }
val gridState = rememberLazyGridState(
initialFirstVisibleItemIndex = safeInitialIndex
)
LaunchedEffect(gameTiles.size) {
if (focusedIndex >= gameTiles.size) {
focusedIndex = 0
gridState.scrollToItem(0)
}
}
val tileFocusRequesters = remember(gameTiles.size) {
List(gameTiles.size) { FocusRequester() }
}
LaunchedEffect(focusRequester) {
if (tileFocusRequesters.isNotEmpty()) {
focusRequester.requestFocus()
}
}
LaunchedEffect(Unit) {
if (tileFocusRequesters.isNotEmpty() && safeInitialIndex in tileFocusRequesters.indices) {
gridState.scrollToItem(safeInitialIndex)
tileFocusRequesters[safeInitialIndex].requestFocus()
}
}
LaunchedEffect(focusedIndex) {
if (focusedIndex in tileFocusRequesters.indices) {
tileFocusRequesters[focusedIndex].requestFocus()
onFocusedIndexChanged(focusedIndex)
}
}
val outerPaddingTop = 62.dp
val outerPaddingBottom = 16.dp
val outerPaddingHorizontal = 0.dp
val gridVerticalSpacing = 0.dp
val gridVerticalPadding = 12.dp
val spacerHeight = 2.dp
val minTextHeight = 42.dp
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(
top = outerPaddingTop,
start = outerPaddingHorizontal,
end = outerPaddingHorizontal,
bottom = outerPaddingBottom
)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && gameTiles.isNotEmpty()) {
val currentRow = focusedIndex % rowCount
when (keyEvent.key) {
Key.ButtonX -> {
onXPress(focusedIndex)
true
}
Key.DirectionRight, Key.D -> {
val nextIndex = focusedIndex + rowCount
if (nextIndex < gameTiles.size) {
focusedIndex = nextIndex
}
true
}
Key.DirectionLeft, Key.A -> {
val prevIndex = focusedIndex - rowCount
if (prevIndex >= 0) {
focusedIndex = prevIndex
}
true
}
Key.DirectionDown, Key.S -> {
val nextIndex = focusedIndex + 1
if (nextIndex < gameTiles.size && nextIndex % rowCount != 0) {
focusedIndex = nextIndex
}
true
}
Key.DirectionUp, Key.W -> {
if (currentRow > 0) {
focusedIndex -= 1
true
} else {
onNavigateUp()
true
}
}
else -> false
}
} else {
false
}
},
contentAlignment = Alignment.TopStart
) {
val availableHeight = maxHeight
val heightForRows = (availableHeight - (gridVerticalPadding * 2) -
(gridVerticalSpacing * (rowCount - 1))).coerceAtLeast(0.dp)
val perRowHeight = heightForRows.safeDiv(rowCount)
val iconMaxSize = tileIconSize * 2
val iconSizeForGrid = (perRowHeight - spacerHeight - minTextHeight)
.coerceAtLeast(96.dp)
.coerceAtMost(iconMaxSize)
val tileHeight = perRowHeight.coerceAtLeast(iconSizeForGrid + spacerHeight + minTextHeight)
LazyHorizontalGrid(
rows = GridCells.Fixed(rowCount),
state = gridState,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = gridVerticalPadding),
) {
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
val tileModifier = if (index == focusedIndex) {
Modifier
.focusRequester(focusRequester)
.focusRequester(tileFocusRequesters[index])
} else {
Modifier.focusRequester(tileFocusRequesters[index])
}
EdenTile(
title = tile.title,
iconSize = iconSizeForGrid,
tileHeight = tileHeight,
onClick = {
focusedIndex = index
onGameClick(tile)
},
onLongClick = {
focusedIndex = index
onGameLongClick(tile)
},
modifier = tileModifier,
imageUri = tile.iconUri,
)
}
}
}
}
@Composable
private fun LoadingMessage() {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator(
modifier = Modifier.padding(bottom = 16.dp),
color = EdenTheme.colors.primary,
)
BasicText(
text = context.getString(R.string.loading),
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
)
}
}
@Composable
private fun EmptyMessage(message: String) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
BasicText(
text = message,
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
)
BasicText(
text = context.getString(R.string.manage_game_folders),
style = EdenTheme.typography.label.copy(color = EdenTheme.colors.onBackground),
modifier = Modifier.padding(top = 8.dp)
)
}
}
private fun Dp.safeDiv(divisor: Int): Dp = if (divisor > 0) this / divisor else 0.dp
@@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import org.yuzu.yuzu_emu.model.Game
data class GameTile(
val id: String,
val title: String,
val uri: String? = null,
val iconUri: String? = null,
val programId: Long = 0L,
val developer: String = "",
val version: String = "",
val isHomebrew: Boolean = false,
) {
companion object {
fun fromGame(game: Game): GameTile = GameTile(
id = game.path,
title = game.title,
uri = game.path,
iconUri = game.path,
programId = game.programId.toLongOrNull() ?: 0L,
developer = game.developer,
version = game.version,
isHomebrew = game.isHomebrew,
)
}
}
@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.eden.emu.ui.theme.Dimens
import org.yuzu.yuzu_emu.R
enum class FooterButton(@DrawableRes val iconRes: Int) {
A(R.drawable.facebutton_a),
B(R.drawable.facebutton_b),
X(R.drawable.facebutton_x),
Y(R.drawable.facebutton_y),
}
@Composable
fun HomeFooter(
actions: List<FooterAction>,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = Dimens.paddingLg, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingXl),
verticalAlignment = Alignment.CenterVertically,
) {
actions.forEach { action ->
Row(
modifier = Modifier.padding(horizontal = Dimens.paddingXs, vertical = 2.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = action.button.iconRes),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.9f)),
)
BasicText(action.text, style = TextStyle(Color.White.copy(0.7f), 14.sp))
}
}
}
}
data class FooterAction(val button: FooterButton, val text: String)
data class FooterFocusRequesters(val primaryAction: FocusRequester, val secondaryAction: FocusRequester)
@@ -1,186 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.ConfirmKeys
import java.io.File
@Composable
fun HomeHeader(
currentUser: String,
onUserClick: () -> Unit,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
isSearchExpanded: Boolean,
onSearchExpandedChange: (Boolean) -> Unit,
onSortClick: () -> Unit,
onFilterClick: () -> Unit,
onSettingsClick: () -> Unit,
focusRequesters: HeaderFocusRequesters,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var username by remember { mutableStateOf("Eden") }
var imagePath by remember { mutableStateOf<String?>(null) }
LaunchedEffect(currentUser) {
val uuid = currentUser.ifEmpty { NativeLibrary.getCurrentUser() ?: "" }
if (uuid.isNotEmpty()) {
username = NativeLibrary.getUserUsername(uuid) ?: "Eden"
imagePath = NativeLibrary.getUserImagePath(uuid)
}
}
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = Dimens.paddingXl, vertical = Dimens.paddingMd),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
UserProfileButton(username, imagePath, onUserClick, focusRequesters.userButton)
Row(
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingLg),
verticalAlignment = Alignment.CenterVertically,
) {
ExpandableSearchBar(searchQuery, onSearchQueryChange, isSearchExpanded, onSearchExpandedChange, focusRequesters.searchButton)
HeaderIconButton(R.drawable.ic_sort, context.getString(R.string.home_sort), onSortClick, focusRequesters.sortButton)
HeaderIconButton(R.drawable.ic_filter, context.getString(R.string.home_layout), onFilterClick, focusRequesters.filterButton)
HeaderIconButton(R.drawable.ic_settings, context.getString(R.string.home_settings), onSettingsClick, focusRequesters.settingsButton)
}
}
}
@Composable
private fun UserProfileButton(
username: String,
imagePath: String?,
onClick: () -> Unit,
focusRequester: FocusRequester,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val bitmap = remember(imagePath) {
imagePath?.takeIf { it.isNotEmpty() }?.let { path ->
File(path).takeIf { it.exists() }?.let { BitmapFactory.decodeFile(path) }
}
}
val defaultBitmap = remember {
NativeLibrary.getDefaultAccountBackupJpeg().let { BitmapFactory.decodeByteArray(it, 0, it.size) }
}
Row(
modifier = Modifier
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
}
.clickable(interactionSource, null, onClick = onClick)
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
.border(Dimens.borderWidth, if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
.padding(horizontal = Dimens.paddingMd, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingMd),
verticalAlignment = Alignment.CenterVertically,
) {
(bitmap ?: defaultBitmap)?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(46.dp).clip(CircleShape)
)
}
BasicText(username, style = EdenTheme.typography.body.copy(fontSize = 18.sp, color = Color.White))
}
}
@Composable
fun HeaderIconButton(
iconRes: Int,
contentDescription: String,
onClick: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
Box(
modifier = modifier
.size(Dimens.iconLg)
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
}
.clip(CircleShape)
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface)
.clickable(interactionSource, null, onClick = onClick),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(iconRes),
contentDescription = contentDescription,
tint = Color.White,
modifier = Modifier.size(Dimens.iconSm),
)
}
}
data class HeaderFocusRequesters(
val userButton: FocusRequester,
val searchButton: FocusRequester,
val sortButton: FocusRequester,
val filterButton: FocusRequester,
val settingsButton: FocusRequester,
)
@@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.DialogButton
enum class LayoutMode { TWO_ROW, CAROUSEL /*, ONE_ROW */ }
@Composable
fun LayoutSelectionDialog(
onDismiss: () -> Unit,
onSelectGrid: () -> Unit,
onSelectCarousel: () -> Unit,
) {
val context = LocalContext.current
val focusRequesters = remember { List(3) { FocusRequester() } }
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Column(
modifier = Modifier
.clip(Shapes.large)
.background(EdenTheme.colors.surface)
.padding(Dimens.paddingXl),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BasicText(
text = context.getString(R.string.home_layout),
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.view_grid),
onClick = { onSelectGrid(); onDismiss() },
focusRequester = focusRequesters[0],
)
Spacer(Modifier.height(Dimens.paddingMd))
DialogButton(
text = context.getString(R.string.view_carousel),
onClick = { onSelectCarousel(); onDismiss() },
focusRequester = focusRequesters[1],
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.cancel),
onClick = onDismiss,
focusRequester = focusRequesters[2],
isSecondary = true,
)
}
}
}
@@ -1,82 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.DialogButton
enum class SortMode { LAST_PLAYED, ALPHABETICAL }
@Composable
fun SortSelectionDialog(
currentSortMode: SortMode,
onDismiss: () -> Unit,
onSelectSort: (SortMode) -> Unit,
) {
val context = LocalContext.current
val focusRequesters = remember { List(3) { FocusRequester() } }
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Column(
modifier = Modifier
.clip(Shapes.large)
.background(EdenTheme.colors.surface)
.padding(Dimens.paddingXl),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BasicText(
text = context.getString(R.string.home_sort),
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.search_recently_played),
onClick = { onSelectSort(SortMode.LAST_PLAYED); onDismiss() },
focusRequester = focusRequesters[0],
isSelected = currentSortMode == SortMode.LAST_PLAYED,
)
Spacer(Modifier.height(Dimens.paddingMd))
DialogButton(
text = context.getString(R.string.alphabetical),
onClick = { onSelectSort(SortMode.ALPHABETICAL); onDismiss() },
focusRequester = focusRequesters[1],
isSelected = currentSortMode == SortMode.ALPHABETICAL,
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.cancel),
onClick = onDismiss,
focusRequester = focusRequesters[2],
isSecondary = true,
)
}
}
}
@@ -1,93 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.navigation
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.*
import dev.eden.emu.ui.components.HeaderFocusRequesters
import dev.eden.emu.ui.utils.NavKeys
/** Navigation zones for the home screen */
enum class NavigationZone { HEADER, CONTENT/*, FOOTER */ }
/**
* Manages focus and navigation between Header and Content zones.
* Uses MutableState for reactive UI updates.
* (Default Focus Manager breaks and is based on "placement", not suitable)
*/
class HomeNavigationManager(
private val headerFocusRequesters: HeaderFocusRequesters,
private val contentFocusRequester: FocusRequester,
) {
private val _currentZone: MutableState<NavigationZone> = mutableStateOf(NavigationZone.CONTENT)
val currentZone: NavigationZone get() = _currentZone.value
private val _headerIndex: MutableState<Int> = mutableIntStateOf(0)
val headerIndex: Int get() = _headerIndex.value
// Expose state for Compose observation
val currentZoneState: MutableState<NavigationZone> get() = _currentZone
val headerIndexState: MutableState<Int> get() = _headerIndex
private val headerElements = listOf(
headerFocusRequesters.userButton,
headerFocusRequesters.searchButton,
headerFocusRequesters.sortButton,
headerFocusRequesters.filterButton,
headerFocusRequesters.settingsButton,
)
fun navigateUp() {
if (_currentZone.value == NavigationZone.CONTENT) {
_currentZone.value = NavigationZone.HEADER
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun navigateDown() {
if (_currentZone.value == NavigationZone.HEADER) {
_currentZone.value = NavigationZone.CONTENT
contentFocusRequester.requestFocus()
}
}
fun navigateLeft() {
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value > 0) {
_headerIndex.value--
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun navigateRight() {
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value < headerElements.lastIndex) {
_headerIndex.value++
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun requestInitialFocus() {
_currentZone.value = NavigationZone.CONTENT
contentFocusRequester.requestFocus()
}
fun resetToContent() {
_currentZone.value = NavigationZone.CONTENT
}
}
/** Handle D-Pad navigation for home screen */
fun handleHomeNavigation(keyEvent: KeyEvent, nav: HomeNavigationManager): Boolean {
if (keyEvent.type != KeyEventType.KeyDown) return false
return when (keyEvent.key) {
in NavKeys.up -> { nav.navigateUp(); true }
in NavKeys.down -> { nav.navigateDown(); true }
in NavKeys.left -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateLeft(); true } else false
in NavKeys.right -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateRight(); true } else false
else -> false
}
}
@@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
private val LocalEdenTypography = staticCompositionLocalOf {
EdenTypography(
title = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
),
body = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
),
label = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
),
)
}
object EdenTheme {
val colors: EdenColors
@Composable get() = LocalEdenColors.current
val typography: EdenTypography
@Composable get() = LocalEdenTypography.current
}
@Composable
fun EdenTheme(
colors: EdenColors = LocalEdenColors.current,
typography: EdenTypography = LocalEdenTypography.current,
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalEdenColors provides colors,
LocalEdenTypography provides typography,
content = content,
)
}
data class EdenTypography(
val title: TextStyle,
val body: TextStyle,
val label: TextStyle,
)
@@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
val EdenBrandColor = Color(0xFFA161F3)
val EdenSplashColor = EdenBrandColor
val EdenGridLine = Color(0x2AA161F3)
val EdenBackground = Color(0xFF191521)
val EdenSurface = Color(0xFF221C2B)
val EdenSurfaceVariant = Color(0xFF2D2538)
val EdenOnBackground = Color(0xFFEAE5F2)
val EdenOnSurface = Color(0xFFEAE5F2)
val EdenOnSurfaceVariant = Color(0xFFB0A8BA)
val EdenOnPrimary = Color(0xFFFFFFFF)
internal val LocalEdenColors = staticCompositionLocalOf {
EdenColors(
background = EdenBackground,
surface = EdenSurface,
surfaceVariant = EdenSurfaceVariant,
primary = EdenBrandColor,
onBackground = EdenOnBackground,
onSurface = EdenOnSurface,
onSurfaceVariant = EdenOnSurfaceVariant,
onPrimary = EdenOnPrimary,
)
}
data class EdenColors(
val background: Color,
val surface: Color,
val surfaceVariant: Color,
val primary: Color,
val onBackground: Color,
val onSurface: Color,
val onSurfaceVariant: Color,
val onPrimary: Color,
)
@@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp
// Grid background
val EdenGridSpacing = 58.dp
val EdenGridLineWidth = 2.dp
const val EdenGridSpeedDpPerSecond = 12f
// Common dimensions
object Dimens {
// Padding
val paddingXs = 4.dp
val paddingSm = 8.dp
val paddingMd = 12.dp
val paddingLg = 16.dp
val paddingXl = 24.dp
// Icon sizes
val iconSm = 24.dp
val iconMd = 32.dp
val iconLg = 48.dp
// Border
val borderWidth = 2.dp
// Corner radius
val radiusSm = 8.dp
val radiusMd = 12.dp
val radiusLg = 16.dp
}
// Common shapes
object Shapes {
val small = RoundedCornerShape(Dimens.radiusSm)
val medium = RoundedCornerShape(Dimens.radiusMd)
val large = RoundedCornerShape(Dimens.radiusLg)
}
@@ -1,89 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.utils
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
/**
* New dialog button for consistent styling across dialogs
*/
@Composable
fun DialogButton(
text: String,
onClick: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
isSecondary: Boolean = false,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val backgroundColor = when {
isFocused -> EdenTheme.colors.primary
isSelected -> EdenTheme.colors.primary.copy(alpha = 0.3f)
isSecondary -> Color.Transparent
else -> EdenTheme.colors.background
}
val borderColor = when {
isFocused || isSelected -> EdenTheme.colors.primary
isSecondary -> EdenTheme.colors.onBackground.copy(alpha = 0.3f)
else -> EdenTheme.colors.onBackground.copy(alpha = 0.2f)
}
Box(
modifier = modifier
.fillMaxWidth()
.clip(Shapes.medium)
.background(backgroundColor)
.border(Dimens.borderWidth, borderColor, Shapes.medium)
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key in confirmKeys) {
onClick()
true
} else false
}
.clickable(interactionSource, null, onClick = onClick)
.padding(vertical = 14.dp, horizontal = 20.dp),
contentAlignment = Alignment.Center,
) {
BasicText(
text = if (isSelected && !isSecondary) "$text" else text,
style = EdenTheme.typography.body.copy(color = Color.White, fontSize = 16.sp)
)
}
}
private val confirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
@@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.utils
import androidx.compose.ui.input.key.Key
val ConfirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
val MenuKeys = setOf(Key.ButtonX, Key.Menu)
val BackKeys = setOf(Key.Escape, Key.Back, Key.ButtonB)
object NavKeys {
val up = setOf(Key.DirectionUp, Key.W)
val down = setOf(Key.DirectionDown, Key.S)
val left = setOf(Key.DirectionLeft, Key.A)
val right = setOf(Key.DirectionRight, Key.D)
}
@@ -203,6 +203,24 @@ object NativeLibrary {
external fun getDebugKnobAt(index: Int): Boolean
/**
* Set the current speed limit to the configured turbo speed.
*/
external fun setTurboSpeedLimit(enabled: Boolean)
/**
* Set the current speed limit to the configured slow speed.
*/
external fun setSlowSpeedLimit(enabled: Boolean)
/**
* Set the current speed limit to the configured standard speed.
*/
external fun setStandardSpeedLimit(enabled: Boolean)
external fun isTurboMode(): Boolean
external fun isSlowMode(): Boolean
/**
* Returns Vulkan driver version / API version / GPU model
*/
@@ -18,19 +18,18 @@ import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import android.content.res.Configuration
import android.os.LocaleList
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.PowerStateUpdater
import org.yuzu.yuzu_emu.utils.ControllerNavigationGlobalHook
import java.util.Locale
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class YuzuApplication : Application(), ImageLoaderFactory {
class YuzuApplication : Application() {
private fun createNotificationChannels() {
val name: CharSequence = getString(R.string.app_notification_channel_name)
val description = getString(R.string.app_notification_channel_description)
@@ -74,16 +73,11 @@ class YuzuApplication : Application(), ImageLoaderFactory {
NativeLibrary.logDeviceInfo()
PowerStateUpdater.start()
Log.logDeviceInfo()
ControllerNavigationGlobalHook.install(this)
createNotificationChannels()
}
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.crossfade(true)
.build()
}
companion object {
var documentsTree: DocumentsTree? = null
lateinit var application: YuzuApplication
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -649,7 +649,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
fun launch(activity: AppCompatActivity, game: Game) {
val launcher = Intent(activity, EmulationActivity::class.java)
launcher.putExtra("game", game) // Use "game" to match navigation argus
launcher.putExtra(EXTRA_SELECTED_GAME, game)
activity.startActivity(launcher)
}
@@ -11,6 +11,7 @@ import android.widget.RadioGroup
import android.widget.TextView
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.materialswitch.MaterialSwitch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@@ -42,7 +43,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
statusText.setTextColor(
MaterialColors.getColor(
statusText,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
@@ -135,6 +136,39 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
container.addView(itemView)
}
fun addCustomToggle(
name: Int,
isChecked: Boolean,
isEnabled: Boolean,
container: ViewGroup,
callback: (Boolean) -> Unit
): MaterialSwitch? {
val inflater = LayoutInflater.from(emulationFragment.requireContext())
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false)
val switchContainer = itemView.findViewById<ViewGroup>(R.id.switch_container)
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
val switchView = itemView.findViewById<MaterialSwitch>(R.id.setting_switch)
titleView.text = YuzuApplication.appContext.getString(name)
switchContainer.visibility = View.VISIBLE
switchView.isChecked = isChecked
switchView.setOnCheckedChangeListener { _, checked ->
callback(checked)
saveSettings()
}
switchContainer.setOnClickListener {
switchView.toggle()
}
container.addView(itemView)
return switchView
}
fun addSliderSetting(
name: Int,
container: ViewGroup,
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.fetcher
@@ -171,7 +171,7 @@ class ReleaseAdapter(
iconTint = ColorStateList.valueOf(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
@@ -33,9 +33,11 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_PROVOKING_VERTEX("provoking_vertex"),
RENDERER_DESCRIPTOR_INDEXING("descriptor_indexing"),
RENDERER_SAMPLE_SHADING("sample_shading"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),
INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS("invert_confirm_back_controller_buttons"),
ENABLE_FOLDER_BUTTON("enable_folder_button"),
ENABLE_QLAUNCH_BUTTON("enable_qlaunch_button"),
@@ -29,6 +29,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
RENDERER_DYNA_STATE("dyna_state"),
DMA_ACCURACY("dma_accuracy"),
FRAME_PACING_MODE("frame_pacing_mode"),
AUDIO_OUTPUT_ENGINE("output_engine"),
MAX_ANISOTROPY("max_anisotropy"),
THEME("theme"),
@@ -67,7 +68,6 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MY_PAGE_APPLET("my_page_applet_mode"),
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
OVERLAY_GRID_SIZE("overlay_grid_size"),
DEBUG_KNOBS("debug_knobs"),
GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size")
;
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -6,7 +9,11 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting(override val key: String) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit");
RENDERER_SPEED_LIMIT("speed_limit"),
RENDERER_TURBO_SPEED_LIMIT("turbo_speed_limit"),
RENDERER_SLOW_SPEED_LIMIT("slow_speed_limit"),
DEBUG_KNOBS("debug_knobs")
;
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
@@ -22,4 +29,4 @@ enum class ShortSetting(override val key: String) : AbstractShortSetting {
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}
}
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
class GpuUnswizzleSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
@ArrayRes val textureSizeChoicesId: Int,
@ArrayRes val textureSizeValuesId: Int,
@ArrayRes val streamSizeChoicesId: Int,
@ArrayRes val streamSizeValuesId: Int,
@ArrayRes val chunkSizeChoicesId: Int,
@ArrayRes val chunkSizeValuesId: Int
) : SettingsItem(
object : AbstractSetting {
override val key: String = SettingsItem.GPU_UNSWIZZLE_COMBINED
override val defaultValue: Any = false
override val isSaveable = true
override val isRuntimeModifiable = true
override val isSwitchable = true
override fun getValueAsString(needsGlobal: Boolean): String = "combined"
override fun reset() {
BooleanSetting.GPU_UNSWIZZLE_ENABLED.reset()
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.reset()
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.reset()
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.reset()
}
},
titleId,
titleString,
descriptionId,
descriptionString
) {
override val type = SettingsItem.TYPE_GPU_UNSWIZZLE
// Check if GPU unswizzle is enabled via the dedicated boolean setting
fun isEnabled(needsGlobal: Boolean = false): Boolean =
BooleanSetting.GPU_UNSWIZZLE_ENABLED.getBoolean(needsGlobal)
fun setEnabled(value: Boolean) =
BooleanSetting.GPU_UNSWIZZLE_ENABLED.setBoolean(value)
fun enable() = setEnabled(true)
fun disable() = setEnabled(false)
fun getTextureSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.getInt(needsGlobal)
fun setTextureSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.setInt(value)
fun getStreamSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.getInt(needsGlobal)
fun setStreamSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.setInt(value)
fun getChunkSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.getInt(needsGlobal)
fun setChunkSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.setInt(value)
fun reset() = setting.reset()
}
@@ -104,8 +104,10 @@ abstract class SettingsItem(
const val TYPE_SPINBOX = 12
const val TYPE_LAUNCHABLE = 13
const val TYPE_PATH = 14
const val TYPE_GPU_UNSWIZZLE = 15
const val FASTMEM_COMBINED = "fastmem_combined"
const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined"
val emptySetting = object : AbstractSetting {
override val key: String = ""
@@ -178,6 +180,26 @@ abstract class SettingsItem(
units = "%"
)
)
put(
SliderSetting(
ShortSetting.RENDERER_TURBO_SPEED_LIMIT,
titleId = R.string.turbo_speed_limit,
descriptionId = R.string.turbo_speed_limit_description,
min = 1,
max = 400,
units = "%"
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SLOW_SPEED_LIMIT,
titleId = R.string.slow_speed_limit,
descriptionId = R.string.slow_speed_limit_description,
min = 1,
max = 400,
units = "%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_BACKEND,
@@ -356,6 +378,13 @@ abstract class SettingsItem(
warningMessage = R.string.warning_resolution
)
)
put(
SwitchSetting(
BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS,
titleId = R.string.invert_confirm_back_controller_buttons,
descriptionId = R.string.invert_confirm_back_controller_buttons_description
)
)
put(
SwitchSetting(
BooleanSetting.SHOW_INPUT_OVERLAY,
@@ -641,6 +670,15 @@ abstract class SettingsItem(
valuesId = R.array.dmaAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.FRAME_PACING_MODE,
titleId = R.string.frame_pacing_mode,
descriptionId = R.string.frame_pacing_mode_description,
choicesId = R.array.framePacingModeNames,
valuesId = R.array.framePacingModeValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
@@ -684,6 +722,18 @@ abstract class SettingsItem(
valuesId = R.array.gpuSwizzleChunkValues
)
)
put(
GpuUnswizzleSetting(
titleId = R.string.gpu_unswizzle_settings,
descriptionId = R.string.gpu_unswizzle_settings_description,
textureSizeChoicesId = R.array.gpuTextureSizeSwizzleEntries,
textureSizeValuesId = R.array.gpuTextureSizeSwizzleValues,
streamSizeChoicesId = R.array.gpuSwizzleEntries,
streamSizeValuesId = R.array.gpuSwizzleValues,
chunkSizeChoicesId = R.array.gpuSwizzleChunkEntries,
chunkSizeValuesId = R.array.gpuSwizzleChunkValues
)
)
put(
SingleChoiceSetting(
IntSetting.FAST_CPU_TIME,
@@ -853,7 +903,7 @@ abstract class SettingsItem(
)
put(
SpinBoxSetting(
IntSetting.DEBUG_KNOBS,
ShortSetting.DEBUG_KNOBS,
titleId = R.string.debug_knobs,
descriptionId = R.string.debug_knobs_description,
valueHint = R.string.debug_knobs_hint,
@@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogGpuUnswizzleBinding
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
class GpuUnswizzleDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogGpuUnswizzleBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogGpuUnswizzleBinding.inflate(LayoutInflater.from(requireContext()))
val item = settingsViewModel.clickedItem as GpuUnswizzleSetting
// Setup texture size dropdown
val textureSizeEntries = resources.getStringArray(item.textureSizeChoicesId)
val textureSizeValues = resources.getIntArray(item.textureSizeValuesId)
val textureSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
textureSizeEntries.toMutableList()
)
binding.dropdownTextureSize.setAdapter(textureSizeAdapter)
// Setup stream size dropdown
val streamSizeEntries = resources.getStringArray(item.streamSizeChoicesId)
val streamSizeValues = resources.getIntArray(item.streamSizeValuesId)
val streamSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
streamSizeEntries.toMutableList()
)
binding.dropdownStreamSize.setAdapter(streamSizeAdapter)
// Setup chunk size dropdown
val chunkSizeEntries = resources.getStringArray(item.chunkSizeChoicesId)
val chunkSizeValues = resources.getIntArray(item.chunkSizeValuesId)
val chunkSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
chunkSizeEntries.toMutableList()
)
binding.dropdownChunkSize.setAdapter(chunkSizeAdapter)
// Load current values
val isEnabled = item.isEnabled()
binding.switchEnable.isChecked = isEnabled
if (isEnabled) {
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
if (textureSizeIndex >= 0) {
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
}
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
if (streamSizeIndex >= 0) {
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
}
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
if (chunkSizeIndex >= 0) {
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
}
} else {
// Set default/recommended values when disabling
binding.dropdownTextureSize.setText(textureSizeEntries[3], false)
binding.dropdownStreamSize.setText(streamSizeEntries[3], false)
binding.dropdownChunkSize.setText(chunkSizeEntries[3], false)
}
// Clear adapter filters after setText to fix rotation bug
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
// Enable/disable dropdowns based on switch state
updateDropdownsState(isEnabled)
binding.switchEnable.setOnCheckedChangeListener { _, checked ->
updateDropdownsState(checked)
}
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(binding.root)
.create()
// Setup button listeners
binding.btnDefault.setOnClickListener {
// Reset to defaults
item.reset()
// Refresh values with adapters reset
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
if (textureSizeIndex >= 0) {
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
}
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
if (streamSizeIndex >= 0) {
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
}
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
if (chunkSizeIndex >= 0) {
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
}
// Clear filters
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
binding.btnCancel.setOnClickListener {
dialog.dismiss()
}
binding.btnOk.setOnClickListener {
if (binding.switchEnable.isChecked) {
item.enable()
// Save the selected values
val selectedTextureIndex = textureSizeEntries.indexOf(
binding.dropdownTextureSize.text.toString()
)
if (selectedTextureIndex >= 0) {
item.setTextureSize(textureSizeValues[selectedTextureIndex])
}
val selectedStreamIndex = streamSizeEntries.indexOf(
binding.dropdownStreamSize.text.toString()
)
if (selectedStreamIndex >= 0) {
item.setStreamSize(streamSizeValues[selectedStreamIndex])
}
val selectedChunkIndex = chunkSizeEntries.indexOf(
binding.dropdownChunkSize.text.toString()
)
if (selectedChunkIndex >= 0) {
item.setChunkSize(chunkSizeValues[selectedChunkIndex])
}
} else {
// Disable GPU unswizzle
item.disable()
}
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
dialog.dismiss()
}
// Ensure filters are cleared after dialog is shown
binding.root.post {
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
}
return dialog
}
private fun updateDropdownsState(enabled: Boolean) {
binding.layoutTextureSize.isEnabled = enabled
binding.dropdownTextureSize.isEnabled = enabled
binding.layoutStreamSize.isEnabled = enabled
binding.dropdownStreamSize.isEnabled = enabled
binding.layoutChunkSize.isEnabled = enabled
binding.dropdownChunkSize.isEnabled = enabled
}
companion object {
const val TAG = "GpuUnswizzleDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
item: GpuUnswizzleSetting,
position: Int
): GpuUnswizzleDialogFragment {
val dialog = GpuUnswizzleDialogFragment()
val args = Bundle()
args.putInt(POSITION, position)
dialog.arguments = args
settingsViewModel.clickedItem = item
return dialog
}
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
@@ -101,6 +101,11 @@ class SettingsAdapter(
SettingsItem.TYPE_PATH -> {
PathViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_GPU_UNSWIZZLE -> {
GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> {
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
@@ -474,6 +479,14 @@ class SettingsAdapter(
settingsViewModel.setShouldShowPathResetDialog(true)
}
fun onGpuUnswizzleClick(item: GpuUnswizzleSetting, position: Int) {
GpuUnswizzleDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, GpuUnswizzleDialogFragment.TAG)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key
@@ -227,6 +227,8 @@ class SettingsFragmentPresenter(
add(StringSetting.DEVICE_NAME.key)
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_TURBO_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SLOW_SPEED_LIMIT.key)
add(BooleanSetting.USE_DOCKED_MODE.key)
add(IntSetting.REGION_INDEX.key)
add(IntSetting.LANGUAGE_INDEX.key)
@@ -265,6 +267,7 @@ class SettingsFragmentPresenter(
add(IntSetting.RENDERER_ACCURACY.key)
add(IntSetting.DMA_ACCURACY.key)
add(IntSetting.FRAME_PACING_MODE.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
@@ -282,9 +285,7 @@ class SettingsFragmentPresenter(
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.key)
add(IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.key)
add(IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.key)
add(SettingsItem.GPU_UNSWIZZLE_COMBINED)
add(HeaderSetting(R.string.extensions))
@@ -1075,6 +1076,7 @@ class SettingsFragmentPresenter(
}
add(BooleanSetting.ENABLE_QUICK_SETTINGS.key)
add(BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS.key)
add(HeaderSetting(R.string.theme_and_color))
@@ -1228,7 +1230,7 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.general))
add(IntSetting.DEBUG_KNOBS.key)
add(ShortSetting.DEBUG_KNOBS.key)
add(HeaderSetting(R.string.gpu_logging_header))
add(BooleanSetting.GPU_LOGGING_ENABLED.key)
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class GpuUnswizzleViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: GpuUnswizzleSetting
override fun bind(item: SettingsItem) {
setting = item as GpuUnswizzleSetting
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(true)
val resMgr = binding.root.context.resources
if (setting.isEnabled()) {
// Show a summary of current settings
val textureSizeEntries = resMgr.getStringArray(setting.textureSizeChoicesId)
val textureSizeValues = resMgr.getIntArray(setting.textureSizeValuesId)
val textureSizeIndex = textureSizeValues.indexOf(setting.getTextureSize())
val textureSizeLabel = if (textureSizeIndex >= 0) textureSizeEntries[textureSizeIndex] else "?"
val streamSizeEntries = resMgr.getStringArray(setting.streamSizeChoicesId)
val streamSizeValues = resMgr.getIntArray(setting.streamSizeValuesId)
val streamSizeIndex = streamSizeValues.indexOf(setting.getStreamSize())
val streamSizeLabel = if (streamSizeIndex >= 0) streamSizeEntries[streamSizeIndex] else "?"
val chunkSizeEntries = resMgr.getStringArray(setting.chunkSizeChoicesId)
val chunkSizeValues = resMgr.getIntArray(setting.chunkSizeValuesId)
val chunkSizeIndex = chunkSizeValues.indexOf(setting.getChunkSize())
val chunkSizeLabel = if (chunkSizeIndex >= 0) chunkSizeEntries[chunkSizeIndex] else "?"
binding.textSettingValue.text = "$textureSizeLabel$streamSizeLabel$chunkSizeLabel"
} else {
binding.textSettingValue.text = resMgr.getString(R.string.gpu_unswizzle_disabled)
}
binding.buttonClear.setVisible(setting.clearable)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}
override fun onClick(clicked: View) {
if (!setting.isEditable) {
return
}
adapter.onGpuUnswizzleClick(setting, bindingAdapterPosition)
}
override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) {
return adapter.onLongClick(setting, bindingAdapterPosition)
}
return false
}
}
@@ -55,6 +55,7 @@ import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textview.MaterialTextView
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -1055,11 +1056,47 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
quickSettings.addPerGameConfigStatusIndicator(container)
}
quickSettings.addBooleanSetting(
lateinit var slowSpeed: MaterialSwitch
lateinit var turboSpeed: MaterialSwitch
turboSpeed = quickSettings.addCustomToggle(
R.string.turbo_speed_limit,
NativeLibrary.isTurboMode(),
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
container
) { enabled ->
if (enabled)
slowSpeed.isChecked = false
NativeLibrary.setTurboSpeedLimit(enabled)
}!!
slowSpeed = quickSettings.addCustomToggle(
R.string.slow_speed_limit,
NativeLibrary.isSlowMode(),
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
container
) { enabled ->
if (enabled)
turboSpeed.isChecked = false
NativeLibrary.setSlowSpeedLimit(enabled)
}!!
quickSettings.addCustomToggle(
R.string.frame_limit_enable,
container,
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
)
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
true,
container
) { enabled ->
if (!enabled) {
turboSpeed.isChecked = false
slowSpeed.isChecked = false
}
turboSpeed.isEnabled = enabled
slowSpeed.isEnabled = enabled
NativeLibrary.setStandardSpeedLimit(enabled)
}!!
quickSettings.addSliderSetting(
R.string.frame_limit_slider,
@@ -1310,7 +1347,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
}
@@ -1497,7 +1534,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
}
@@ -61,26 +61,16 @@ class GamesViewModel : ViewModel() {
}
fun setGames(games: List<Game>) {
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
val sortedList = games.sortedWith(
compareByDescending<Game> { game ->
preferences.getLong(game.keyLastPlayedTime, 0L)
}.thenBy { it.title.lowercase(Locale.getDefault()) }
.thenBy { it.path }
compareBy(
{ it.title.lowercase(Locale.getDefault()) },
{ it.path }
)
)
_games.value = sortedList
}
fun resortGames() {
val currentGames = _games.value
if (currentGames.isNotEmpty()) {
setGames(currentGames)
_shouldScrollToTop.value = true
}
}
fun setShouldSwapData(shouldSwap: Boolean) {
_shouldSwapData.value = shouldSwap
}
@@ -3,468 +3,565 @@
package org.yuzu.yuzu_emu.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import java.util.Locale
import androidx.preference.PreferenceManager
import kotlinx.coroutines.delay
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.model.AppletInfo
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import dev.eden.emu.ui.background.RetroGridBackground
import dev.eden.emu.ui.components.FooterAction
import dev.eden.emu.ui.components.FooterButton
import dev.eden.emu.ui.components.GameCarousel
import dev.eden.emu.ui.components.GameGrid
import dev.eden.emu.ui.components.GameTile
import dev.eden.emu.ui.components.HeaderFocusRequesters
import dev.eden.emu.ui.components.HomeFooter
import dev.eden.emu.ui.components.HomeHeader
import dev.eden.emu.ui.components.LayoutMode
import dev.eden.emu.ui.components.LayoutSelectionDialog
import dev.eden.emu.ui.components.SortMode
import dev.eden.emu.ui.components.SortSelectionDialog
import dev.eden.emu.ui.navigation.HomeNavigationManager
import dev.eden.emu.ui.navigation.NavigationZone
import dev.eden.emu.ui.navigation.handleHomeNavigation
import dev.eden.emu.ui.theme.EdenTheme
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.Locale
import androidx.core.content.edit
import androidx.core.view.doOnNextLayout
private const val PREF_LAYOUT_MODE = "home_layout_mode"
private const val PREF_SORT_MODE = "home_sort_mode"
private const val PREF_LAST_FOCUSED_INDEX = "home_last_focused_index"
private const val PREF_GAME_WAS_LAUNCHED = "home_game_was_launched"
/**
* Compose-based Games Fragment with Header/Footer navigation
*/
class GamesFragment : Fragment() {
private var _binding: FragmentGamesBinding? = null
private val binding get() = _binding!!
private var originalHeaderTopMargin: Int? = null
private var originalHeaderBottomMargin: Int? = null
private var originalHeaderRightMargin: Int? = null
private var originalHeaderLeftMargin: Int? = null
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
private var fallbackBottomInset: Int = 0
companion object {
private const val SEARCH_TEXT = "SearchText"
private const val PREF_SORT_TYPE = "GamesSortType"
}
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private lateinit var gameAdapter: GameAdapter
private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var mainActivity: MainActivity
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
mainActivity.processGamesDir(result, true)
}
}
private fun getCurrentViewType(): Int {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID
return preferences.getInt(key, fallback)
}
private fun setCurrentViewType(type: Int) {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
preferences.edit { putInt(key, type) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
EdenTheme {
HomeScreen(
gamesViewModel = gamesViewModel,
onGameLaunch = { path: String ->
val game = gamesViewModel.games.value.find { it.path == path }
if (game != null) {
// Save last played time for sorting
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putLong(game.keyLastPlayedTime, System.currentTimeMillis())
.putBoolean(PREF_GAME_WAS_LAUNCHED, true)
.apply()
EmulationActivity.launch(
requireActivity() as androidx.appcompat.app.AppCompatActivity,
game
)
}
},
onNavigateToSettings = {
findNavController().navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
},
onNavigateToUserManagement = {
findNavController().navigate(R.id.action_gamesFragment_to_profileManagerFragment)
},
onShowGameInfo = { game: Game ->
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
findNavController().navigate(action)
}
)
}
}
}
_binding = FragmentGamesBinding.inflate(inflater)
return binding.root
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(true)
mainActivity = requireActivity() as MainActivity
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
gameAdapter = GameAdapter(
requireActivity() as AppCompatActivity
)
applyGridGamesBinding()
binding.swipeRefresh.apply {
(binding.swipeRefresh as? SwipeRefreshLayout)?.setOnRefreshListener {
gamesViewModel.reloadGames(false)
}
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressBackgroundColorSchemeColor(
com.google.android.material.color.MaterialColors.getColor(
binding.swipeRefresh,
com.google.android.material.R.attr.colorPrimary
)
)
(binding.swipeRefresh as? SwipeRefreshLayout)?.setColorSchemeColors(
com.google.android.material.color.MaterialColors.getColor(
binding.swipeRefresh,
com.google.android.material.R.attr.colorOnPrimary
)
)
post {
if (_binding == null) {
return@post
}
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = gamesViewModel.isReloading.value
}
}
gamesViewModel.isReloading.collect(viewLifecycleOwner) {
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = it
binding.noticeText.setVisible(
visible = gamesViewModel.games.value.isEmpty() && !it,
gone = false
)
}
gamesViewModel.games.collect(viewLifecycleOwner) {
if (it.isNotEmpty()) {
setAdapter(it)
}
}
gamesViewModel.shouldSwapData.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldSwapData(false) }
) {
if (it) {
setAdapter(gamesViewModel.games.value)
}
}
gamesViewModel.shouldScrollToTop.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldScrollToTop(false) }
) { if (it) scrollToTop() }
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
if (shouldScroll) {
binding.gridGames.post {
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
gameAdapter.notifyDataSetChanged()
}
gamesViewModel.setShouldScrollAfterReload(false)
}
}
setupTopView()
updateButtonsVisibility()
binding.addDirectory.setOnClickListener {
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
binding.launchQlaunch?.setOnClickListener {
launchQLaunch()
}
setInsets()
}
val applyGridGamesBinding = {
(binding.gridGames as? RecyclerView)?.apply {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val currentViewType = getCurrentViewType()
val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID
//This prevents Grid/List views from reusing scaled or otherwise modified ViewHolders left over from the carousel.
adapter = null
recycledViewPool.clear()
gameAdapter.setViewType(savedViewType)
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
// Set the correct layout manager
layoutManager = when (savedViewType) {
GameAdapter.VIEW_TYPE_GRID -> {
val columns = resources.getInteger(R.integer.game_columns_grid)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_GRID_COMPACT -> {
val columns = resources.getInteger(R.integer.game_columns_grid)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_LIST -> {
val columns = resources.getInteger(R.integer.game_columns_list)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_CAROUSEL -> {
LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
}
else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
}
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)}
doOnNextLayout { //Carousel: important to avoid overlap issues
(this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset)
}
} else {
(this as? CarouselRecyclerView)?.setupCarousel(false)
}
adapter = gameAdapter
lastViewType = savedViewType
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (_binding != null) {
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
}
override fun onPause() {
super.onPause()
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0
}
}
override fun onResume() {
super.onResume()
// Only resort games if a game was actually launched (not when coming back from settings)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (prefs.getBoolean(PREF_GAME_WAS_LAUNCHED, false)) {
prefs.edit().putBoolean(PREF_GAME_WAS_LAUNCHED, false).apply()
gamesViewModel.resortGames()
}
}
}
@Composable
private fun HomeScreen(
gamesViewModel: GamesViewModel,
onGameLaunch: (String) -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToUserManagement: () -> Unit,
onShowGameInfo: (Game) -> Unit,
) {
val context = LocalContext.current
val games by gamesViewModel.games.collectAsState()
val isLoading by gamesViewModel.isReloading.collectAsState()
val shouldScrollToTop by gamesViewModel.shouldScrollToTop.collectAsState()
val preferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val savedLayoutMode = remember {
val savedValue = preferences.getString(PREF_LAYOUT_MODE, LayoutMode.CAROUSEL.name)
try {
LayoutMode.valueOf(savedValue ?: LayoutMode.CAROUSEL.name)
} catch (_: Exception) {
LayoutMode.CAROUSEL
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
}
}
// Layout mode state (Grid or Carousel)
var layoutMode by remember { mutableStateOf(savedLayoutMode) }
var showLayoutDialog by remember { mutableStateOf(false) }
var showSortDialog by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
var isSearchExpanded by remember { mutableStateOf(false) }
private var lastSearchText: String = ""
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
// Game properties screen state
var selectedGameTile by remember { mutableStateOf<GameTile?>(null) }
private fun setAdapter(games: List<Game>) {
val currentSearchText = binding.searchText.text.toString()
val currentFilter = binding.filterButton.id
// Sort mode state
val savedSortMode = remember {
val savedValue = preferences.getString(PREF_SORT_MODE, SortMode.LAST_PLAYED.name)
try {
SortMode.valueOf(savedValue ?: SortMode.LAST_PLAYED.name)
} catch (_: Exception) {
SortMode.LAST_PLAYED
}
}
var sortMode by remember { mutableStateOf(savedSortMode) }
val searchChanged = currentSearchText != lastSearchText
val filterChanged = currentFilter != lastFilter
// Save sort mode when it changes
LaunchedEffect(sortMode) {
preferences.edit().putString(PREF_SORT_MODE, sortMode.name).apply()
}
// Save layout mode when it changes
LaunchedEffect(layoutMode) {
preferences.edit().putString(PREF_LAYOUT_MODE, layoutMode.name).apply()
}
// Remember last focused index across layout changes - load from preferences
val savedFocusedIndex = remember {
preferences.getInt(PREF_LAST_FOCUSED_INDEX, 0)
}
var lastFocusedIndex by remember { mutableStateOf(savedFocusedIndex) }
LaunchedEffect(lastFocusedIndex) {
preferences.edit().putInt(PREF_LAST_FOCUSED_INDEX, lastFocusedIndex).apply()
}
var listVersion by remember { mutableStateOf(0) }
LaunchedEffect(sortMode) {
lastFocusedIndex = 0
listVersion++
}
// Reset to first item when games are resorted (e.g. after playing a game)
// Else the sorting only takes effect after relaunch
LaunchedEffect(shouldScrollToTop) {
if (shouldScrollToTop) {
lastFocusedIndex = 0
listVersion++ // Force complete recomposition
gamesViewModel.setShouldScrollToTop(false)
}
}
// Load current user UUID
var currentUserUuid by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
currentUserUuid = NativeLibrary.getCurrentUser() ?: ""
}
val headerFocusRequesters = remember {
HeaderFocusRequesters(
userButton = FocusRequester(),
searchButton = FocusRequester(),
sortButton = FocusRequester(),
filterButton = FocusRequester(),
settingsButton = FocusRequester(),
)
}
// Don't recreate on layoutMode change - this causes zones state loss
val contentFocusRequester = remember { FocusRequester() }
val navigationManager = remember {
HomeNavigationManager(
headerFocusRequesters = headerFocusRequesters,
contentFocusRequester = contentFocusRequester,
)
}
// Request initial focus
LaunchedEffect(Unit) {
delay(200)
navigationManager.requestInitialFocus()
}
// Filter and sort games based on search query and sort mode
val filteredGames = remember(games, searchQuery, sortMode) {
val filtered = if (searchQuery.isBlank()) {
games
if (searchChanged || filterChanged) {
filterAndSearch(games)
lastSearchText = currentSearchText
lastFilter = currentFilter
} else {
games.filter { game ->
game.title.lowercase(Locale.getDefault())
.contains(searchQuery.lowercase(Locale.getDefault()))
}
}
// Apply sorting
when (sortMode) {
SortMode.LAST_PLAYED -> {
filtered.sortedByDescending { game ->
preferences.getLong(game.keyLastPlayedTime, 0L)
}
}
SortMode.ALPHABETICAL -> {
filtered.sortedBy { it.title.lowercase(Locale.getDefault()) }
}
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
gamesViewModel.setFilteredGames(games)
}
}
val gameTiles = remember(filteredGames) {
filteredGames.map { game ->
GameTile.fromGame(game)
private fun setupTopView() {
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
binding.clearButton.visibility = View.VISIBLE
} else {
binding.clearButton.visibility = View.INVISIBLE
}
filterAndSearch()
}
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchBackground.setOnClickListener { focusSearch() }
// Setup view button
binding.viewButton.setOnClickListener { showViewMenu(it) }
// Setup filter button
binding.filterButton.setOnClickListener { view ->
showFilterMenu(view)
}
// Setup settings button
binding.settingsButton.setOnClickListener { navigateToSettings() }
}
// NO key handling at top level
Box(modifier = Modifier.fillMaxSize()) {
RetroGridBackground()
private fun navigateToSettings() {
val navController = findNavController()
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
}
val contentOffset = 24.dp
key(listVersion, layoutMode) {
when (layoutMode) {
LayoutMode.TWO_ROW -> {
GameGrid(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameGrid
onGameLaunch(path)
},
onGameLongClick = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
onNavigateToSettings = {},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
rowCount = 2,
tileIconSize = 120.dp,
isLoading = isLoading,
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
initialFocusedIndex = lastFocusedIndex,
onFocusedIndexChanged = { newIndex ->
lastFocusedIndex = newIndex
},
onShowGameInfo = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
private fun showViewMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu)
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
if (!isLandscape) {
popup.menu.findItem(R.id.view_carousel)?.isVisible = false
}
val currentViewType = getCurrentViewType()
when (currentViewType) {
GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true
GameAdapter.VIEW_TYPE_GRID_COMPACT -> popup.menu.findItem(R.id.view_grid_compact).isChecked = true
GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true
GameAdapter.VIEW_TYPE_CAROUSEL -> popup.menu.findItem(R.id.view_carousel).isChecked = true
}
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.view_grid -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID)
applyGridGamesBinding()
item.isChecked = true
true
}
LayoutMode.CAROUSEL -> {
GameCarousel(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameCarousel
onGameLaunch(path)
},
onGameLongClick = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
initialFocusedIndex = lastFocusedIndex,
onFocusedIndexChanged = { newIndex ->
lastFocusedIndex = newIndex
},
onShowGameInfo = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
R.id.view_grid_compact -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT)
applyGridGamesBinding()
item.isChecked = true
true
}
else -> {
// Default to Grid
GameGrid(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameGrid
onGameLaunch(path)
},
onNavigateToSettings = {},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
rowCount = 2,
tileIconSize = 120.dp,
isLoading = isLoading,
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
R.id.view_list -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
applyGridGamesBinding()
item.isChecked = true
true
}
}
// Refocus content when layout mode changes
LaunchedEffect(layoutMode) {
delay(200)
contentFocusRequester.requestFocus()
navigationManager.resetToContent()
}
// Refocus content when sort mode changes
LaunchedEffect(sortMode) {
delay(200)
contentFocusRequester.requestFocus()
navigationManager.resetToContent()
}
// Header
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.onPreviewKeyEvent { keyEvent ->
if (navigationManager.currentZone == NavigationZone.HEADER) {
handleHomeNavigation(keyEvent, navigationManager)
} else {
false
}
R.id.view_carousel -> {
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
applyGridGamesBinding()
item.isChecked = true
onResume()
}
) {
HomeHeader(
currentUser = currentUserUuid,
onUserClick = onNavigateToUserManagement,
searchQuery = searchQuery,
onSearchQueryChange = { query -> searchQuery = query },
isSearchExpanded = isSearchExpanded,
onSearchExpandedChange = { expanded -> isSearchExpanded = expanded },
onSortClick = {
showSortDialog = true
},
onFilterClick = {
showLayoutDialog = true
},
onSettingsClick = onNavigateToSettings,
focusRequesters = headerFocusRequesters,
)
}
true
}
val currentZone by navigationManager.currentZoneState
val headerIdx by navigationManager.headerIndexState
val footerActions = when (currentZone) {
NavigationZone.HEADER -> {
val headerText = when (headerIdx) {
0 -> context.getString(R.string.footer_open) // User profile
1 -> context.getString(R.string.home_search) // Search
else -> context.getString(R.string.footer_open) // Sort, Filter, Settings
}
listOf(FooterAction(FooterButton.A, headerText))
}
NavigationZone.CONTENT -> {
listOf(
FooterAction(FooterButton.A, context.getString(R.string.home_start)),
FooterAction(FooterButton.X, context.getString(R.string.footer_game_info)),
)
}
else -> false
}
HomeFooter(
actions = footerActions,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
// Layout selection dialog
if (showLayoutDialog) {
LayoutSelectionDialog(
onDismiss = { showLayoutDialog = false },
onSelectGrid = { layoutMode = LayoutMode.TWO_ROW },
onSelectCarousel = { layoutMode = LayoutMode.CAROUSEL },
)
popup.show()
}
private fun showFilterMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_filters, popup.menu)
// Set checked state based on current filter
when (currentFilter) {
R.id.alphabetical -> popup.menu.findItem(R.id.alphabetical).isChecked = true
R.id.filter_recently_played -> popup.menu.findItem(R.id.filter_recently_played).isChecked =
true
R.id.filter_recently_added -> popup.menu.findItem(R.id.filter_recently_added).isChecked =
true
}
// Sort selection dialog
if (showSortDialog) {
SortSelectionDialog(
currentSortMode = sortMode,
onDismiss = { showSortDialog = false },
onSelectSort = { selectedSortMode ->
sortMode = selectedSortMode
gamesViewModel.resortGames()
},
popup.setOnMenuItemClickListener { item ->
currentFilter = item.itemId
preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) }
filterAndSearch()
true
}
popup.show()
}
// Track current filter
private var currentFilter = View.NO_ID
private fun filterAndSearch(baseList: List<Game> = gamesViewModel.games.value) {
val filteredList: List<Game> = when (currentFilter) {
R.id.alphabetical -> baseList.sortedBy { it.title }
R.id.filter_recently_played -> {
baseList.filter {
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
}
R.id.filter_recently_added -> {
baseList.filter {
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
}
else -> baseList
}
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
if (searchTerm.isEmpty()) {
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
filteredList
)
gamesViewModel.setFilteredGames(filteredList)
return
}
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
val sortedList = filteredList.mapNotNull { game ->
val title = game.title.lowercase(Locale.getDefault())
val score = searchAlgorithm.similarity(searchTerm, title)
if (score > 0.03) {
ScoredGame(score, game)
} else {
null
}
}.sortedByDescending { it.score }.map { it.item }
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
gamesViewModel.setFilteredGames(sortedList)
}
private inner class ScoredGame(val score: Double, val item: Game)
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun scrollToTop() {
if (_binding != null) {
(binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0)
}
}
private fun launchQLaunch() {
try {
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.QLaunch.entryId)
if (appletPath.isEmpty()) {
Toast.makeText(
requireContext(),
R.string.applets_error_applet,
Toast.LENGTH_SHORT
).show()
return
}
NativeLibrary.setCurrentAppletId(AppletInfo.QLaunch.appletId)
val qlaunchGame = Game(
title = getString(R.string.qlaunch_applet),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(qlaunchGame)
findNavController().navigate(action)
} catch (e: Exception) {
Toast.makeText(
requireContext(),
"Failed to launch QLaunch: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
private fun updateButtonsVisibility() {
val showQLaunch = BooleanSetting.ENABLE_QLAUNCH_BUTTON.getBoolean()
val showFolder = BooleanSetting.ENABLE_FOLDER_BUTTON.getBoolean()
val isFirmwareAvailable = NativeLibrary.isFirmwareAvailable()
val shouldShowQLaunch = showQLaunch && isFirmwareAvailable
binding.launchQlaunch.visibility = if (shouldShowQLaunch) View.VISIBLE else View.GONE
binding.addDirectory.visibility = if (showFolder) View.VISIBLE else View.GONE
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressViewEndTarget(
false,
barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
)
val leftInset = barInsets.left + cutoutInsets.left
val rightInset = barInsets.right + cutoutInsets.right
val topInset = maxOf(barInsets.top, cutoutInsets.top)
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
mlpSwipe.leftMargin = leftInset
mlpSwipe.rightMargin = rightInset
binding.swipeRefresh.layoutParams = mlpSwipe
val mlpHeader = binding.header.layoutParams as ViewGroup.MarginLayoutParams
// Store original margins only once
if (originalHeaderTopMargin == null) {
originalHeaderTopMargin = mlpHeader.topMargin
originalHeaderRightMargin = mlpHeader.rightMargin
originalHeaderLeftMargin = mlpHeader.leftMargin
}
// Always set margin as original + insets
mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset
mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset
mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize(
R.dimen.spacing_med
)
binding.header.layoutParams = mlpHeader
binding.noticeText.updatePadding(bottom = spacingNavigation)
binding.gridGames.updatePadding(
top = resources.getDimensionPixelSize(R.dimen.spacing_med)
)
val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
mlpFab.leftMargin = leftInset + fabPadding
mlpFab.bottomMargin = barInsets.bottom + fabPadding
mlpFab.rightMargin = rightInset + fabPadding
binding.addDirectory.layoutParams = mlpFab
binding.launchQlaunch?.let { qlaunchButton ->
val mlpQLaunch = qlaunchButton.layoutParams as ViewGroup.MarginLayoutParams
mlpQLaunch.leftMargin = leftInset + fabPadding
mlpQLaunch.bottomMargin = barInsets.bottom + fabPadding
qlaunchButton.layoutParams = mlpQLaunch
}
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
fallbackBottomInset = bottomInset
(binding.gridGames as? CarouselRecyclerView)?.notifyInsetsReady(bottomInset)
windowInsets
}
}
@@ -124,14 +124,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
// Hide status bar and navigation bar for fullscreen experience
// Maybe a setting? Reminds me: SafeArea Testing!
WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.statusBars())
hide(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
window.statusBarColor =
ContextCompat.getColor(applicationContext, android.R.color.transparent)
window.navigationBarColor =
@@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.View
import android.view.Window
import androidx.activity.ComponentActivity
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import java.util.concurrent.atomic.AtomicBoolean
object ControllerNavigationGlobalHook {
private val installed = AtomicBoolean(false)
fun install(application: Application) {
if (!installed.compareAndSet(false, true)) {
return
}
application.registerActivityLifecycleCallbacks(HookInstaller)
}
private object HookInstaller : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
installHookIfNeeded(activity)
}
override fun onActivityResumed(activity: Activity) {
installHookIfNeeded(activity)
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
}
private fun installHookIfNeeded(activity: Activity) {
val window = activity.window ?: return
val currentCallback = window.callback ?: return
if (currentCallback is ControllerNavigationWindowCallback) {
return
}
window.callback = ControllerNavigationWindowCallback(activity, currentCallback)
}
private class ControllerNavigationWindowCallback(
private val activity: Activity,
private val delegate: Window.Callback
) : Window.Callback by delegate {
private val componentActivity = activity as? ComponentActivity
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (activity.isFinishing || activity.isDestroyed) {
return delegate.dispatchKeyEvent(event)
}
if (!BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS.getBoolean()) {
return delegate.dispatchKeyEvent(event)
}
if (!isControllerInput(event) || componentActivity == null) {
return delegate.dispatchKeyEvent(event)
}
if (shouldBypassInGameplay()) {
return delegate.dispatchKeyEvent(event)
}
if (isConfirmAction(event.keyCode)) {
return when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (event.repeatCount == 0) {
componentActivity.onBackPressedDispatcher.onBackPressed()
}
true
}
KeyEvent.ACTION_UP -> true
else -> false
}
}
if (isBackAction(event.keyCode)) {
val remappedEvent = KeyEvent(
event.downTime,
event.eventTime,
event.action,
KeyEvent.KEYCODE_DPAD_CENTER,
event.repeatCount,
event.metaState,
event.deviceId,
event.scanCode,
event.flags,
event.source
)
return delegate.dispatchKeyEvent(remappedEvent)
}
return delegate.dispatchKeyEvent(event)
}
private fun shouldBypassInGameplay(): Boolean {
if (activity.javaClass.name != "org.yuzu.yuzu_emu.activities.EmulationActivity") {
return false
}
if (!NativeLibrary.isRunning()) {
return false
}
val surface = activity.findViewById<View?>(R.id.surface_emulation) ?: return false
if (!surface.isShown || surface.visibility != View.VISIBLE) {
return false
}
val focused = activity.currentFocus
if (focused != null) {
val inputOverlay = activity.findViewById<View?>(R.id.surface_input_overlay)
if (!isDescendantOf(focused, inputOverlay)) {
return false
}
}
return true
}
private fun isDescendantOf(view: View, parent: View?): Boolean {
if (parent == null) {
return false
}
var current: View? = view
while (current != null) {
if (current === parent) {
return true
}
current = current.parent as? View
}
return false
}
private fun isControllerInput(event: KeyEvent): Boolean {
val source = event.source
val deviceSources = event.device?.sources ?: InputDevice.getDevice(event.deviceId)?.sources ?: 0
return hasControllerSource(source) || hasControllerSource(deviceSources)
}
private fun isConfirmAction(keyCode: Int): Boolean {
return keyCode == KeyEvent.KEYCODE_BUTTON_A
}
private fun isBackAction(keyCode: Int): Boolean {
return keyCode == KeyEvent.KEYCODE_BUTTON_B
}
private fun hasControllerSource(source: Int): Boolean {
return source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK ||
source and InputDevice.SOURCE_DPAD == InputDevice.SOURCE_DPAD
}
}
}
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -28,29 +25,31 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
class GameObjectIconFetcher(
class GameIconFetcher(
private val game: Game,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
val bitmap = decodeGameIcon(game.path)
?: throw IllegalStateException("Failed to decode game icon for: ${game.title}")
return DrawableResult(
drawable = bitmap.toDrawable(options.context.resources),
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.DISK
)
}
private fun decodeGameIcon(path: String): Bitmap? {
val iconBytes = GameMetadata.getIcon(path)
return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
private fun decodeGameIcon(uri: String): Bitmap? {
val data = GameMetadata.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
class Factory : Fetcher.Factory<Game> {
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
GameObjectIconFetcher(data, options)
GameIconFetcher(data, options)
}
}
@@ -62,7 +61,7 @@ object GameIconUtils {
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
.components {
add(GameIconKeyer())
add(GameObjectIconFetcher.Factory())
add(GameIconFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(YuzuApplication.appContext)
@@ -100,6 +99,9 @@ object GameIconUtils {
R.id.shortcut_foreground,
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
)
val inset = YuzuApplication.appContext.resources
.getDimensionPixelSize(R.dimen.icon_inset)
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
return IconCompat.createWithAdaptiveBitmap(
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
)
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -74,7 +74,7 @@ class GradientBorderCardView @JvmOverloads constructor(
borderPaint.shader = null
val typedValue = android.util.TypedValue()
context.theme.resolveAttribute(
androidx.appcompat.R.attr.colorPrimary,
com.google.android.material.R.attr.colorPrimary,
typedValue,
true
)
@@ -65,6 +65,8 @@ namespace AndroidSettings {
Settings::Category::Android};
Settings::Setting<bool> enable_qlaunch_button{linkage, false, "enable_qlaunch_button",
Settings::Category::Android};
Settings::Setting<bool> invert_confirm_back_controller_buttons{
linkage, false, "invert_confirm_back_controller_buttons", Settings::Category::Android};
// Input/performance overlay settings
std::vector<OverlayControlData> overlay_control_data;
+33
View File
@@ -1233,6 +1233,39 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobje
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
if (enabled) {
Settings::values.use_speed_limit.SetValue(true);
Settings::SetSpeedMode(Settings::SpeedMode::Turbo);
} else {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setSlowSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
if (enabled) {
Settings::values.use_speed_limit.SetValue(true);
Settings::SetSpeedMode(Settings::SpeedMode::Slow);
} else {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setStandardSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
Settings::values.use_speed_limit.SetValue(enabled);
if (enabled) {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isTurboMode(JNIEnv *env, jobject jobj) {
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Turbo;
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isSlowMode(JNIEnv *env, jobject jobj) {
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Slow;
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run(JNIEnv* env, jobject jobj, jstring j_path,
jint j_program_index,
jboolean j_frontend_initiated) {
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zm0,7h12v-2H3v2z"/>
</vector>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_enable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/gpu_unswizzle_enable" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_texture_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/gpu_unswizzle_texture_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_texture_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_stream_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/gpu_unswizzle_stream_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_stream_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_chunk_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/gpu_unswizzle_chunk_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_chunk_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal"
android:gravity="end"
android:spacing="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_default"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/gpu_unswizzle_default_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_ok"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -8,7 +8,7 @@
<fragment
android:id="@+id/gamesFragment"
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
android:label="PlatformGamesFragment">
android:label="PlatformGamesFragment" />
<action
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
app:destination="@id/homeSettingsFragment"
@@ -16,14 +16,6 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_gamesFragment_to_profileManagerFragment"
app:destination="@id/profileManagerFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/homeSettingsFragment"
@@ -166,9 +158,6 @@
<action
android:id="@+id/action_global_perGamePropertiesFragment"
app:destination="@id/perGamePropertiesFragment" />
<action
android:id="@+id/action_global_gameInfoFragment"
app:destination="@id/gameInfoFragment" />
<fragment
android:id="@+id/gameInfoFragment"
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
@@ -229,10 +229,6 @@
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
<string name="home_search">Suche</string>
<string name="home_settings">Einstellungen</string>
<string name="home_sort">Sortieren</string>
<string name="home_layout">Layout</string>
<string name="home_start">Start</string>
<string name="home_clear">Löschen</string>
<string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
<string name="manage_game_folders">Spiele-Ordner verwalten</string>
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
@@ -709,11 +705,6 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
<string name="path_set">Pfad erfolgreich gesetzt</string>
<string name="skip_migration">Überspringen</string>
<!-- Footer actions -->
<string name="footer_open">Öffnen</string>
<string name="footer_game_info">Spielinfo</string>
<string name="footer_back">Zurück</string>
<!-- Game properties -->
<string name="info">Info</string>
<string name="info_description">Programm-ID, Entwickler, Version</string>
@@ -530,6 +530,21 @@
<item>2</item>
</integer-array>
<string-array name="framePacingModeNames">
<item>@string/frame_pacing_mode_target_Auto</item>
<item>@string/frame_pacing_mode_target_30</item>
<item>@string/frame_pacing_mode_target_60</item>
<item>@string/frame_pacing_mode_target_90</item>
<item>@string/frame_pacing_mode_target_120</item>
</string-array>
<integer-array name="framePacingModeValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<string-array name="appletEntries">
<item>@string/applet_hle</item>
<item>@string/applet_lle</item>
+21 -10
View File
@@ -36,6 +36,8 @@
<string name="enable_input_overlay_auto_hide">Enable Overlay Auto Hide</string>
<string name="hide_overlay_on_controller_input">Hide Overlay on Controller Input</string>
<string name="hide_overlay_on_controller_input_description">Automatically hide the touch controls overlay when a physical controller is used. Overlay reappears when controller is disconnected.</string>
<string name="invert_confirm_back_controller_buttons">Invert Confirm/Back Controller Buttons</string>
<string name="invert_confirm_back_controller_buttons_description">Swap Android Confirm and Back button handling to match both Switch and Xbox styles while using the app UI.</string>
<string name="input_overlay_options">Input Overlay</string>
<string name="input_overlay_options_description">Configure on-screen controls</string>
@@ -240,10 +242,6 @@
<string name="update_install_failed">Failed to install update: %1$s</string>
<string name="home_search">Search</string>
<string name="home_settings">Settings</string>
<string name="home_sort">Sort</string>
<string name="home_layout">Layout</string>
<string name="home_start">Start</string>
<string name="home_clear">Clear</string>
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
<string name="manage_game_folders">Manage game folders</string>
<string name="select_games_folder_description">Allows Eden to populate the games list</string>
@@ -426,6 +424,10 @@
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
<string name="frame_limit_slider">Limit speed percent</string>
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
<string name="turbo_speed_limit">Turbo speed</string>
<string name="turbo_speed_limit_description">When Turbo Mode is enabled, emulation will run at this speed.</string>
<string name="slow_speed_limit">Slow speed</string>
<string name="slow_speed_limit_description">When Slow Mode is enabled, emulation will run at this speed.</string>
<string name="cpu_backend">CPU backend</string>
<string name="cpu_accuracy">CPU accuracy</string>
<string name="value_with_units">%1$s%2$s</string>
@@ -477,6 +479,8 @@
<string name="renderer_accuracy_description">Controls the GPU emulation mode. Most games render fine with Fast or Balanced modes, but Accurate is still required for some. Particles tend to only render correctly with Accurate mode.</string>
<string name="dma_accuracy">DMA Accuracy</string>
<string name="dma_accuracy_description">Controls the DMA precision accuracy. Safe precision can fix issues in some games, but it can also impact performance in some cases. If unsure, leave this on Default.</string>
<string name="frame_pacing_mode">Frame Pacing Mode</string>
<string name="frame_pacing_mode_description">Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent.</string>
<string name="anisotropic_filtering">Anisotropic filtering</string>
<string name="anisotropic_filtering_description">Improves the quality of textures when viewed at oblique angles</string>
<string name="vram_usage_mode">VRAM Usage Mode</string>
@@ -505,15 +509,20 @@
<string name="skip_cpu_inner_invalidation">Skip CPU Inner Invalidation</string>
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
<string name="fix_bloom_effects">Fix Bloom Effects</string>
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout</string>
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout. Warning: may cause graphical artifacts in other games.</string>
<string name="renderer_asynchronous_shaders">Use asynchronous shaders</string>
<string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously. This may reduce stutters but may also introduce glitches.</string>
<string name="gpu_unswizzle_settings">GPU Unswizzle Settings</string>
<string name="gpu_unswizzle_settings_description">Configure GPU-based texture unswizzling parameters or disable it entirely. Adjust these settings to balance performance and texture loading quality.</string>
<string name="gpu_unswizzle_enable">Enable GPU Unswizzle</string>
<string name="gpu_unswizzle_disabled">Disabled</string>
<string name="gpu_unswizzle_texture_size">GPU Unswizzle Max Texture Size</string>
<string name="gpu_unswizzle_texture_size_description">Sets the maximum size (MB) for GPU-based texture unswizzling. While the GPU is faster for medium and large textures, the CPU may be more efficient for very small ones. Adjust this to find the balance between GPU acceleration and CPU overhead.</string>
<string name="gpu_unswizzle_stream_size">GPU Unswizzle Stream Size</string>
<string name="gpu_unswizzle_stream_size_description">Sets the data limit per frame for unswizzling large textures. Higher values speed up texture loading at the cost of higher frame latency; lower values reduce GPU overhead but may cause visible texture pop-in.</string>
<string name="gpu_unswizzle_chunk_size">GPU Unswizzle Chunk Size</string>
<string name="gpu_unswizzle_chunk_size_description">Defines the number of depth slices processed per batch for 3D textures. Increasing this improves throughput efficiency on powerful GPUs but may cause stuttering or driver timeouts on weaker hardware.</string>
<string name="gpu_unswizzle_default_button">Default</string>
<string name="extensions">Extensions</string>
@@ -770,11 +779,6 @@
<string name="path_set">Path set successfully</string>
<string name="skip_migration">Skip</string>
<!-- Footer actions -->
<string name="footer_open">Open</string>
<string name="footer_game_info">Game Info</string>
<string name="footer_back">Back</string>
<!-- Game properties -->
<string name="info">Info</string>
<string name="info_description">Program ID, developer, version</string>
@@ -1032,6 +1036,13 @@
<string name="dma_accuracy_unsafe">Unsafe</string>
<string name="dma_accuracy_safe">Safe</string>
<!-- Frame Pacing Mode -->
<string name="frame_pacing_mode_target_Auto">Auto</string>
<string name="frame_pacing_mode_target_30">30 FPS</string>
<string name="frame_pacing_mode_target_60">60 FPS</string>
<string name="frame_pacing_mode_target_90">90 FPS</string>
<string name="frame_pacing_mode_target_120">120 FPS</string>
<!-- ASTC Decoding Method Choices -->
<string name="accelerate_astc_cpu" translatable="false">CPU</string>
<string name="accelerate_astc_gpu" translatable="false">GPU</string>
+1 -5
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -8,8 +5,7 @@
plugins {
id("com.android.application") version "8.9.1" apply false
id("com.android.library") version "8.1.4" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}
tasks.register("clean").configure {
+2
View File
@@ -186,6 +186,8 @@ if(ARCHITECTURE_x86_64)
common
PRIVATE x64/cpu_detect.cpp
x64/cpu_detect.h
x64/cpu_wait.cpp
x64/cpu_wait.h
x64/native_clock.cpp
x64/native_clock.h
x64/rdtsc.cpp
+2 -1
View File
@@ -25,7 +25,8 @@ void AssertFailSoftImpl();
#define ASSERT_MSG(_a_, ...) \
([&]() YUZU_NO_INLINE { \
if (!(_a_)) [[unlikely]] { \
auto&& assert_condition = (_a_); \
if (!(assert_condition)) [[unlikely]] { \
LOG_CRITICAL(Debug, __FILE__ ": assert " __VA_ARGS__); \
AssertFailSoftImpl(); \
} \
+1 -26
View File
@@ -219,11 +219,6 @@ public:
}
}
bool ClearBackingRegion(size_t physical_offset, size_t length) {
// TODO: This does not seem to be possible on Windows.
return false;
}
void EnableDirectMappedAddress() {
// TODO
UNREACHABLE();
@@ -617,24 +612,6 @@ public:
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
}
bool ClearBackingRegion(size_t physical_offset, size_t length) {
#ifdef __linux__
// Only incur syscall cost IF memset would be slower (theshold = 16MiB)
// TODO(lizzie): Smarter way to dynamically get this threshold (broadwell != raptor lake) for example
if (length >= 2097152UL * 8) {
// Set MADV_REMOVE on backing map to destroy it instantly.
// This also deletes the area from the backing file.
int ret = madvise(backing_base + physical_offset, length, MADV_REMOVE);
ASSERT_MSG(ret == 0, "madvise failed: {}", strerror(errno));
return true;
} else {
return false;
}
#else
return false;
#endif
}
void EnableDirectMappedAddress() {
virtual_base = nullptr;
}
@@ -748,9 +725,7 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission
}
void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value) {
if (!impl || fill_value != 0 || !impl->ClearBackingRegion(physical_offset, length)) {
std::memset(backing_base + physical_offset, fill_value, length);
}
std::memset(backing_base + physical_offset, fill_value, length);
}
void HostMemory::EnableDirectMappedAddress() {
+2
View File
@@ -20,6 +20,7 @@
#define COMPILER_ID "@CXX_COMPILER@"
#define BUILD_AUTO_UPDATE_WEBSITE "@BUILD_AUTO_UPDATE_WEBSITE@"
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
@@ -42,6 +43,7 @@ constexpr const bool g_is_nightly_build = IS_NIGHTLY_BUILD;
constexpr const char g_build_auto_update_website[] = BUILD_AUTO_UPDATE_WEBSITE;
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
} // namespace Common
+1
View File
@@ -26,6 +26,7 @@ extern const bool g_is_nightly_build;
extern const char g_build_auto_update_website[];
extern const char g_build_auto_update_api[];
extern const char g_build_auto_update_api_path[];
extern const char g_build_auto_update_repo[];
} // namespace Common
+48
View File
@@ -379,4 +379,52 @@ void SetConfiguringGlobal(bool is_global) {
configuring_global = is_global;
}
u16 SpeedLimit() {
switch (SpeedMode(values.current_speed_mode)) {
case SpeedMode::Standard:
return values.speed_limit.GetValue();
case SpeedMode::Turbo:
return values.turbo_speed_limit.GetValue();
case SpeedMode::Slow:
return values.slow_speed_limit.GetValue();
default:
UNIMPLEMENTED();
}
return 100;
}
void SetSpeedMode(const SpeedMode& mode) {
values.current_speed_mode.SetValue(mode);
switch (mode) {
case SpeedMode::Turbo:
case SpeedMode::Slow:
values.use_speed_limit.SetValue(true);
break;
case SpeedMode::Standard:
default:
break;
}
}
void ToggleStandardMode() {
values.use_speed_limit.SetValue(!values.use_speed_limit.GetValue());
SetSpeedMode(SpeedMode::Standard);
}
void ToggleTurboMode() {
if (values.current_speed_mode.GetValue() != SpeedMode::Turbo)
SetSpeedMode(SpeedMode::Turbo);
else
SetSpeedMode(SpeedMode::Standard);
}
void ToggleSlowMode() {
if (values.current_speed_mode.GetValue() != SpeedMode::Slow)
SetSpeedMode(SpeedMode::Slow);
else
SetSpeedMode(SpeedMode::Standard);
}
} // namespace Settings
+51 -14
View File
@@ -204,6 +204,7 @@ struct Values {
true};
SwitchableSetting<bool> use_speed_limit{
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, true, true};
SwitchableSetting<u16, true> speed_limit{linkage,
100,
0,
@@ -214,6 +215,30 @@ struct Values {
true,
true,
&use_speed_limit};
SwitchableSetting<u16, true> slow_speed_limit{linkage,
50,
0,
9999,
"slow_speed_limit",
Category::Core,
Specialization::Countable | Specialization::Percentage,
true,
true};
SwitchableSetting<u16, true> turbo_speed_limit{linkage,
200,
0,
9999,
"turbo_speed_limit",
Category::Core,
Specialization::Countable | Specialization::Percentage,
true,
true};
// The currently used speed mode.
Setting<SpeedMode> current_speed_mode{linkage, SpeedMode::Standard, "current_speed_mode", Category::Core, Specialization::Default, false, true};
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
Specialization::Default};
@@ -434,6 +459,16 @@ struct Values {
"accelerate_astc",
Category::RendererAdvanced};
SwitchableSetting<FramePacingMode, true> frame_pacing_mode{linkage,
FramePacingMode::Target_Auto,
FramePacingMode::Target_Auto,
FramePacingMode::Target_120,
"frame_pacing_mode",
Category::RendererAdvanced,
Specialization::Default,
true,
true};
SwitchableSetting<AstcRecompression, true> astc_recompression{linkage,
AstcRecompression::Uncompressed,
"astc_recompression",
@@ -532,14 +567,11 @@ struct Values {
Category::RendererHacks,
Specialization::Default};
SwitchableSetting<bool> gpu_unswizzle_enabled{linkage, false, "gpu_unswizzle_enabled",
Category::RendererHacks};
SwitchableSetting<ExtendedDynamicState> dyna_state{linkage,
#if defined (_WIN32)
ExtendedDynamicState::EDS3,
#elif defined (__FreeBSD__)
ExtendedDynamicState::EDS3,
#elif defined (ANDROID)
ExtendedDynamicState::Disabled,
#elif defined (__APPLE__)
#if defined (ANDROID) || defined (__APPLE__)
ExtendedDynamicState::Disabled,
#else
ExtendedDynamicState::EDS2,
@@ -591,11 +623,11 @@ struct Values {
"language_index",
Category::System};
SwitchableSetting<Region, true> region_index{linkage, Region::Usa, "region_index", Category::System};
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto,
"time_zone_index", Category::System};
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto, "time_zone_index", Category::System};
Setting<u32> serial_battery{linkage, 0, "serial_battery", Category::System};
Setting<u32> serial_unit{linkage, 0, "serial_unit", Category::System};
// Measured in seconds since epoch
SwitchableSetting<bool> custom_rtc_enabled{
linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
SwitchableSetting<bool> custom_rtc_enabled{linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
SwitchableSetting<s64> custom_rtc{
linkage, 0, "custom_rtc", Category::System, Specialization::Time,
false, true, &custom_rtc_enabled};
@@ -759,14 +791,12 @@ struct Values {
0,
65535,
"debug_knobs",
Category::Debugging,
Category::Core,
Specialization::Countable,
true,
true};
// Miscellaneous
Setting<std::string> serial_battery{linkage, std::string(), "serial_battery", Category::Miscellaneous};
Setting<std::string> serial_unit{linkage, std::string(), "serial_unit", Category::Miscellaneous};
Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Miscellaneous};
Setting<bool> log_flush_line{linkage, false, "flush_line", Category::Miscellaneous, Specialization::Default, true, true};
Setting<bool> censor_username{linkage, true, "censor_username", Category::Miscellaneous};
@@ -814,6 +844,13 @@ bool IsDockedMode();
float Volume();
// speed limit ops
u16 SpeedLimit();
void SetSpeedMode(const SpeedMode &mode);
void ToggleStandardMode();
void ToggleTurboMode();
void ToggleSlowMode();
std::string GetTimeZoneString(TimeZone time_zone);
void LogSettings();
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -162,7 +162,7 @@ public:
/**
* @returns A unique identifier for the Setting's internal data type.
*/
[[nodiscard]] virtual std::type_index TypeId() const = 0;
[[nodiscard]] virtual std::string_view TypeId() const = 0;
/**
* Returns true if the Setting's internal data type is an enum.
+2
View File
@@ -129,6 +129,7 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt
ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16, X32, X64, None);
ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous);
ENUM(AstcRecompression, Uncompressed, Bc1, Bc3);
ENUM(FramePacingMode, Target_Auto, Target_30, Target_60, Target_90, Target_120);
ENUM(VSyncMode, Immediate, Mailbox, Fifo, FifoRelaxed);
ENUM(VramUsageMode, Conservative, Aggressive);
ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV);
@@ -156,6 +157,7 @@ ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(GameListMode, TreeView, GridView);
ENUM(SpeedMode, Standard, Turbo, Slow);
template <typename Type>
inline std::string_view CanonicalizeEnum(Type id) {
+9 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -220,8 +220,14 @@ public:
*
* @returns the type_index of the setting's type
*/
[[nodiscard]] std::type_index TypeId() const override final {
return std::type_index(typeid(Type));
[[nodiscard]] std::string_view TypeId() const override final {
if constexpr (std::is_same_v<Type, std::string>) {
return "string";
} else if constexpr (std::is_same_v<Type, bool>) {
return "bool";
} else {
return "other";
}
}
[[nodiscard]] constexpr u32 EnumIndex() const override final {
+75
View File
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <thread>
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include "common/x64/cpu_detect.h"
#include "common/x64/cpu_wait.h"
#include "common/x64/rdtsc.h"
namespace Common::X64 {
namespace {
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
// For reference:
// At 1 GHz, 100K cycles is 100us
// At 2 GHz, 100K cycles is 50us
// At 4 GHz, 100K cycles is 25us
constexpr auto PauseCycles = 100'000U;
} // Anonymous namespace
#if defined(_MSC_VER) && !defined(__clang__)
__forceinline static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
}
__forceinline static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
_mm_monitorx(&monitor_var, 0, 0);
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
}
#else
static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
const auto tsc = FencedRDTSC() + PauseCycles;
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
const auto edx = static_cast<u32>(tsc >> 32);
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
}
static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
}
#endif
void MicroSleep() {
static const bool has_waitpkg = GetCPUCaps().waitpkg;
static const bool has_monitorx = GetCPUCaps().monitorx;
if (has_waitpkg) {
TPAUSE();
} else if (has_monitorx) {
MWAITX();
} else {
std::this_thread::yield();
}
}
} // namespace Common::X64
+10
View File
@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
namespace Common::X64 {
void MicroSleep();
} // namespace Common::X64
+19 -23
View File
@@ -1219,12 +1219,9 @@ target_link_libraries(core PRIVATE
fmt::fmt
nlohmann_json::nlohmann_json
RenderDoc::API
MbedTLS::mbedcrypto${MBEDTLS_LIB_SUFFIX}
MbedTLS::mbedtls${MBEDTLS_LIB_SUFFIX})
ZLIB::ZLIB)
if (ENABLE_WEB_SERVICE OR ENABLE_OPENSSL)
target_link_libraries(core PRIVATE httplib::httplib)
endif()
target_link_libraries(core PRIVATE httplib::httplib)
if (ENABLE_WEB_SERVICE)
target_compile_definitions(core PUBLIC ENABLE_WEB_SERVICE)
@@ -1270,25 +1267,24 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
target_link_libraries(core PRIVATE dynarmic::dynarmic)
endif()
if(ENABLE_OPENSSL)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_openssl.cpp)
target_sources(core PRIVATE hle/service/ssl/ssl_backend_openssl.cpp)
find_package(OpenSSL REQUIRED)
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
elseif (APPLE)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_securetransport.cpp)
target_link_libraries(core PRIVATE "-framework Security")
elseif (WIN32)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_schannel.cpp)
target_link_libraries(core PRIVATE crypt32 secur32)
else()
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_none.cpp)
endif()
# TODO
# elseif (APPLE)
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_securetransport.cpp)
# target_link_libraries(core PRIVATE "-framework Security")
# elseif (WIN32)
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_schannel.cpp)
# target_link_libraries(core PRIVATE crypt32 secur32)
# else()
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_none.cpp)
# endif()
create_target_directory_groups(core)
+8 -9
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -38,10 +38,16 @@ u64 DynarmicCallbacks32::MemoryRead64(u32 vaddr) {
CheckMemoryAccess(vaddr, 8, Kernel::DebugWatchpointType::Read);
return m_memory.Read64(vaddr);
}
std::optional<u32> DynarmicCallbacks32::MemoryReadCode(u32 vaddr) {
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
return std::nullopt;
return m_memory.Read32(vaddr);
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
if (last_code_addr != aligned_vaddr) {
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
last_code_addr = aligned_vaddr;
}
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
}
void DynarmicCallbacks32::MemoryWrite8(u32 vaddr, u8 value) {
@@ -82,13 +88,6 @@ bool DynarmicCallbacks32::MemoryWriteExclusive64(u32 vaddr, u64 value, u64 expec
m_memory.WriteExclusive64(vaddr, value, expected);
}
void DynarmicCallbacks32::InterpreterFallback(u32 pc, std::size_t num_instructions) {
m_parent.LogBacktrace(m_process);
LOG_ERROR(Core_ARM,
"Unimplemented instruction @ {:#X} for {} instructions (instr = {:08X})", pc,
num_instructions, m_memory.Read32(pc));
}
void DynarmicCallbacks32::ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) {
switch (exception) {
case Dynarmic::A32::Exception::NoExecuteFault:
+8 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -7,6 +7,7 @@
#pragma once
#include <dynarmic/interface/A32/a32.h>
#include <dynarmic/interface/code_page.h>
#include "core/arm/arm_interface.h"
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
@@ -34,6 +35,9 @@ public:
u32 MemoryRead32(u32 vaddr) override;
u64 MemoryRead64(u32 vaddr) override;
std::optional<u32> MemoryReadCode(u32 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u32 vaddr, u8 value) override;
void MemoryWrite16(u32 vaddr, u16 value) override;
void MemoryWrite32(u32 vaddr, u32 value) override;
@@ -42,13 +46,15 @@ public:
bool MemoryWriteExclusive16(u32 vaddr, u16 value, u16 expected) override;
bool MemoryWriteExclusive32(u32 vaddr, u32 value, u32 expected) override;
bool MemoryWriteExclusive64(u32 vaddr, u64 value, u64 expected) override;
void InterpreterFallback(u32 pc, std::size_t num_instructions) override;
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override;
void CallSVC(u32 swi) override;
void AddTicks(u64 ticks) override;
u64 GetTicksRemaining() override;
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
void ReturnException(u32 pc, Dynarmic::HaltReason hr);
//
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = 0;
ArmDynarmic32& m_parent;
Core::Memory::Memory& m_memory;
Kernel::KProcess* m_process{};
+9 -9
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -41,10 +41,17 @@ Dynarmic::A64::Vector DynarmicCallbacks64::MemoryRead128(u64 vaddr) {
CheckMemoryAccess(vaddr, 16, Kernel::DebugWatchpointType::Read);
return {m_memory.Read64(vaddr), m_memory.Read64(vaddr + 8)};
}
std::optional<u32> DynarmicCallbacks64::MemoryReadCode(u64 vaddr) {
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
return std::nullopt;
return m_memory.Read32(vaddr);
// return m_memory.Read32(vaddr);
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
if (last_code_addr != aligned_vaddr) {
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
last_code_addr = aligned_vaddr;
}
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
}
void DynarmicCallbacks64::MemoryWrite8(u64 vaddr, u8 value) {
@@ -95,13 +102,6 @@ bool DynarmicCallbacks64::MemoryWriteExclusive128(u64 vaddr, Dynarmic::A64::Vect
m_memory.WriteExclusive128(vaddr, value, expected);
}
void DynarmicCallbacks64::InterpreterFallback(u64 pc, std::size_t num_instructions) {
m_parent.LogBacktrace(m_process);
LOG_ERROR(Core_ARM, "Unimplemented instruction @ {:#X} for {} instructions (instr = {:08X})", pc,
num_instructions, m_memory.Read32(pc));
ReturnException(pc, PrefetchAbort);
}
void DynarmicCallbacks64::InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, u64 value) {
switch (op) {
case Dynarmic::A64::InstructionCacheOperation::InvalidateByVAToPoU: {
+6 -1
View File
@@ -11,6 +11,7 @@
#include <ankerl/unordered_dense.h>
#include <dynarmic/interface/A64/a64.h>
#include <dynarmic/interface/code_page.h>
#include "common/common_types.h"
#include "common/hash.h"
#include "core/arm/arm_interface.h"
@@ -41,6 +42,9 @@ public:
u64 MemoryRead64(u64 vaddr) override;
Dynarmic::A64::Vector MemoryRead128(u64 vaddr) override;
std::optional<u32> MemoryReadCode(u64 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u64 vaddr, u8 value) override;
void MemoryWrite16(u64 vaddr, u16 value) override;
void MemoryWrite32(u64 vaddr, u32 value) override;
@@ -51,7 +55,6 @@ public:
bool MemoryWriteExclusive32(u64 vaddr, std::uint32_t value, std::uint32_t expected) override;
bool MemoryWriteExclusive64(u64 vaddr, std::uint64_t value, std::uint64_t expected) override;
bool MemoryWriteExclusive128(u64 vaddr, Dynarmic::A64::Vector value, Dynarmic::A64::Vector expected) override;
void InterpreterFallback(u64 pc, std::size_t num_instructions) override;
void InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, u64 value) override;
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception exception) override;
void CallSVC(u32 svc) override;
@@ -61,6 +64,8 @@ public:
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
void ReturnException(u64 pc, Dynarmic::HaltReason hr);
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = 0;
ArmDynarmic64& m_parent;
Core::Memory::Memory& m_memory;
u64 m_tpidrro_el0{};
+3 -7
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -772,12 +772,8 @@ std::optional<u64> MatchAndExecuteOneInstruction(Core::Memory::Memory& memory, m
u32 instruction = memory.Read32(pc);
bool was_executed = false;
if (auto decoder = Dynarmic::A64::Decode<VisitorBase>(instruction)) {
was_executed = decoder->get().call(visitor, instruction);
} else {
LOG_ERROR(Core_ARM, "Unallocated encoding: {:#x}", instruction);
}
auto decoder = Dynarmic::A64::Decode<VisitorBase>(instruction);
was_executed = decoder.get().call(visitor, instruction);
return was_executed ? std::optional<u64>(pc + 4) : std::nullopt;
}
-4
View File
@@ -806,10 +806,6 @@ void System::RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
impl->content_provider->SetSlot(slot, provider);
}
void System::ClearContentProvider(FileSys::ContentProviderUnionSlot slot) {
impl->content_provider->ClearSlot(slot);
}
const Reporter& System::GetReporter() const {
return impl->reporter;
}
+2 -5
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -358,10 +358,7 @@ public:
[[nodiscard]] Service::FileSystem::FileSystemController& GetFileSystemController();
[[nodiscard]] const Service::FileSystem::FileSystemController& GetFileSystemController() const;
void RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
FileSys::ContentProvider* provider);
void ClearContentProvider(FileSys::ContentProviderUnionSlot slot);
void RegisterContentProvider(FileSys::ContentProviderUnionSlot slot, FileSys::ContentProvider* provider);
[[nodiscard]] const Reporter& GetReporter() const;
+27 -2
View File
@@ -13,6 +13,10 @@
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
#include "common/x64/cpu_wait.h"
#endif
#include "common/settings.h"
#include "core/core_timing.h"
#include "core/hardware_properties.h"
@@ -201,7 +205,7 @@ u64 CoreTiming::GetClockTicks() const {
if (Settings::values.sync_core_speed.GetValue()) {
const auto ticks = double(fres);
const auto speed_limit = double(Settings::values.speed_limit.GetValue())*0.01;
const auto speed_limit = double(Settings::SpeedLimit())*0.01;
return u64(ticks/speed_limit);
} else {
return fres;
@@ -283,7 +287,28 @@ void CoreTiming::ThreadLoop() {
if (next_time) {
// There are more events left in the queue, wait until the next event.
auto wait_time = *next_time - GetGlobalTimeNs().count();
event.WaitFor(std::chrono::nanoseconds(wait_time));
if (wait_time > 0) {
#ifdef _WIN32
while (!paused && !event.IsSet() && wait_time > 0) {
wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time >= timer_resolution_ns) {
Common::Windows::SleepForOneTick();
} else {
#ifdef ARCHITECTURE_x86_64
Common::X64::MicroSleep();
#else
std::this_thread::yield();
#endif
}
}
if (event.IsSet()) {
event.Reset();
}
#else
event.WaitFor(std::chrono::nanoseconds(wait_time));
#endif
}
} else {
// Queue is empty, wait until another event is scheduled and signals us to
// continue.
+99 -60
View File
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cstring>
#include <mbedtls/cipher.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/crypto/aes_util.h"
@@ -28,83 +28,121 @@ NintendoTweak CalculateNintendoTweak(std::size_t sector_id) {
}
} // Anonymous namespace
static_assert(static_cast<std::size_t>(Mode::CTR) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_CTR),
"CTR has incorrect value.");
static_assert(static_cast<std::size_t>(Mode::ECB) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_ECB),
"ECB has incorrect value.");
static_assert(static_cast<std::size_t>(Mode::XTS) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_XTS),
"XTS has incorrect value.");
// Structure to hide mbedtls types from header file
// Structure to hide OpenSSL types from header file
struct CipherContext {
mbedtls_cipher_context_t encryption_context;
mbedtls_cipher_context_t decryption_context;
EVP_CIPHER_CTX* encryption_context = nullptr;
EVP_CIPHER_CTX* decryption_context = nullptr;
EVP_CIPHER* cipher = nullptr;
};
static inline const std::string GetCipherName(Mode mode, u32 key_size) {
std::string cipher;
std::size_t effective_bits = key_size * 8;
switch (mode) {
case Mode::CTR:
cipher = "CTR";
break;
case Mode::ECB:
cipher = "ECB";
break;
case Mode::XTS:
cipher = "XTS";
effective_bits /= 2;
break;
default:
UNREACHABLE();
}
return fmt::format("AES-{}-{}", effective_bits, cipher);
};
static EVP_CIPHER *GetCipher(Mode mode, u32 key_size) {
static auto fetch_cipher = [](Mode m, u32 k) {
return EVP_CIPHER_fetch(nullptr, GetCipherName(m, k).c_str(), nullptr);
};
static const struct {
EVP_CIPHER* ctr_16 = fetch_cipher(Mode::CTR, 16);
EVP_CIPHER* ecb_16 = fetch_cipher(Mode::ECB, 16);
EVP_CIPHER* xts_16 = fetch_cipher(Mode::XTS, 16);
EVP_CIPHER* ctr_32 = fetch_cipher(Mode::CTR, 32);
EVP_CIPHER* ecb_32 = fetch_cipher(Mode::ECB, 32);
EVP_CIPHER* xts_32 = fetch_cipher(Mode::XTS, 32);
} ciphers = {};
switch (mode) {
case Mode::CTR:
return key_size == 16 ? ciphers.ctr_16 : ciphers.ctr_32;
case Mode::ECB:
return key_size == 16 ? ciphers.ecb_16 : ciphers.ecb_32;
case Mode::XTS:
return key_size == 16 ? ciphers.xts_16 : ciphers.xts_32;
default:
UNIMPLEMENTED();
}
return nullptr;
}
// TODO: WHY TEMPLATE???????
template <typename Key, std::size_t KeySize>
Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode)
: ctx(std::make_unique<CipherContext>()) {
mbedtls_cipher_init(&ctx->encryption_context);
mbedtls_cipher_init(&ctx->decryption_context);
ASSERT_MSG((mbedtls_cipher_setup(
&ctx->encryption_context,
mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode))) ||
mbedtls_cipher_setup(
&ctx->decryption_context,
mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode)))) == 0,
"Failed to initialize mbedtls ciphers.");
ctx->encryption_context = EVP_CIPHER_CTX_new();
ctx->decryption_context = EVP_CIPHER_CTX_new();
ctx->cipher = GetCipher(mode, KeySize);
if (ctx->cipher) {
EVP_CIPHER_up_ref(ctx->cipher);
} else {
UNIMPLEMENTED();
}
ASSERT(
!mbedtls_cipher_setkey(&ctx->encryption_context, key.data(), KeySize * 8, MBEDTLS_ENCRYPT));
ASSERT(
!mbedtls_cipher_setkey(&ctx->decryption_context, key.data(), KeySize * 8, MBEDTLS_DECRYPT));
//"Failed to set key on mbedtls ciphers.");
ASSERT_MSG(ctx->encryption_context && ctx->decryption_context && ctx->cipher,
"OpenSSL cipher context failed init!");
// now init ciphers
ASSERT(EVP_CipherInit_ex2(ctx->encryption_context, ctx->cipher, key.data(), NULL, 1, NULL));
ASSERT(EVP_CipherInit_ex2(ctx->decryption_context, ctx->cipher, key.data(), NULL, 0, NULL));
EVP_CIPHER_CTX_set_padding(ctx->encryption_context, 0);
EVP_CIPHER_CTX_set_padding(ctx->decryption_context, 0);
}
template <typename Key, std::size_t KeySize>
AESCipher<Key, KeySize>::~AESCipher() {
mbedtls_cipher_free(&ctx->encryption_context);
mbedtls_cipher_free(&ctx->decryption_context);
EVP_CIPHER_CTX_free(ctx->encryption_context);
EVP_CIPHER_CTX_free(ctx->decryption_context);
EVP_CIPHER_free(ctx->cipher);
}
template <typename Key, std::size_t KeySize>
void AESCipher<Key, KeySize>::Transcode(const u8* src, std::size_t size, u8* dest, Op op) const {
auto* const context = op == Op::Encrypt ? &ctx->encryption_context : &ctx->decryption_context;
mbedtls_cipher_reset(context);
auto* const context = op == Op::Encrypt ? ctx->encryption_context : ctx->decryption_context;
if (size == 0)
return;
const auto mode = mbedtls_cipher_get_cipher_mode(context);
std::size_t written = 0;
// reset
ASSERT(EVP_CipherInit_ex(context, nullptr, nullptr, nullptr, nullptr, -1));
if (mode != MBEDTLS_MODE_ECB) {
const int ret = mbedtls_cipher_update(context, src, size, dest, &written);
ASSERT(ret == 0);
if (written != size) {
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.", size, written);
}
return;
}
const auto block_size = mbedtls_cipher_get_block_size(context);
ASSERT(block_size <= AesBlockBytes);
const int block_size = EVP_CIPHER_CTX_get_block_size(context);
ASSERT(block_size > 0 && block_size <= int(AesBlockBytes));
const std::size_t whole_block_bytes = size - (size % block_size);
int written = 0;
if (whole_block_bytes != 0) {
const int ret = mbedtls_cipher_update(context, src, whole_block_bytes, dest, &written);
ASSERT(ret == 0);
if (written != whole_block_bytes) {
ASSERT(EVP_CipherUpdate(context, dest, &written, src, static_cast<int>(whole_block_bytes)));
if (std::size_t(written) != whole_block_bytes) {
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.",
whole_block_bytes, written);
}
}
// tail
const std::size_t tail = size - whole_block_bytes;
if (tail == 0)
return;
@@ -112,13 +150,13 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, std::size_t size, u8* des
std::array<u8, AesBlockBytes> tail_buffer{};
std::memcpy(tail_buffer.data(), src + whole_block_bytes, tail);
std::size_t tail_written = 0;
const int ret = mbedtls_cipher_update(context, tail_buffer.data(), block_size, tail_buffer.data(),
&tail_written);
ASSERT(ret == 0);
int tail_written = 0;
ASSERT(EVP_CipherUpdate(context, tail_buffer.data(), &tail_written, tail_buffer.data(), block_size));
if (tail_written != block_size) {
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.", block_size,
tail_written);
LOG_WARNING(Crypto, "Tail block not fully processed requested={:016X}, actual={:016X}.",
block_size, tail_written);
}
std::memcpy(dest + whole_block_bytes, tail_buffer.data(), tail);
@@ -137,9 +175,10 @@ void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, std::size_t size, u8*
template <typename Key, std::size_t KeySize>
void AESCipher<Key, KeySize>::SetIV(std::span<const u8> data) {
ASSERT_MSG((mbedtls_cipher_set_iv(&ctx->encryption_context, data.data(), data.size()) ||
mbedtls_cipher_set_iv(&ctx->decryption_context, data.data(), data.size())) == 0,
"Failed to set IV on mbedtls ciphers.");
const int ret_enc = EVP_CipherInit_ex(ctx->encryption_context, nullptr, nullptr, nullptr, data.data(), -1);
const int ret_dec = EVP_CipherInit_ex(ctx->decryption_context, nullptr, nullptr, nullptr, data.data(), -1);
ASSERT_MSG(ret_enc == 1 && ret_dec == 1, "Failed to set IV on OpenSSL contexts");
}
template class AESCipher<Key128>;
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -10,7 +10,6 @@
#include <span>
#include <type_traits>
#include "common/common_types.h"
#include "core/file_sys/vfs/vfs.h"
namespace Core::Crypto {
@@ -62,4 +61,5 @@ public:
private:
std::unique_ptr<CipherContext> ctx;
};
} // namespace Core::Crypto
+48 -39
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -9,35 +9,26 @@
#include <bitset>
#include <cctype>
#include <fstream>
#include <locale>
#include <map>
#include <sstream>
#include <tuple>
#include <vector>
#include <mbedtls/bignum.h>
#include <mbedtls/cipher.h>
#include <mbedtls/cmac.h>
#include <mbedtls/sha256.h>
#include <openssl/evp.h>
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/crypto/aes_util.h"
#include "core/crypto/key_manager.h"
#include "core/crypto/partition_data_manager.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/registered_cache.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#ifndef MBEDTLS_CMAC_C
#error mbedtls was compiled without CMAC support. Check your USE flags (Gentoo) or contact your package maintainer.
#endif
namespace Core::Crypto {
namespace {
@@ -527,15 +518,27 @@ static std::array<u8, target_size> MGF1(const std::array<u8, in_size>& seed) {
std::array<u8, in_size + 4> seed_exp{};
std::memcpy(seed_exp.data(), seed.data(), in_size);
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
const EVP_MD* sha256 = EVP_sha256();
std::vector<u8> out;
size_t i = 0;
while (out.size() < target_size) {
out.resize(out.size() + 0x20);
seed_exp[in_size + 3] = static_cast<u8>(i);
mbedtls_sha256(seed_exp.data(), seed_exp.size(), out.data() + out.size() - 0x20, 0);
size_t offset = out.size();
out.resize(offset + 0x20);
seed_exp[in_size + 3] = u8(i);
u32 hash_len = 0;
EVP_DigestInit_ex(ctx, sha256, nullptr);
EVP_DigestUpdate(ctx, seed_exp.data(), seed_exp.size());
EVP_DigestFinal_ex(ctx, out.data() + offset, &hash_len);
++i;
}
EVP_MD_CTX_free(ctx);
std::array<u8, target_size> target;
std::memcpy(target.data(), out.data(), target_size);
return target;
@@ -588,32 +591,28 @@ std::optional<Key128> KeyManager::ParseTicketTitleKey(const Ticket& ticket) {
return std::nullopt;
}
mbedtls_mpi D; // RSA Private Exponent
mbedtls_mpi N; // RSA Modulus
mbedtls_mpi S; // Input
mbedtls_mpi M; // Output
mbedtls_mpi_init(&D);
mbedtls_mpi_init(&N);
mbedtls_mpi_init(&S);
mbedtls_mpi_init(&M);
const auto& title_key_block = ticket.GetData().title_key_block;
mbedtls_mpi_read_binary(&D, eticket_rsa_keypair.decryption_key.data(),
eticket_rsa_keypair.decryption_key.size());
mbedtls_mpi_read_binary(&N, eticket_rsa_keypair.modulus.data(),
eticket_rsa_keypair.modulus.size());
mbedtls_mpi_read_binary(&S, title_key_block.data(), title_key_block.size());
mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr);
std::array<u8, 0x100> rsa_step;
mbedtls_mpi_write_binary(&M, rsa_step.data(), rsa_step.size());
{
// Private context for OpenSSL bignumbers
// Inside block because I dont wanna pollute the space...
const auto& title_key_block = ticket.GetData().title_key_block;
BIGNUM* D = BN_bin2bn(eticket_rsa_keypair.decryption_key.data(), int(eticket_rsa_keypair.decryption_key.size()), NULL);
BIGNUM* N = BN_bin2bn(eticket_rsa_keypair.modulus.data(), int(eticket_rsa_keypair.modulus.size()), NULL);
BIGNUM* S = BN_bin2bn(title_key_block.data(), int(title_key_block.size()), NULL);
BIGNUM* M = BN_new();
// M = S ^ D mod N
BN_mod_exp(M, S, D, N, NULL);
BN_bn2bin(M, rsa_step.data());
BN_free(D);
BN_free(N);
BN_free(S);
BN_free(M);
}
u8 m_0 = rsa_step[0];
std::array<u8, 0x20> m_1;
std::memcpy(m_1.data(), rsa_step.data() + 0x01, m_1.size());
std::array<u8, 0xDF> m_2;
std::memcpy(m_1.data(), rsa_step.data() + 0x01, m_1.size());
std::memcpy(m_2.data(), rsa_step.data() + 0x21, m_2.size());
if (m_0 != 0) {
@@ -954,8 +953,18 @@ void KeyManager::DeriveSDSeedLazy() {
static Key128 CalculateCMAC(const u8* source, size_t size, const Key128& key) {
Key128 out{};
mbedtls_cipher_cmac(mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB), key.data(),
key.size() * 8, source, size, out.data());
static EVP_MAC* mac = EVP_MAC_fetch(nullptr, "cmac", nullptr);
if (!mac) return out;
static EVP_MAC_CTX* ctx = EVP_MAC_CTX_new(mac);
if (!ctx) return out;
EVP_MAC_init(ctx, key.data(), key.size() * CHAR_BIT, NULL);
EVP_MAC_update(ctx, source, size);
size_t len;
EVP_MAC_final(ctx, out.data(), &len, out.size());
return out;
}
+2 -7
View File
@@ -1,25 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <mbedtls/sha256.h>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "common/swap.h"
#include "core/crypto/key_manager.h"
#include "core/crypto/partition_data_manager.h"
#include "core/crypto/xts_encryption_layer.h"
#include "core/file_sys/kernel_executable.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_offset.h"
#include "core/file_sys/vfs/vfs_vector.h"
#include "core/loader/loader.h"
@@ -255,4 +250,4 @@ std::array<u8, 576> PartitionDataManager::GetETicketExtendedKek() const {
prodinfo_decrypted->Read(out.data(), out.size(), 0x3890);
return out;
}
} // namespace Core::Crypto
} // namespace Core::Crypto
+7
View File
@@ -127,6 +127,11 @@ public:
void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta);
private:
struct TranslationEntry {
DAddr guest_page{};
u8* host_ptr{};
};
// Internal helper that performs the update assuming the caller already holds the necessary lock.
void UpdatePagesCachedCountNoLock(DAddr addr, size_t size, s32 delta);
@@ -195,6 +200,8 @@ private:
}
Common::VirtualBuffer<VAddr> cpu_backing_address;
std::array<TranslationEntry, 4> t_slot{};
u32 cache_cursor = 0;
using CounterType = u8;
using CounterAtomicType = std::atomic_uint8_t;
static constexpr size_t subentries = 8 / sizeof(CounterType);
+43 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -247,6 +247,7 @@ void DeviceMemoryManager<Traits>::Map(DAddr address, VAddr virtual_address, size
}
impl->multi_dev_address.Register(new_dev, start_id);
}
t_slot = {};
if (track) {
TrackContinuityImpl(address, virtual_address, size, asid);
}
@@ -278,6 +279,7 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) {
compressed_device_addr[phys_addr - 1] = new_start | MULTI_FLAG;
}
}
t_slot = {};
}
template <typename Traits>
void DeviceMemoryManager<Traits>::TrackContinuityImpl(DAddr address, VAddr virtual_address,
@@ -417,6 +419,26 @@ void DeviceMemoryManager<Traits>::WalkBlock(DAddr addr, std::size_t size, auto o
template <typename Traits>
void DeviceMemoryManager<Traits>::ReadBlock(DAddr address, void* dest_pointer, size_t size) {
device_inter->FlushRegion(address, size);
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
for (size_t i = 0; i < 4; ++i) {
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
return;
}
}
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
const auto phys_addr = compressed_physical_ptr[page_index];
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
cache_cursor = (cache_cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
}
WalkBlock(
address, size,
[&](size_t copy_amount, DAddr current_vaddr) {
@@ -455,6 +477,26 @@ void DeviceMemoryManager<Traits>::WriteBlock(DAddr address, const void* src_poin
template <typename Traits>
void DeviceMemoryManager<Traits>::ReadBlockUnsafe(DAddr address, void* dest_pointer, size_t size) {
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
for (size_t i = 0; i < 4; ++i) {
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
return;
}
}
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
const auto phys_addr = compressed_physical_ptr[page_index];
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
cache_cursor = (cache_cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
}
WalkBlock(
address, size,
[&](size_t copy_amount, DAddr current_vaddr) {
+75
View File
@@ -4,6 +4,13 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <cstddef>
#include <cstring>
#include <limits>
#include <span>
#include <zlib.h>
#include "common/settings.h"
#include "common/string_util.h"
#include "common/swap.h"
@@ -31,6 +38,73 @@ const std::array<const char*, 16> LANGUAGE_NAMES{{
"BrazilianPortuguese",
}};
namespace {
constexpr std::size_t LEGACY_LANGUAGE_REGION_SIZE = sizeof(std::array<LanguageEntry, 16>);
constexpr std::size_t PACKED_LANGUAGE_REGION_MAX_SIZE = sizeof(LanguageEntry) * 32;
bool InflateRawDeflate(std::span<const u8> compressed, std::vector<u8>& out) {
if (compressed.empty() || compressed.size() > std::numeric_limits<uInt>::max()) {
return false;
}
z_stream stream{};
stream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(compressed.data()));
stream.avail_in = static_cast<uInt>(compressed.size());
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
return false;
}
std::array<u8, 0x1000> chunk{};
int ret = Z_OK;
while (ret == Z_OK) {
stream.next_out = reinterpret_cast<Bytef*>(chunk.data());
stream.avail_out = static_cast<uInt>(chunk.size());
ret = inflate(&stream, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END) {
inflateEnd(&stream);
return false;
}
const auto produced = chunk.size() - static_cast<std::size_t>(stream.avail_out);
if (produced != 0) {
if (out.size() + produced > PACKED_LANGUAGE_REGION_MAX_SIZE) {
inflateEnd(&stream);
return false;
}
out.insert(out.end(), chunk.begin(),
chunk.begin() + static_cast<std::ptrdiff_t>(produced));
}
}
inflateEnd(&stream);
return ret == Z_STREAM_END;
}
void DecodePackedLanguageEntries(RawNACP& raw) {
auto* packed_region = reinterpret_cast<u8*>(raw.language_entries.data());
u16_le compressed_size_le{};
std::memcpy(&compressed_size_le, packed_region, sizeof(compressed_size_le));
const auto compressed_size = static_cast<std::size_t>(compressed_size_le);
if (compressed_size == 0 || compressed_size > LEGACY_LANGUAGE_REGION_SIZE - sizeof(u16_le)) {
return;
}
std::vector<u8> decompressed;
if (!InflateRawDeflate(
std::span<const u8>(packed_region + sizeof(u16_le), compressed_size), decompressed)) {
return;
}
if (decompressed.size() < LEGACY_LANGUAGE_REGION_SIZE ||
decompressed.size() % sizeof(LanguageEntry) != 0) {
return;
}
std::memcpy(raw.language_entries.data(), decompressed.data(), LEGACY_LANGUAGE_REGION_SIZE);
}
} // namespace
std::string LanguageEntry::GetApplicationName() const {
return Common::StringFromFixedZeroTerminatedBuffer(application_name.data(),
application_name.size());
@@ -66,6 +140,7 @@ NACP::NACP() = default;
NACP::NACP(VirtualFile file) {
file->ReadObject(&raw);
DecodePackedLanguageEntries(raw);
}
NACP::~NACP() = default;
+4
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -37,6 +40,7 @@ enum class ContentRecordType : u8 {
HtmlDocument = 4,
LegalInformation = 5,
DeltaFragment = 6,
Count,
};
struct ContentRecord {
+7 -7
View File
@@ -143,7 +143,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
const auto update_tid = GetUpdateTitleID(title_id);
if (content_union) {
@@ -167,7 +167,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
// Also check ManualContentProvider (for Android)
if (!checked_external) {
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
@@ -243,7 +243,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
// Also try ManualContentProvider
if (update == nullptr) {
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
@@ -570,7 +570,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider
const auto* external_provider = content_union->GetExternalProvider();
@@ -592,7 +592,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
}
if (!checked_external) {
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
@@ -690,7 +690,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::vector<Patch> external_update_patches;
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider for updates
@@ -721,7 +721,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
}
}
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
const auto* manual_provider = static_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider && external_update_patches.empty()) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
+103 -157
View File
@@ -7,12 +7,11 @@
#include <algorithm>
#include <random>
#include <regex>
#include <mbedtls/sha256.h>
#include <openssl/evp.h>
#include "common/assert.h"
#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/string_util.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
@@ -64,17 +63,23 @@ static bool FollowsNcaIdFormat(std::string_view name) {
static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
bool within_two_digit, bool cnmt_suffix) {
const auto nca_str = Common::HexToString(nca_id, second_hex_upper);
if (!within_two_digit) {
const auto format_str = fmt::runtime(cnmt_suffix ? "{}.cnmt.nca" : "/{}.nca");
return fmt::format(format_str, Common::HexToString(nca_id, second_hex_upper));
return fmt::format(format_str, nca_str);
}
Core::Crypto::SHA256Hash hash{};
mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
u32 hash_len = 0;
EVP_Digest(nca_id.data(), nca_id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
const auto format_str =
fmt::runtime(cnmt_suffix ? "/000000{:02X}/{}.cnmt.nca" : "/000000{:02X}/{}.nca");
return fmt::format(format_str, hash[0], Common::HexToString(nca_id, second_hex_upper));
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, nca_str);
return fmt::format(format_str, hash[0], nca_str);
}
static std::string GetCNMTName(TitleType type, u64 title_id) {
@@ -152,7 +157,11 @@ bool PlaceholderCache::Create(const NcaID& id, u64 size) const {
}
Core::Crypto::SHA256Hash hash{};
mbedtls_sha256(id.data(), id.size(), hash.data(), 0);
u32 hash_len = 0;
EVP_Digest(id.data(), id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, id);
const auto dirname = fmt::format("000000{:02X}", hash[0]);
const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname);
@@ -176,7 +185,11 @@ bool PlaceholderCache::Delete(const NcaID& id) const {
}
Core::Crypto::SHA256Hash hash{};
mbedtls_sha256(id.data(), id.size(), hash.data(), 0);
u32 hash_len = 0;
EVP_Digest(id.data(), id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, id);
const auto dirname = fmt::format("000000{:02X}", hash[0]);
const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname);
@@ -353,8 +366,7 @@ VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
return file;
}
static std::optional<NcaID> CheckMapForContentRecord(const std::map<u64, CNMT>& map, u64 title_id,
ContentRecordType type) {
static std::optional<NcaID> CheckMapForContentRecord(const ankerl::unordered_dense::map<u64, CNMT>& map, u64 title_id, ContentRecordType type) {
const auto cmnt_iter = map.find(title_id);
if (cmnt_iter == map.cend()) {
return std::nullopt;
@@ -671,7 +683,12 @@ InstallResult RegisteredCache::InstallEntry(const NCA& nca, TitleType type,
const OptionalHeader opt_header{0, 0};
ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca.GetType()), {}};
const auto& data = nca.GetBaseFile()->ReadBytes(0x100000);
mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0);
u32 hash_len = 0;
EVP_Digest(data.data(), data.size(), c_rec.hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca {}", hash_len, nca.GetName());
std::memcpy(&c_rec.nca_id, &c_rec.hash, 16);
const CNMT new_cnmt(header, opt_header, {c_rec}, {});
if (!RawInstallYuzuMeta(new_cnmt)) {
@@ -782,7 +799,12 @@ InstallResult RegisteredCache::RawInstallNCA(const NCA& nca, const VfsCopyFuncti
id = *override_id;
} else {
const auto& data = in->ReadBytes(0x100000);
mbedtls_sha256(data.data(), data.size(), hash.data(), 0);
u32 hash_len = 0;
EVP_Digest(data.data(), data.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca {}", hash_len, nca.GetName());
memcpy(id.data(), hash.data(), 16);
}
@@ -829,133 +851,96 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
}
}
Refresh();
return std::find_if(yuzu_meta.begin(), yuzu_meta.end(),
[&cnmt](const std::pair<u64, CNMT>& kv) {
return kv.second.GetType() == cnmt.GetType() &&
kv.second.GetTitleID() == cnmt.GetTitleID();
}) != yuzu_meta.end();
return std::find_if(yuzu_meta.begin(), yuzu_meta.end(), [&cnmt](const std::pair<u64, CNMT>& kv) {
return kv.second.GetType() == cnmt.GetType() && kv.second.GetTitleID() == cnmt.GetTitleID();
}) != yuzu_meta.end();
}
ContentProviderUnion::~ContentProviderUnion() = default;
void ContentProviderUnion::SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider) {
providers[slot] = provider;
}
void ContentProviderUnion::ClearSlot(ContentProviderUnionSlot slot) {
providers[slot] = nullptr;
providers[size_t(slot)] = provider;
}
void ContentProviderUnion::Refresh() {
for (auto& provider : providers) {
if (provider.second == nullptr)
continue;
provider.second->Refresh();
}
for (auto e : providers)
if (e != nullptr)
e->Refresh();
}
bool ContentProviderUnion::HasEntry(u64 title_id, ContentRecordType type) const {
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (provider.second->HasEntry(title_id, type))
for (auto const e : providers)
if (e && e->HasEntry(title_id, type))
return true;
}
return false;
}
std::optional<u32> ContentProviderUnion::GetEntryVersion(u64 title_id) const {
for (const auto& provider : providers) {
if (provider.second == nullptr)
for (auto const e : providers) {
if (e == nullptr)
continue;
const auto res = provider.second->GetEntryVersion(title_id);
if (res != std::nullopt)
if (auto const res = e->GetEntryVersion(title_id); res != std::nullopt)
return res;
}
return std::nullopt;
}
VirtualFile ContentProviderUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
for (const auto& provider : providers) {
if (provider.second == nullptr)
for (auto const e : providers) {
if (e == nullptr)
continue;
const auto res = provider.second->GetEntryUnparsed(title_id, type);
if (res != nullptr)
if (auto const res = e->GetEntryUnparsed(title_id, type); res != nullptr)
return res;
}
return nullptr;
}
VirtualFile ContentProviderUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
for (const auto& provider : providers) {
if (provider.second == nullptr)
for (auto const e : providers) {
if (e == nullptr)
continue;
const auto res = provider.second->GetEntryRaw(title_id, type);
if (res != nullptr)
if (auto const res = e->GetEntryRaw(title_id, type); res != nullptr)
return res;
}
return nullptr;
}
std::unique_ptr<NCA> ContentProviderUnion::GetEntry(u64 title_id, ContentRecordType type) const {
for (const auto& provider : providers) {
if (provider.second == nullptr)
for (auto const e : providers) {
if (e == nullptr)
continue;
auto res = provider.second->GetEntry(title_id, type);
if (res != nullptr)
if (auto res = e->GetEntry(title_id, type); res != nullptr)
return res;
}
return nullptr;
}
std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
std::optional<u64> title_id) const {
std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type, std::optional<u64> title_id) const {
std::vector<ContentProviderEntry> out;
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
std::copy(vec.begin(), vec.end(), std::back_inserter(out));
for (auto const& e : providers) {
if (e != nullptr) {
auto const vec = e->ListEntriesFilter(title_type, record_type, title_id);
std::copy(vec.begin(), vec.end(), std::back_inserter(out));
}
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>>
ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnionSlot> origin,
std::optional<TitleType> title_type,
std::optional<ContentRecordType> record_type,
std::optional<u64> title_id) const {
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnionSlot> origin, std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type, std::optional<u64> title_id) const {
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> out;
for (const auto& provider : providers) {
if (provider.second == nullptr)
for (size_t i = 0; i < providers.size(); ++i) {
auto const& e = providers[i];
if (e == nullptr)
continue;
if (origin.has_value() && *origin != provider.first)
if (origin.has_value() && *origin != ContentProviderUnionSlot(i))
continue;
const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
std::transform(vec.begin(), vec.end(), std::back_inserter(out),
[&provider](const ContentProviderEntry& entry) {
return std::make_pair(provider.first, entry);
});
auto const vec = e->ListEntriesFilter(title_type, record_type, title_id);
std::transform(vec.begin(), vec.end(), std::back_inserter(out), [i](const ContentProviderEntry& entry) {
return std::make_pair(ContentProviderUnionSlot(i), entry);
});
}
std::sort(out.begin(), out.end());
@@ -963,40 +948,22 @@ ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnion
return out;
}
std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
u64 title_id, ContentRecordType type) const {
const auto iter =
std::find_if(providers.begin(), providers.end(), [title_id, type](const auto& provider) {
return provider.second != nullptr && provider.second->HasEntry(title_id, type);
});
if (iter == providers.end()) {
return std::nullopt;
std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(u64 title_id, ContentRecordType type) const {
for (size_t i = 0; i < providers.size(); ++i) {
auto const& e = providers[i];
if (e != nullptr && e->HasEntry(title_id, type))
return {ContentProviderUnionSlot(i)};
}
return iter->first;
return std::nullopt;
}
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
auto it = providers.find(ContentProviderUnionSlot::External);
if (it != providers.end() && it->second != nullptr) {
return dynamic_cast<const ExternalContentProvider*>(it->second);
}
return nullptr;
}
const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const {
auto it = providers.find(slot);
if (it != providers.end()) {
return it->second;
}
return nullptr;
return static_cast<const ExternalContentProvider*>(providers[size_t(ContentProviderUnionSlot::External)]);
}
ManualContentProvider::~ManualContentProvider() = default;
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
u64 title_id, VirtualFile file) {
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id, VirtualFile file) {
entries.insert_or_assign({title_type, content_type, title_id}, file);
}
@@ -1004,14 +971,13 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
u64 title_id, u32 version,
const std::string& version_string, VirtualFile file) {
if (title_type == TitleType::Update) {
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(),
[title_id, version](const ExternalUpdateEntry& entry) {
return entry.title_id == title_id && entry.version == version;
});
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(), [title_id, version](const ExternalUpdateEntry& entry) {
return entry.title_id == title_id && entry.version == version;
});
if (it != multi_version_entries.end()) {
// Update existing entry
it->files[content_type] = file;
it->files[size_t(content_type)] = file;
if (!version_string.empty()) {
it->version_string = version_string;
}
@@ -1021,7 +987,7 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
new_entry.title_id = title_id;
new_entry.version = version;
new_entry.version_string = version_string;
new_entry.files[content_type] = file;
new_entry.files[size_t(content_type)] = file;
multi_version_entries.push_back(new_entry);
}
@@ -1118,26 +1084,19 @@ std::vector<ExternalUpdateEntry> ManualContentProvider::ListUpdateVersions(u64 t
VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.version == version) {
auto it = entry.files.find(type);
if (it != entry.files.end()) {
return it->second;
}
if (auto const p = entry.files[size_t(type)])
return p;
}
}
return nullptr;
}
bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
int count = 0;
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.files.count(type) > 0) {
count++;
if (count > 1) {
return true;
}
}
}
return false;
size_t count = 0;
for (const auto& entry : multi_version_entries)
if (entry.title_id == title_id && entry.files[size_t(type)])
++count;
return count > 0;
}
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
@@ -1256,7 +1215,7 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
}
}
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
for (const auto& [title_id, nca_map] : ncas) {
for (const auto& [type_pair, nca] : nca_map) {
@@ -1277,7 +1236,7 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
version = ver_it->second;
}
version_files[{title_id, version}][content_type] = nca_file;
version_files[{title_id, version}][size_t(content_type)] = nca_file;
}
LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}",
@@ -1298,9 +1257,7 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
bool version_exists = false;
for (auto& existing : multi_version_entries) {
if (existing.title_id == title_id && existing.version == version) {
for (const auto& [content_type, _file] : files_map) {
existing.files[content_type] = _file;
}
existing.files = files_map;
if (existing.version_string.empty() && !ver_str.empty()) {
existing.version_string = ver_str;
}
@@ -1383,7 +1340,7 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
}
}
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
for (const auto& [title_id, nca_map] : ncas) {
for (const auto& [type_pair, nca] : nca_map) {
@@ -1404,7 +1361,7 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
version = ver_it->second;
}
version_files[{title_id, version}][content_type] = nca_file;
version_files[{title_id, version}][size_t(content_type)] = nca_file;
}
}
}
@@ -1422,9 +1379,7 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
bool version_exists = false;
for (auto& existing : multi_version_entries) {
if (existing.title_id == title_id && existing.version == version) {
for (const auto& [content_type, _file] : files_map) {
existing.files[content_type] = _file;
}
existing.files = files_map;
if (existing.version_string.empty() && !ver_str.empty()) {
existing.version_string = ver_str;
}
@@ -1529,28 +1484,19 @@ std::vector<ExternalUpdateEntry> ExternalContentProvider::ListUpdateVersions(u64
}
VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.version == version) {
auto it = entry.files.find(type);
if (it != entry.files.end()) {
return it->second;
}
}
}
for (const auto& entry : multi_version_entries)
if (entry.title_id == title_id && entry.version == version)
if (auto const p = entry.files[size_t(type)])
return p;
return nullptr;
}
bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
size_t count = 0;
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.files.count(type) > 0) {
count++;
if (count > 1) {
return true;
}
}
}
return false;
for (const auto& entry : multi_version_entries)
if (entry.title_id == title_id && entry.files[size_t(type)])
++count;
return count > 1;
}
} // namespace FileSys
+16 -17
View File
@@ -11,19 +11,19 @@
#include <memory>
#include <string>
#include <vector>
#include <ankerl/unordered_dense.h>
#include <boost/container/flat_map.hpp>
#include "common/common_types.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/nca_metadata.h"
namespace FileSys {
class ExternalContentProvider;
class CNMT;
class ExternalContentProvider;
class CNMT;
class NCA;
class NSP;
class XCI;
enum class ContentRecordType : u8;
enum class NCAContentType : u8;
enum class TitleType : u8;
@@ -56,7 +56,7 @@ struct ExternalUpdateEntry {
u64 title_id;
u32 version;
std::string version_string;
std::map<ContentRecordType, VirtualFile> files;
std::array<VirtualFile, size_t(ContentRecordType::Count)> files;
};
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
@@ -207,11 +207,11 @@ private:
ContentProviderParsingFunction parser;
// maps tid -> NcaID of meta
std::map<u64, NcaID> meta_id;
ankerl::unordered_dense::map<u64, NcaID> meta_id;
// maps tid -> meta
std::map<u64, CNMT> meta;
ankerl::unordered_dense::map<u64, CNMT> meta;
// maps tid -> meta for CNMT in yuzu_meta
std::map<u64, CNMT> yuzu_meta;
ankerl::unordered_dense::map<u64, CNMT> yuzu_meta;
};
enum class ContentProviderUnionSlot {
@@ -220,6 +220,7 @@ enum class ContentProviderUnionSlot {
SDMC, ///< SD Card
FrontendManual, ///< Frontend-defined game list or similar
External, ///< External content from NSP/XCI files in configured directories
Count,
};
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
@@ -228,8 +229,6 @@ public:
~ContentProviderUnion() override;
void SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider);
void ClearSlot(ContentProviderUnionSlot slot);
void Refresh() override;
bool HasEntry(u64 title_id, ContentRecordType type) const override;
std::optional<u32> GetEntryVersion(u64 title_id) const override;
@@ -241,18 +240,18 @@ public:
std::optional<u64> title_id) const override;
const ExternalContentProvider* GetExternalProvider() const;
const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const;
[[nodiscard]] inline const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const {
return providers[size_t(slot)];
}
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
std::optional<ContentProviderUnionSlot> origin = {},
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
std::optional<u64> title_id = {}) const;
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id,
ContentRecordType type) const;
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id, ContentRecordType type) const;
private:
std::map<ContentProviderUnionSlot, ContentProvider*> providers;
std::array<ContentProvider*, size_t(ContentProviderUnionSlot::Count)> providers;
};
class ManualContentProvider : public ContentProvider {
@@ -312,8 +311,8 @@ private:
void ProcessXCI(const VirtualFile& file);
std::vector<VirtualDir> load_dirs;
std::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
std::map<u64, u32> versions;
ankerl::unordered_dense::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
ankerl::unordered_dense::map<u64, u32> versions;
std::vector<ExternalUpdateEntry> multi_version_entries;
};
+7 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -52,7 +52,9 @@ constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(OpenMode mode) {
} // Anonymous namespace
RealVfsFilesystem::RealVfsFilesystem() : VfsFilesystem(nullptr) {}
RealVfsFilesystem::~RealVfsFilesystem() = default;
RealVfsFilesystem::~RealVfsFilesystem() {
in_dtor = true;
}
std::string RealVfsFilesystem::GetName() const {
return "Real";
@@ -224,6 +226,9 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
}
void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
if (in_dtor)
return;
std::scoped_lock lk{list_lock};
// Remove from list.
+5
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -52,6 +55,8 @@ private:
ReferenceListType closed_references;
std::mutex list_lock;
size_t num_open_files{};
// TODO: Workaround for improper dtor() ordering on clang + FreeBSD
bool in_dtor = false;
private:
friend class RealVfsFile;

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