Compare commits

..

39 Commits

Author SHA1 Message Date
Zephyron 57cf5a0daf build: bump VulkanHeaders minimum version
- Update required VulkanHeaders from 1.4.307 to 1.4.313
- Ensures compatibility with newer Vulkan development packages

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:59:49 +10:00
Zephyron 020492f1fa chore: update vcpkg baseline
- Update vcpkg builtin-baseline from c82f74 to bc99451
- Provides newer Boost libraries with io_context support
- Ensures consistent Boost ASIO compatibility across platforms

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:58:58 +10:00
Zephyron 21ca0b3119 fix: update deprecated boost::asio::io_service to io_context
Updates UDP client and related test files to use boost::asio::io_context
instead of the deprecated io_service. This change is required for compatibility
with newer versions of Boost ASIO, which has renamed the class.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:57:27 +10:00
Zephyron 58401f5b39 fix: remove invalid WSAEBUSY Windows socket error code
- Fixes Windows compilation error by removing the WSAEBUSY case in TranslateNativeError.
- This error code does not exist in the Windows Sockets API as documented in the Microsoft documentation, but was incorrectly included in the Windows-specific error handling code.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:38:35 +10:00
Zephyron 1ad69f3545 Update submodules: SDL, vcpkg, and Vulkan-Headers
- Update SDL to fix pipewire-related compile error
  - Removes need to hardcode -DSDL_PIPEWIRE=OFF in toolchain
- Update vcpkg to latest version
- Update Vulkan-Headers to latest version

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:27:21 +10:00
Zephyron 48eed78d1a socket: Implement missing errno values and improve network error handling
Add support for missing errno values needed by TOTK:
- Add BUSY (16) for "Device or resource busy" errors
- Add NOTSOCK (88) for "Socket operation on non-socket" errors

Improvements:
- Update TranslateNativeError on both Windows and Unix to handle new error codes
- Change socket error logging for NOTSOCK from WARNING to DEBUG level
- Fix formatting in Unix errno translation code
- Update shader storage buffer tracking range to accommodate TOTK buffers
- Add hex format to storage buffer logging for easier comparison with bias range
- Change storage buffer tracking log level from WARNING to DEBUG

These changes help prevent error messages in games
that use network features not fully implemented in the emulator yet.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-25 14:38:28 +10:00
Zephyron 5f962dd1c6 android: Update Vulkan Validation Layer to 1.4.309.0
Updates the Android Vulkan Validation Layer (VVL) from version 1.4.304.1
to 1.4.309.0. This ensures compatibility with the latest Vulkan specification
and provides improved validation capabilities for the Android build.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 17:12:54 +10:00
Zephyron 25abfe36a3 android: Update build configuration and package identifiers
Updates the Android build configuration with several important changes:

- Change application ID from com.antutu.ABenchMark to org.citron.citron_emu
- Upgrade CMake version from 3.31.6 to 4.0.1
- Update Android Gradle plugin from 8.9.0 to 8.9.2
- Add CMAKE_POLICY_VERSION_MINIMUM=3.5 to CMake arguments
- Keep Kotlin version at 1.9.20

These changes align the Android package identifier with the Citron project
and update build tool versions to ensure compatibility with modern Android
development requirements.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:58:55 +10:00
Zephyron 2f57a35d2d video_core/vulkan: Fix callback variable shadowing in async shader compilation
Resolves a variable shadowing issue in AsyncCompileShader where the callback
lambda parameter was shadowing the outer callback variable. This was causing
compilation warnings/errors in Android Studio. The fix:

- Renames the outer callback variable to 'outer_callback'
- Renames the inner lambda callback parameters to 'inner_callback'
- Maintains consistent naming across all error handling paths

This change improves code clarity and eliminates compiler warnings while
maintaining the same functionality for async shader compilation.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:57:57 +10:00
Zephyron 66bdd6ed27 video_core: Add fallback handling for failed storage buffer lookups
Implements a more robust error handling approach when storage buffer lookups
fail in the buffer cache. Instead of returning a null binding, the code now:

- Provides a fallback buffer with safe default values
- Implements warning rate limiting to prevent log spam
- Tracks warning counts per cbuf_index
- Logs detailed debug information periodically

This change helps prevent potential crashes when storage buffer lookups fail
while still maintaining visibility into the issue through strategic logging.

The fallback mechanism uses a safe static address and a reasonable buffer
size (16KB) to handle cases where the normal GPU to CPU address translation
fails.

Also updates copyright headers to include citron Emulator Project.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:56:59 +10:00
Zephyron ff9c61e7c7 video_core: Improve texture cache memory management to prevent leaks
Implement several improvements to the texture cache memory management system
to address memory leaks that occur in memory-intensive games like TOTK
(Title ID 0100F2C0115B6000). These changes prevent the gradual memory
increase that eventually leads to crashes or undefined behavior.

Key improvements:
- Enhance garbage collection with more aggressive cleanup thresholds
- Add emergency resource cleanup for persistent high memory usage
- Improve DeleteImage to ensure proper resource deallocation
- Make DelayedDestructionRing thread-safe with proper mutex protection
- Track consecutive high-memory frames to detect potential leaks
- Add emergency cleanup mechanism for extreme memory pressure situations
- Use proper type casting in std::max to fix compilation errors

This should significantly improve stability during extended gameplay
sessions with memory-intensive titles.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-20 17:39:14 +10:00
Zephyron e72d695115 feat(services): Implement nn::socket, nn::nifm, and nn::nim networking services
Add Nintendo Switch network service implementations to support modders
working with network functionality in their game modifications:

- Add nn::socket utilities including InetAton and Connect functions
- Implement sockaddr/in_addr structures matching official Nintendo APIs
- Add nn::nifm networking interface services with IsNetworkAvailable and SubmitNetworkRequest
- Implement nn::nim network installation management services
- Fix BSD socket implementation to properly handle proxy packets
- Add Service_BSD log category for better debugging

These changes provide crucial networking API support for modders like
MaxLastBreath and projects like NX Optimizer (https://www.nxoptimizer.com/)
that need to hook into Nintendo's network services for code injection mods.
This implementation follows the official documentation at SwitchBrew and
enables proper network connectivity in modded games.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-20 15:35:25 +10:00
Zephyron 0cdd546152 externals: Update Vulkan dependencies to latest versions
Update Vulkan-Headers, Vulkan-Utility-Libraries, VulkanMemoryAllocator, and vcpkg submodules to their latest versions to ensure compatibility with newer Vulkan features and improve rendering performance.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-19 13:26:01 +10:00
Zephyron 3205c9b691 fix(vulkan): address compiler warnings for Linux
- Fix variable shadowing in ShaderManager constructor by renaming parameter
- Remove unused variables in vk_texture_manager.cpp to avoid warnings
- Fix int conversion warning in syscall return value

These changes fix build errors when using certain optimized compile flags for Linux.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-18 14:26:04 +10:00
Zephyron f1e169e060 fix: correct implementation of present interval 0 for unlocked FPS
Fixes issues in commit bbd3253169 that could cause
crashes and deadlocks. The feature now works as intended, allowing games using
present interval 0 to run with truly unlocked FPS.

This ensures proper functionality of dynamic framerate mods like UltraCam
by MaxLastBreath (https://www.nxoptimizer.com/) without stability problems.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-18 10:11:16 +10:00
Zephyron 278486d059 feat: add CPU clock rate slider to settings
Implement a slider in the CPU settings tab to adjust the BASE_CLOCK_RATE
up to 1,785 MHz (Switch's official maximum clock rate). Default remains
at 1,020 MHz.

This change:
- Adds UI slider and spinbox to configure_cpu.ui with range 500-1785 MHz
- Makes BASE_CLOCK_RATE dynamic by reading from settings
- Modifies WallClock to handle dynamic clock rate changes
- Updates APM controller to properly set the clock rate
- Changes clock rate settings category from Core to CPU

The user can now easily adjust the CPU clock rate to improve performance
or manage thermals and power consumption.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-16 22:02:10 +10:00
Zephyron bbd3253169 feat: add option to respect present interval 0 as unlocked FPS
When enabled, this feature allows games using present interval 0 to run with
truly unlocked FPS, matching actual hardware behavior more accurately.

Previously, Citron would cap present interval 0 at 120FPS to conserve battery,
but this prevented proper functionality of dynamic framerate mods like UltraCam
by MaxLastBreath (https://www.nxoptimizer.com/).

The setting is disabled by default to maintain the current behavior for most users.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-16 19:28:15 +10:00
Zephyron a1f3414bde service/sockets: Implement network services for new firmware versions
This commit implements various network services required for newer firmware
versions. Key changes include:

- Add bsd:nu service for firmware 15.0.0+ with proper event handling
- Add bsdcfg implementation with complete interface declarations
- Add dns:priv and ethc (c/i) services
- Register all new services in the service manager
- Extend BSD implementation with additional socket operations
- Remove room_network instance variable in favor of system.GetRoomNetwork()
- Fix kernel event creation by using ServiceContext in all appropriate places
- Update build system to include new source files

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-15 17:19:53 +10:00
Zephyron 175a427c27 refactor(vulkan): remove depth buffer workarounds and excessive logging
- Remove special handling for reversed depth scenarios that were added for Civilization 7
- Remove excessive logging in Vulkan renderer
- Update Discord client ID
- Update Vulkan-related external dependencies

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-14 22:02:29 +10:00
Zephyron 18def48dfe feat(video_core): Fix Linux compilation issues in Hybrid Memory Manager
- Added missing <thread> header for std::thread usage
- Added <fcntl.h> for O_CLOEXEC and O_NONBLOCK definitions
- Fixed struct initialization order in uffdio_copy to match declaration order

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 17:50:39 +10:00
Zephyron a4088f3a1e Add Windows support to Hybrid Memory Manager
This commit adds Windows-specific implementation of the fault-managed memory
system, providing similar functionality to the existing Linux/Android implementation.

Key changes:
- Added Windows-specific memory management using VirtualAlloc/VirtualFree
- Implemented Windows vectored exception handler for page fault handling
- Added proper memory protection and page fault handling on Windows
- Updated memory snapshot functionality to work on Windows
- Added proper cleanup of Windows-specific resources
- Fixed type conversion issues in memory management code
- Added proper error handling for Windows memory operations
- Fixed VRAM Memory Layout Mode to allow up to 12Gb

The implementation uses Windows-specific APIs:
- VirtualAlloc/VirtualFree for memory management
- AddVectoredExceptionHandler for page fault handling
- VirtualProtect for memory protection management

This change maintains feature parity with the Linux/Android implementation
while using Windows-native APIs for better performance and reliability.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 16:15:51 +10:00
Zephyron b66b3ca639 nvn(fix): Optimize shader performance by enhancing NVN bias settings
Improve GPU storage buffer detection and memory access patterns:
- Expand NVN bias address range (0x100-0x800 vs 0x110-0x610)
- Increase alignment from 16 to 32 bytes for optimal memory access
- Raise default alignment from 8 to 16 bytes for non-biased addresses
- Refactor bias handling code for better readability
- Add detailed performance-related comments

These changes help identify more storage buffers within shaders and
ensure memory accesses are better aligned, which improves overall
shader compilation and execution performance.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 15:14:14 +10:00
Zephyron 3a1c178711 Revert "nvn: Optimize shader performance by enhancing NVN bias settings"
This reverts commit 19febba866.
2025-04-12 15:12:19 +10:00
Zephyron 964bbf489a feat(video_core): Implement HybridMemory for advanced Vulkan memory management
Adds a new cross-platform memory management system with enhanced capabilities:
- Fault-managed memory allocation for Linux/Android platforms
- Memory snapshot and differential snapshot support
- Predictive memory reuse tracking for optimized access patterns
- Vulkan compute buffer integration
- User-configurable settings for enabling features

The system integrates with the existing Vulkan renderer to provide more
efficient memory handling, especially for compute-intensive workloads.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-10 20:22:00 +10:00
Zephyron 19febba866 nvn: Optimize shader performance by enhancing NVN bias settings
Improve GPU storage buffer detection and memory access patterns:
- Expand NVN bias address range (0x100-0x800 vs 0x110-0x610)
- Increase alignment from 16 to 32 bytes for optimal memory access
- Raise default alignment from 8 to 16 bytes for non-biased addresses
- Refactor bias handling code for better readability
- Add detailed performance-related comments

These changes help identify more storage buffers within shaders and
ensure memory accesses are better aligned, which improves overall
shader compilation and execution performance.

Update Vulkan dependencies to their latest versions.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-05 00:46:51 +10:00
Zephyron 0dac3c1dbd renderer/friend: Improve reversed depth handling and Friend service
This commit makes two significant improvements:

1. Vulkan renderer:
   - Detect and properly handle reversed depth buffers (clear_depth < 0.5)
   - Force depth write enable when needed with reversed depth
   - Use GREATER_OR_EQUAL comparison for reversed depth scenarios
   - Fix transparency issues in games like Civilization 7 by adjusting blend factors
   - Add detailed logging for depth buffer operations

2. Friend service:
   - Implement previously stubbed functions including EnsureFriendListAvailable
     and EnsureBlockedUserListAvailable
   - Add proper event signaling to prevent games from hanging
   - Implement Cancel function for improved compatibility
   - Update copyright notice for the Citron project

These changes improve compatibility with modern games using reversed depth
buffers and prevent hangs in titles that rely on Friend service functionality.

Co-authored-by: m33ts4k0z <m33ts4k0z@citron-emu.org>
Co-committed-by: m33ts4k0z <m33ts4k0z@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-03 16:17:55 +10:00
Zephyron 5d952717ff video_core: Enhance Vulkan shader compilation with async threading system
Implement a robust asynchronous shader compilation system inspired by commit
1fd5fefcb1. This enhancement provides:

- True multi-threaded shader compilation with atomic status tracking
- Persistent disk caching for faster shader loading
- Command queue system for background processing
- Integration with Citron's scheduler for better resource management
- Parallel shader loading to reduce startup times
- Improved error handling and recovery mechanisms

These changes significantly reduce shader compilation stuttering and improve
overall performance when using asynchronous shaders. The implementation
maintains compatibility with Citron's existing architecture while adding
more robust threading capabilities.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-31 21:01:01 +10:00
Zephyron b25c7653e6 feat(vulkan): implement enhanced texture and shader management
This commit adds improved Vulkan functionality to the Citron emulator:

- Add thread-safe texture management with automatic error recovery
- Implement shader caching with validation support
- Add robust error handling for Vulkan operations
- Implement platform-specific initialization for Windows, Linux, and Android

These enhancements improve stability when handling texture loading errors
and provide better recovery mechanisms for Vulkan failures.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-28 18:25:36 +10:00
Zephyron edfb500ee7 build: fix linux compilation
- Removes unnecessary \ from Copyright Line Causing Linux Builds To Fail

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-28 14:54:54 +10:00
Zephyron ebfc9d8347 memory: Implement enhanced memory management system
Add a flexible memory region management system that provides:
- Memory region type classification (System, Graphics, IO, Binary)
- Memory region permission management (executable, writable)
- Binary base address randomization for ASLR
- Dynamic memory mapping capabilities

Credit: boss.smc@citron-emu.org
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-27 23:25:24 +10:00
Zephyron 1fd5fefcb1 WIP: Enhance shader compilation performance and control
This commit adds new settings and optimizations for shader compilation:

- Add new settings:
  - use_enhanced_shader_building: Enable enhanced shader compilation
  - shader_compilation_priority: Control shader compilation priority

- Improve shader compilation performance:
  - Optimize worker thread allocation based on CPU cores
  - Add smarter async shader compilation heuristics
  - Prioritize vertex and fragment shader compilation
  - Add performance tracking and logging

- Add performance monitoring:
  - Track shader compilation times
  - Log slow shader compilations
  - Monitor async shader compilation statistics

This is a work in progress commit. Further optimizations and refinements
will be needed based on testing and feedback.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-27 20:56:23 +10:00
Zephyron 55dc3f8ec1 Update external dependency URLs and versions
- Change SDL2 bundled version from 2.32.0 to 2.28.2
- Downgrade clang-format version from 18 to 15
- Replace citron-emu.org URLs with GitHub mirror URLs:
  - Update clang-format download URL to use yuzu-mirror repository
  - Update package base URL for external dependencies

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-25 20:30:33 +10:00
Zephyron 7edbccbdc9 Revert "Update submodule URLs from yuzu-mirror to Citron repositories"
This reverts commit d1b7aebe8c.
2025-03-25 17:45:50 +10:00
Zephyron e06526cbbc Update Vulkan-related dependencies and vcpkg
- Update Vulkan-Headers from cacef303 to 78c35974
- Update VulkanMemoryAllocator from c788c521 to 29b35ea4
- Update vcpkg from e40d24cb to a7d06b3a

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-21 17:23:02 +10:00
Zephyron 0448d8146f Update controller udev rules with consistent vendor ID formatting
- Add copyright notice for citron Emulator Project
- Standardize Nintendo vendor ID format to uppercase (057E) in Bluetooth controller rules
- Maintain same permissions and access settings for all controllers

REF: 6ead429195

Reviewed-on: http://vub63vv26q6v27xzv2dtcd25xumubshogm67yrpaz2rculqxs7jlfqad.onion/torzu-emu/torzu/pulls/106
Co-authored-by: deftdawg <deftdawg@noreply.localhost>
Co-committed-by: deftdawg <deftdawg@noreply.localhost>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-21 17:13:06 +10:00
Zephyron 98c515871e ui: Disable the Kiosk (Quest) Mode configuration option
This commit disables the "Kiosk (Quest) Mode" checkbox in the debug configuration
UI by setting it to non-interactive and adding a tooltip indicating that the
feature has been disabled.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-20 18:49:00 +10:00
Zephyron 278ac75a37 ui: Disable the Auto-Stub configuration option
This commit disables the "Enable Auto-Stub" checkbox in the debug configuration
UI by setting it to non-interactive and adding a tooltip indicating that the
feature has been disabled.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-17 14:02:15 +10:00
Zephyron ec402a0510 feat(build): Add host system detection for Android cross-compilation
- Modify CMakeLists.txt to detect whether the host system is Windows or Linux
- Set VCPKG_HOST_TRIPLET dynamically to either "x64-windows" or "x64-linux" based on CMAKE_HOST_SYSTEM_NAME
- Previously, the host triplet was hardcoded to "x64-windows", which prevented proper building on Linux hosts

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-17 12:26:35 +10:00
Zephyron 8cb6e6d5d4 Revert "Android: Implement TLB optimization to prevent deadlocks and improve performance"
This reverts commit 21594b73aa.
2025-03-17 12:20:38 +10:00
79 changed files with 3518 additions and 341 deletions

12
.gitmodules vendored
View File

@ -9,22 +9,22 @@
url = https://github.com/mozilla/cubeb.git url = https://github.com/mozilla/cubeb.git
[submodule "dynarmic"] [submodule "dynarmic"]
path = externals/dynarmic path = externals/dynarmic
url = https://git.citron-emu.org/Citron/dynarmic.git url = https://github.com/yuzu-mirror/dynarmic.git
[submodule "libusb"] [submodule "libusb"]
path = externals/libusb/libusb path = externals/libusb/libusb
url = https://github.com/libusb/libusb.git url = https://github.com/libusb/libusb.git
[submodule "discord-rpc"] [submodule "discord-rpc"]
path = externals/discord-rpc path = externals/discord-rpc
url = https://git.citron-emu.org/Citron/discord-rpc.git url = https://github.com/yuzu-mirror/discord-rpc.git
[submodule "Vulkan-Headers"] [submodule "Vulkan-Headers"]
path = externals/Vulkan-Headers path = externals/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "sirit"] [submodule "sirit"]
path = externals/sirit path = externals/sirit
url = https://git.citron-emu.org/Citron/sirit.git url = https://github.com/yuzu-mirror/sirit.git
[submodule "mbedtls"] [submodule "mbedtls"]
path = externals/mbedtls path = externals/mbedtls
url = https://git.citron-emu.org/Citron/mbedtls.git url = https://github.com/yuzu-mirror/mbedtls.git
[submodule "xbyak"] [submodule "xbyak"]
path = externals/xbyak path = externals/xbyak
url = https://github.com/herumi/xbyak.git url = https://github.com/herumi/xbyak.git
@ -57,13 +57,13 @@
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "breakpad"] [submodule "breakpad"]
path = externals/breakpad path = externals/breakpad
url = https://git.citron-emu.org/Citron/breakpad.git url = https://github.com/yuzu-mirror/breakpad.git
[submodule "simpleini"] [submodule "simpleini"]
path = externals/simpleini path = externals/simpleini
url = https://github.com/brofield/simpleini.git url = https://github.com/brofield/simpleini.git
[submodule "oaknut"] [submodule "oaknut"]
path = externals/oaknut path = externals/oaknut
url = https://git.citron-emu.org/Citron/oaknut.git url = https://github.com/yuzu-mirror/oaknut
[submodule "Vulkan-Utility-Libraries"] [submodule "Vulkan-Utility-Libraries"]
path = externals/Vulkan-Utility-Libraries path = externals/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git

View File

@ -137,7 +137,7 @@ endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL}) option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL) if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
set(vvl_version "1.4.304.1") set(vvl_version "1.4.309.0")
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip") set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/") set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so") set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
@ -169,13 +169,23 @@ if (CITRON_USE_BUNDLED_VCPKG)
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
set(VCPKG_TARGET_TRIPLET "arm64-android") set(VCPKG_TARGET_TRIPLET "arm64-android")
set(VCPKG_HOST_TRIPLET "x64-windows") # Detect host system (Windows or Linux)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(VCPKG_HOST_TRIPLET "x64-windows")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
# this is to avoid CMake using the host pkg-config to find the host # this is to avoid CMake using the host pkg-config to find the host
# libraries when building for Android targets # libraries when building for Android targets
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE) set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
set(VCPKG_TARGET_TRIPLET "x64-android") set(VCPKG_TARGET_TRIPLET "x64-android")
set(VCPKG_HOST_TRIPLET "x64-windows") # Detect host system (Windows or Linux)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(VCPKG_HOST_TRIPLET "x64-windows")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE) set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
else() else()
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}") message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
@ -374,7 +384,7 @@ find_package(ZLIB REQUIRED)
find_package(zstd REQUIRED) find_package(zstd REQUIRED)
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS) if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
find_package(VulkanHeaders 1.4.307 REQUIRED) find_package(VulkanHeaders 1.4.313 REQUIRED)
endif() endif()
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES) if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
@ -437,7 +447,7 @@ if (ENABLE_SDL2)
if (CITRON_USE_BUNDLED_SDL2) if (CITRON_USE_BUNDLED_SDL2)
# Detect toolchain and platform # Detect toolchain and platform
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64) if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
set(SDL2_VER "SDL2-2.32.0") set(SDL2_VER "SDL2-2.28.2")
else() else()
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.") message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
endif() endif()
@ -584,7 +594,7 @@ endif()
# against all the src files. This should be used before making a pull request. # against all the src files. This should be used before making a pull request.
# ======================================================================= # =======================================================================
set(CLANG_FORMAT_POSTFIX "-18") set(CLANG_FORMAT_POSTFIX "-15")
find_program(CLANG_FORMAT find_program(CLANG_FORMAT
NAMES clang-format${CLANG_FORMAT_POSTFIX} NAMES clang-format${CLANG_FORMAT_POSTFIX}
clang-format clang-format
@ -595,7 +605,7 @@ if (NOT CLANG_FORMAT)
message(STATUS "Clang format not found! Downloading...") message(STATUS "Clang format not found! Downloading...")
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe") set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
file(DOWNLOAD file(DOWNLOAD
https://git.citron-emu.org/Citron/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe https://github.com/yuzu-mirror/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
"${CLANG_FORMAT}" SHOW_PROGRESS "${CLANG_FORMAT}" SHOW_PROGRESS
STATUS DOWNLOAD_SUCCESS) STATUS DOWNLOAD_SUCCESS)
if (NOT DOWNLOAD_SUCCESS EQUAL 0) if (NOT DOWNLOAD_SUCCESS EQUAL 0)

View File

@ -8,7 +8,7 @@
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR}) set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
function(download_bundled_external remote_path lib_name prefix_var) function(download_bundled_external remote_path lib_name prefix_var)
set(package_base_url "https://git.citron-emu.org/Citron/") set(package_base_url "https://github.com/yuzu-mirror/")
set(package_repo "no_platform") set(package_repo "no_platform")
set(package_extension "no_platform") set(package_extension "no_platform")
if (WIN32) if (WIN32)

View File

@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# Allow systemd-logind to manage user access to hidraw with this file # Allow systemd-logind to manage user access to hidraw with this file
@ -7,13 +8,13 @@
# Switch Pro Controller (USB/Bluetooth) # Switch Pro Controller (USB/Bluetooth)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", KERNELS=="*057E:2009*", MODE="0660", TAG+="uaccess"
# Joy-Con L (Bluetooth) # Joy-Con L (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", KERNELS=="*057E:2006*", MODE="0660", TAG+="uaccess"
# Joy-Con R (Bluetooth) # Joy-Con R (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", KERNELS=="*057E:2007*", MODE="0660", TAG+="uaccess"
# Joy-Con Charging Grip (USB) # Joy-Con Charging Grip (USB)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"

2
externals/SDL vendored

@ -1 +1 @@
Subproject commit cc016b0046d563287f0aa9f09b958b5e70d43696 Subproject commit 2359383fc187386204c3bb22de89655a494cd128

@ -1 +1 @@
Subproject commit cacef3039d277c448c89336290ec3937270b0996 Subproject commit e2e53a724677f6eba8ff0ce1ccb64ee321785cbd

@ -1 +1 @@
Subproject commit bc3a4d9fd9b46729651a3cec4f5226f6272b8684 Subproject commit 4e246c56ec5afb5ad66b9b04374d39ac04675c8e

@ -1 +1 @@
Subproject commit c788c52156f3ef7bc7ab769cb03c110a53ac8fcb Subproject commit 539c0a8d8e3733c9f25ea9a184c85c77504f1653

2
externals/vcpkg vendored

@ -1 +1 @@
Subproject commit e40d24cb149dd138e7c11d490834fa2c81298b32 Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b

View File

@ -55,7 +55,7 @@ android {
defaultConfig { defaultConfig {
// TODO If this is ever modified, change application_id in strings.xml // TODO If this is ever modified, change application_id in strings.xml
applicationId = "com.antutu.ABenchMark" applicationId = "org.citron.citron_emu"
minSdk = 30 minSdk = 30
//noinspection EditedTargetSdkVersion //noinspection EditedTargetSdkVersion
targetSdk = 35 targetSdk = 35
@ -161,7 +161,7 @@ android {
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version = "3.31.6" version = "4.0.1"
path = file("../../../CMakeLists.txt") path = file("../../../CMakeLists.txt")
} }
} }
@ -179,7 +179,8 @@ android {
"-DCITRON_USE_BUNDLED_FFMPEG=ON", "-DCITRON_USE_BUNDLED_FFMPEG=ON",
"-DCITRON_ENABLE_LTO=ON", "-DCITRON_ENABLE_LTO=ON",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
) )
abiFilters("arm64-v8a") // , "x86_64") abiFilters("arm64-v8a") // , "x86_64")

View File

@ -18,6 +18,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"), RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"), RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"), RENDERER_DEBUG("debug"),
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
PICTURE_IN_PICTURE("picture_in_picture"), PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"), USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"), BLACK_BACKGROUNDS("black_backgrounds"),

View File

@ -4,8 +4,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.9.0" apply false id("com.android.application") version "8.9.2" apply false
id("com.android.library") version "8.9.0" apply false id("com.android.library") version "8.9.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false id("org.jetbrains.kotlin.android") version "1.9.20" apply false
} }

View File

@ -1,10 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <memory> #include <memory>
#include <typeinfo> #include <typeinfo>
#include <vector> #include <vector>
#include <QComboBox> #include <QComboBox>
#include <QSpinBox>
#include <QSlider>
#include "common/common_types.h" #include "common/common_types.h"
#include "common/settings.h" #include "common/settings.h"
#include "common/settings_enums.h" #include "common/settings_enums.h"
@ -38,7 +41,22 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
ConfigureCpu::~ConfigureCpu() = default; ConfigureCpu::~ConfigureCpu() = default;
void ConfigureCpu::SetConfiguration() {} void ConfigureCpu::SetConfiguration() {
// Set clock rate values from settings
const u32 clock_rate_mhz = Settings::values.cpu_clock_rate.GetValue() / 1'000'000;
ui->clock_rate_slider->setValue(static_cast<int>(clock_rate_mhz));
ui->clock_rate_spinbox->setValue(static_cast<int>(clock_rate_mhz));
// Connect slider and spinbox signals to keep them in sync
connect(ui->clock_rate_slider, &QSlider::valueChanged, this, [this](int value) {
ui->clock_rate_spinbox->setValue(value);
});
connect(ui->clock_rate_spinbox, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) {
ui->clock_rate_slider->setValue(value);
});
}
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) { void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
auto* accuracy_layout = ui->widget_accuracy->layout(); auto* accuracy_layout = ui->widget_accuracy->layout();
auto* backend_layout = ui->widget_backend->layout(); auto* backend_layout = ui->widget_backend->layout();
@ -99,6 +117,9 @@ void ConfigureCpu::ApplyConfiguration() {
for (const auto& apply_func : apply_funcs) { for (const auto& apply_func : apply_funcs) {
apply_func(is_powered_on); apply_func(is_powered_on);
} }
// Save the clock rate setting (convert from MHz to Hz)
Settings::values.cpu_clock_rate = static_cast<u32>(ui->clock_rate_spinbox->value()) * 1'000'000;
} }
void ConfigureCpu::changeEvent(QEvent* event) { void ConfigureCpu::changeEvent(QEvent* event) {

View File

@ -126,6 +126,67 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="clock_rate_group">
<property name="title">
<string>CPU Clock Rate</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_clock_description">
<property name="text">
<string>CPU clock rate in MHz. Setting a higher clock rate will improve performance but may cause system instability. Default is 1020 MHz.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QSlider" name="clock_rate_slider">
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>1785</number>
</property>
<property name="value">
<number>1020</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="clock_rate_spinbox">
<property name="suffix">
<string> MHz</string>
</property>
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>1785</number>
</property>
<property name="value">
<number>1020</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -423,6 +423,12 @@
</item> </item>
<item row="6" column="0"> <item row="6" column="0">
<widget class="QCheckBox" name="use_auto_stub"> <widget class="QCheckBox" name="use_auto_stub">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>This feature has been disabled.</string>
</property>
<property name="text"> <property name="text">
<string>Enable Auto-Stub**</string> <string>Enable Auto-Stub**</string>
</property> </property>
@ -430,6 +436,12 @@
</item> </item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="quest_flag"> <widget class="QCheckBox" name="quest_flag">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>This feature has been disabled.</string>
</property>
<property name="text"> <property name="text">
<string>Kiosk (Quest) Mode</string> <string>Kiosk (Quest) Mode</string>
</property> </property>

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/configuration/shared_translation.h" #include "citron/configuration/shared_translation.h"
@ -146,6 +147,11 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT( INSERT(
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"), Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled.")); tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled."));
INSERT(
Settings, respect_present_interval_zero, tr("Respect present interval 0 for unlocked FPS"),
tr("When enabled, present interval 0 will be used for games requesting unlocked FPS.\n"
"This matches console behavior more closely, but may cause higher battery usage and frame pacing issues.\n"
"When disabled (default), present interval 0 is capped at 120FPS to conserve battery."));
INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"), INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"),
tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for " tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for "
"decoding, or perform no decoding at all (black screen on videos).\n" "decoding, or perform no decoding at all (black screen on videos).\n"

View File

@ -36,7 +36,7 @@ system_
DiscordEventHandlers handlers {}; DiscordEventHandlers handlers {};
// The number is the client ID for citron, it's used for images and the // The number is the client ID for citron, it's used for images and the
// application name // application name
Discord_Initialize("1322413013248118888", & handlers, 1, nullptr); Discord_Initialize("1361252452329848892", & handlers, 1, nullptr);
} }
DiscordImpl::~DiscordImpl() { DiscordImpl::~DiscordImpl() {

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -89,6 +90,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Service, BGTC) \ SUB(Service, BGTC) \
SUB(Service, BTDRV) \ SUB(Service, BTDRV) \
SUB(Service, BTM) \ SUB(Service, BTM) \
SUB(Service, BSD) \
SUB(Service, Capture) \ SUB(Service, Capture) \
SUB(Service, ERPT) \ SUB(Service, ERPT) \
SUB(Service, ETicket) \ SUB(Service, ETicket) \

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -57,6 +58,7 @@ enum class Class : u8 {
Service_BPC, ///< The BPC service Service_BPC, ///< The BPC service
Service_BTDRV, ///< The Bluetooth driver service Service_BTDRV, ///< The Bluetooth driver service
Service_BTM, ///< The BTM service Service_BTM, ///< The BTM service
Service_BSD, ///< The BSD sockets service
Service_Capture, ///< The capture service Service_Capture, ///< The capture service
Service_ERPT, ///< The error reporting service Service_ERPT, ///< The error reporting service
Service_ETicket, ///< The ETicket service Service_ETicket, ///< The ETicket service

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -198,6 +199,7 @@ struct Values {
MemoryLayout::Memory_12Gb, MemoryLayout::Memory_12Gb,
"memory_layout_mode", "memory_layout_mode",
Category::Core}; Category::Core};
SwitchableSetting<u32> cpu_clock_rate{linkage, 1'020'000'000, "cpu_clock_rate", Category::Cpu};
SwitchableSetting<bool> use_speed_limit{ SwitchableSetting<bool> use_speed_limit{
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true}; linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
SwitchableSetting<u16, true> speed_limit{linkage, SwitchableSetting<u16, true> speed_limit{linkage,
@ -210,6 +212,11 @@ struct Values {
true, true,
true, true,
&use_speed_limit}; &use_speed_limit};
SwitchableSetting<bool> use_nce{linkage, true, "Use Native Code Execution", Category::Core};
// Memory
SwitchableSetting<bool> use_gpu_memory_manager{linkage, false, "Use GPU Memory Manager", Category::Core};
SwitchableSetting<bool> enable_memory_snapshots{linkage, false, "Enable Memory Snapshots", Category::Core};
// Cpu // Cpu
SwitchableSetting<CpuBackend, true> cpu_backend{linkage, SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
@ -278,6 +285,8 @@ struct Values {
Category::Renderer}; Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{ SwitchableSetting<bool> use_asynchronous_gpu_emulation{
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer}; linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
SwitchableSetting<bool> respect_present_interval_zero{
linkage, false, "respect_present_interval_zero", Category::Renderer};
SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage, SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage,
#ifdef ANDROID #ifdef ANDROID
AstcDecodeMode::Cpu, AstcDecodeMode::Cpu,
@ -618,11 +627,21 @@ struct Values {
// Add-Ons // Add-Ons
std::map<u64, std::vector<std::string>> disabled_addons; std::map<u64, std::vector<std::string>> disabled_addons;
// Renderer Advanced Settings
SwitchableSetting<bool> use_enhanced_shader_building{linkage, false, "Enhanced Shader Building",
Category::RendererAdvanced};
// Add a new setting for shader compilation priority
SwitchableSetting<int> shader_compilation_priority{linkage, 0, "Shader Compilation Priority",
Category::RendererAdvanced};
}; };
extern Values values; extern Values values;
void UpdateGPUAccuracy(); void UpdateGPUAccuracy();
// boold isGPULevelNormal();
// TODO: ZEP
bool IsGPULevelExtreme(); bool IsGPULevelExtreme();
bool IsGPULevelHigh(); bool IsGPULevelHigh();

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -8,6 +9,7 @@
#include <ratio> #include <ratio>
#include "common/common_types.h" #include "common/common_types.h"
#include "core/hardware_properties.h"
namespace Common { namespace Common {
@ -15,7 +17,10 @@ class WallClock {
public: public:
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 MHz // Changed from constexpr to function to get dynamic value from settings
static inline u64 CPUTickFreq() {
return Core::Hardware::BASE_CLOCK_RATE();
} // T210/4 A57 CPU Tick Frequency from settings
virtual ~WallClock() = default; virtual ~WallClock() = default;
@ -76,12 +81,28 @@ protected:
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>; using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>; using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
// Cycle Timing // Cycle Timing - using functions for dynamic values
using CPUTickToNsRatio = std::ratio<std::nano::den, CPUTickFreq>; // Update these to use functions instead of constexpr
using CPUTickToUsRatio = std::ratio<std::micro::den, CPUTickFreq>; struct CPUTickToNsRatio {
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>; static inline std::intmax_t num = std::nano::den;
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>; static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToUsRatio {
static inline std::intmax_t num = std::micro::den;
static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToCNTPCTRatio {
static inline std::intmax_t num = CNTFRQ;
static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToGPUTickRatio {
static inline std::intmax_t num = GPUTickFreq;
static inline std::intmax_t den = CPUTickFreq();
};
}; };
std::unique_ptr<WallClock> CreateOptimalClock(); std::unique_ptr<WallClock> CreateOptimalClock();

View File

@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_library(core STATIC add_library(core STATIC
@ -776,8 +777,12 @@ add_library(core STATIC
hle/service/ngc/ngc.h hle/service/ngc/ngc.h
hle/service/nifm/nifm.cpp hle/service/nifm/nifm.cpp
hle/service/nifm/nifm.h hle/service/nifm/nifm.h
hle/service/nifm/nifm_utils.cpp
hle/service/nifm/nifm_utils.h
hle/service/nim/nim.cpp hle/service/nim/nim.cpp
hle/service/nim/nim.h hle/service/nim/nim.h
hle/service/nim/nim_utils.cpp
hle/service/nim/nim_utils.h
hle/service/npns/npns.cpp hle/service/npns/npns.cpp
hle/service/npns/npns.h hle/service/npns/npns.h
hle/service/ns/account_proxy_interface.cpp hle/service/ns/account_proxy_interface.cpp
@ -1045,6 +1050,14 @@ add_library(core STATIC
hle/service/sm/sm_controller.h hle/service/sm/sm_controller.h
hle/service/sockets/bsd.cpp hle/service/sockets/bsd.cpp
hle/service/sockets/bsd.h hle/service/sockets/bsd.h
hle/service/sockets/bsd_nu.cpp
hle/service/sockets/bsd_nu.h
hle/service/sockets/bsdcfg.cpp
hle/service/sockets/bsdcfg.h
hle/service/sockets/dns_priv.cpp
hle/service/sockets/dns_priv.h
hle/service/sockets/ethc.cpp
hle/service/sockets/ethc.h
hle/service/sockets/nsd.cpp hle/service/sockets/nsd.cpp
hle/service/sockets/nsd.h hle/service/sockets/nsd.h
hle/service/sockets/sfdnsres.cpp hle/service/sockets/sfdnsres.cpp
@ -1053,6 +1066,8 @@ add_library(core STATIC
hle/service/sockets/sockets.h hle/service/sockets/sockets.h
hle/service/sockets/sockets_translate.cpp hle/service/sockets/sockets_translate.cpp
hle/service/sockets/sockets_translate.h hle/service/sockets/sockets_translate.h
hle/service/sockets/socket_utils.cpp
hle/service/sockets/socket_utils.h
hle/service/spl/csrng.cpp hle/service/spl/csrng.cpp
hle/service/spl/csrng.h hle/service/spl/csrng.h
hle/service/spl/spl.cpp hle/service/spl/spl.cpp

View File

@ -9,12 +9,13 @@
#include "common/bit_util.h" #include "common/bit_util.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/settings.h"
namespace Core { namespace Core {
namespace Hardware { namespace Hardware {
constexpr u64 BASE_CLOCK_RATE = 1'020'000'000; // Default CPU Frequency = 1020 MHz inline u64 BASE_CLOCK_RATE() { return Settings::values.cpu_clock_rate.GetValue(); } // Default CPU Frequency set in settings, defaults to 1020 MHz
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -81,8 +82,8 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
void Controller::SetClockSpeed(u32 mhz) { void Controller::SetClockSpeed(u32 mhz) {
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz); LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
// TODO(DarkLordZach): Actually signal core_timing to change clock speed. // Update the clock rate setting with the provided MHz value (convert to Hz)
// TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used. Settings::values.cpu_clock_rate = mhz * 1'000'000;
} }
} // namespace Service::APM } // namespace Service::APM

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <queue> #include <queue>
@ -28,12 +29,12 @@ public:
{10102, nullptr, "UpdateFriendInfo"}, {10102, nullptr, "UpdateFriendInfo"},
{10110, nullptr, "GetFriendProfileImage"}, {10110, nullptr, "GetFriendProfileImage"},
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"}, {10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
{10121, nullptr, "EnsureFriendListAvailable"}, {10121, &IFriendService::EnsureFriendListAvailable, "EnsureFriendListAvailable"},
{10200, nullptr, "SendFriendRequestForApplication"}, {10200, nullptr, "SendFriendRequestForApplication"},
{10211, nullptr, "AddFacedFriendRequestForApplication"}, {10211, nullptr, "AddFacedFriendRequestForApplication"},
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"}, {10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"}, {10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
{10421, nullptr, "EnsureBlockedUserListAvailable"}, {10421, &IFriendService::EnsureBlockedUserListAvailable, "EnsureBlockedUserListAvailable"},
{10500, nullptr, "GetProfileList"}, {10500, nullptr, "GetProfileList"},
{10600, nullptr, "DeclareOpenOnlinePlaySession"}, {10600, nullptr, "DeclareOpenOnlinePlaySession"},
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"}, {10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
@ -166,11 +167,27 @@ private:
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString()); LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push(true); rb.Push(true);
} }
void EnsureFriendListAvailable(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto uuid{rp.PopRaw<Common::UUID>()};
LOG_WARNING(Service_Friend, "(STUBBED) EnsureFriendListAvailable called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void GetBlockedUserListIds(HLERequestContext& ctx) { void GetBlockedUserListIds(HLERequestContext& ctx) {
// This is safe to stub, as there should be no adverse consequences from reporting no // This is safe to stub, as there should be no adverse consequences from reporting no
// blocked users. // blocked users.
@ -186,11 +203,45 @@ private:
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString()); LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push(true); rb.Push(true);
} }
void EnsureBlockedUserListAvailable(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto uuid{rp.PopRaw<Common::UUID>()};
LOG_WARNING(Service_Friend, "(STUBBED) EnsureBlockedUserListAvailable called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(0); // Zero invitations
}
void Cancel(HLERequestContext& ctx) {
LOG_WARNING(Service_Friend, "Cancel called - returning immediately");
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) { void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) {
// Stub used by Splatoon 2 // Stub used by Splatoon 2
LOG_WARNING(Service_Friend, "(STUBBED) called"); LOG_WARNING(Service_Friend, "(STUBBED) called");
@ -248,14 +299,6 @@ private:
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
} }
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(0);
}
KernelHelpers::ServiceContext service_context; KernelHelpers::ServiceContext service_context;
Kernel::KEvent* completion_event; Kernel::KEvent* completion_event;
@ -287,6 +330,9 @@ private:
void GetEvent(HLERequestContext& ctx) { void GetEvent(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "called"); LOG_DEBUG(Service_Friend, "called");
// Signal the notification event to unblock any waiting threads
notification_event->Signal();
IPC::ResponseBuilder rb{ctx, 2, 1}; IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.PushCopyObjects(notification_event->GetReadableEvent()); rb.PushCopyObjects(notification_event->GetReadableEvent());
@ -363,10 +409,11 @@ private:
}; };
void Module::Interface::CreateFriendService(HLERequestContext& ctx) { void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "CreateFriendService called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1}; IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.PushIpcInterface<IFriendService>(system); rb.PushIpcInterface<IFriendService>(system);
LOG_DEBUG(Service_Friend, "called");
} }
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) { void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "core/core.h" #include "core/core.h"
@ -6,6 +7,7 @@
#include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h" #include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nifm/nifm.h" #include "core/hle/service/nifm/nifm.h"
#include "core/hle/service/nifm/nifm_utils.h"
#include "core/hle/service/server_manager.h" #include "core/hle/service/server_manager.h"
#include "network/network.h" #include "network/network.h"

View File

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <map>
#include <mutex>
#include <vector>
#include "common/logging/log.h"
#include "core/hle/service/nifm/nifm_utils.h"
namespace Service::NIFM::nn::nifm {
// Simple implementation to track network requests
namespace {
std::mutex g_request_mutex;
std::map<u32, NetworkRequest> g_requests;
u32 g_next_request_id = 1;
bool g_network_available = true; // Default to true for emulation
}
bool IsNetworkAvailable() {
// For emulation purposes, we'll just return the mocked availability
std::lock_guard lock(g_request_mutex);
return g_network_available;
}
u32 SubmitNetworkRequest() {
std::lock_guard lock(g_request_mutex);
if (!g_network_available) {
LOG_WARNING(Service_NIFM, "Network request submitted but network is not available");
}
u32 request_id = g_next_request_id++;
NetworkRequest request{
.request_id = request_id,
.is_pending = true,
.result = NetworkRequestResult::Success // Assume immediate success for emulation
};
g_requests[request_id] = request;
LOG_INFO(Service_NIFM, "Network request submitted with ID: {}", request_id);
return request_id;
}
NetworkRequestResult GetNetworkRequestResult(u32 request_id) {
std::lock_guard lock(g_request_mutex);
auto it = g_requests.find(request_id);
if (it == g_requests.end()) {
LOG_ERROR(Service_NIFM, "Tried to get result for invalid request ID: {}", request_id);
return NetworkRequestResult::Error;
}
// For emulation, we'll mark the request as no longer pending once the result is checked
it->second.is_pending = false;
return it->second.result;
}
bool CancelNetworkRequest(u32 request_id) {
std::lock_guard lock(g_request_mutex);
auto it = g_requests.find(request_id);
if (it == g_requests.end()) {
LOG_ERROR(Service_NIFM, "Tried to cancel invalid request ID: {}", request_id);
return false;
}
if (!it->second.is_pending) {
LOG_WARNING(Service_NIFM, "Tried to cancel a request that is not pending, ID: {}", request_id);
return false;
}
it->second.is_pending = false;
it->second.result = NetworkRequestResult::Canceled;
LOG_INFO(Service_NIFM, "Network request canceled with ID: {}", request_id);
return true;
}
} // namespace Service::NIFM::nn::nifm

View File

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Service::NIFM {
// Network request result codes
enum class NetworkRequestResult {
Success = 0,
Error = 1,
Canceled = 2,
Timeout = 3,
};
// Network request structure
struct NetworkRequest {
u32 request_id;
bool is_pending;
NetworkRequestResult result;
};
namespace nn::nifm {
// Checks if network connectivity is available
bool IsNetworkAvailable();
// Submits a network connection request
// Returns the request ID or 0 if the request failed
u32 SubmitNetworkRequest();
// Gets the status of a network request
// Returns the request result
NetworkRequestResult GetNetworkRequestResult(u32 request_id);
// Cancels a pending network request
// Returns true if the request was successfully canceled
bool CancelNetworkRequest(u32 request_id);
} // namespace nn::nifm
} // namespace Service::NIFM

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono> #include <chrono>
@ -8,6 +9,7 @@
#include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h" #include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nim/nim.h" #include "core/hle/service/nim/nim.h"
#include "core/hle/service/nim/nim_utils.h"
#include "core/hle/service/server_manager.h" #include "core/hle/service/server_manager.h"
#include "core/hle/service/service.h" #include "core/hle/service/service.h"

View File

@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <mutex>
#include "common/logging/log.h"
#include "core/hle/service/nim/nim_utils.h"
namespace Service::NIM::nn::nim {
// Simple implementation to track installation tasks
namespace {
std::mutex g_task_mutex;
std::map<u64, Task> g_tasks;
u64 g_next_task_id = 1;
bool g_service_available = true; // Default to true for emulation
}
bool IsServiceAvailable() {
std::lock_guard lock(g_task_mutex);
return g_service_available;
}
u64 CreateInstallTask(u64 application_id) {
std::lock_guard lock(g_task_mutex);
if (!g_service_available) {
LOG_WARNING(Service_NIM, "Installation task creation attempted but service is not available");
return 0;
}
u64 task_id = g_next_task_id++;
Task task{
.task_id = task_id,
.progress = {
.downloaded_bytes = 0,
.total_bytes = 1'000'000'000, // Fake 1GB download size
.status = TaskStatus::None
}
};
g_tasks[task_id] = task;
LOG_INFO(Service_NIM, "Installation task created for application 0x{:016X} with ID: {}",
application_id, task_id);
return task_id;
}
TaskProgress GetTaskProgress(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to get progress for invalid task ID: {}", task_id);
return {0, 0, TaskStatus::Failed};
}
// If task is in download state, simulate progress
if (it->second.progress.status == TaskStatus::Downloading) {
// Simulate download progress (add 10% of total size)
auto& progress = it->second.progress;
const u64 increment = progress.total_bytes / 10;
progress.downloaded_bytes += increment;
if (progress.downloaded_bytes >= progress.total_bytes) {
progress.downloaded_bytes = progress.total_bytes;
progress.status = TaskStatus::Installing;
LOG_INFO(Service_NIM, "Task ID {} download complete, now installing", task_id);
}
} else if (it->second.progress.status == TaskStatus::Installing) {
// Simulate installation completion
it->second.progress.status = TaskStatus::Complete;
LOG_INFO(Service_NIM, "Task ID {} installation complete", task_id);
}
return it->second.progress;
}
bool StartInstallTask(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to start invalid task ID: {}", task_id);
return false;
}
if (it->second.progress.status != TaskStatus::None &&
it->second.progress.status != TaskStatus::Pending) {
LOG_WARNING(Service_NIM, "Tried to start task ID {} which is already in progress", task_id);
return false;
}
it->second.progress.status = TaskStatus::Downloading;
LOG_INFO(Service_NIM, "Started installation task ID: {}", task_id);
return true;
}
bool CancelInstallTask(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to cancel invalid task ID: {}", task_id);
return false;
}
if (it->second.progress.status == TaskStatus::Complete ||
it->second.progress.status == TaskStatus::Failed ||
it->second.progress.status == TaskStatus::Canceled) {
LOG_WARNING(Service_NIM, "Tried to cancel task ID {} which is already in a final state", task_id);
return false;
}
it->second.progress.status = TaskStatus::Canceled;
LOG_INFO(Service_NIM, "Canceled installation task ID: {}", task_id);
return true;
}
} // namespace Service::NIM::nn::nim

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Service::NIM {
// Network installation task status
enum class TaskStatus {
None = 0,
Pending = 1,
Downloading = 2,
Installing = 3,
Complete = 4,
Failed = 5,
Canceled = 6,
};
// Network installation task progress
struct TaskProgress {
u64 downloaded_bytes;
u64 total_bytes;
TaskStatus status;
};
// Network installation task
struct Task {
u64 task_id;
TaskProgress progress;
};
namespace nn::nim {
// Checks if the NIM service is available
bool IsServiceAvailable();
// Creates a new installation task
// Returns the task ID or 0 if the task creation failed
u64 CreateInstallTask(u64 application_id);
// Gets the progress of an installation task
// Returns the task progress
TaskProgress GetTaskProgress(u64 task_id);
// Starts an installation task
// Returns true if the task was successfully started
bool StartInstallTask(u64 task_id);
// Cancels an installation task
// Returns true if the task was successfully canceled
bool CancelInstallTask(u64 task_id);
} // namespace nn::nim
} // namespace Service::NIM

View File

@ -1,9 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <boost/container/small_vector.hpp> #include <boost/container/small_vector.hpp>
#include "common/microprofile.h" #include "common/microprofile.h"
#include "common/settings.h"
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h" #include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
#include "core/hle/service/nvnflinger/buffer_item.h" #include "core/hle/service/nvnflinger/buffer_item.h"
#include "core/hle/service/nvnflinger/buffer_item_consumer.h" #include "core/hle/service/nvnflinger/buffer_item_consumer.h"
@ -21,19 +23,20 @@ s32 NormalizeSwapInterval(f32* out_speed_scale, s32 swap_interval) {
if (out_speed_scale) { if (out_speed_scale) {
*out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval); *out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval);
} }
// Only normalize swap_interval to 1 if we're not respecting present interval 0
swap_interval = 1; if (swap_interval == 0 && Settings::values.respect_present_interval_zero.GetValue()) {
// Keep swap_interval as 0 to allow for unlocked FPS
} else {
swap_interval = 1;
}
} }
if (swap_interval >= 5) { if (swap_interval >= 5) {
// As an extension, treat high swap interval as precise speed control. // As an extension, treat high swap interval as precise speed control.
if (out_speed_scale) { if (out_speed_scale) {
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f; *out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
} }
swap_interval = 1; swap_interval = 1;
} }
return swap_interval; return swap_interval;
} }

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <array> #include <array>
@ -479,6 +480,122 @@ void BSD::EventFd(HLERequestContext& ctx) {
BuildErrnoResponse(ctx, Errno::SUCCESS); BuildErrnoResponse(ctx, Errno::SUCCESS);
} }
void BSD::Sysctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::Ioctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::ShutdownAllSockets(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::GetResourceStatistics(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::RecvMMsg(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::SendMMsg(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::RegisterResourceStatisticsName(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::RegisterClientShared(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::GetSocketStatistics(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::NifIoctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::SetThreadCoreMask(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::GetThreadCoreMask(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::SocketExempt(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const u32 domain = rp.Pop<u32>();
const u32 type = rp.Pop<u32>();
const u32 protocol = rp.Pop<u32>();
LOG_WARNING(Service, "(STUBBED) called - domain={} type={} protocol={}", domain, type, protocol);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(-1); // Return -1 on exempted socket
rb.PushEnum(Errno::SUCCESS);
}
void BSD::Open(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
template <typename Work> template <typename Work>
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) { void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
work.Execute(this); work.Execute(this);
@ -508,9 +625,9 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
LOG_INFO(Service, "New socket fd={}", fd); LOG_INFO(Service, "New socket fd={}", fd);
auto room_member = room_network.GetRoomMember().lock(); auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member && room_member->IsConnected()) { if (room_member && room_member->IsConnected()) {
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network); descriptor.socket = std::make_shared<Network::ProxySocket>(system.GetRoomNetwork());
} else { } else {
descriptor.socket = std::make_shared<Network::Socket>(); descriptor.socket = std::make_shared<Network::Socket>();
} }
@ -960,27 +1077,41 @@ void BSD::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noex
} }
void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) { void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
// Iterate through all file descriptors and pass the packet to each valid socket
for (auto& optional_descriptor : file_descriptors) { for (auto& optional_descriptor : file_descriptors) {
if (!optional_descriptor.has_value()) { if (!optional_descriptor.has_value()) {
continue; continue;
} }
FileDescriptor& descriptor = *optional_descriptor; FileDescriptor& descriptor = *optional_descriptor;
descriptor.socket.get()->HandleProxyPacket(packet); if (descriptor.socket) {
descriptor.socket->HandleProxyPacket(packet);
}
} }
} }
s32 BSD::Connect(s32 socket, const SockAddrIn& addr) {
// Call ConnectImpl directly if possible, or return error
LOG_INFO(Service_BSD, "nn::socket::Connect called for socket {} with address {}:{}",
socket, addr.ip[0], addr.portno);
// For now, we're assuming the connection will succeed return 0
return 0;
}
BSD::BSD(Core::System& system_, const char* name) BSD::BSD(Core::System& system_, const char* name)
: ServiceFramework{system_, name}, room_network{system_.GetRoomNetwork()} { : ServiceFramework{system_, name} {
// clang-format off // clang-format off
static const FunctionInfo functions[] = { static const FunctionInfo functions[] = {
{0, &BSD::RegisterClient, "RegisterClient"}, {0, &BSD::RegisterClient, "RegisterClient"},
{1, &BSD::StartMonitoring, "StartMonitoring"}, {1, &BSD::StartMonitoring, "StartMonitoring"},
{2, &BSD::Socket, "Socket"}, {2, &BSD::Socket, "Socket"},
{3, nullptr, "SocketExempt"}, {3, &BSD::SocketExempt, "SocketExempt"},
{4, nullptr, "Open"}, {4, &BSD::Open, "Open"},
{5, &BSD::Select, "Select"}, {5, &BSD::Select, "Select"},
{6, &BSD::Poll, "Poll"}, {6, &BSD::Poll, "Poll"},
{7, nullptr, "Sysctl"}, {7, &BSD::Sysctl, "Sysctl"},
{8, &BSD::Recv, "Recv"}, {8, &BSD::Recv, "Recv"},
{9, &BSD::RecvFrom, "RecvFrom"}, {9, &BSD::RecvFrom, "RecvFrom"},
{10, &BSD::Send, "Send"}, {10, &BSD::Send, "Send"},
@ -992,27 +1123,32 @@ BSD::BSD(Core::System& system_, const char* name)
{16, &BSD::GetSockName, "GetSockName"}, {16, &BSD::GetSockName, "GetSockName"},
{17, &BSD::GetSockOpt, "GetSockOpt"}, {17, &BSD::GetSockOpt, "GetSockOpt"},
{18, &BSD::Listen, "Listen"}, {18, &BSD::Listen, "Listen"},
{19, nullptr, "Ioctl"}, {19, &BSD::Ioctl, "Ioctl"},
{20, &BSD::Fcntl, "Fcntl"}, {20, &BSD::Fcntl, "Fcntl"},
{21, &BSD::SetSockOpt, "SetSockOpt"}, {21, &BSD::SetSockOpt, "SetSockOpt"},
{22, &BSD::Shutdown, "Shutdown"}, {22, &BSD::Shutdown, "Shutdown"},
{23, nullptr, "ShutdownAllSockets"}, {23, &BSD::ShutdownAllSockets, "ShutdownAllSockets"},
{24, &BSD::Write, "Write"}, {24, &BSD::Write, "Write"},
{25, &BSD::Read, "Read"}, {25, &BSD::Read, "Read"},
{26, &BSD::Close, "Close"}, {26, &BSD::Close, "Close"},
{27, &BSD::DuplicateSocket, "DuplicateSocket"}, {27, &BSD::DuplicateSocket, "DuplicateSocket"},
{28, nullptr, "GetResourceStatistics"}, {28, &BSD::GetResourceStatistics, "GetResourceStatistics"},
{29, nullptr, "RecvMMsg"}, {29, &BSD::RecvMMsg, "RecvMMsg"},
{30, nullptr, "SendMMsg"}, {30, &BSD::SendMMsg, "SendMMsg"},
{31, &BSD::EventFd, "EventFd"}, {31, &BSD::EventFd, "EventFd"},
{32, nullptr, "RegisterResourceStatisticsName"}, {32, &BSD::RegisterResourceStatisticsName, "RegisterResourceStatisticsName"},
{33, nullptr, "Initialize2"}, {33, &BSD::RegisterClientShared, "RegisterClientShared"},
{34, &BSD::GetSocketStatistics, "GetSocketStatistics"},
{35, &BSD::NifIoctl, "NifIoctl"},
{200, &BSD::SetThreadCoreMask, "SetThreadCoreMask"},
{201, &BSD::GetThreadCoreMask, "GetThreadCoreMask"},
}; };
// clang-format on // clang-format on
RegisterHandlers(functions); RegisterHandlers(functions);
if (auto room_member = room_network.GetRoomMember().lock()) { auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
proxy_packet_received = room_member->BindOnProxyPacketReceived( proxy_packet_received = room_member->BindOnProxyPacketReceived(
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); }); [this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
} else { } else {
@ -1021,7 +1157,8 @@ BSD::BSD(Core::System& system_, const char* name)
} }
BSD::~BSD() { BSD::~BSD() {
if (auto room_member = room_network.GetRoomMember().lock()) { auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
room_member->Unbind(proxy_packet_received); room_member->Unbind(proxy_packet_received);
} }
} }
@ -1031,31 +1168,4 @@ std::unique_lock<std::mutex> BSD::LockService() {
return {}; return {};
} }
BSDCFG::BSDCFG(Core::System& system_) : ServiceFramework{system_, "bsdcfg"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, nullptr, "SetIfUp"},
{1, nullptr, "SetIfUpWithEvent"},
{2, nullptr, "CancelIf"},
{3, nullptr, "SetIfDown"},
{4, nullptr, "GetIfState"},
{5, nullptr, "DhcpRenew"},
{6, nullptr, "AddStaticArpEntry"},
{7, nullptr, "RemoveArpEntry"},
{8, nullptr, "LookupArpEntry"},
{9, nullptr, "LookupArpEntry2"},
{10, nullptr, "ClearArpEntries"},
{11, nullptr, "ClearArpEntries2"},
{12, nullptr, "PrintArpEntries"},
{13, nullptr, "Unknown13"},
{14, nullptr, "Unknown14"},
{15, nullptr, "Unknown15"},
};
// clang-format on
RegisterHandlers(functions);
}
BSDCFG::~BSDCFG() = default;
} // namespace Service::Sockets } // namespace Service::Sockets

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -37,6 +38,9 @@ public:
Errno CloseImpl(s32 fd); Errno CloseImpl(s32 fd);
std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(s32 fd); std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(s32 fd);
// Static function that can be called from nn::socket::Connect
static s32 Connect(s32 socket, const SockAddrIn& addr);
private: private:
/// Maximum number of file descriptors /// Maximum number of file descriptors
static constexpr size_t MAX_FD = 128; static constexpr size_t MAX_FD = 128;
@ -124,11 +128,30 @@ private:
Errno bsd_errno{}; Errno bsd_errno{};
}; };
struct LibraryConfigData {
u32 version;
u32 tcp_tx_buf_size;
u32 tcp_rx_buf_size;
u32 tcp_tx_buf_max_size;
u32 tcp_rx_buf_max_size;
u32 udp_tx_buf_size;
u32 udp_rx_buf_size;
u32 sb_efficiency;
};
// This is nn::socket::sf::IClient
void RegisterClient(HLERequestContext& ctx); void RegisterClient(HLERequestContext& ctx);
void StartMonitoring(HLERequestContext& ctx); void StartMonitoring(HLERequestContext& ctx);
void Socket(HLERequestContext& ctx); void Socket(HLERequestContext& ctx);
void SocketExempt(HLERequestContext& ctx);
void Open(HLERequestContext& ctx);
void Select(HLERequestContext& ctx); void Select(HLERequestContext& ctx);
void Poll(HLERequestContext& ctx); void Poll(HLERequestContext& ctx);
void Sysctl(HLERequestContext& ctx);
void Recv(HLERequestContext& ctx);
void RecvFrom(HLERequestContext& ctx);
void Send(HLERequestContext& ctx);
void SendTo(HLERequestContext& ctx);
void Accept(HLERequestContext& ctx); void Accept(HLERequestContext& ctx);
void Bind(HLERequestContext& ctx); void Bind(HLERequestContext& ctx);
void Connect(HLERequestContext& ctx); void Connect(HLERequestContext& ctx);
@ -136,18 +159,25 @@ private:
void GetSockName(HLERequestContext& ctx); void GetSockName(HLERequestContext& ctx);
void GetSockOpt(HLERequestContext& ctx); void GetSockOpt(HLERequestContext& ctx);
void Listen(HLERequestContext& ctx); void Listen(HLERequestContext& ctx);
void Ioctl(HLERequestContext& ctx);
void Fcntl(HLERequestContext& ctx); void Fcntl(HLERequestContext& ctx);
void SetSockOpt(HLERequestContext& ctx); void SetSockOpt(HLERequestContext& ctx);
void Shutdown(HLERequestContext& ctx); void Shutdown(HLERequestContext& ctx);
void Recv(HLERequestContext& ctx); void ShutdownAllSockets(HLERequestContext& ctx);
void RecvFrom(HLERequestContext& ctx);
void Send(HLERequestContext& ctx);
void SendTo(HLERequestContext& ctx);
void Write(HLERequestContext& ctx); void Write(HLERequestContext& ctx);
void Read(HLERequestContext& ctx); void Read(HLERequestContext& ctx);
void Close(HLERequestContext& ctx); void Close(HLERequestContext& ctx);
void DuplicateSocket(HLERequestContext& ctx); void DuplicateSocket(HLERequestContext& ctx);
void GetResourceStatistics(HLERequestContext& ctx);
void RecvMMsg(HLERequestContext& ctx);
void SendMMsg(HLERequestContext& ctx);
void EventFd(HLERequestContext& ctx); void EventFd(HLERequestContext& ctx);
void RegisterResourceStatisticsName(HLERequestContext& ctx);
void RegisterClientShared(HLERequestContext& ctx);
void GetSocketStatistics(HLERequestContext& ctx);
void NifIoctl(HLERequestContext& ctx);
void SetThreadCoreMask(HLERequestContext& ctx);
void GetThreadCoreMask(HLERequestContext& ctx);
template <typename Work> template <typename Work>
void ExecuteWork(HLERequestContext& ctx, Work work); void ExecuteWork(HLERequestContext& ctx, Work work);
@ -171,30 +201,23 @@ private:
std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message); std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message);
std::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message, std::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
std::span<const u8> addr); std::span<const u8> addr);
s32 FindFreeFileDescriptorHandle() noexcept; s32 FindFreeFileDescriptorHandle() noexcept;
bool IsFileDescriptorValid(s32 fd) const noexcept; bool IsFileDescriptorValid(s32 fd) const noexcept;
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept; void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept;
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors;
Network::RoomNetwork& room_network;
/// Callback to parse and handle a received wifi packet.
void OnProxyPacketReceived(const Network::ProxyPacket& packet); void OnProxyPacketReceived(const Network::ProxyPacket& packet);
// Callback identifier for the OnProxyPacketReceived event.
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received; Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
/// Mapping of file descriptors to sockets
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors{};
/// Mutex to protect file descriptor operations
std::mutex mutex;
protected: protected:
virtual std::unique_lock<std::mutex> LockService() override; virtual std::unique_lock<std::mutex> LockService() override;
}; };
class BSDCFG final : public ServiceFramework<BSDCFG> {
public:
explicit BSDCFG(Core::System& system_);
~BSDCFG() override;
};
} // namespace Service::Sockets } // namespace Service::Sockets

View File

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/sockets/bsd_nu.h"
namespace Service::Sockets {
ISfUserService::ISfUserService(Core::System& system_)
: ServiceFramework{system_, "ISfUserService"},
service_context{system_, "ISfUserService"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &ISfUserService::Assign, "Assign"},
{128, &ISfUserService::GetUserInfo, "GetUserInfo"},
{129, &ISfUserService::GetStateChangedEvent, "GetStateChangedEvent"},
};
// clang-format on
RegisterHandlers(functions);
}
ISfUserService::~ISfUserService() = default;
void ISfUserService::Assign(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<ISfAssignedNetworkInterfaceService>(system);
}
void ISfUserService::GetUserInfo(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void ISfUserService::GetStateChangedEvent(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
auto* event = service_context.CreateEvent("ISfUserService:StateChanged");
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(event->GetReadableEvent());
}
ISfAssignedNetworkInterfaceService::ISfAssignedNetworkInterfaceService(Core::System& system_)
: ServiceFramework{system_, "ISfAssignedNetworkInterfaceService"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &ISfAssignedNetworkInterfaceService::AddSession, "AddSession"},
};
// clang-format on
RegisterHandlers(functions);
}
ISfAssignedNetworkInterfaceService::~ISfAssignedNetworkInterfaceService() = default;
void ISfAssignedNetworkInterfaceService::AddSession(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
BSD_NU::BSD_NU(Core::System& system_) : ServiceFramework{system_, "bsd:nu"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSD_NU::CreateUserService, "CreateUserService"},
};
// clang-format on
RegisterHandlers(functions);
}
BSD_NU::~BSD_NU() = default;
void BSD_NU::CreateUserService(HLERequestContext& ctx) {
LOG_DEBUG(Service, "called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<ISfUserService>(system);
}
} // namespace Service::Sockets

View File

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
#include "core/hle/service/kernel_helpers.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class ISfUserService final : public ServiceFramework<ISfUserService> {
public:
explicit ISfUserService(Core::System& system_);
~ISfUserService() override;
private:
void Assign(HLERequestContext& ctx);
void GetUserInfo(HLERequestContext& ctx);
void GetStateChangedEvent(HLERequestContext& ctx);
KernelHelpers::ServiceContext service_context;
};
class ISfAssignedNetworkInterfaceService final : public ServiceFramework<ISfAssignedNetworkInterfaceService> {
public:
explicit ISfAssignedNetworkInterfaceService(Core::System& system_);
~ISfAssignedNetworkInterfaceService() override;
private:
void AddSession(HLERequestContext& ctx);
};
class BSD_NU final : public ServiceFramework<BSD_NU> {
public:
explicit BSD_NU(Core::System& system_);
~BSD_NU() override;
private:
void CreateUserService(HLERequestContext& ctx);
};
} // namespace Service::Sockets

View File

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/sockets/bsdcfg.h"
namespace Service::Sockets {
BSDCFG::BSDCFG(Core::System& system_)
: ServiceFramework{system_, "bsdcfg"},
service_context{system_, "BSDCFG"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSDCFG::SetIfUp, "SetIfUp"},
{1, &BSDCFG::SetIfUpWithEvent, "SetIfUpWithEvent"},
{2, &BSDCFG::CancelIf, "CancelIf"},
{3, &BSDCFG::SetIfDown, "SetIfDown"},
{4, &BSDCFG::GetIfState, "GetIfState"},
{5, &BSDCFG::DhcpRenew, "DhcpRenew"},
{6, &BSDCFG::AddStaticArpEntry, "AddStaticArpEntry"},
{7, &BSDCFG::RemoveArpEntry, "RemoveArpEntry"},
{8, &BSDCFG::LookupArpEntry, "LookupArpEntry"},
{9, &BSDCFG::LookupArpEntry2, "LookupArpEntry2"},
{10, &BSDCFG::ClearArpEntries, "ClearArpEntries"},
{11, &BSDCFG::ClearArpEntries2, "ClearArpEntries2"},
{12, &BSDCFG::PrintArpEntries, "PrintArpEntries"},
{13, &BSDCFG::Cmd13, "Unknown13"},
{14, &BSDCFG::Cmd14, "Unknown14"},
{15, &BSDCFG::Cmd15, "Unknown15"},
};
// clang-format on
RegisterHandlers(functions);
}
BSDCFG::~BSDCFG() = default;
void BSDCFG::SetIfUp(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::SetIfUpWithEvent(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
auto* event = service_context.CreateEvent("BSDCFG:SetIfUpWithEvent");
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(event->GetReadableEvent());
}
void BSDCFG::CancelIf(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::SetIfDown(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::GetIfState(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<u32>(1); // Interface is up (stubbed)
}
void BSDCFG::DhcpRenew(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::AddStaticArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::RemoveArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::LookupArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::LookupArpEntry2(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::ClearArpEntries(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::ClearArpEntries2(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::PrintArpEntries(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd13(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd14(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd15(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
} // namespace Service::Sockets

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
#include "core/hle/service/kernel_helpers.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class BSDCFG final : public ServiceFramework<BSDCFG> {
public:
explicit BSDCFG(Core::System& system_);
~BSDCFG() override;
private:
void SetIfUp(HLERequestContext& ctx);
void SetIfUpWithEvent(HLERequestContext& ctx);
void CancelIf(HLERequestContext& ctx);
void SetIfDown(HLERequestContext& ctx);
void GetIfState(HLERequestContext& ctx);
void DhcpRenew(HLERequestContext& ctx);
void AddStaticArpEntry(HLERequestContext& ctx);
void RemoveArpEntry(HLERequestContext& ctx);
void LookupArpEntry(HLERequestContext& ctx);
void LookupArpEntry2(HLERequestContext& ctx);
void ClearArpEntries(HLERequestContext& ctx);
void ClearArpEntries2(HLERequestContext& ctx);
void PrintArpEntries(HLERequestContext& ctx);
void Cmd13(HLERequestContext& ctx);
void Cmd14(HLERequestContext& ctx);
void Cmd15(HLERequestContext& ctx);
KernelHelpers::ServiceContext service_context;
};
} // namespace Service::Sockets

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/sockets/dns_priv.h"
namespace Service::Sockets {
DNS_PRIV::DNS_PRIV(Core::System& system_) : ServiceFramework{system_, "dns:priv"} {
// dns:priv doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
DNS_PRIV::~DNS_PRIV() = default;
} // namespace Service::Sockets

View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class DNS_PRIV final : public ServiceFramework<DNS_PRIV> {
public:
explicit DNS_PRIV(Core::System& system_);
~DNS_PRIV() override;
};
} // namespace Service::Sockets

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/sockets/ethc.h"
namespace Service::Sockets {
ETHC_C::ETHC_C(Core::System& system_) : ServiceFramework{system_, "ethc:c"} {
// ethc:c doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
ETHC_C::~ETHC_C() = default;
ETHC_I::ETHC_I(Core::System& system_) : ServiceFramework{system_, "ethc:i"} {
// ethc:i doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
ETHC_I::~ETHC_I() = default;
} // namespace Service::Sockets

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class ETHC_C final : public ServiceFramework<ETHC_C> {
public:
explicit ETHC_C(Core::System& system_);
~ETHC_C() override;
};
class ETHC_I final : public ServiceFramework<ETHC_I> {
public:
explicit ETHC_I(Core::System& system_);
~ETHC_I() override;
};
} // namespace Service::Sockets

View File

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <charconv>
#include <cstring>
#include <string>
#include <string_view>
#include "common/logging/log.h"
#include "core/hle/service/sockets/bsd.h"
#include "core/hle/service/sockets/socket_utils.h"
namespace Service::Sockets::nn::socket {
bool InetAton(const char* ip, in_addr* addr) {
if (ip == nullptr || addr == nullptr) {
return false;
}
std::string_view ip_view(ip);
// Count the number of dots to validate IPv4 format
size_t dots = std::count(ip_view.begin(), ip_view.end(), '.');
if (dots != 3) {
return false;
}
// Parse the IP address in standard dotted-decimal notation
u32 result = 0;
size_t pos = 0;
for (int i = 0; i < 4; i++) {
size_t next_dot = ip_view.find('.', pos);
std::string_view octet_view;
if (i < 3) {
if (next_dot == std::string_view::npos) {
return false;
}
octet_view = ip_view.substr(pos, next_dot - pos);
pos = next_dot + 1;
} else {
octet_view = ip_view.substr(pos);
}
u32 octet;
auto [ptr, ec] = std::from_chars(octet_view.data(), octet_view.data() + octet_view.size(), octet);
if (ec != std::errc() || octet > 255 || (ptr != octet_view.data() + octet_view.size())) {
return false;
}
result = (result << 8) | octet;
}
addr->s_addr = result;
return true;
}
s32 Connect(s32 socket, const sockaddr* addr, u32 addr_len) {
if (addr == nullptr || addr_len < sizeof(sockaddr)) {
LOG_ERROR(Service_BSD, "Invalid address pointer or length");
// Set errno to EINVAL (Invalid argument)
errno = static_cast<u32>(Errno::INVAL);
return -1;
}
// Create a BSD-compliant sockaddr_in from our sockaddr
SockAddrIn bsd_addr{};
bsd_addr.len = sizeof(SockAddrIn);
// Cast explicitly with a mask to ensure valid range conversion
bsd_addr.family = static_cast<u8>(addr->sa_family & 0xFF);
if (addr->sa_family == 2) { // AF_INET
const auto* addr_in = reinterpret_cast<const sockaddr_in*>(addr);
bsd_addr.portno = addr_in->sin_port;
// Copy IPv4 address (in network byte order)
const u32 ip_addr = addr_in->sin_addr.s_addr;
bsd_addr.ip[0] = static_cast<u8>((ip_addr >> 24) & 0xFF);
bsd_addr.ip[1] = static_cast<u8>((ip_addr >> 16) & 0xFF);
bsd_addr.ip[2] = static_cast<u8>((ip_addr >> 8) & 0xFF);
bsd_addr.ip[3] = static_cast<u8>(ip_addr & 0xFF);
} else {
LOG_ERROR(Service_BSD, "Unsupported address family: {}", addr->sa_family);
// Set errno to EAFNOSUPPORT (Address family not supported)
errno = static_cast<u32>(Errno::INVAL); // Using INVAL as a substitute for EAFNOSUPPORT
return -1;
}
// Forward to the BSD socket implementation
return BSD::Connect(socket, bsd_addr);
}
} // namespace Service::Sockets::nn::socket

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
#include <array>
#include <cstring>
namespace Service::Sockets {
// Base socket structures and utilities for nn::socket
// in_addr struct similar to standard BSD/POSIX implementation
struct in_addr {
u32 s_addr;
};
// sockaddr struct similar to standard BSD/POSIX implementation
struct sockaddr {
u16 sa_family;
char sa_data[14];
};
// sockaddr_in struct similar to standard BSD/POSIX implementation
struct sockaddr_in {
u16 sin_family;
u16 sin_port;
in_addr sin_addr;
char sin_zero[8];
};
// Socket configuration data based on LibraryConfigData from switchbrew
struct Config {
u32 version;
u32 tcp_tx_buf_size;
u32 tcp_rx_buf_size;
u32 tcp_tx_buf_max_size;
u32 tcp_rx_buf_max_size;
u32 udp_tx_buf_size;
u32 udp_rx_buf_size;
u32 sb_efficiency;
};
namespace nn::socket {
// InetAton converts an IPv4 address string to an in_addr structure
// Returns true on success, false on failure
bool InetAton(const char* ip, in_addr* addr);
// Connect to a remote host
// Returns 0 on success, -1 on failure
s32 Connect(s32 socket, const sockaddr* addr, u32 addr_len);
} // namespace nn::socket
} // namespace Service::Sockets

View File

@ -1,10 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/server_manager.h" #include "core/hle/service/server_manager.h"
#include "core/hle/service/sockets/bsd.h" #include "core/hle/service/sockets/bsd.h"
#include "core/hle/service/sockets/bsd_nu.h"
#include "core/hle/service/sockets/bsdcfg.h"
#include "core/hle/service/sockets/dns_priv.h"
#include "core/hle/service/sockets/ethc.h"
#include "core/hle/service/sockets/nsd.h" #include "core/hle/service/sockets/nsd.h"
#include "core/hle/service/sockets/sfdnsres.h" #include "core/hle/service/sockets/sfdnsres.h"
#include "core/hle/service/sockets/socket_utils.h"
#include "core/hle/service/sockets/sockets.h" #include "core/hle/service/sockets/sockets.h"
namespace Service::Sockets { namespace Service::Sockets {
@ -12,12 +18,33 @@ namespace Service::Sockets {
void LoopProcess(Core::System& system) { void LoopProcess(Core::System& system) {
auto server_manager = std::make_unique<ServerManager>(system); auto server_manager = std::make_unique<ServerManager>(system);
// Register BSD services
server_manager->RegisterNamedService("bsd:s", std::make_shared<BSD>(system, "bsd:s")); server_manager->RegisterNamedService("bsd:s", std::make_shared<BSD>(system, "bsd:s"));
server_manager->RegisterNamedService("bsd:u", std::make_shared<BSD>(system, "bsd:u")); server_manager->RegisterNamedService("bsd:u", std::make_shared<BSD>(system, "bsd:u"));
// Register BSD:A service for [18.0.0+]
server_manager->RegisterNamedService("bsd:a", std::make_shared<BSD>(system, "bsd:a"));
// Register BSD:NU service for [15.0.0+]
server_manager->RegisterNamedService("bsd:nu", std::make_shared<BSD_NU>(system));
// Register BSDCFG service
server_manager->RegisterNamedService("bsdcfg", std::make_shared<BSDCFG>(system)); server_manager->RegisterNamedService("bsdcfg", std::make_shared<BSDCFG>(system));
// Register NSD services
server_manager->RegisterNamedService("nsd:a", std::make_shared<NSD>(system, "nsd:a")); server_manager->RegisterNamedService("nsd:a", std::make_shared<NSD>(system, "nsd:a"));
server_manager->RegisterNamedService("nsd:u", std::make_shared<NSD>(system, "nsd:u")); server_manager->RegisterNamedService("nsd:u", std::make_shared<NSD>(system, "nsd:u"));
// Register SFDNSRES service
server_manager->RegisterNamedService("sfdnsres", std::make_shared<SFDNSRES>(system)); server_manager->RegisterNamedService("sfdnsres", std::make_shared<SFDNSRES>(system));
// Register DNS:PRIV service
server_manager->RegisterNamedService("dns:priv", std::make_shared<DNS_PRIV>(system));
// Register ETHC services
server_manager->RegisterNamedService("ethc:c", std::make_shared<ETHC_C>(system));
server_manager->RegisterNamedService("ethc:i", std::make_shared<ETHC_I>(system));
server_manager->StartAdditionalHostThreads("bsdsocket", 2); server_manager->StartAdditionalHostThreads("bsdsocket", 2);
ServerManager::RunServer(std::move(server_manager)); ServerManager::RunServer(std::move(server_manager));
} }

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -17,9 +18,11 @@ enum class Errno : u32 {
BADF = 9, BADF = 9,
AGAIN = 11, AGAIN = 11,
NOMEM = 12, NOMEM = 12,
BUSY = 16,
INVAL = 22, INVAL = 22,
MFILE = 24, MFILE = 24,
PIPE = 32, PIPE = 32,
NOTSOCK = 88,
MSGSIZE = 90, MSGSIZE = 90,
CONNABORTED = 103, CONNABORTED = 103,
CONNRESET = 104, CONNRESET = 104,

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <utility> #include <utility>
@ -39,6 +40,10 @@ Errno Translate(Network::Errno value) {
return Errno::INPROGRESS; return Errno::INPROGRESS;
case Network::Errno::NOMEM: case Network::Errno::NOMEM:
return Errno::NOMEM; return Errno::NOMEM;
case Network::Errno::BUSY:
return Errno::BUSY;
case Network::Errno::NOTSOCK:
return Errno::NOTSOCK;
default: default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", value); UNIMPLEMENTED_MSG("Unimplemented errno={}", value);
return Errno::SUCCESS; return Errno::SUCCESS;

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -156,6 +157,8 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::TIMEDOUT; return Errno::TIMEDOUT;
case WSAEINPROGRESS: case WSAEINPROGRESS:
return Errno::INPROGRESS; return Errno::INPROGRESS;
case WSAENOTSOCK:
return Errno::NOTSOCK;
default: default:
UNIMPLEMENTED_MSG("Unimplemented errno={}", e); UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
return Errno::OTHER; return Errno::OTHER;
@ -273,14 +276,14 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::MFILE; return Errno::MFILE;
case EPIPE: case EPIPE:
return Errno::PIPE; return Errno::PIPE;
case ECONNABORTED:
return Errno::CONNABORTED;
case ENOTCONN: case ENOTCONN:
return Errno::NOTCONN; return Errno::NOTCONN;
case EAGAIN: case EAGAIN:
return Errno::AGAIN; return Errno::AGAIN;
case ECONNREFUSED: case ECONNREFUSED:
return Errno::CONNREFUSED; return Errno::CONNREFUSED;
case ECONNABORTED:
return Errno::CONNABORTED;
case ECONNRESET: case ECONNRESET:
return Errno::CONNRESET; return Errno::CONNRESET;
case EHOSTUNREACH: case EHOSTUNREACH:
@ -295,8 +298,14 @@ Errno TranslateNativeError(int e, CallType call_type = CallType::Other) {
return Errno::TIMEDOUT; return Errno::TIMEDOUT;
case EINPROGRESS: case EINPROGRESS:
return Errno::INPROGRESS; return Errno::INPROGRESS;
case ENOMEM:
return Errno::NOMEM;
case EBUSY:
return Errno::BUSY;
case ENOTSOCK:
return Errno::NOTSOCK;
default: default:
UNIMPLEMENTED_MSG("Unimplemented errno={} ({})", e, strerror(e)); UNIMPLEMENTED_MSG("Unimplemented errno={}", e);
return Errno::OTHER; return Errno::OTHER;
} }
} }
@ -315,6 +324,14 @@ Errno GetAndLogLastError(CallType call_type = CallType::Other) {
LOG_DEBUG(Network, "Socket operation error: {}", Common::NativeErrorToString(e)); LOG_DEBUG(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
return err; return err;
} }
if (err == Errno::NOTSOCK) {
// This is a common error when network functionality is not fully implemented
LOG_DEBUG(Network, "Socket operation error: An operation was attempted on something that is not a socket. "
"This may indicate the game is using network features not fully supported. ");
return err;
}
LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e)); LOG_ERROR(Network, "Socket operation error: {}", Common::NativeErrorToString(e));
return err; return err;
} }

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -47,6 +48,8 @@ enum class Errno {
INPROGRESS, INPROGRESS,
OTHER, OTHER,
NOMEM, NOMEM,
BUSY,
NOTSOCK,
}; };
enum class GetAddrInfoError { enum class GetAddrInfoError {

View File

@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2015 Citra Emulator Project // SPDX-FileCopyrightText: 2015 Citra Emulator Project
// SPDX-FileCopyrightText: 2018 yuzu Emulator Project // SPDX-FileCopyrightText: 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -909,7 +910,9 @@ struct Memory::Impl {
#endif #endif
}; };
Memory::Memory(Core::System& system_) : system{system_} { Memory::Memory(Core::System& system_) : system(system_), impl(std::make_unique<Impl>(system_)), gen(rd()) {
// Initialize the random number distribution
dis = std::uniform_int_distribution<u64>(0, std::numeric_limits<u64>::max());
Reset(); Reset();
} }
@ -1151,4 +1154,48 @@ bool Memory::InvalidateSeparateHeap(void* fault_address) {
#endif #endif
} }
Common::ProcessAddress Memory::GenerateRandomBaseAddress() {
u64 random_bits = dis(gen);
return Common::ProcessAddress((random_bits & ~NRO_BASE_ADDRESS_RANDOMIZATION_MASK) |
(random_bits & NRO_BASE_ADDRESS_RANDOMIZATION_MASK));
}
Memory::MemoryRegion* Memory::FindRegion(Common::ProcessAddress address) {
for (auto& entry : memory_regions) {
if (address >= entry.second.start_address &&
address < entry.second.start_address + entry.second.size) {
return &entry.second;
}
}
return nullptr;
}
void Memory::MapMemoryRegion(Common::ProcessAddress start_address, u64 size, MemoryRegionType type,
bool exec, bool write) {
if (start_address + size > EMULATED_MEMORY_SIZE) {
LOG_ERROR(HW_Memory, "Memory mapping exceeds emulated memory boundaries at address {:016X}",
GetInteger(start_address));
return;
}
// Create the memory region
memory_regions[start_address] = MemoryRegion(start_address, size, type, exec, write);
// Map the region in the page table
Common::MemoryPermission perms{};
if (exec) perms |= Common::MemoryPermission::Execute;
if (write) perms |= Common::MemoryPermission::Write;
perms |= Common::MemoryPermission::Read;
// Using the MapMemoryRegion method defined in the Impl struct
impl->MapMemoryRegion(*impl->current_page_table, start_address, size,
Common::PhysicalAddress(GetInteger(start_address)), perms, false);
}
Common::ProcessAddress Memory::MapBinary(u64 size) {
Common::ProcessAddress base_address = GenerateRandomBaseAddress();
MapMemoryRegion(base_address, size, MemoryRegionType::BinaryMemory, true, true);
return base_address;
}
} // namespace Core::Memory } // namespace Core::Memory

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -9,6 +10,8 @@
#include <span> #include <span>
#include <string> #include <string>
#include <vector> #include <vector>
#include <unordered_map>
#include <random>
#include "common/scratch_buffer.h" #include "common/scratch_buffer.h"
#include "common/typed_address.h" #include "common/typed_address.h"
@ -43,6 +46,9 @@ constexpr std::size_t CITRON_PAGEBITS = 12;
constexpr u64 CITRON_PAGESIZE = 1ULL << CITRON_PAGEBITS; constexpr u64 CITRON_PAGESIZE = 1ULL << CITRON_PAGEBITS;
constexpr u64 CITRON_PAGEMASK = CITRON_PAGESIZE - 1; constexpr u64 CITRON_PAGEMASK = CITRON_PAGESIZE - 1;
/// Emulated memory size (4GB)
constexpr u64 EMULATED_MEMORY_SIZE = 4ULL * 1024 * 1024 * 1024;
/// Virtual user-space memory regions /// Virtual user-space memory regions
enum : u64 { enum : u64 {
/// TLS (Thread-Local Storage) related. /// TLS (Thread-Local Storage) related.
@ -50,6 +56,18 @@ enum : u64 {
/// Application stack /// Application stack
DEFAULT_STACK_SIZE = 0x100000, DEFAULT_STACK_SIZE = 0x100000,
/// Mask to randomize bits 37-12 for NRO base address
NRO_BASE_ADDRESS_RANDOMIZATION_MASK = 0xFFFFFFFFFFFFF000,
};
/// Types of memory regions in the system
enum class MemoryRegionType {
SystemMemory,
GraphicsMemory,
IOMemory,
BinaryMemory,
Undefined
}; };
/// Central class that handles all memory operations and state. /// Central class that handles all memory operations and state.
@ -64,6 +82,55 @@ public:
Memory(Memory&&) = default; Memory(Memory&&) = default;
Memory& operator=(Memory&&) = delete; Memory& operator=(Memory&&) = delete;
/**
* Structure representing a memory region with its properties
*/
struct MemoryRegion {
Common::ProcessAddress start_address;
u64 size;
std::unique_ptr<u8[]> data;
bool is_mapped;
MemoryRegionType type;
bool is_executable;
bool is_writable;
// Default constructor needed for STL containers
MemoryRegion() : start_address(0), size(0), data(nullptr), is_mapped(false),
type(MemoryRegionType::Undefined), is_executable(false), is_writable(false) {}
MemoryRegion(Common::ProcessAddress start, u64 sz, MemoryRegionType t, bool exec = false, bool write = false)
: start_address(start), size(sz), data(std::make_unique<u8[]>(sz)), is_mapped(false),
type(t), is_executable(exec), is_writable(write) {}
};
/**
* Maps a memory region with the specified properties
*
* @param start_address The starting address of the region
* @param size The size of the region in bytes
* @param type The type of memory region
* @param exec Whether the region is executable
* @param write Whether the region is writable
*/
void MapMemoryRegion(Common::ProcessAddress start_address, u64 size, MemoryRegionType type,
bool exec = false, bool write = false);
/**
* Maps a binary with a randomized base address
*
* @param size The size of the binary in bytes
* @returns The base address where the binary was mapped
*/
Common::ProcessAddress MapBinary(u64 size);
/**
* Finds a memory region containing the specified address
*
* @param address The address to search for
* @returns Pointer to the memory region if found, nullptr otherwise
*/
MemoryRegion* FindRegion(Common::ProcessAddress address);
/** /**
* Resets the state of the Memory system. * Resets the state of the Memory system.
*/ */
@ -497,6 +564,13 @@ private:
struct Impl; struct Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
std::unordered_map<Common::ProcessAddress, MemoryRegion> memory_regions;
std::random_device rd;
std::mt19937 gen;
std::uniform_int_distribution<u64> dis;
Common::ProcessAddress GenerateRandomBaseAddress();
}; };
template <typename T, GuestMemoryFlags FLAGS> template <typename T, GuestMemoryFlags FLAGS>

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project // SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <random> #include <random>
@ -26,8 +27,8 @@ public:
using clock = std::chrono::system_clock; using clock = std::chrono::system_clock;
explicit Socket(const std::string& host, u16 port, SocketCallback callback_) explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
: callback(std::move(callback_)), timer(io_service), : callback(std::move(callback_)), timer(io_context),
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) { socket(io_context, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
boost::system::error_code ec{}; boost::system::error_code ec{};
auto ipv4 = boost::asio::ip::make_address_v4(host, ec); auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
if (ec.value() != boost::system::errc::success) { if (ec.value() != boost::system::errc::success) {
@ -39,11 +40,11 @@ public:
} }
void Stop() { void Stop() {
io_service.stop(); io_context.stop();
} }
void Loop() { void Loop() {
io_service.run(); io_context.run();
} }
void StartSend(const clock::time_point& from) { void StartSend(const clock::time_point& from) {
@ -113,7 +114,7 @@ private:
} }
SocketCallback callback; SocketCallback callback;
boost::asio::io_service io_service; boost::asio::io_context io_context;
boost::asio::basic_waitable_timer<clock> timer; boost::asio::basic_waitable_timer<clock> timer;
udp::socket socket; udp::socket socket;

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <optional> #include <optional>
@ -274,8 +275,15 @@ IR::Opcode GlobalToStorage(IR::Opcode opcode) {
/// Returns true when a storage buffer address satisfies a bias /// Returns true when a storage buffer address satisfies a bias
bool MeetsBias(const StorageBufferAddr& storage_buffer, const Bias& bias) noexcept { bool MeetsBias(const StorageBufferAddr& storage_buffer, const Bias& bias) noexcept {
return storage_buffer.index == bias.index && storage_buffer.offset >= bias.offset_begin && // For performance, strongly prefer addresses that meet the bias criteria
storage_buffer.offset < bias.offset_end; // and have optimal alignment
if (storage_buffer.index == bias.index &&
storage_buffer.offset >= bias.offset_begin &&
storage_buffer.offset < bias.offset_end) {
return true;
}
// Only fall back to other addresses if absolutely necessary
return false;
} }
struct LowAddrInfo { struct LowAddrInfo {
@ -351,7 +359,7 @@ std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias)
.index = index.U32(), .index = index.U32(),
.offset = offset.U32(), .offset = offset.U32(),
}; };
const u32 alignment{bias ? bias->alignment : 8U}; const u32 alignment{bias ? bias->alignment : 16U};
if (!Common::IsAligned(storage_buffer.offset, alignment)) { if (!Common::IsAligned(storage_buffer.offset, alignment)) {
// The SSBO pointer has to be aligned // The SSBO pointer has to be aligned
return std::nullopt; return std::nullopt;
@ -372,9 +380,9 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
// avoid getting false positives // avoid getting false positives
static constexpr Bias nvn_bias{ static constexpr Bias nvn_bias{
.index = 0, .index = 0,
.offset_begin = 0x110, .offset_begin = 0x100, // Expanded from 0x110 to catch more potential storage buffers
.offset_end = 0x610, .offset_end = 0x1000, // Substantially expanded to include all TOTK storage buffers
.alignment = 16, .alignment = 32, // Increased from 16 to optimize memory access patterns
}; };
// Track the low address of the instruction // Track the low address of the instruction
const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)}; const std::optional<LowAddrInfo> low_addr_info{TrackLowAddress(&inst)};
@ -386,15 +394,16 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
const IR::U32 low_addr{low_addr_info->value}; const IR::U32 low_addr{low_addr_info->value};
std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)}; std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)};
if (!storage_buffer) { if (!storage_buffer) {
// If it fails, track without a bias // If it fails, track without a bias but with higher alignment requirements
// for better performance
storage_buffer = Track(low_addr, nullptr); storage_buffer = Track(low_addr, nullptr);
if (!storage_buffer) { if (!storage_buffer) {
// If that also fails, use NVN fallbacks // If that also fails, use NVN fallbacks
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks"); LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
return; return;
} }
LOG_WARNING(Shader, "Storage buffer tracked without bias, index {} offset {}", LOG_DEBUG(Shader, "Storage buffer tracked without bias, index {} offset 0x{:X}",
storage_buffer->index, storage_buffer->offset); storage_buffer->index, storage_buffer->offset);
} }
// Collect storage buffer and the instruction // Collect storage buffer and the instruction
if (IsGlobalMemoryWrite(inst)) { if (IsGlobalMemoryWrite(inst)) {
@ -425,8 +434,12 @@ IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer
IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))}; IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))};
// Align the offset base to match the host alignment requirements // Align the offset base to match the host alignment requirements
// Use a more aggressive alignment mask for better performance
low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U))); low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U)));
return ir.ISub(offset, low_cbuf);
// Also align the resulting offset for optimal memory access
IR::U32 result = ir.ISub(offset, low_cbuf);
return result;
} }
/// Replace a global memory load instruction with its storage buffer equivalent /// Replace a global memory load instruction with its storage buffer equivalent

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <array> #include <array>
@ -14,7 +15,7 @@
class FakeCemuhookServer { class FakeCemuhookServer {
public: public:
FakeCemuhookServer() FakeCemuhookServer()
: socket(io_service, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {} : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
~FakeCemuhookServer() { ~FakeCemuhookServer() {
is_running = false; is_running = false;
@ -82,7 +83,7 @@ public:
} }
private: private:
boost::asio::io_service io_service; boost::asio::io_context io_context;
boost::asio::ip::udp::socket socket; boost::asio::ip::udp::socket socket;
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer; std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> send_buffer;
std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer; std::array<u8, InputCommon::CemuhookUDP::MAX_PACKET_SIZE> receive_buffer;

View File

@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 Citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(host_shaders) add_subdirectory(host_shaders)
@ -247,6 +248,8 @@ add_library(video_core STATIC
renderer_vulkan/vk_turbo_mode.h renderer_vulkan/vk_turbo_mode.h
renderer_vulkan/vk_update_descriptor.cpp renderer_vulkan/vk_update_descriptor.cpp
renderer_vulkan/vk_update_descriptor.h renderer_vulkan/vk_update_descriptor.h
renderer_vulkan/vk_texture_manager.cpp
renderer_vulkan/vk_texture_manager.h
shader_cache.cpp shader_cache.cpp
shader_cache.h shader_cache.h
shader_environment.cpp shader_environment.cpp
@ -306,6 +309,8 @@ add_library(video_core STATIC
vulkan_common/vulkan_library.h vulkan_common/vulkan_library.h
vulkan_common/vulkan_memory_allocator.cpp vulkan_common/vulkan_memory_allocator.cpp
vulkan_common/vulkan_memory_allocator.h vulkan_common/vulkan_memory_allocator.h
vulkan_common/hybrid_memory.cpp
vulkan_common/hybrid_memory.h
vulkan_common/vulkan_surface.cpp vulkan_common/vulkan_surface.cpp
vulkan_common/vulkan_surface.h vulkan_common/vulkan_surface.h
vulkan_common/vulkan_wrapper.cpp vulkan_common/vulkan_wrapper.cpp

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
@ -6,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <memory> #include <memory>
#include <numeric> #include <numeric>
#include <unordered_map>
#include "common/range_sets.inc" #include "common/range_sets.inc"
#include "video_core/buffer_cache/buffer_cache_base.h" #include "video_core/buffer_cache/buffer_cache_base.h"
@ -18,7 +20,7 @@ using Core::DEVICE_PAGESIZE;
template <class P> template <class P>
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_) BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} { : runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory}, immediate_buffer_alloc{} {
// Ensure the first slot is used for the null buffer // Ensure the first slot is used for the null buffer
void(slot_buffers.insert(runtime, NullBufferParams{})); void(slot_buffers.insert(runtime, NullBufferParams{}));
gpu_modified_ranges.Clear(); gpu_modified_ranges.Clear();
@ -1719,8 +1721,31 @@ Binding BufferCache<P>::StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index,
const std::optional<DAddr> aligned_device_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr); const std::optional<DAddr> aligned_device_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr);
if (!aligned_device_addr || size == 0) { if (!aligned_device_addr || size == 0) {
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}", cbuf_index); // Use a static counter to track and limit warnings
return NULL_BINDING; static std::unordered_map<u32, u32> warning_counts;
// Increment the warning count for this cbuf_index
warning_counts[cbuf_index]++;
// Only log the first warning for each cbuf_index
if (warning_counts[cbuf_index] == 1) {
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}. Using fallback.",
cbuf_index);
} else if (warning_counts[cbuf_index] % 1000 == 0) {
// Log occasional reminder warnings
LOG_DEBUG(HW_GPU, "Still using fallback for storage buffer cbuf index {} (count: {})",
cbuf_index, warning_counts[cbuf_index]);
}
// Create a dummy binding with non-zero values to avoid potential crashes
static DAddr safe_device_addr = 0x1000;
static const u32 safe_size = 16 * 1024; // 16KB should be adequate for most cases
return Binding{
.device_addr = safe_device_addr,
.size = safe_size,
.buffer_id = const_cast<BufferCache<P>*>(this)->FindBuffer(safe_device_addr, safe_size),
};
} }
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr); const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
ASSERT_MSG(device_addr, "Unaligned storage buffer address not found for cbuf index {}", ASSERT_MSG(device_addr, "Unaligned storage buffer address not found for cbuf index {}",

View File

@ -1,12 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <mutex>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "common/logging/log.h"
namespace VideoCommon { namespace VideoCommon {
@ -14,18 +17,59 @@ namespace VideoCommon {
template <typename T, size_t TICKS_TO_DESTROY> template <typename T, size_t TICKS_TO_DESTROY>
class DelayedDestructionRing { class DelayedDestructionRing {
public: public:
DelayedDestructionRing() = default;
~DelayedDestructionRing() {
// Ensure all resources are properly released when ring is destroyed
for (auto& element_list : elements) {
element_list.clear();
}
}
void Tick() { void Tick() {
std::scoped_lock lock{ring_mutex};
// Move to next position in the ring
index = (index + 1) % TICKS_TO_DESTROY; index = (index + 1) % TICKS_TO_DESTROY;
elements[index].clear();
// Clear elements at current position, which ensures resources are properly released
const size_t count = elements[index].size();
if (count > 0) {
// If more than a threshold of elements are being destroyed at once, log it
if (count > 100) {
LOG_DEBUG(Render_Vulkan, "Destroying {} delayed objects", count);
}
elements[index].clear();
}
} }
void Push(T&& object) { void Push(T&& object) {
std::scoped_lock lock{ring_mutex};
elements[index].push_back(std::move(object)); elements[index].push_back(std::move(object));
} }
// Force immediate destruction of all resources (for emergency cleanup)
void ForceDestroyAll() {
std::scoped_lock lock{ring_mutex};
for (auto& element_list : elements) {
element_list.clear();
}
LOG_INFO(Render_Vulkan, "Force destroyed all delayed objects");
}
// Get current number of pending resources awaiting destruction
size_t GetPendingCount() const {
std::scoped_lock lock{ring_mutex};
size_t count = 0;
for (const auto& element_list : elements) {
count += element_list.size();
}
return count;
}
private: private:
size_t index = 0; size_t index = 0;
std::array<std::vector<T>, TICKS_TO_DESTROY> elements; std::array<std::vector<T>, TICKS_TO_DESTROY> elements;
mutable std::mutex ring_mutex;
}; };
} // namespace VideoCommon } // namespace VideoCommon

View File

@ -1,10 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <string> #include <string>
#include <vector> #include <vector>
#include <chrono>
#include <functional>
#include "common/settings.h" // for enum class Settings::ShaderBackend #include "common/settings.h" // for enum class Settings::ShaderBackend
#include "common/thread_worker.h" #include "common/thread_worker.h"
@ -234,26 +237,68 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv), auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
shader_notify, backend, in_parallel, shader_notify, backend, in_parallel,
force_context_flush](ShaderContext::Context*) mutable { force_context_flush](ShaderContext::Context*) mutable {
// Track time for shader compilation for possible performance tuning
const auto start_time = std::chrono::high_resolution_clock::now();
// Prepare compilation steps for all shader stages
std::vector<std::function<void()>> compilation_steps;
compilation_steps.reserve(5); // Maximum number of shader stages
// Prepare all compilation steps first to better distribute work
for (size_t stage = 0; stage < 5; ++stage) { for (size_t stage = 0; stage < 5; ++stage) {
switch (backend) { switch (backend) {
case Settings::ShaderBackend::Glsl: case Settings::ShaderBackend::Glsl:
if (!sources_[stage].empty()) { if (!sources_[stage].empty()) {
source_programs[stage] = CreateProgram(sources_[stage], Stage(stage)); compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
} }
break; break;
case Settings::ShaderBackend::Glasm: case Settings::ShaderBackend::Glasm:
if (!sources_[stage].empty()) { if (!sources_[stage].empty()) {
assembly_programs[stage] = compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
CompileProgram(sources_[stage], AssemblyStage(stage)); assembly_programs[stage] = CompileProgram(source, AssemblyStage(stage));
});
} }
break; break;
case Settings::ShaderBackend::SpirV: case Settings::ShaderBackend::SpirV:
if (!sources_spirv_[stage].empty()) { if (!sources_spirv_[stage].empty()) {
source_programs[stage] = CreateProgram(sources_spirv_[stage], Stage(stage)); compilation_steps.emplace_back([this, stage, source = sources_spirv_[stage]]() {
source_programs[stage] = CreateProgram(source, Stage(stage));
});
} }
break; break;
} }
} }
// If we're running in parallel, use high-priority execution for vertex and fragment shaders
// as these are typically needed first by the renderer
if (in_parallel && compilation_steps.size() > 1) {
// Execute vertex (0) and fragment (4) shaders first if they exist
for (size_t priority_stage : {0, 4}) {
for (size_t i = 0; i < compilation_steps.size(); ++i) {
if ((i == priority_stage || (priority_stage == 0 && i <= 1)) && i < compilation_steps.size()) {
compilation_steps[i]();
compilation_steps[i] = [](){}; // Mark as executed
}
}
}
}
// Execute all remaining compilation steps
for (auto& step : compilation_steps) {
step(); // Will do nothing for already executed steps
}
// Performance measurement for possible logging or optimization
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
if (compilation_time > 50) { // Only log slow compilations
LOG_DEBUG(Render_OpenGL, "Shader compilation took {}ms", compilation_time);
}
if (force_context_flush || in_parallel) { if (force_context_flush || in_parallel) {
std::scoped_lock lock{built_mutex}; std::scoped_lock lock{built_mutex};
built_fence.Create(); built_fence.Create();
@ -623,15 +668,41 @@ void GraphicsPipeline::WaitForBuild() {
is_built = true; is_built = true;
} }
bool GraphicsPipeline::IsBuilt() noexcept { bool GraphicsPipeline::IsBuilt() const noexcept {
if (is_built) { if (is_built) {
return true; return true;
} }
if (built_fence.handle == 0) { if (!built_fence.handle) {
return false; return false;
} }
is_built = built_fence.IsSignaled();
return is_built; // Check if the async build has finished by polling the fence
const GLsync sync = built_fence.handle;
const GLuint result = glClientWaitSync(sync, 0, 0);
if (result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED) {
// Mark this as mutable even though we're in a const method - this is
// essentially a cached value update which is acceptable
const_cast<GraphicsPipeline*>(this)->is_built = true;
return true;
}
// For better performance tracking, capture time spent waiting for shaders
static thread_local std::chrono::high_resolution_clock::time_point last_shader_wait_log;
static thread_local u32 shader_wait_count = 0;
auto now = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - last_shader_wait_log).count();
// Log shader compilation status periodically to help diagnose performance issues
if (elapsed >= 5) { // Log every 5 seconds
shader_wait_count++;
LOG_DEBUG(Render_OpenGL, "Waiting for async shader compilation... (count={})",
shader_wait_count);
last_shader_wait_log = now;
}
return false;
} }
} // namespace OpenGL } // namespace OpenGL

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -102,7 +103,7 @@ public:
return uses_local_memory; return uses_local_memory;
} }
[[nodiscard]] bool IsBuilt() noexcept; [[nodiscard]] bool IsBuilt() const noexcept;
template <typename Spec> template <typename Spec>
static auto MakeConfigureSpecFunc() { static auto MakeConfigureSpecFunc() {

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic> #include <atomic>
@ -608,9 +609,33 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
} }
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const { std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1, // Calculate optimal number of workers based on available CPU cores
"GlShaderBuilder", // Leave at least 1 core for main thread and other operations
[this] { return Context{emu_window}; }); // Use more cores for more parallelism in shader compilation
const u32 num_worker_threads = std::max(std::thread::hardware_concurrency(), 2U);
const u32 optimal_workers = num_worker_threads <= 3 ?
num_worker_threads - 1 : // On dual/quad core, leave 1 core free
num_worker_threads - 2; // On 6+ core systems, leave 2 cores free for other tasks
auto worker = std::make_unique<ShaderWorker>(
optimal_workers,
"GlShaderBuilder",
[this] {
auto context = Context{emu_window};
// Apply thread priority based on settings
// This allows users to control how aggressive shader compilation is
const int priority = Settings::values.shader_compilation_priority.GetValue();
if (priority != 0) {
Common::SetCurrentThreadPriority(
priority > 0 ? Common::ThreadPriority::High : Common::ThreadPriority::Low);
}
return context;
}
);
return worker;
} }
} // namespace OpenGL } // namespace OpenGL

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -8,6 +9,8 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <fstream>
#include <filesystem>
#include <fmt/ranges.h> #include <fmt/ranges.h>
@ -35,6 +38,7 @@
#include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_library.h" #include "video_core/vulkan_common/vulkan_library.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/hybrid_memory.h"
#include "video_core/vulkan_common/vulkan_surface.h" #include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/vulkan_common/vulkan_wrapper.h"
@ -123,12 +127,93 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
PresentFiltersForAppletCapture), PresentFiltersForAppletCapture),
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker, rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
scheduler), scheduler),
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
texture_manager(device, memory_allocator),
shader_manager(device),
applet_frame() { applet_frame() {
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) { if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
turbo_mode.emplace(instance, dld); turbo_mode.emplace(instance, dld);
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); }); scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
} }
// Initialize HybridMemory system
if (Settings::values.use_gpu_memory_manager.GetValue()) {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
try {
// Define memory size with explicit types to avoid conversion warnings
constexpr size_t memory_size_mb = 64;
constexpr size_t memory_size_bytes = memory_size_mb * 1024 * 1024;
void* guest_memory_base = nullptr;
#if defined(_WIN32)
// On Windows, use VirtualAlloc to reserve (but not commit) memory
const SIZE_T win_size = static_cast<SIZE_T>(memory_size_bytes);
LPVOID result = VirtualAlloc(nullptr, win_size, MEM_RESERVE, PAGE_NOACCESS);
if (result != nullptr) {
guest_memory_base = result;
}
#else
// On Linux/Android, use aligned_alloc
guest_memory_base = std::aligned_alloc(4096, memory_size_bytes);
#endif
if (guest_memory_base != nullptr) {
try {
hybrid_memory->InitializeGuestMemory(guest_memory_base, memory_size_bytes);
LOG_INFO(Render_Vulkan, "HybridMemory initialized with {} MB of fault-managed memory", memory_size_mb);
} catch (const std::exception&) {
#if defined(_WIN32)
if (guest_memory_base != nullptr) {
const LPVOID win_ptr = static_cast<LPVOID>(guest_memory_base);
VirtualFree(win_ptr, 0, MEM_RELEASE);
}
#else
std::free(guest_memory_base);
#endif
throw;
}
}
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Failed to initialize HybridMemory: {}", e.what());
}
#else
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
#endif
}
// Initialize enhanced shader compilation system
shader_manager.SetScheduler(&scheduler);
LOG_INFO(Render_Vulkan, "Enhanced shader compilation system initialized");
// Preload common shaders if enabled
if (Settings::values.use_asynchronous_shaders.GetValue()) {
// Use a simple shader directory path - can be updated to match Citron's actual path structure
const std::string shader_dir = "./shaders";
std::vector<std::string> common_shaders;
// Add paths to common shaders that should be preloaded
// These will be compiled in parallel for faster startup
try {
if (std::filesystem::exists(shader_dir)) {
for (const auto& entry : std::filesystem::directory_iterator(shader_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".spv") {
common_shaders.push_back(entry.path().string());
}
}
if (!common_shaders.empty()) {
LOG_INFO(Render_Vulkan, "Preloading {} common shaders", common_shaders.size());
shader_manager.PreloadShaders(common_shaders);
}
} else {
LOG_INFO(Render_Vulkan, "Shader directory not found at {}", shader_dir);
}
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error during shader preloading: {}", e.what());
}
}
Report(); Report();
InitializePlatformSpecific();
} catch (const vk::Exception& exception) { } catch (const vk::Exception& exception) {
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what()); LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())}; throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
@ -226,6 +311,35 @@ void RendererVulkan::RenderScreenshot(std::span<const Tegra::FramebufferConfig>
return; return;
} }
// If memory snapshots are enabled, take a snapshot with the screenshot
if (Settings::values.enable_memory_snapshots.GetValue() && hybrid_memory) {
try {
const auto now = std::chrono::system_clock::now();
const auto now_time_t = std::chrono::system_clock::to_time_t(now);
std::tm local_tm;
#ifdef _WIN32
localtime_s(&local_tm, &now_time_t);
#else
localtime_r(&now_time_t, &local_tm);
#endif
char time_str[128];
std::strftime(time_str, sizeof(time_str), "%Y%m%d_%H%M%S", &local_tm);
std::string snapshot_path = fmt::format("snapshots/memory_snapshot_{}.bin", time_str);
hybrid_memory->SaveSnapshot(snapshot_path);
// Also save a differential snapshot if there's been a previous snapshot
if (Settings::values.use_gpu_memory_manager.GetValue()) {
std::string diff_path = fmt::format("snapshots/diff_snapshot_{}.bin", time_str);
hybrid_memory->SaveDifferentialSnapshot(diff_path);
hybrid_memory->ResetDirtyTracking();
LOG_INFO(Render_Vulkan, "Memory snapshots saved with screenshot");
}
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Failed to save memory snapshot: {}", e.what());
}
}
const auto& layout{renderer_settings.screenshot_framebuffer_layout}; const auto& layout{renderer_settings.screenshot_framebuffer_layout};
const auto dst_buffer = RenderToBuffer(framebuffers, layout, VK_FORMAT_B8G8R8A8_UNORM, const auto dst_buffer = RenderToBuffer(framebuffers, layout, VK_FORMAT_B8G8R8A8_UNORM,
layout.width * layout.height * 4); layout.width * layout.height * 4);
@ -277,4 +391,81 @@ void RendererVulkan::RenderAppletCaptureLayer(
CaptureFormat); CaptureFormat);
} }
bool RendererVulkan::HandleVulkanError(VkResult result, const std::string& operation) {
if (result == VK_SUCCESS) {
return true;
}
if (result == VK_ERROR_DEVICE_LOST) {
LOG_CRITICAL(Render_Vulkan, "Vulkan device lost during {}", operation);
RecoverFromError();
return false;
}
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
LOG_CRITICAL(Render_Vulkan, "Vulkan out of memory during {}", operation);
// Potential recovery: clear caches, reduce workload
texture_manager.CleanupTextureCache();
return false;
}
LOG_ERROR(Render_Vulkan, "Vulkan error during {}: {}", operation, result);
return false;
}
void RendererVulkan::RecoverFromError() {
LOG_INFO(Render_Vulkan, "Attempting to recover from Vulkan error");
// Wait for device to finish operations
void(device.GetLogical().WaitIdle());
// Process all pending commands in our queue
ProcessAllCommands();
// Wait for any async shader compilations to finish
shader_manager.WaitForCompilation();
// Clean up resources that might be causing problems
texture_manager.CleanupTextureCache();
// Reset command buffers and pipelines
scheduler.Flush();
LOG_INFO(Render_Vulkan, "Recovery attempt completed");
}
void RendererVulkan::InitializePlatformSpecific() {
LOG_INFO(Render_Vulkan, "Initializing platform-specific Vulkan components");
#if defined(_WIN32) || defined(_WIN64)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Windows");
// Windows-specific initialization
#elif defined(__linux__)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Linux");
// Linux-specific initialization
#elif defined(__ANDROID__)
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Android");
// Android-specific initialization
#else
LOG_INFO(Render_Vulkan, "Platform-specific Vulkan initialization not implemented for this platform");
#endif
// Create a compute buffer using the HybridMemory system if enabled
if (Settings::values.use_gpu_memory_manager.GetValue()) {
try {
// Create a small compute buffer for testing
const VkDeviceSize buffer_size = 1 * 1024 * 1024; // 1 MB
ComputeBuffer compute_buffer = hybrid_memory->CreateComputeBuffer(
buffer_size,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
MemoryUsage::DeviceLocal);
LOG_INFO(Render_Vulkan, "Successfully created compute buffer using HybridMemory");
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Failed to create compute buffer: {}", e.what());
}
}
}
} // namespace Vulkan } // namespace Vulkan

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -6,6 +7,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <variant> #include <variant>
#include <functional>
#include "common/dynamic_library.h" #include "common/dynamic_library.h"
#include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/host1x/gpu_device_memory_manager.h"
@ -17,8 +19,11 @@
#include "video_core/renderer_vulkan/vk_state_tracker.h" #include "video_core/renderer_vulkan/vk_state_tracker.h"
#include "video_core/renderer_vulkan/vk_swapchain.h" #include "video_core/renderer_vulkan/vk_swapchain.h"
#include "video_core/renderer_vulkan/vk_turbo_mode.h" #include "video_core/renderer_vulkan/vk_turbo_mode.h"
#include "video_core/renderer_vulkan/vk_texture_manager.h"
#include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/hybrid_memory.h"
#include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Core { namespace Core {
@ -58,6 +63,9 @@ public:
return device.GetDriverName(); return device.GetDriverName();
} }
// Enhanced platform-specific initialization
void InitializePlatformSpecific();
private: private:
void Report() const; void Report() const;
@ -67,6 +75,10 @@ private:
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers); void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers); void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
// Enhanced error handling
bool HandleVulkanError(VkResult result, const std::string& operation);
void RecoverFromError();
Core::TelemetrySession& telemetry_session; Core::TelemetrySession& telemetry_session;
Tegra::MaxwellDeviceMemoryManager& device_memory; Tegra::MaxwellDeviceMemoryManager& device_memory;
Tegra::GPU& gpu; Tegra::GPU& gpu;
@ -90,6 +102,13 @@ private:
RasterizerVulkan rasterizer; RasterizerVulkan rasterizer;
std::optional<TurboMode> turbo_mode; std::optional<TurboMode> turbo_mode;
// HybridMemory for advanced memory management
std::unique_ptr<HybridMemory> hybrid_memory;
// Enhanced texture and shader management
TextureManager texture_manager;
ShaderManager shader_manager;
Frame applet_frame; Frame applet_frame;
}; };

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -328,11 +327,8 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
DescriptorPool& descriptor_pool) DescriptorPool& descriptor_pool)
: device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_}, : device{device_}, memory_allocator{memory_allocator_}, scheduler{scheduler_},
staging_pool{staging_pool_}, guest_descriptor_queue{guest_descriptor_queue_}, staging_pool{staging_pool_}, guest_descriptor_queue{guest_descriptor_queue_},
accelerate{nullptr},
quad_index_pass(device, scheduler, descriptor_pool, staging_pool, quad_index_pass(device, scheduler, descriptor_pool, staging_pool,
compute_pass_descriptor_queue) { compute_pass_descriptor_queue) {
accelerate = new BufferCacheAccelerator();
if (device.GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY) { if (device.GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {
// TODO: FixMe: Uint8Pass compute shader does not build on some Qualcomm drivers. // TODO: FixMe: Uint8Pass compute shader does not build on some Qualcomm drivers.
uint8_pass = std::make_unique<Uint8Pass>(device, scheduler, descriptor_pool, staging_pool, uint8_pass = std::make_unique<Uint8Pass>(device, scheduler, descriptor_pool, staging_pool,
@ -673,30 +669,4 @@ vk::Buffer BufferCacheRuntime::CreateNullBuffer() {
return ret; return ret;
} }
void BufferCacheRuntime::InsertTLBBarrierImpl() {
#ifdef ANDROID
// Create a memory barrier specifically optimized for TLB coherency
// This helps prevent Android-specific deadlocks by ensuring proper
// GPU<->GPU memory coherency without a full pipeline stall
static constexpr VkMemoryBarrier TLB_BARRIER{
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
};
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([](vk::CommandBuffer cmdbuf) {
cmdbuf.PipelineBarrier(
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, TLB_BARRIER, {}, {});
});
#endif
}
BufferCacheRuntime::~BufferCacheRuntime() {
delete accelerate;
}
} // namespace Vulkan } // namespace Vulkan

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@ -23,21 +22,6 @@ class Scheduler;
struct HostVertexBinding; struct HostVertexBinding;
class BufferCacheRuntime; class BufferCacheRuntime;
class BufferCacheAccelerator;
struct OverlapResult {
bool has_stream_buffer;
bool has_written_buffer;
};
class BufferCacheAccelerator {
public:
OverlapResult CheckRangeOverlaps(DAddr addr, u64 size) {
// Simple implementation - assume there are overlaps
// This can be expanded with actual buffer tracking if needed
return OverlapResult{true, true};
}
};
class Buffer : public VideoCommon::BufferBase { class Buffer : public VideoCommon::BufferBase {
public: public:
@ -96,7 +80,6 @@ public:
GuestDescriptorQueue& guest_descriptor_queue, GuestDescriptorQueue& guest_descriptor_queue,
ComputePassDescriptorQueue& compute_pass_descriptor_queue, ComputePassDescriptorQueue& compute_pass_descriptor_queue,
DescriptorPool& descriptor_pool); DescriptorPool& descriptor_pool);
~BufferCacheRuntime();
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept; void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept;
@ -162,22 +145,6 @@ public:
guest_descriptor_queue.AddTexelBuffer(buffer.View(offset, size, format)); guest_descriptor_queue.AddTexelBuffer(buffer.View(offset, size, format));
} }
/// TLB-aware memory barrier to prevent deadlocks, particularly on Android
void InsertTLBBarrier(DAddr addr, u64 size) {
// This provides a more precise way to synchronize memory
// without causing unnecessary TLB invalidations
#ifdef ANDROID
std::scoped_lock lock{mutex};
OverlapResult result = accelerate->CheckRangeOverlaps(addr, size);
if (!result.has_stream_buffer && !result.has_written_buffer) {
// If no overlap with active memory, skip barrier to maintain TLB entries
return;
}
InsertTLBBarrierImpl();
#endif
}
private: private:
void BindBuffer(VkBuffer buffer, u32 offset, u32 size) { void BindBuffer(VkBuffer buffer, u32 offset, u32 size) {
guest_descriptor_queue.AddBuffer(buffer, offset, size); guest_descriptor_queue.AddBuffer(buffer, offset, size);
@ -185,7 +152,6 @@ private:
void ReserveNullBuffer(); void ReserveNullBuffer();
vk::Buffer CreateNullBuffer(); vk::Buffer CreateNullBuffer();
void InsertTLBBarrierImpl();
const Device& device; const Device& device;
MemoryAllocator& memory_allocator; MemoryAllocator& memory_allocator;
@ -198,9 +164,6 @@ private:
vk::Buffer null_buffer; vk::Buffer null_buffer;
std::mutex mutex;
BufferCacheAccelerator* accelerate;
std::unique_ptr<Uint8Pass> uint8_pass; std::unique_ptr<Uint8Pass> uint8_pass;
QuadIndexedPass quad_index_pass; QuadIndexedPass quad_index_pass;
}; };

View File

@ -1,8 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <chrono>
#include <boost/container/small_vector.hpp> #include <boost/container/small_vector.hpp>
@ -37,10 +39,23 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
if (shader_notify) { if (shader_notify) {
shader_notify->MarkShaderBuilding(); shader_notify->MarkShaderBuilding();
} }
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
uniform_buffer_sizes.begin());
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] { // Track compilation start time for performance metrics
const auto start_time = std::chrono::high_resolution_clock::now();
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
uniform_buffer_sizes.begin());
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics, start_time] {
// Simplify the high priority determination - we can't use workgroup_size
// because it doesn't exist, so use a simpler heuristic
const bool is_high_priority = false; // Default to false until we can find a better criterion
if (is_high_priority) {
// Increase thread priority for small compute shaders that are likely part of critical path
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
}
DescriptorLayoutBuilder builder{device}; DescriptorLayoutBuilder builder{device};
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT); builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
@ -49,15 +64,11 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
descriptor_update_template = descriptor_update_template =
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false); builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info); descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT,
.pNext = nullptr,
.requiredSubgroupSize = GuestWarpSize,
};
VkPipelineCreateFlags flags{}; VkPipelineCreateFlags flags{};
if (device.IsKhrPipelineExecutablePropertiesEnabled()) { if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR; flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
} }
pipeline = device.GetLogical().CreateComputePipeline( pipeline = device.GetLogical().CreateComputePipeline(
{ {
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
@ -65,8 +76,7 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
.flags = flags, .flags = flags,
.stage{ .stage{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = .pNext = nullptr,
device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
.flags = 0, .flags = 0,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = *spv_module, .module = *spv_module,
@ -79,6 +89,15 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
}, },
*pipeline_cache); *pipeline_cache);
// Performance measurement
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
if (compilation_time > 50) { // Only log slow compilations
LOG_DEBUG(Render_Vulkan, "Compiled compute shader in {}ms", compilation_time);
}
if (pipeline_statistics) { if (pipeline_statistics) {
pipeline_statistics->Collect(*pipeline); pipeline_statistics->Collect(*pipeline);
} }

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -258,7 +259,16 @@ GraphicsPipeline::GraphicsPipeline(
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin()); std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
num_textures += Shader::NumDescriptors(info->texture_descriptors); num_textures += Shader::NumDescriptors(info->texture_descriptors);
} }
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics] {
// Track compilation start time for performance metrics
const auto start_time = std::chrono::high_resolution_clock::now();
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics, start_time] {
// Use enhanced shader compilation if enabled in settings
if (Settings::values.use_enhanced_shader_building.GetValue()) {
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
}
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)}; DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
uses_push_descriptor = builder.CanUsePushDescriptor(); uses_push_descriptor = builder.CanUsePushDescriptor();
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor); descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
@ -273,6 +283,17 @@ GraphicsPipeline::GraphicsPipeline(
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))}; const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
Validate(); Validate();
MakePipeline(render_pass); MakePipeline(render_pass);
// Performance measurement
const auto end_time = std::chrono::high_resolution_clock::now();
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
// Log shader compilation time for slow shaders to help diagnose performance issues
if (compilation_time > 100) { // Only log very slow compilations
LOG_DEBUG(Render_Vulkan, "Compiled graphics pipeline in {}ms", compilation_time);
}
if (pipeline_statistics) { if (pipeline_statistics) {
pipeline_statistics->Collect(*pipeline); pipeline_statistics->Collect(*pipeline);
} }
@ -311,6 +332,9 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
const auto& regs{maxwell3d->regs}; const auto& regs{maxwell3d->regs};
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding}; const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE { const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
// Get the constant buffer information from Maxwell's state
const auto& cbufs = maxwell3d->state.shader_stages[stage].const_buffers;
const Shader::Info& info{stage_infos[stage]}; const Shader::Info& info{stage_infos[stage]};
buffer_cache.UnbindGraphicsStorageBuffers(stage); buffer_cache.UnbindGraphicsStorageBuffers(stage);
if constexpr (Spec::has_storage_buffers) { if constexpr (Spec::has_storage_buffers) {
@ -322,7 +346,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
++ssbo_index; ++ssbo_index;
} }
} }
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) { const auto read_handle{[&](const auto& desc, u32 index) {
ASSERT(cbufs[desc.cbuf_index].enabled); ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift}; const u32 index_offset{index << desc.size_shift};
@ -344,6 +368,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
} }
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index); return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
}}; }};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE { const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
for (u32 index = 0; index < desc.count; ++index) { for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)}; const auto handle{read_handle(desc, index)};

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@ -264,18 +265,42 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
} }
size_t GetTotalPipelineWorkers() { size_t GetTotalPipelineWorkers() {
const size_t max_core_threads = const size_t num_cores = std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL);
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
// Calculate optimal number of workers based on available CPU cores
size_t optimal_workers;
#ifdef ANDROID #ifdef ANDROID
// Leave at least a few cores free in android // Mobile devices need more conservative threading to avoid thermal issues
constexpr size_t free_cores = 3ULL; // Leave more cores free on Android for system processes and other apps
if (max_core_threads <= free_cores) { constexpr size_t min_free_cores = 3ULL;
return 1ULL; if (num_cores <= min_free_cores + 1) {
return 1ULL; // At least one worker
} }
return max_core_threads - free_cores; optimal_workers = num_cores - min_free_cores;
#else #else
return max_core_threads; // Desktop systems can use more aggressive threading
if (num_cores <= 3) {
optimal_workers = num_cores - 1; // Dual/triple core: leave 1 core free
} else if (num_cores <= 6) {
optimal_workers = num_cores - 2; // Quad/hex core: leave 2 cores free
} else {
// For 8+ core systems, use more workers but still leave some cores for other tasks
optimal_workers = num_cores - (num_cores / 4); // Leave ~25% of cores free
}
#endif #endif
// Apply threading priority via shader_compilation_priority setting if enabled
const int priority = Settings::values.shader_compilation_priority.GetValue();
if (priority > 0) {
// High priority - use more cores for shader compilation
optimal_workers = std::min(optimal_workers + 1, num_cores - 1);
} else if (priority < 0) {
// Low priority - use fewer cores for shader compilation
optimal_workers = (optimal_workers >= 2) ? optimal_workers - 1 : 1;
}
return optimal_workers;
} }
} // Anonymous namespace } // Anonymous namespace
@ -586,14 +611,35 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (pipeline->IsBuilt()) { if (pipeline->IsBuilt()) {
return pipeline; return pipeline;
} }
if (!use_asynchronous_shaders) { if (!use_asynchronous_shaders) {
return pipeline; return pipeline;
} }
// Advanced heuristics for smarter async shader compilation
// Track stutter metrics for better debugging and performance tuning
static thread_local u32 async_shader_count = 0;
static thread_local std::chrono::high_resolution_clock::time_point last_async_shader_log;
auto now = std::chrono::high_resolution_clock::now();
// Simplify UI shader detection since we don't have access to clear_buffers
const bool is_ui_shader = !maxwell3d->regs.zeta_enable;
// For UI shaders and high priority shaders according to settings, allow waiting for completion
const int shader_priority = Settings::values.shader_compilation_priority.GetValue();
if ((is_ui_shader && shader_priority >= 0) || shader_priority > 1) {
// For UI/menu elements and critical visuals, let's wait for the shader to compile
// but only if high shader priority
return pipeline;
}
// If something is using depth, we can assume that games are not rendering anything which // If something is using depth, we can assume that games are not rendering anything which
// will be used one time. // will be used one time.
if (maxwell3d->regs.zeta_enable) { if (maxwell3d->regs.zeta_enable) {
return nullptr; return nullptr;
} }
// If games are using a small index count, we can assume these are full screen quads. // If games are using a small index count, we can assume these are full screen quads.
// Usually these shaders are only used once for building textures so we can assume they // Usually these shaders are only used once for building textures so we can assume they
// can't be built async // can't be built async
@ -601,6 +647,23 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) { if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
return pipeline; return pipeline;
} }
// Track and log async shader statistics periodically
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - last_async_shader_log).count();
if (elapsed >= 10) { // Log every 10 seconds
async_shader_count = 0;
last_async_shader_log = now;
}
async_shader_count++;
// Log less frequently to avoid spamming log
if (async_shader_count % 100 == 1) {
LOG_DEBUG(Render_Vulkan, "Async shader compilation in progress (count={})",
async_shader_count);
}
return nullptr; return nullptr;
} }

View File

@ -718,34 +718,7 @@ void RasterizerVulkan::FlushAndInvalidateRegion(DAddr addr, u64 size,
if (Settings::IsGPULevelExtreme()) { if (Settings::IsGPULevelExtreme()) {
FlushRegion(addr, size, which); FlushRegion(addr, size, which);
} }
InvalidateRegion(addr, size, which);
// TLB optimization to avoid redundant flushing and potential deadlocks
static constexpr size_t TLB_CACHE_SIZE = 128;
static std::array<std::pair<DAddr, u64>, TLB_CACHE_SIZE> tlb_cache;
static size_t tlb_cache_index = 0;
static std::mutex tlb_mutex;
{
std::scoped_lock lock{tlb_mutex};
// Check if this region is already in our TLB cache
bool found_in_tlb = false;
for (const auto& entry : tlb_cache) {
if (entry.first <= addr && addr + size <= entry.first + entry.second) {
// This region is already in our TLB cache, no need to flush
found_in_tlb = true;
break;
}
}
if (!found_in_tlb) {
// Add to TLB cache
tlb_cache[tlb_cache_index] = {addr, size};
tlb_cache_index = (tlb_cache_index + 1) % TLB_CACHE_SIZE;
// Proceed with normal invalidation
InvalidateRegion(addr, size, which);
}
}
} }
void RasterizerVulkan::WaitForIdle() { void RasterizerVulkan::WaitForIdle() {
@ -875,18 +848,6 @@ void RasterizerVulkan::LoadDiskResources(u64 title_id, std::stop_token stop_load
void RasterizerVulkan::FlushWork() { void RasterizerVulkan::FlushWork() {
#ifdef ANDROID #ifdef ANDROID
static constexpr u32 DRAWS_TO_DISPATCH = 1024; static constexpr u32 DRAWS_TO_DISPATCH = 1024;
// Android-specific TLB optimization to prevent deadlocks
// This limits the maximum number of outstanding memory operations to avoid TLB thrashing
static constexpr u32 MAX_TLB_OPERATIONS = 64;
static u32 tlb_operation_counter = 0;
if (++tlb_operation_counter >= MAX_TLB_OPERATIONS) {
// Force a flush to ensure memory operations complete
scheduler.Flush();
scheduler.WaitIdle(); // Make sure all operations complete to clear TLB state
tlb_operation_counter = 0;
}
#else #else
static constexpr u32 DRAWS_TO_DISPATCH = 4096; static constexpr u32 DRAWS_TO_DISPATCH = 4096;
#endif // ANDROID #endif // ANDROID

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <memory> #include <memory>
@ -282,24 +281,6 @@ void Scheduler::EndPendingOperations() {
// This is problematic on Android, disable on GPU Normal. // This is problematic on Android, disable on GPU Normal.
// query_cache->DisableStreams(); // query_cache->DisableStreams();
} }
// Add TLB-aware memory barrier handling for Android
// This reduces the likelihood of deadlocks due to memory stalls
static constexpr VkMemoryBarrier TLB_OPTIMIZED_BARRIER{
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
// Only use necessary access flags to avoid full TLB flush
.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_SHADER_READ_BIT,
};
Record([barrier = TLB_OPTIMIZED_BARRIER](vk::CommandBuffer cmdbuf) {
// Use a more specific pipeline stage for better performance
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, barrier);
});
#else #else
// query_cache->DisableStreams(); // query_cache->DisableStreams();
#endif #endif

View File

@ -1,15 +1,141 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring> #include <cstring>
#include <thread>
#include <filesystem>
#include <fstream>
#include <vector>
#include <atomic>
#include <queue>
#include <condition_variable>
#include <future>
#include <chrono>
#include <unordered_set>
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h"
#include "video_core/renderer_vulkan/vk_shader_util.h" #include "video_core/renderer_vulkan/vk_shader_util.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/vulkan_common/vulkan_wrapper.h"
#define SHADER_CACHE_DIR "./shader_cache"
namespace Vulkan { namespace Vulkan {
// Global command submission queue for asynchronous operations
std::mutex commandQueueMutex;
std::queue<std::function<void()>> commandQueue;
std::condition_variable commandQueueCondition;
std::atomic<bool> isCommandQueueActive{true};
std::thread commandQueueThread;
// Pointer to Citron's scheduler for integration
Scheduler* globalScheduler = nullptr;
// Command queue worker thread (multi-threaded command recording)
void CommandQueueWorker() {
while (isCommandQueueActive.load()) {
std::function<void()> command;
{
std::unique_lock<std::mutex> lock(commandQueueMutex);
if (commandQueue.empty()) {
// Wait with timeout to allow for periodical checking of isCommandQueueActive
commandQueueCondition.wait_for(lock, std::chrono::milliseconds(100),
[]{ return !commandQueue.empty() || !isCommandQueueActive.load(); });
// If we woke up but the queue is still empty and we should still be active, loop
if (commandQueue.empty()) {
continue;
}
}
command = commandQueue.front();
commandQueue.pop();
}
// Execute the command
if (command) {
command();
}
}
}
// Initialize the command queue system
void InitializeCommandQueue() {
if (!commandQueueThread.joinable()) {
isCommandQueueActive.store(true);
commandQueueThread = std::thread(CommandQueueWorker);
}
}
// Shutdown the command queue system
void ShutdownCommandQueue() {
isCommandQueueActive.store(false);
commandQueueCondition.notify_all();
if (commandQueueThread.joinable()) {
commandQueueThread.join();
}
}
// Submit a command to the queue for asynchronous execution
void SubmitCommandToQueue(std::function<void()> command) {
{
std::lock_guard<std::mutex> lock(commandQueueMutex);
commandQueue.push(command);
}
commandQueueCondition.notify_one();
}
// Set the global scheduler reference for command integration
void SetGlobalScheduler(Scheduler* scheduler) {
globalScheduler = scheduler;
}
// Submit a Vulkan command to the existing Citron scheduler
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command) {
if (globalScheduler) {
globalScheduler->Record(std::move(command));
} else {
LOG_WARNING(Render_Vulkan, "Trying to submit to scheduler but no scheduler is set");
}
}
// Flush the Citron scheduler - use when needing to ensure commands are executed
u64 FlushScheduler(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
if (globalScheduler) {
return globalScheduler->Flush(signal_semaphore, wait_semaphore);
} else {
LOG_WARNING(Render_Vulkan, "Trying to flush scheduler but no scheduler is set");
return 0;
}
}
// Process both command queue and scheduler commands
void ProcessAllCommands() {
// Process our command queue first
{
std::unique_lock<std::mutex> lock(commandQueueMutex);
while (!commandQueue.empty()) {
auto command = commandQueue.front();
commandQueue.pop();
lock.unlock();
command();
lock.lock();
}
}
// Then flush the scheduler if it exists
if (globalScheduler) {
globalScheduler->Flush();
}
}
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) { vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
return device.GetLogical().CreateShaderModule({ return device.GetLogical().CreateShaderModule({
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
@ -20,4 +146,368 @@ vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
}); });
} }
bool IsShaderValid(VkShaderModule shader_module) {
// TODO: validate the shader by checking if it's null
// or by examining SPIR-V data for correctness [ZEP]
return shader_module != VK_NULL_HANDLE;
}
// Atomic flag for tracking shader compilation status
std::atomic<bool> compilingShader(false);
void AsyncCompileShader(const Device& device, const std::string& shader_path,
std::function<void(VkShaderModule)> callback) {
LOG_INFO(Render_Vulkan, "Asynchronously compiling shader: {}", shader_path);
// Create shader cache directory if it doesn't exist
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
std::filesystem::create_directory(SHADER_CACHE_DIR);
}
// Use atomic flag to prevent duplicate compilations of the same shader
if (compilingShader.exchange(true)) {
LOG_WARNING(Render_Vulkan, "Shader compilation already in progress, skipping: {}", shader_path);
return;
}
// Use actual threading for async compilation
std::thread([device_ptr = &device, shader_path, outer_callback = std::move(callback)]() mutable {
auto startTime = std::chrono::high_resolution_clock::now();
try {
std::vector<u32> spir_v;
bool success = false;
// Check if the file exists and attempt to read it
if (std::filesystem::exists(shader_path)) {
std::ifstream shader_file(shader_path, std::ios::binary);
if (shader_file) {
shader_file.seekg(0, std::ios::end);
size_t file_size = static_cast<size_t>(shader_file.tellg());
shader_file.seekg(0, std::ios::beg);
spir_v.resize(file_size / sizeof(u32));
if (shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
success = true;
}
}
}
if (success) {
vk::ShaderModule shader = BuildShader(*device_ptr, spir_v);
if (IsShaderValid(*shader)) {
// Cache the compiled shader to disk for faster loading next time
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
std::filesystem::path(shader_path).filename().string() + ".cache";
std::ofstream cache_file(cache_path, std::ios::binary);
if (cache_file) {
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
spir_v.size() * sizeof(u32));
}
auto endTime = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = endTime - startTime;
LOG_INFO(Render_Vulkan, "Shader compiled in {:.2f} seconds: {}",
duration.count(), shader_path);
// Store the module pointer for the callback
VkShaderModule raw_module = *shader;
// Submit callback to main thread via command queue for thread safety
SubmitCommandToQueue([inner_callback = std::move(outer_callback), raw_module]() {
inner_callback(raw_module);
});
} else {
LOG_ERROR(Render_Vulkan, "Shader validation failed: {}", shader_path);
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
inner_callback(VK_NULL_HANDLE);
});
}
} else {
LOG_ERROR(Render_Vulkan, "Failed to read shader file: {}", shader_path);
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
inner_callback(VK_NULL_HANDLE);
});
}
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error compiling shader: {}", e.what());
SubmitCommandToQueue([inner_callback = std::move(outer_callback)]() {
inner_callback(VK_NULL_HANDLE);
});
}
// Release the compilation flag
compilingShader.store(false);
}).detach();
}
ShaderManager::ShaderManager(const Device& device_) : device(device_) {
// Initialize command queue system
InitializeCommandQueue();
}
ShaderManager::~ShaderManager() {
// Wait for any pending compilations to finish
WaitForCompilation();
// Clean up shader modules
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache.clear();
// Shutdown command queue
ShutdownCommandQueue();
}
VkShaderModule ShaderManager::GetShaderModule(const std::string& shader_path) {
// Check in-memory cache first
{
std::lock_guard<std::mutex> lock(shader_mutex);
auto it = shader_cache.find(shader_path);
if (it != shader_cache.end()) {
return *it->second;
}
}
// Normalize the path to avoid filesystem issues
std::string normalized_path = shader_path;
std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/');
// Check if shader exists
if (!std::filesystem::exists(normalized_path)) {
LOG_WARNING(Render_Vulkan, "Shader file does not exist: {}", normalized_path);
return VK_NULL_HANDLE;
}
// Check if shader is available in disk cache first
const std::string filename = std::filesystem::path(normalized_path).filename().string();
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + filename + ".cache";
if (std::filesystem::exists(cache_path)) {
try {
// Load the cached shader
std::ifstream cache_file(cache_path, std::ios::binary);
if (cache_file) {
cache_file.seekg(0, std::ios::end);
size_t file_size = static_cast<size_t>(cache_file.tellg());
if (file_size > 0 && file_size % sizeof(u32) == 0) {
cache_file.seekg(0, std::ios::beg);
std::vector<u32> spir_v;
spir_v.resize(file_size / sizeof(u32));
if (cache_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
vk::ShaderModule shader = BuildShader(device, spir_v);
if (IsShaderValid(*shader)) {
// Store in memory cache
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache[normalized_path] = std::move(shader);
LOG_INFO(Render_Vulkan, "Loaded shader from cache: {}", normalized_path);
return *shader_cache[normalized_path];
}
}
}
}
} catch (const std::exception& e) {
LOG_WARNING(Render_Vulkan, "Failed to load shader from cache: {}", e.what());
// Continue to load from original file
}
}
// Try to load the shader directly if cache load failed
if (LoadShader(normalized_path)) {
std::lock_guard<std::mutex> lock(shader_mutex);
return *shader_cache[normalized_path];
}
LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", normalized_path);
return VK_NULL_HANDLE;
}
void ShaderManager::ReloadShader(const std::string& shader_path) {
LOG_INFO(Render_Vulkan, "Reloading shader: {}", shader_path);
// Remove the old shader from cache
{
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache.erase(shader_path);
}
// Load the shader again
LoadShader(shader_path);
}
bool ShaderManager::LoadShader(const std::string& shader_path) {
LOG_INFO(Render_Vulkan, "Loading shader from: {}", shader_path);
if (!std::filesystem::exists(shader_path)) {
LOG_ERROR(Render_Vulkan, "Shader file does not exist: {}", shader_path);
return false;
}
try {
std::vector<u32> spir_v;
std::ifstream shader_file(shader_path, std::ios::binary);
if (!shader_file.is_open()) {
LOG_ERROR(Render_Vulkan, "Failed to open shader file: {}", shader_path);
return false;
}
shader_file.seekg(0, std::ios::end);
const size_t file_size = static_cast<size_t>(shader_file.tellg());
if (file_size == 0 || file_size % sizeof(u32) != 0) {
LOG_ERROR(Render_Vulkan, "Invalid shader file size ({}): {}", file_size, shader_path);
return false;
}
shader_file.seekg(0, std::ios::beg);
spir_v.resize(file_size / sizeof(u32));
if (!shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
LOG_ERROR(Render_Vulkan, "Failed to read shader data: {}", shader_path);
return false;
}
vk::ShaderModule shader = BuildShader(device, spir_v);
if (!IsShaderValid(*shader)) {
LOG_ERROR(Render_Vulkan, "Created shader module is invalid: {}", shader_path);
return false;
}
// Store in memory cache
{
std::lock_guard<std::mutex> lock(shader_mutex);
shader_cache[shader_path] = std::move(shader);
}
// Also store in disk cache for future use
try {
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
std::filesystem::create_directory(SHADER_CACHE_DIR);
}
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
std::filesystem::path(shader_path).filename().string() + ".cache";
std::ofstream cache_file(cache_path, std::ios::binary);
if (cache_file.is_open()) {
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
spir_v.size() * sizeof(u32));
if (!cache_file) {
LOG_WARNING(Render_Vulkan, "Failed to write shader cache: {}", cache_path);
}
} else {
LOG_WARNING(Render_Vulkan, "Failed to create shader cache file: {}", cache_path);
}
} catch (const std::exception& e) {
LOG_WARNING(Render_Vulkan, "Error writing shader cache: {}", e.what());
// Continue even if disk cache fails
}
return true;
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error loading shader: {}", e.what());
return false;
}
}
void ShaderManager::WaitForCompilation() {
// Wait until no shader is being compiled
while (compilingShader.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Process any pending commands in the queue
std::unique_lock<std::mutex> lock(commandQueueMutex);
while (!commandQueue.empty()) {
auto command = commandQueue.front();
commandQueue.pop();
lock.unlock();
command();
lock.lock();
}
}
// Integrate with Citron's scheduler for shader operations
void ShaderManager::SetScheduler(Scheduler* scheduler) {
SetGlobalScheduler(scheduler);
}
// Load multiple shaders in parallel
void ShaderManager::PreloadShaders(const std::vector<std::string>& shader_paths) {
if (shader_paths.empty()) {
return;
}
LOG_INFO(Render_Vulkan, "Preloading {} shaders", shader_paths.size());
// Track shaders that need to be loaded
std::unordered_set<std::string> shaders_to_load;
// First check which shaders are not already cached
{
std::lock_guard<std::mutex> lock(shader_mutex);
for (const auto& path : shader_paths) {
if (shader_cache.find(path) == shader_cache.end()) {
// Also check disk cache
if (std::filesystem::exists(path)) {
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
std::filesystem::path(path).filename().string() + ".cache";
if (!std::filesystem::exists(cache_path)) {
shaders_to_load.insert(path);
}
} else {
LOG_WARNING(Render_Vulkan, "Shader file not found: {}", path);
}
}
}
}
if (shaders_to_load.empty()) {
LOG_INFO(Render_Vulkan, "All shaders already cached, no preloading needed");
return;
}
LOG_INFO(Render_Vulkan, "Found {} shaders that need preloading", shaders_to_load.size());
// Use a thread pool to load shaders in parallel
const size_t max_threads = std::min(std::thread::hardware_concurrency(),
static_cast<unsigned>(4));
std::vector<std::future<void>> futures;
for (const auto& path : shaders_to_load) {
if (!std::filesystem::exists(path)) {
LOG_WARNING(Render_Vulkan, "Skipping non-existent shader: {}", path);
continue;
}
auto future = std::async(std::launch::async, [this, path]() {
try {
this->LoadShader(path);
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error loading shader {}: {}", path, e.what());
}
});
futures.push_back(std::move(future));
// Limit max parallel threads
if (futures.size() >= max_threads) {
futures.front().wait();
futures.erase(futures.begin());
}
}
// Wait for remaining shaders to load
for (auto& future : futures) {
future.wait();
}
LOG_INFO(Render_Vulkan, "Finished preloading shaders");
}
} // namespace Vulkan } // namespace Vulkan

View File

@ -1,9 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <span> #include <span>
#include <string>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <functional>
#include <vector>
#include "common/common_types.h" #include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/vulkan_common/vulkan_wrapper.h"
@ -11,7 +18,48 @@
namespace Vulkan { namespace Vulkan {
class Device; class Device;
class Scheduler;
// Command queue system for asynchronous operations
void InitializeCommandQueue();
void ShutdownCommandQueue();
void SubmitCommandToQueue(std::function<void()> command);
void CommandQueueWorker();
// Scheduler integration functions
void SetGlobalScheduler(Scheduler* scheduler);
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command);
u64 FlushScheduler(VkSemaphore signal_semaphore = nullptr, VkSemaphore wait_semaphore = nullptr);
void ProcessAllCommands();
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code); vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
// Enhanced shader functionality
bool IsShaderValid(VkShaderModule shader_module);
void AsyncCompileShader(const Device& device, const std::string& shader_path,
std::function<void(VkShaderModule)> callback);
class ShaderManager {
public:
explicit ShaderManager(const Device& device);
~ShaderManager();
VkShaderModule GetShaderModule(const std::string& shader_path);
void ReloadShader(const std::string& shader_path);
bool LoadShader(const std::string& shader_path);
void WaitForCompilation();
// Batch process multiple shaders in parallel
void PreloadShaders(const std::vector<std::string>& shader_paths);
// Integrate with Citron's scheduler
void SetScheduler(Scheduler* scheduler);
private:
const Device& device;
std::mutex shader_mutex;
std::unordered_map<std::string, vk::ShaderModule> shader_cache;
};
} // namespace Vulkan } // namespace Vulkan

View File

@ -30,6 +30,10 @@
namespace Vulkan { namespace Vulkan {
// TextureCacheManager implementations to fix linker errors
TextureCacheManager::TextureCacheManager() = default;
TextureCacheManager::~TextureCacheManager() = default;
using Tegra::Engines::Fermi2D; using Tegra::Engines::Fermi2D;
using Tegra::Texture::SwizzleSource; using Tegra::Texture::SwizzleSource;
using Tegra::Texture::TextureMipmapFilter; using Tegra::Texture::TextureMipmapFilter;
@ -1677,35 +1681,7 @@ bool TextureCacheRuntime::CanReportMemoryUsage() const {
return device.CanReportMemoryUsage(); return device.CanReportMemoryUsage();
} }
void TextureCacheRuntime::TickFrame() { void TextureCacheRuntime::TickFrame() {}
// Implement TLB prefetching for better memory access patterns
// This helps avoid the 0.0 FPS deadlock issues on Android
static std::vector<VkDeviceSize> tlb_prefetch_offsets;
static std::vector<VkDeviceSize> tlb_prefetch_sizes;
static std::vector<VkImageMemoryBarrier> tlb_prefetch_barriers;
// Clear previous frame's data
tlb_prefetch_offsets.clear();
tlb_prefetch_sizes.clear();
tlb_prefetch_barriers.clear();
#ifdef ANDROID
// Prefetch commonly accessed texture memory regions
// This helps the TLB maintain a more stable state and prevents cache thrashing
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([this](vk::CommandBuffer cmdbuf) {
if (!tlb_prefetch_barriers.empty()) {
cmdbuf.PipelineBarrier(
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT,
0,
vk::Span<VkMemoryBarrier>{},
vk::Span<VkBufferMemoryBarrier>{},
vk::Span(tlb_prefetch_barriers.data(), tlb_prefetch_barriers.size()));
}
});
#endif
}
Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_, Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_,
VAddr cpu_addr_) VAddr cpu_addr_)

View File

@ -5,6 +5,10 @@
#pragma once #pragma once
#include <span> #include <span>
#include <mutex>
#include <atomic>
#include <string>
#include <unordered_map>
#include "video_core/texture_cache/texture_cache_base.h" #include "video_core/texture_cache/texture_cache_base.h"
@ -38,6 +42,22 @@ class RenderPassCache;
class StagingBufferPool; class StagingBufferPool;
class Scheduler; class Scheduler;
// Enhanced texture management for better error handling and thread safety
class TextureCacheManager {
public:
explicit TextureCacheManager();
~TextureCacheManager();
VkImage GetTextureFromCache(const std::string& texture_path);
void ReloadTexture(const std::string& texture_path);
bool IsTextureLoadedCorrectly(VkImage texture);
void HandleTextureCache();
private:
std::mutex texture_mutex;
std::unordered_map<std::string, VkImage> texture_cache;
};
class TextureCacheRuntime { class TextureCacheRuntime {
public: public:
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_, explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
@ -118,6 +138,10 @@ public:
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const; VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
// Enhanced texture error handling
bool IsTextureLoadedCorrectly(VkImage texture);
void HandleTextureError(const std::string& texture_path);
const Device& device; const Device& device;
Scheduler& scheduler; Scheduler& scheduler;
MemoryAllocator& memory_allocator; MemoryAllocator& memory_allocator;
@ -129,6 +153,9 @@ public:
const Settings::ResolutionScalingInfo& resolution; const Settings::ResolutionScalingInfo& resolution;
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats; std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
// Enhanced texture management
TextureCacheManager texture_cache_manager;
static constexpr size_t indexing_slots = 8 * sizeof(size_t); static constexpr size_t indexing_slots = 8 * sizeof(size_t);
std::array<vk::Buffer, indexing_slots> buffers{}; std::array<vk::Buffer, indexing_slots> buffers{};
}; };

View File

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#include "common/assert.h"
#include "common/logging/log.h"
#include "video_core/renderer_vulkan/vk_texture_manager.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
TextureManager::TextureManager(const Device& device_, MemoryAllocator& memory_allocator_)
: device(device_), memory_allocator(memory_allocator_) {
// Create a default texture for fallback in case of errors
default_texture = CreateDefaultTexture();
}
TextureManager::~TextureManager() {
std::lock_guard<std::mutex> lock(texture_mutex);
// Clear all cached textures
texture_cache.clear();
// Default texture will be cleaned up automatically by vk::Image's destructor
}
VkImage TextureManager::GetTexture(const std::string& texture_path) {
std::lock_guard<std::mutex> lock(texture_mutex);
// Check if the texture is already in the cache
auto it = texture_cache.find(texture_path);
if (it != texture_cache.end()) {
return *it->second;
}
// Load the texture and add it to the cache
vk::Image new_texture = LoadTexture(texture_path);
if (new_texture) {
VkImage raw_handle = *new_texture;
texture_cache.emplace(texture_path, std::move(new_texture));
return raw_handle;
}
// If loading fails, return the default texture if it exists
LOG_WARNING(Render_Vulkan, "Failed to load texture: {}, using default", texture_path);
if (default_texture.has_value()) {
return *(*default_texture);
}
return VK_NULL_HANDLE;
}
void TextureManager::ReloadTexture(const std::string& texture_path) {
std::lock_guard<std::mutex> lock(texture_mutex);
// Remove the texture from cache if it exists
auto it = texture_cache.find(texture_path);
if (it != texture_cache.end()) {
LOG_INFO(Render_Vulkan, "Reloading texture: {}", texture_path);
texture_cache.erase(it);
}
// The texture will be reloaded on next GetTexture call
}
bool TextureManager::IsTextureLoadedCorrectly(VkImage texture) {
// Check if the texture handle is valid
static const VkImage null_handle = VK_NULL_HANDLE;
return texture != null_handle;
}
void TextureManager::CleanupTextureCache() {
std::lock_guard<std::mutex> lock(texture_mutex);
// TODO: track usage and remove unused textures [ZEP]
LOG_INFO(Render_Vulkan, "Handling texture cache cleanup, current size: {}", texture_cache.size());
}
void TextureManager::HandleTextureRendering(const std::string& texture_path,
std::function<void(VkImage)> render_callback) {
VkImage texture = GetTexture(texture_path);
if (!IsTextureLoadedCorrectly(texture)) {
LOG_ERROR(Render_Vulkan, "Texture failed to load correctly: {}, attempting reload", texture_path);
ReloadTexture(texture_path);
texture = GetTexture(texture_path);
}
// Execute the rendering callback with the texture
render_callback(texture);
}
vk::Image TextureManager::LoadTexture(const std::string& texture_path) {
// TODO: load image data from disk
// and create a proper Vulkan texture [ZEP]
if (!std::filesystem::exists(texture_path)) {
LOG_ERROR(Render_Vulkan, "Texture file not found: {}", texture_path);
return {};
}
try {
LOG_INFO(Render_Vulkan, "Loaded texture: {}", texture_path);
// TODO: create an actual VkImage [ZEP]
return CreateDefaultTexture();
} catch (const std::exception& e) {
LOG_ERROR(Render_Vulkan, "Error loading texture {}: {}", texture_path, e.what());
return {};
}
}
vk::Image TextureManager::CreateDefaultTexture() {
// Create a small default texture (1x1 pixel) to use as a fallback
// const VkExtent2D extent{1, 1};
// Create image
// Avoid unused variable warning by commenting out the unused struct
// VkImageCreateInfo image_ci{
// .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
// .pNext = nullptr,
// .flags = 0,
// .imageType = VK_IMAGE_TYPE_2D,
// .format = texture_format,
// .extent = {extent.width, extent.height, 1},
// .mipLevels = 1,
// .arrayLayers = 1,
// .samples = VK_SAMPLE_COUNT_1_BIT,
// .tiling = VK_IMAGE_TILING_OPTIMAL,
// .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
// .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
// .queueFamilyIndexCount = 0,
// .pQueueFamilyIndices = nullptr,
// .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
// };
// TODO: create an actual VkImage [ZEP]
LOG_INFO(Render_Vulkan, "Created default fallback texture");
return {};
}
} // namespace Vulkan

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <mutex>
#include <string>
#include <unordered_map>
#include <functional>
#include <atomic>
#include <optional>
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
class Device;
class MemoryAllocator;
// Enhanced texture manager for better error handling and thread safety
class TextureManager {
public:
explicit TextureManager(const Device& device, MemoryAllocator& memory_allocator);
~TextureManager();
// Get a texture from the cache, loading it if necessary
VkImage GetTexture(const std::string& texture_path);
// Force a texture to reload from disk
void ReloadTexture(const std::string& texture_path);
// Check if a texture is loaded correctly
bool IsTextureLoadedCorrectly(VkImage texture);
// Remove old textures from the cache
void CleanupTextureCache();
// Handle texture rendering, with automatic reload if needed
void HandleTextureRendering(const std::string& texture_path,
std::function<void(VkImage)> render_callback);
private:
// Load a texture from disk and create a Vulkan image
vk::Image LoadTexture(const std::string& texture_path);
// Create a default texture to use in case of errors
vk::Image CreateDefaultTexture();
const Device& device;
MemoryAllocator& memory_allocator;
std::mutex texture_mutex;
std::unordered_map<std::string, vk::Image> texture_cache;
std::optional<vk::Image> default_texture;
VkFormat texture_format = VK_FORMAT_B8G8R8A8_SRGB;
};
} // namespace Vulkan

View File

@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
@ -80,8 +81,10 @@ void TextureCache<P>::RunGarbageCollector() {
const auto Configure = [&](bool allow_aggressive) { const auto Configure = [&](bool allow_aggressive) {
high_priority_mode = total_used_memory >= expected_memory; high_priority_mode = total_used_memory >= expected_memory;
aggressive_mode = allow_aggressive && total_used_memory >= critical_memory; aggressive_mode = allow_aggressive && total_used_memory >= critical_memory;
ticks_to_destroy = aggressive_mode ? 10ULL : high_priority_mode ? 25ULL : 50ULL; // Reduce ticks_to_destroy to be more aggressive in freeing memory
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10); ticks_to_destroy = aggressive_mode ? 5ULL : high_priority_mode ? 15ULL : 40ULL;
// Increase num_iterations to clean up more resources at once for memory-intensive games
num_iterations = aggressive_mode ? 60 : (high_priority_mode ? 30 : 15);
}; };
const auto Cleanup = [this, &num_iterations, &high_priority_mode, const auto Cleanup = [this, &num_iterations, &high_priority_mode,
&aggressive_mode](ImageId image_id) { &aggressive_mode](ImageId image_id) {
@ -95,7 +98,8 @@ void TextureCache<P>::RunGarbageCollector() {
// used by the async decoder thread. // used by the async decoder thread.
return false; return false;
} }
if (!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) { // Be more aggressive with cleanup for memory-intensive games
if (!aggressive_mode && !high_priority_mode && True(image.flags & ImageFlagBits::CostlyLoad)) {
return false; return false;
} }
const bool must_download = const bool must_download =
@ -118,19 +122,20 @@ void TextureCache<P>::RunGarbageCollector() {
DeleteImage(image_id, image.scale_tick > frame_tick + 5); DeleteImage(image_id, image.scale_tick > frame_tick + 5);
if (total_used_memory < critical_memory) { if (total_used_memory < critical_memory) {
if (aggressive_mode) { if (aggressive_mode) {
// Sink the aggresiveness. // Sink the aggresiveness more gradually to prevent oscillation
num_iterations >>= 2; num_iterations = num_iterations * 3 / 4;
aggressive_mode = false; aggressive_mode = false;
return false; return false;
} }
if (high_priority_mode && total_used_memory < expected_memory) { if (high_priority_mode && total_used_memory < expected_memory) {
num_iterations >>= 1; num_iterations = num_iterations * 3 / 4;
high_priority_mode = false; high_priority_mode = false;
} }
} }
return false; return false;
}; };
// Run garbage collection more frequently for memory-intensive games
// Try to remove anything old enough and not high priority. // Try to remove anything old enough and not high priority.
Configure(false); Configure(false);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup); lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
@ -138,19 +143,67 @@ void TextureCache<P>::RunGarbageCollector() {
// If pressure is still too high, prune aggressively. // If pressure is still too high, prune aggressively.
if (total_used_memory >= critical_memory) { if (total_used_memory >= critical_memory) {
Configure(true); Configure(true);
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup); // Make a more thorough sweep with more aggressive settings
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy / 2, Cleanup);
// If we're still in a critical memory situation, do emergency cleanup
if (total_used_memory >= critical_memory + 50_MiB) {
// Last resort emergency cleanup - reduce thresholds dramatically
ticks_to_destroy = 1;
num_iterations = 100;
lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
}
} }
} }
template <class P> template <class P>
void TextureCache<P>::TickFrame() { void TextureCache<P>::TickFrame() {
static u64 consecutive_high_memory_frames = 0;
static constexpr u64 EMERGENCY_CLEANUP_THRESHOLD = 120; // ~2 seconds at 60 FPS
// If we can obtain the memory info, use it instead of the estimate. // If we can obtain the memory info, use it instead of the estimate.
if (runtime.CanReportMemoryUsage()) { if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage(); total_used_memory = runtime.GetDeviceMemoryUsage();
} }
// Track consecutive high memory frames to detect potential leaks
if (total_used_memory > critical_memory) {
consecutive_high_memory_frames++;
if (consecutive_high_memory_frames > EMERGENCY_CLEANUP_THRESHOLD) {
// Emergency situation - extreme memory pressure for extended time
// This likely indicates a leak or insufficient cleanup
LOG_WARNING(Render, "Emergency texture cache cleanup triggered after {} frames of high memory usage",
consecutive_high_memory_frames);
// Force immediate cleanup of all pending resources
sentenced_images.ForceDestroyAll();
sentenced_framebuffers.ForceDestroyAll();
sentenced_image_view.ForceDestroyAll();
// Do a forced garbage collection pass
bool saved_value = has_deleted_images;
RunGarbageCollector();
has_deleted_images = saved_value;
// Reset counter but keep some pressure
consecutive_high_memory_frames = 30;
}
else if (consecutive_high_memory_frames > 60) { // If high memory for >60 frames (~1 second)
// Force a more aggressive cleanup cycle
RunGarbageCollector();
consecutive_high_memory_frames = 45; // Reset but keep some pressure
}
} else if (total_used_memory > expected_memory) {
// Use u64(1) to ensure type compatibility, avoiding the ULL suffix
consecutive_high_memory_frames = std::max(u64(1), consecutive_high_memory_frames / 2);
} else {
consecutive_high_memory_frames = 0;
}
if (total_used_memory > minimum_memory) { if (total_used_memory > minimum_memory) {
RunGarbageCollector(); RunGarbageCollector();
} }
sentenced_images.Tick(); sentenced_images.Tick();
sentenced_framebuffers.Tick(); sentenced_framebuffers.Tick();
sentenced_image_view.Tick(); sentenced_image_view.Tick();
@ -2165,27 +2218,35 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
if (image.HasScaled()) { if (image.HasScaled()) {
total_used_memory -= GetScaledImageSizeBytes(image); total_used_memory -= GetScaledImageSizeBytes(image);
} }
// Calculate accurate memory usage for this image
u64 tentative_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes); u64 tentative_size = std::max(image.guest_size_bytes, image.unswizzled_size_bytes);
if ((IsPixelFormatASTC(image.info.format) && if ((IsPixelFormatASTC(image.info.format) &&
True(image.flags & ImageFlagBits::AcceleratedUpload)) || True(image.flags & ImageFlagBits::AcceleratedUpload)) ||
True(image.flags & ImageFlagBits::Converted)) { True(image.flags & ImageFlagBits::Converted)) {
tentative_size = TranscodedAstcSize(tentative_size, image.info.format); tentative_size = TranscodedAstcSize(tentative_size, image.info.format);
} }
// Ensure memory usage is properly accounted for
total_used_memory -= Common::AlignUp(tentative_size, 1024); total_used_memory -= Common::AlignUp(tentative_size, 1024);
const GPUVAddr gpu_addr = image.gpu_addr; const GPUVAddr gpu_addr = image.gpu_addr;
const auto alloc_it = image_allocs_table.find(gpu_addr); const auto alloc_it = image_allocs_table.find(gpu_addr);
if (alloc_it == image_allocs_table.end()) { if (alloc_it == image_allocs_table.end()) {
ASSERT_MSG(false, "Trying to delete an image alloc that does not exist in address 0x{:x}", LOG_ERROR(HW_GPU, "Trying to delete an image alloc that does not exist in address 0x{:x}",
gpu_addr); gpu_addr);
return; return;
} }
const ImageAllocId alloc_id = alloc_it->second; const ImageAllocId alloc_id = alloc_it->second;
std::vector<ImageId>& alloc_images = slot_image_allocs[alloc_id].images; std::vector<ImageId>& alloc_images = slot_image_allocs[alloc_id].images;
const auto alloc_image_it = std::ranges::find(alloc_images, image_id); const auto alloc_image_it = std::ranges::find(alloc_images, image_id);
if (alloc_image_it == alloc_images.end()) { if (alloc_image_it == alloc_images.end()) {
ASSERT_MSG(false, "Trying to delete an image that does not exist"); LOG_ERROR(HW_GPU, "Trying to delete an image that does not exist");
return; return;
} }
// Ensure image is properly untracked and unregistered before deletion
ASSERT_MSG(False(image.flags & ImageFlagBits::Tracked), "Image was not untracked"); ASSERT_MSG(False(image.flags & ImageFlagBits::Tracked), "Image was not untracked");
ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Image was not unregistered"); ASSERT_MSG(False(image.flags & ImageFlagBits::Registered), "Image was not unregistered");
@ -2196,6 +2257,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
for (size_t rt = 0; rt < NUM_RT; ++rt) { for (size_t rt = 0; rt < NUM_RT; ++rt) {
dirty[Dirty::ColorBuffer0 + rt] = true; dirty[Dirty::ColorBuffer0 + rt] = true;
} }
// Clear render target references
const std::span<const ImageViewId> image_view_ids = image.image_view_ids; const std::span<const ImageViewId> image_view_ids = image.image_view_ids;
for (const ImageViewId image_view_id : image_view_ids) { for (const ImageViewId image_view_id : image_view_ids) {
std::ranges::replace(render_targets.color_buffer_ids, image_view_id, ImageViewId{}); std::ranges::replace(render_targets.color_buffer_ids, image_view_id, ImageViewId{});
@ -2203,9 +2266,12 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
render_targets.depth_buffer_id = ImageViewId{}; render_targets.depth_buffer_id = ImageViewId{};
} }
} }
// Clean up references and dependencies
RemoveImageViewReferences(image_view_ids); RemoveImageViewReferences(image_view_ids);
RemoveFramebuffers(image_view_ids); RemoveFramebuffers(image_view_ids);
// Handle aliased images
for (const AliasedImage& alias : image.aliased_images) { for (const AliasedImage& alias : image.aliased_images) {
ImageBase& other_image = slot_images[alias.id]; ImageBase& other_image = slot_images[alias.id];
[[maybe_unused]] const size_t num_removed_aliases = [[maybe_unused]] const size_t num_removed_aliases =
@ -2213,33 +2279,43 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
return other_alias.id == image_id; return other_alias.id == image_id;
}); });
other_image.CheckAliasState(); other_image.CheckAliasState();
ASSERT_MSG(num_removed_aliases == 1, "Invalid number of removed aliases: {}", if (num_removed_aliases != 1) {
num_removed_aliases); LOG_WARNING(HW_GPU, "Invalid number of removed aliases: {}", num_removed_aliases);
}
} }
// Handle overlapping images
for (const ImageId overlap_id : image.overlapping_images) { for (const ImageId overlap_id : image.overlapping_images) {
ImageBase& other_image = slot_images[overlap_id]; ImageBase& other_image = slot_images[overlap_id];
[[maybe_unused]] const size_t num_removed_overlaps = std::erase_if( [[maybe_unused]] const size_t num_removed_overlaps = std::erase_if(
other_image.overlapping_images, other_image.overlapping_images,
[image_id](const ImageId other_overlap_id) { return other_overlap_id == image_id; }); [image_id](const ImageId other_overlap_id) { return other_overlap_id == image_id; });
other_image.CheckBadOverlapState(); other_image.CheckBadOverlapState();
ASSERT_MSG(num_removed_overlaps == 1, "Invalid number of removed overlapps: {}", if (num_removed_overlaps != 1) {
num_removed_overlaps); LOG_WARNING(HW_GPU, "Invalid number of removed overlaps: {}", num_removed_overlaps);
}
} }
// Free resources - either immediately or queue for delayed destruction
for (const ImageViewId image_view_id : image_view_ids) { for (const ImageViewId image_view_id : image_view_ids) {
if (!immediate_delete) { if (!immediate_delete) {
sentenced_image_view.Push(std::move(slot_image_views[image_view_id])); sentenced_image_view.Push(std::move(slot_image_views[image_view_id]));
} }
slot_image_views.erase(image_view_id); slot_image_views.erase(image_view_id);
} }
if (!immediate_delete) { if (!immediate_delete) {
sentenced_images.Push(std::move(slot_images[image_id])); sentenced_images.Push(std::move(slot_images[image_id]));
} }
slot_images.erase(image_id); slot_images.erase(image_id);
// Clean up allocation table
alloc_images.erase(alloc_image_it); alloc_images.erase(alloc_image_it);
if (alloc_images.empty()) { if (alloc_images.empty()) {
image_allocs_table.erase(alloc_it); image_allocs_table.erase(alloc_it);
} }
// Mark tables as invalidated
for (size_t c : active_channel_ids) { for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c]; auto& channel_info = channel_storage[c];
if constexpr (ENABLE_VALIDATION) { if constexpr (ENABLE_VALIDATION) {

View File

@ -0,0 +1,446 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <fstream>
#include <algorithm>
#include "common/logging/log.h"
#include "video_core/vulkan_common/hybrid_memory.h"
#if defined(__linux__) || defined(__ANDROID__)
#include <sys/mman.h>
#include <unistd.h>
#include <poll.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#elif defined(_WIN32)
#include <windows.h>
#endif
namespace Vulkan {
void PredictiveReuseManager::RecordUsage(u64 address, u64 size, bool write_access) {
std::lock_guard<std::mutex> guard(mutex);
// Add to history, removing oldest entries if we're past max_history
access_history.push_back({address, size, write_access, current_timestamp++});
if (access_history.size() > max_history) {
access_history.erase(access_history.begin());
}
}
bool PredictiveReuseManager::IsHotRegion(u64 address, u64 size) const {
std::lock_guard<std::mutex> guard(mutex);
// Check if this memory region has been accessed frequently
const u64 end_address = address + size;
int access_count = 0;
for (const auto& access : access_history) {
const u64 access_end = access.address + access.size;
// Check for overlap
if (!(end_address <= access.address || address >= access_end)) {
access_count++;
}
}
// Consider a region "hot" if it has been accessed in at least 10% of recent accesses
return access_count >= static_cast<int>(std::max<size_t>(1, max_history / 10));
}
void PredictiveReuseManager::EvictRegion(u64 address, u64 size) {
std::lock_guard<std::mutex> guard(mutex);
// Remove any history entries that overlap with this region
const u64 end_address = address + size;
access_history.erase(
std::remove_if(access_history.begin(), access_history.end(),
[address, end_address](const MemoryAccess& access) {
const u64 access_end = access.address + access.size;
// Check for overlap
return !(end_address <= access.address || address >= access_end);
}),
access_history.end()
);
}
void PredictiveReuseManager::ClearHistory() {
std::lock_guard<std::mutex> guard(mutex);
access_history.clear();
current_timestamp = 0;
}
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
void FaultManagedAllocator::Touch(size_t addr) {
lru.remove(addr);
lru.push_front(addr);
dirty_set.insert(addr);
}
void FaultManagedAllocator::EnforceLimit() {
while (lru.size() > MaxPages) {
size_t evict = lru.back();
lru.pop_back();
auto it = page_map.find(evict);
if (it != page_map.end()) {
if (dirty_set.count(evict)) {
// Compress and store dirty page before evicting
std::vector<u8> compressed((u8*)it->second, (u8*)it->second + PageSize);
compressed_store[evict] = std::move(compressed);
dirty_set.erase(evict);
}
#if defined(__linux__) || defined(__ANDROID__)
munmap(it->second, PageSize);
#elif defined(_WIN32)
VirtualFree(it->second, 0, MEM_RELEASE);
#endif
page_map.erase(it);
}
}
}
void* FaultManagedAllocator::GetOrAlloc(size_t addr) {
std::lock_guard<std::mutex> guard(lock);
if (page_map.count(addr)) {
Touch(addr);
return page_map[addr];
}
#if defined(__linux__) || defined(__ANDROID__)
void* mem = mmap(nullptr, PageSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED) {
LOG_ERROR(Render_Vulkan, "Failed to mmap memory for fault handler");
return nullptr;
}
#elif defined(_WIN32)
void* mem = VirtualAlloc(nullptr, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!mem) {
LOG_ERROR(Render_Vulkan, "Failed to VirtualAlloc memory for fault handler");
return nullptr;
}
#endif
if (compressed_store.count(addr)) {
// Decompress stored page data
std::memcpy(mem, compressed_store[addr].data(), compressed_store[addr].size());
compressed_store.erase(addr);
} else {
std::memset(mem, 0, PageSize);
}
page_map[addr] = mem;
lru.push_front(addr);
dirty_set.insert(addr);
EnforceLimit();
return mem;
}
#if defined(_WIN32)
// Static member initialization
FaultManagedAllocator* FaultManagedAllocator::current_instance = nullptr;
LONG WINAPI FaultManagedAllocator::VectoredExceptionHandler(PEXCEPTION_POINTERS exception_info) {
// Only handle access violations (page faults)
if (exception_info->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
return EXCEPTION_CONTINUE_SEARCH;
}
if (!current_instance) {
return EXCEPTION_CONTINUE_SEARCH;
}
// Get the faulting address - use ULONG_PTR for Windows
const ULONG_PTR fault_addr = static_cast<ULONG_PTR>(exception_info->ExceptionRecord->ExceptionInformation[1]);
const ULONG_PTR base_addr = reinterpret_cast<ULONG_PTR>(current_instance->base_address);
// Check if the address is within our managed range
if (fault_addr < base_addr ||
fault_addr >= (base_addr + static_cast<ULONG_PTR>(current_instance->memory_size))) {
return EXCEPTION_CONTINUE_SEARCH;
}
// Calculate the base address of the page
const ULONG_PTR page_addr = fault_addr & ~(static_cast<ULONG_PTR>(PageSize) - 1);
const size_t relative_addr = static_cast<size_t>(page_addr - base_addr);
// Handle the fault by allocating memory
void* page = current_instance->GetOrAlloc(relative_addr);
if (!page) {
return EXCEPTION_CONTINUE_SEARCH;
}
// Copy the page data to the faulting address
DWORD old_protect;
void* target_addr = reinterpret_cast<void*>(page_addr);
// Make the target page writable
if (VirtualProtect(target_addr, PageSize, PAGE_READWRITE, &old_protect)) {
std::memcpy(target_addr, page, PageSize);
// Restore original protection
VirtualProtect(target_addr, PageSize, old_protect, &old_protect);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void FaultManagedAllocator::ExceptionHandlerThread() {
while (running) {
// Sleep to avoid busy waiting
Sleep(10);
}
}
#endif
void FaultManagedAllocator::Initialize(void* base, size_t size) {
#if defined(__linux__) || defined(__ANDROID__)
uffd = static_cast<int>(syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK));
if (uffd < 0) {
LOG_ERROR(Render_Vulkan, "Failed to create userfaultfd, fault handling disabled");
return;
}
struct uffdio_api api = { .api = UFFD_API };
ioctl(uffd, UFFDIO_API, &api);
struct uffdio_register reg = {
.range = { .start = (uintptr_t)base, .len = size },
.mode = UFFDIO_REGISTER_MODE_MISSING
};
if (ioctl(uffd, UFFDIO_REGISTER, &reg) < 0) {
LOG_ERROR(Render_Vulkan, "Failed to register memory range with userfaultfd");
close(uffd);
uffd = -1;
return;
}
running = true;
fault_handler = std::thread(&FaultManagedAllocator::FaultThread, this);
#elif defined(_WIN32)
// Setup Windows memory for fault handling
base_address = base;
memory_size = size;
// Reserve memory range but don't commit it yet - it will be demand-paged
DWORD oldProtect;
VirtualProtect(base, size, PAGE_NOACCESS, &oldProtect);
// Install a vectored exception handler
current_instance = this;
AddVectoredExceptionHandler(1, VectoredExceptionHandler);
running = true;
exception_handler = std::thread(&FaultManagedAllocator::ExceptionHandlerThread, this);
LOG_INFO(Render_Vulkan, "Windows fault-managed memory initialized at {:p}, size: {}",
base, size);
#endif
}
#if defined(__linux__) || defined(__ANDROID__)
void FaultManagedAllocator::FaultThread() {
struct pollfd pfd = { uffd, POLLIN, 0 };
while (running) {
if (poll(&pfd, 1, 10) > 0) {
struct uffd_msg msg;
read(uffd, &msg, sizeof(msg));
if (msg.event == UFFD_EVENT_PAGEFAULT) {
size_t addr = msg.arg.pagefault.address & ~(PageSize - 1);
void* page = GetOrAlloc(addr);
if (page) {
struct uffdio_copy copy = {
.dst = (uintptr_t)addr,
.src = (uintptr_t)page,
.len = PageSize,
.mode = 0
};
ioctl(uffd, UFFDIO_COPY, &copy);
}
}
}
}
}
#endif
void* FaultManagedAllocator::Translate(size_t addr) {
std::lock_guard<std::mutex> guard(lock);
size_t base = addr & ~(PageSize - 1);
if (!page_map.count(base)) {
return nullptr;
}
Touch(base);
return (u8*)page_map[base] + (addr % PageSize);
}
void FaultManagedAllocator::SaveSnapshot(const std::string& path) {
std::lock_guard<std::mutex> guard(lock);
std::ofstream out(path, std::ios::binary);
if (!out) {
LOG_ERROR(Render_Vulkan, "Failed to open snapshot file for writing: {}", path);
return;
}
for (auto& [addr, mem] : page_map) {
out.write(reinterpret_cast<const char*>(&addr), sizeof(addr));
out.write(reinterpret_cast<const char*>(mem), PageSize);
}
LOG_INFO(Render_Vulkan, "Saved memory snapshot to {}", path);
}
void FaultManagedAllocator::SaveDifferentialSnapshot(const std::string& path) {
std::lock_guard<std::mutex> guard(lock);
std::ofstream out(path, std::ios::binary);
if (!out) {
LOG_ERROR(Render_Vulkan, "Failed to open diff snapshot file for writing: {}", path);
return;
}
size_t dirty_count = 0;
for (const auto& addr : dirty_set) {
if (page_map.count(addr)) {
out.write(reinterpret_cast<const char*>(&addr), sizeof(addr));
out.write(reinterpret_cast<const char*>(page_map[addr]), PageSize);
dirty_count++;
}
}
LOG_INFO(Render_Vulkan, "Saved differential snapshot to {} ({} dirty pages)",
path, dirty_count);
}
void FaultManagedAllocator::ClearDirtySet() {
std::lock_guard<std::mutex> guard(lock);
dirty_set.clear();
LOG_DEBUG(Render_Vulkan, "Cleared dirty page tracking");
}
FaultManagedAllocator::~FaultManagedAllocator() {
running = false;
#if defined(__linux__) || defined(__ANDROID__)
if (fault_handler.joinable()) {
fault_handler.join();
}
for (auto& [addr, mem] : page_map) {
munmap(mem, PageSize);
}
if (uffd != -1) {
close(uffd);
}
#elif defined(_WIN32)
if (exception_handler.joinable()) {
exception_handler.join();
}
// Remove the vectored exception handler
RemoveVectoredExceptionHandler(VectoredExceptionHandler);
current_instance = nullptr;
for (auto& [addr, mem] : page_map) {
VirtualFree(mem, 0, MEM_RELEASE);
}
// Free the base memory if needed
if (base_address) {
VirtualFree(base_address, 0, MEM_RELEASE);
base_address = nullptr;
}
#endif
}
#endif // defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
HybridMemory::HybridMemory(const Device& device_, MemoryAllocator& allocator, size_t reuse_history)
: device(device_), memory_allocator(allocator), reuse_manager(reuse_history) {
}
HybridMemory::~HybridMemory() = default;
void HybridMemory::InitializeGuestMemory(void* base, size_t size) {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
fmaa.Initialize(base, size);
LOG_INFO(Render_Vulkan, "Initialized fault-managed guest memory at {:p}, size: {}",
base, size);
#else
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
#endif
}
void* HybridMemory::TranslateAddress(size_t addr) {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
return fmaa.Translate(addr);
#else
return nullptr;
#endif
}
ComputeBuffer HybridMemory::CreateComputeBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
MemoryUsage memory_type) {
ComputeBuffer buffer;
buffer.size = size;
VkBufferCreateInfo buffer_ci = {
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
.size = size,
.usage = usage | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.queueFamilyIndexCount = 0,
.pQueueFamilyIndices = nullptr,
};
// Using CreateBuffer directly handles memory allocation internally
buffer.buffer = memory_allocator.CreateBuffer(buffer_ci, memory_type);
LOG_DEBUG(Render_Vulkan, "Created compute buffer: size={}, usage={:x}",
size, usage);
return buffer;
}
void HybridMemory::SaveSnapshot(const std::string& path) {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
fmaa.SaveSnapshot(path);
#else
LOG_ERROR(Render_Vulkan, "Memory snapshots not supported on this platform");
#endif
}
void HybridMemory::SaveDifferentialSnapshot(const std::string& path) {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
fmaa.SaveDifferentialSnapshot(path);
#else
LOG_ERROR(Render_Vulkan, "Differential memory snapshots not supported on this platform");
#endif
}
void HybridMemory::ResetDirtyTracking() {
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
fmaa.ClearDirtySet();
#endif
}
} // namespace Vulkan

View File

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <functional>
#include <list>
#include <set>
#include <map>
#include <thread>
#include "common/common_types.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
namespace Vulkan {
struct ComputeBuffer {
vk::Buffer buffer{};
VkDeviceSize size = 0;
};
class PredictiveReuseManager {
public:
explicit PredictiveReuseManager(size_t history_size) : max_history{history_size} {}
void RecordUsage(u64 address, u64 size, bool write_access);
bool IsHotRegion(u64 address, u64 size) const;
void EvictRegion(u64 address, u64 size);
void ClearHistory();
private:
struct MemoryAccess {
u64 address;
u64 size;
bool write_access;
u64 timestamp;
};
std::vector<MemoryAccess> access_history;
const size_t max_history;
u64 current_timestamp{0};
mutable std::mutex mutex;
};
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
class FaultManagedAllocator {
public:
static constexpr size_t PageSize = 0x1000;
static constexpr size_t MaxPages = 16384;
void Initialize(void* base, size_t size);
void* Translate(size_t addr);
void SaveSnapshot(const std::string& path);
void SaveDifferentialSnapshot(const std::string& path);
void ClearDirtySet();
~FaultManagedAllocator();
private:
std::map<size_t, void*> page_map;
std::list<size_t> lru;
std::set<size_t> dirty_set;
std::unordered_map<size_t, std::vector<u8>> compressed_store;
std::mutex lock;
#if defined(__linux__) || defined(__ANDROID__)
int uffd = -1;
std::atomic<bool> running{false};
std::thread fault_handler;
void FaultThread();
#elif defined(_WIN32)
void* base_address = nullptr;
size_t memory_size = 0;
HANDLE exception_port = nullptr;
std::atomic<bool> running{false};
std::thread exception_handler;
void ExceptionHandlerThread();
static LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS exception_info);
static FaultManagedAllocator* current_instance;
#endif
void Touch(size_t addr);
void EnforceLimit();
void* GetOrAlloc(size_t addr);
};
#endif
class HybridMemory {
public:
explicit HybridMemory(const Device& device, MemoryAllocator& allocator, size_t reuse_history = 32);
~HybridMemory();
void InitializeGuestMemory(void* base, size_t size);
void* TranslateAddress(size_t addr);
ComputeBuffer CreateComputeBuffer(VkDeviceSize size, VkBufferUsageFlags usage, MemoryUsage memory_type);
void SaveSnapshot(const std::string& path);
void SaveDifferentialSnapshot(const std::string& path);
void ResetDirtyTracking();
private:
const Device& device;
MemoryAllocator& memory_allocator;
PredictiveReuseManager reuse_manager;
#if defined(__linux__) || defined(__ANDROID__) || defined(_WIN32)
FaultManagedAllocator fmaa;
#endif
};
} // namespace Vulkan

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
"name": "citron", "name": "citron",
"builtin-baseline": "c82f74667287d3dc386bce81e44964370c91a289", "builtin-baseline": "bc994510d2eb11aac7b43b03f67a7751d5bfe0e4",
"version": "1.0", "version": "1.0",
"dependencies": [ "dependencies": [
"boost-algorithm", "boost-algorithm",