Compare commits

...

115 Commits

Author SHA1 Message Date
2ca0b17339 New Crowdin updates (#6590)
* New translations en_us.json (Arabic)

* New translations en_us.json (Russian)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (French)

* New translations en_us.json (Russian)

* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Thai)

* New translations en_us.json (French)

* New translations en_us.json (Arabic)

* New translations en_us.json (Italian)

* New translations en_us.json (Korean)

* New translations en_us.json (Russian)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Arabic)

* New translations en_us.json (Polish)

* New translations en_us.json (Turkish)

* New translations en_us.json (Arabic)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Russian)

* New translations en_us.json (French)

* New translations en_us.json (Thai)

* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Thai)

* New translations en_us.json (French)

* New translations en_us.json (Arabic)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Arabic)

* New translations en_us.json (Italian)

* New translations en_us.json (Arabic)

* New translations en_us.json (Italian)

* New translations en_us.json (Spanish)

* New translations en_us.json (Russian)

* New translations en_us.json (Russian)

* New translations en_us.json (Thai)

* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Thai)

* New translations en_us.json (French)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Arabic)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Russian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Russian)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (German)
2024-05-14 17:47:03 +02:00
47639e6eeb Bump Avalonia.Svg (#6603)
* Bump Avalonia.Svg

* Remove using

* Bump Version

* Remove other reload
2024-05-14 17:36:11 +02:00
cd78adf07f Add missing lock on texture cache UpdateMapping method (#6657) 2024-05-14 17:23:13 +02:00
a3dc295c5f Disable keyboard controller input while swkbd is open (foreground) (#6646)
* Block input updates while swkbd is open in foreground mode

* Flush internal driver state before unblocking input updates

* Rename Flush to Clear and remove unnecessary attribute
2024-05-14 17:14:39 +02:00
2ef4f92b07 Make TextureGroup.ClearModified thread safe (#6686) 2024-05-14 17:06:36 +02:00
2b6cc4b353 Add the "Auto" theme option in setting (#6611)
* Add "Follow OS theme" option

* Update App.axaml.cs

* Add "Follow OS theme" option

* Update App.axaml.cs

* Remove `this`

* Remove annotation for nullable reference

* Change into switch expression to make it concise

* Change comments to XML docs

* Update en_US.json

* Fix icons in About dialog do not response to "auto" theme

The theme icons seemingly use Dark variant event when the OS theme is light. In addition, I added ThemeManager common to make it accessible for both App and AboutWindow

* Newline at the end

* newline moment

* Update ThemeManager.cs

* bait to switch to lf

* change to lf

* temp. revert

* Add back ThemeManager.cs common, pls pass the format check

* I found the mistake: should have put `ThemeManager.OnThemeChanged();` in try block

Finally solve the formatting check

* test formatting

* Update App.axaml.cs

* Ok i seem to forget to add version lol

* Fix info CA1816
2024-05-14 17:00:03 +02:00
075575200d Update compatibility information in README.md (#6801) 2024-05-14 16:58:48 +02:00
3a3b51893e Add support for bindless textures from storage buffer on Vulkan (#6721)
* Halve primitive ID when converting quads to triangles

* Shader cache version bump

* Add support for bindless textures from storage buffer on Vulkan
2024-05-14 16:47:16 +02:00
44dbab3848 discordRPC: Truncate game title and details if they exceed discord byte limit. (#6581)
* Truncate game title and details if they exceed DiscordRPC limit.

* Update implementation to a byte total check.

* Track if the string has actually been modified correctly.

* Allow an early function return and simplify logic.

* Better handling of large input strings and minimise loop opportunities.

* Remove unused using.

* Update to `applicationName` over `titleName`.
2024-05-14 16:36:44 +02:00
cada4d04ef HID: Stub IHidServer: 134 (SetNpadAnalogStickUseCenterClamp) (#6664)
* Add files via upload

* Update IHidServer.cs

mistakes...

* format

how do i do it

* Update src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs

Co-authored-by: Agatem <agaatem@outlook.com>

* Update src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs

Co-authored-by: Agatem <agaatem@outlook.com>

* bruh

* Apply suggestions from code review

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* use readuint32 instead

* second thought

* i hope it works

thanks someone higher up with the same thing

* pid

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* styles i think

* Apply suggestions from code review

Co-authored-by: makigumo <makigumo@users.noreply.github.com>

---------

Co-authored-by: Agatem <agaatem@outlook.com>
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: makigumo <makigumo@users.noreply.github.com>
2024-05-14 16:26:49 +02:00
e9edf0ab7f Update outdated Windows version warning (#6481)
* Change text

* clarify version number

* update gtk
2024-05-14 16:19:43 +02:00
6e40b64554 Add linux specific files to local builds (#6762) 2024-05-14 16:06:40 +02:00
1a676ee913 Update DotSettings (#6535) 2024-05-14 15:59:28 +02:00
a23d8cb92f Replace "List.ForEach" for "foreach" (#6783)
* Replace "List.ForEach" for "foreach"

* dotnet format

* Update Ptc.cs

* Update GpuContext.cs
2024-05-08 13:53:25 +02:00
ab12fbe963 Fix system dateTime loading in avalonia LoadCurrentConfiguration (#6676)
* Fix system dateTime loading in avalonia LoadCurrentConfiguration

* Rename local var to not use upper camel case
2024-05-02 13:33:28 +02:00
d0cc13ce0b UI: Fix some MainWindow bugs and implement menubar items to change window size. (#6750)
* Do not save window dimensions when maximized.

* Implement option to disable window size/position memory.

* Remove logging statements

* Implement menubar items for common window sizes.

* formatting fixes

* Set 720p window as a composite value.

* Remove unused using

* Fix exception paramter.

* Force restore window when a size change is requested

* Fix some resizing bugs.
2024-05-01 18:21:24 +02:00
65c035cdf8 Fix alt key appearing as control in settings menus (#6742) 2024-04-29 18:18:27 +01:00
56c5dbe557 Fix Cursor States On Windows (#6725)
* [Ava]: Fix Cursor States On Windows

It's been sometime since the last PR #5415 was made and last time i was waiting for Ava 11 to be merged before re-writing the code and along the way forgot about the PR.

Anyway this PR supersedes both #5288 and #5415, and fixes issue: #5136

* Now, the bounds for which the cursor should be detected in renderer should be accurate to any scaling / resolution, taking into account the status and the menu bar. ( This issue was partially resolved by #6450 )

* Reduced the number of times the cursor updates from per frame update to updating only when the cursor state needs to be changed.

* Fixed the issue wherein you weren't able to resize the window, because of the cursor passthrough which caused the cursor to reset from the reset icon or flicker.

* Fixed the issue caused by #6450 which caused the cursor to disappear over the submenus while cursor was set to always hide.

* Changed the cursor state to not disappear while the game is being loaded. ( Needs Feedback ).

* Removed an unused library import.

* PR feedback

* Fix excessive line breaks and whitespaces and other feedback
* Add a check before calculating cursor idle time, such that it calculates only while the cursor mode is OnIdle.

* PR Feedback

* Rework the cursor state check code block

Co-Authored-By: gdkchan <5624669+gdkchan@users.noreply.github.com>

* PR Feedback

* A simpler version of the previous implementation.

Co-Authored-By: gdkchan <5624669+gdkchan@users.noreply.github.com>

* PR Feedback

* PR Feedback

---------

Co-authored-by: gdkchan <5624669+gdkchan@users.noreply.github.com>
2024-04-28 20:21:08 -03:00
5976a5161b Fix direct keyboard not working when using a Controller. (#6716)
* Fix direct keyboard not working when connected with a controller

- Pass KeyboardDriver to NpadController.GetHLEKeyboardInput().
- Always fetch all keyboards if Direct Keyboard is turned on.
- Remove unnecessary return null.

* Get Keyboard Inputs outside of the controller loop.

- Moved GetHLEKeyboardInput outside of the controller loop.
- Made GetHLEKeyboardInput public static from public

* Removed extra newline

* Update src/Ryujinx.Input/HLE/NpadManager.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Update src/Ryujinx.Input/HLE/NpadController.cs

Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
2024-04-28 19:02:29 +02:00
3d4dea624d HID: Correct direct mouse deltas (#6736)
The delta position of the mouse should be the difference between the current and last position. Subtracting the last deltas doesn't really make sense.

Won't implement pointer lock for first person games, but might stop some super weird behaviour with the mouse values appearing totally random.
2024-04-28 18:55:37 +02:00
89a274c6a6 ci: Replace macos-latest label with macos-13 (#6729)
Due to a change to the GitHub runner labels a few days ago (see: actions/runner#3256) our build workflows for macOS x64 didn't work anymore.
According to https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories the macos-13 label is not using arm64 yet.

Until a better solution is offered in the linked issue above, we'll keep using the macos-13 label which hopefully doesn't switch to arm64 soon.
2024-04-26 20:36:35 -03:00
c6f8bfed90 Add support for bindless textures from shader input (vertex buffer) on Vulkan (#6577)
* Add support for bindless textures from shader input (vertex buffer)

* Shader cache version bump

* Format whitespace

* Remove cache entries on pool removal, disable for OpenGL

* PR feedback
2024-04-22 15:05:55 -03:00
9b94662b4b implement MemoryManagerHostTracked.GetReadOnlySequence() (#6695)
* implement `MemoryManagerHostTracked.GetReadOnlySequence()`, fixes crashes on game starts on MacOS

* whitespace fixes

* whitespace fixes

* add missing call to `SignalMemoryTracking()`

* adjust call to `SignalMemoryTracking()``

* don't use GetPhysicalAddressMemory()

* add newline for consistency
2024-04-21 16:34:04 -03:00
216026c096 Use pooled memory and avoid memory copies (#6691)
* perf: use ByteMemoryPool

* feat: KPageTableBase/KPageTable new methods to read and write `ReadOnlySequence<byte>`

* new: add IWritableBlock.Write(ulong, ReadOnlySequence<byte>) with default impl

* perf: use GetReadOnlySequence() instead of GetSpan()

* perf: make `Parcel` IDisposable, use `ByteMemoryPool` for internal allocation, and make Parcel consumers dispose of it

* remove comment about copySize

* remove unnecessary Clear()
2024-04-21 12:57:35 +02:00
7070cf6ae5 End render target lifetime on syncpoint increment (#6687) 2024-04-20 22:35:20 -03:00
9839cd56fb chore: remove repetitive words (#6690)
Signed-off-by: toofooboo <cmaker@foxmail.com>
2024-04-19 09:45:51 -03:00
99f46e22e2 Do not compare Span<T> to 'null' or 'default' (#6683) 2024-04-19 09:21:21 -03:00
22fb8c9d4f Update to new standard for volatility operations (#6682) 2024-04-19 09:03:52 -03:00
2f93ae9a19 Fix unmapped address check when reading texture handles (#6679) 2024-04-18 19:28:16 -03:00
3224ddeeb4 Update "SixLabors.ImageSharp" (#6680) 2024-04-18 19:52:57 +02:00
446f2854a5 Ava UI: Input Menu Refactor (#5826)
* Refactor

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Update src/Ryujinx.Input/ButtonValueType.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Add empty line

* Requested renames

* Update src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Make parent models private readonly

* Fix ControllerInputView

* Make line shorter

* Mac keys in locale

* Double line break

* Fix build

* Get rid of _isValid

* Fix potential race condition

* Rename HasAnyButtonPressed to IsAnyButtonPressed

* Use switches

* Simplify enumeration

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
2024-04-17 18:52:12 -03:00
8884d1fd73 Fix crash when changing controller config (#6654)
* fix needsMotionInputUpdate conditions

* Fix formatting

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2024-04-15 18:02:09 -03:00
268c9aecf8 Texture loading: reduce memory allocations (#6623)
* rebase

* add methods Ryyjinx.Common EmbeddedResources and SteamUtils

* GAL changes - change SetData() methods and ThreadedTexture commands to use IMemoryOwner<byte> instead of SpanOrArray<byte>

* Ryujinx.Graphics.Texture: change texture conversion methods to return IMemoryOwner<byte> and allocate from ByteMemoryPool

* Ryujinx.Graphics.OpenGL: update ITexture and Texture-like types with SetData() methods to take IMemoryOwner<byte> instead of SpanOrArray<byte>

* Ryujinx.Graphics.Vulkan: update ITexture and Texture-like types with SetData() methods to take IMemoryOwner<byte> instead of SpanOrArray<byte>

* Ryujinx.Graphics.Gpu: update ITexture and Texture-like types with SetData() methods to take IMemoryOwner<byte> instead of SpanOrArray<byte>

* Remove now-unused SpanOrArray<T>

* post-rebase cleanup

* PixelConverter: remove unsafe modifier on safe methods, and remove one unnecessary cast

* use ByteMemoryPool.Rent() in GetWritableRegion() impls

* fix formatting, rename `ReadRentedMemory()` to `ReadFileToRentedMemory()``

* Texture.ConvertToHostCompatibleFormat(): dispose of `result` in Astc decode branch
2024-04-14 17:06:14 -03:00
e916662b0f Account for swapchain image count change after re-creation (#6652) 2024-04-11 17:24:19 -03:00
2ddd3dd4a7 Allow BSD sockets Poll to exit when emulation ends (#6650) 2024-04-11 14:56:21 +02:00
a8f7ababb5 Revert "Update StoreConstantToMemory to match StoreConstantToAddress on value…" (#6649)
This reverts commit 22e3ff06b5.
2024-04-10 21:50:06 -03:00
22e3ff06b5 Update StoreConstantToMemory to match StoreConstantToAddress on value read (#6642) 2024-04-11 00:03:37 +02:00
9480e5c5ce Ava UI: Prevent Status Bar Backend Update (#6506)
* Prevent Status Bar Backend Update

* Make it a switch
2024-04-10 23:40:17 +02:00
0652813b0f nuget: bump Microsoft.IdentityModel.JsonWebTokens from 7.4.0 to 7.5.1 (#6627)
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.4.0 to 7.5.1.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/v7.4.0...7.5.1)

---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-10 23:30:17 +02:00
e7f2342eba Fix input consumed by audio renderer SplitterState.Update (#6640)
* Fix input consumed by audio renderer SplitterState.Update

* Use sizeof(int) to make clear what the value is
2024-04-10 12:07:31 -03:00
543d75a587 CPU: Produce non-inf results for RSQRTE instruction with subnormal inputs (#6634)
* CPU: Produce non-inf results for RSQRTE instruction with subnormal inputs

* PPTC version bump
2024-04-09 14:34:14 -03:00
338ff79e1e Use ResScaleUnsupported flag for texture arrays (#6626) 2024-04-09 14:24:46 -03:00
80201466ae Fast D32S8 2D depth texture copy (#6636) 2024-04-09 14:03:55 -03:00
7a971edb57 Pin audio renderer update output buffers (#6633) 2024-04-08 20:26:47 -03:00
c1b0ab805a Disable CLI setting persistence for HW-accelerated GUI. (#6620) 2024-04-07 20:58:05 -03:00
3e6e0e4afa Add support for large sampler arrays on Vulkan (#6489)
* Add support for large sampler arrays on Vulkan

* Shader cache version bump

* Format whitespace

* Move DescriptorSetManager to PipelineLayoutCacheEntry to allow different pool sizes per layout

* Handle array textures with different types on the same buffer

* Somewhat better caching system

* Avoid useless buffer data modification checks

* Move redundant bindings update checking to the backend

* Fix an issue where texture arrays would get the same bindings across stages on Vulkan

* Backport some fixes from part 2

* Fix typo

* PR feedback

* Format whitespace

* Add some missing XML docs
2024-04-07 18:25:55 -03:00
808803d97a CPU: Fix PC alignment for ADR thumb instruction (#6613)
* Fix PC alignment for ADR thumb instruction

* PPTC version bump
2024-04-07 18:17:49 -03:00
ead9a25141 Audio rendering: reduce memory allocations (#6604)
* - WritableRegion: enable wrapping IMemoryOwner<byte>
- IVirtualMemoryManager impls of GetWritableRegion() use pooled memory when region is non-contiguous.
- IVirtualMemoryManager: add GetReadOnlySequence() and impls
- ByteMemoryPool: add new method RentCopy()
- ByteMemoryPool: make class static, remove ctor and singleton field from earlier impl

* - BytesReadOnlySequenceSegment: move from Ryujinx.Common.Memory to Ryujinx.Memory
- BytesReadOnlySequenceSegment: add IsContiguousWith() and Replace() methods
- VirtualMemoryManagerBase:
  - remove generic type parameters, instead use ulong for virtual addresses and nuint for host/physical addresses
  - implement IWritableBlock
  - add virtual GetReadOnlySequence() with coalescing of contiguous segments
  - add virtual GetSpan()
  - add virtual GetWritableRegion()
  - add abstract IsMapped()
  - add virtual MapForeign(ulong, nuint, ulong)
  - add virtual Read<T>()
  - add virtual Read(ulong, Span<byte>)
  - add virtual ReadTracked<T>()
  - add virtual SignalMemoryTracking()
  - add virtual Write()
  - add virtual Write<T>()
  - add virtual WriteUntracked()
  - add virtual WriteWithRedundancyCheck()
- VirtualMemoryManagerRefCountedBase: remove generic type parameters
- AddressSpaceManager: remove redundant methods, add required overrides
- HvMemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManagerHostMapped: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- NativeMemoryManager: add get properties for Pointer and Length
- throughout: removed invalid <inheritdoc/> comments

* - WritableRegion: enable wrapping IMemoryOwner<byte>
- IVirtualMemoryManager impls of GetWritableRegion() use pooled memory when region is non-contiguous.
- IVirtualMemoryManager: add GetReadOnlySequence() and impls
- ByteMemoryPool: add new method RentCopy()
- ByteMemoryPool: make class static, remove ctor and singleton field from earlier impl

* add PagedMemoryRange enumerator types, use them in IVirtualMemoryManager implementations to consolidate page-handling logic and add a new capability - the coalescing of pages for consolidating memory copies and segmentation.

* new: more tests for PagedMemoryRangeCoalescingEnumerator showing coalescing of contiguous segments

* make some struct properties readonly

* put braces around `foreach` bodies

* encourage inlining of some PagedMemoryRange*Enumerator members

* DynamicRingBuffer:
 - use ByteMemoryPool
 - make some methods return without locking when size/count argument = 0
 - make generic Read<T>()/Write<T>() non-generic because its only usage is as T = byte
 - change Read(byte[]...) to Read(Span<byte>...)
 - change Write(byte[]...) to Write(Span<byte>...)

* change IAudioRenderer.RequestUpdate() to take a ReadOnlySequence<byte>, enabling zero-copy audio rendering

* HipcGenerator: support ReadOnlySequence<byte> as IPC method parameter

* change IAudioRenderer/AudioRenderer RequestUpdate* methods to take input as ReadOnlySequence<byte>

* MemoryManagerHostTracked: use rented memory when contiguous in `GetWritableRegion()`

* rebase cleanup

* dotnet format fixes

* format and comment fixes

* format long parameter list - take 2

* - add support to HipcGenerator for buffers of type `Memory<byte>`
- change `AudioRenderer` `RequestUpdate()` and `RequestUpdateAuto()` to use Memory<byte> for output buffers, removing another memory block allocation/copy

* SplitterContext `UpdateState()` and `UpdateData()` smooth out advance/rewind logic, only rewind if magic is invalid

* DynamicRingBuffer.Write(): change Span<byte> to ReadOnlySpan<byte>
2024-04-07 18:07:32 -03:00
3e0d67533f Enhance Error Handling with Try-Pattern Refactoring (#6610)
* Enhance Error Handling with Try-Pattern Refactoring

* refactoring

* refactoring

* Update src/Ryujinx.HLE/FileSystem/ContentPath.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2024-04-07 17:55:34 -03:00
0b55914864 Replacing the try-catch block with null-conditional and null-coalescing operators (#6612)
* Replacing the try-catch block with null-conditional and null-coalescing operators

* repeating
2024-04-07 16:49:14 -03:00
451a28afb8 misc: Add ANGLE configuration option to JSON and CLI (#6520)
* Add hardware-acceleration toggle to ConfigurationState.

* Add command line override for easier RenderDoc use.

* Adjust CLI arguments.

* fix whitespace format check

* fix copypasta in comment

* Add X11 rendering mode toggle

* Remove ANGLE specific comments.
2024-04-06 14:58:02 -03:00
12b235700c Delete old 16KB page workarounds (#6584)
* Delete old 16KB page workarounds

* Rename Supports4KBPage to UsesPrivateAllocations

* Format whitespace

* This one should be false too

* Update XML doc
2024-04-06 13:51:44 -03:00
3be616207d Vulkan: Fix swapchain image view leak (#6509) 2024-04-06 13:38:52 -03:00
791bf22109 Vulkan: Skip draws when patches topology is used without a tessellation shader (#6508) 2024-04-06 13:25:51 -03:00
66b1d59c66 nuget: bump DynamicData from 8.3.27 to 8.4.1 (#6536)
Bumps [DynamicData](https://github.com/reactiveui/DynamicData) from 8.3.27 to 8.4.1.
- [Release notes](https://github.com/reactiveui/DynamicData/releases)
- [Changelog](https://github.com/reactivemarbles/DynamicData/blob/main/ReleaseNotes.md)
- [Commits](https://github.com/reactiveui/DynamicData/compare/8.3.27...8.4.1)

---
updated-dependencies:
- dependency-name: DynamicData
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 14:07:06 +02:00
c8bb05633e Add mod enablement status in the log message (#6571) 2024-04-06 13:47:01 +02:00
fb1171a21e Update README.md (#6575) 2024-04-06 13:45:24 +02:00
22c0aa9c90 "Task.Wait()" synchronously blocks, use "await" instead (#6598) 2024-04-06 13:36:18 +02:00
6d28b64312 ts: Migrate service to Horizon project (#6514)
* ts: Migrate service to Horizon project

This PR migrate the `ts` service (stored in `ptm`) to the Horizon project:
- It stubs all known IPCs.
- IpcServer consts are checked by RE.

Closes #6480

* Fix args
2024-04-05 15:45:43 -03:00
05c041feeb Ignore diacritics on game search (#6602) 2024-04-05 15:26:45 -03:00
8c2da1aa04 Add missing ModWindowTitle locale key (#6601)
* Add missing ModWindowTitle locale key

* Fix this while I'm at it
2024-04-05 12:54:08 -03:00
5def0429f8 Add support to IVirtualMemoryManager for zero-copy reads (#6251)
* - WritableRegion: enable wrapping IMemoryOwner<byte>
- IVirtualMemoryManager impls of GetWritableRegion() use pooled memory when region is non-contiguous.
- IVirtualMemoryManager: add GetReadOnlySequence() and impls
- ByteMemoryPool: add new method RentCopy()
- ByteMemoryPool: make class static, remove ctor and singleton field from earlier impl

* - BytesReadOnlySequenceSegment: move from Ryujinx.Common.Memory to Ryujinx.Memory
- BytesReadOnlySequenceSegment: add IsContiguousWith() and Replace() methods
- VirtualMemoryManagerBase:
  - remove generic type parameters, instead use ulong for virtual addresses and nuint for host/physical addresses
  - implement IWritableBlock
  - add virtual GetReadOnlySequence() with coalescing of contiguous segments
  - add virtual GetSpan()
  - add virtual GetWritableRegion()
  - add abstract IsMapped()
  - add virtual MapForeign(ulong, nuint, ulong)
  - add virtual Read<T>()
  - add virtual Read(ulong, Span<byte>)
  - add virtual ReadTracked<T>()
  - add virtual SignalMemoryTracking()
  - add virtual Write()
  - add virtual Write<T>()
  - add virtual WriteUntracked()
  - add virtual WriteWithRedundancyCheck()
- VirtualMemoryManagerRefCountedBase: remove generic type parameters
- AddressSpaceManager: remove redundant methods, add required overrides
- HvMemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManager: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- MemoryManagerHostMapped: remove redundant methods, add required overrides, add overrides for _invalidAccessHandler handling
- NativeMemoryManager: add get properties for Pointer and Length
- throughout: removed invalid <inheritdoc/> comments

* make HvMemoryManager class sealed

* remove unused method

* adjust MemoryManagerHostTracked

* let MemoryManagerHostTracked override WriteImpl()
2024-04-04 22:23:03 -03:00
8e74fa3456 Stop clearing Modified flag on DiscardData (#6591) 2024-04-03 21:30:46 -03:00
6208c3e6f0 New Crowdin updates (#6550)
* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Italian)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (French)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Thai)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Polish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Arabic)

* New translations en_us.json (Italian)

* New translations en_us.json (Russian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Italian)

* New translations en_us.json (German)

* New translations en_us.json (Korean)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Arabic)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Arabic)
2024-04-02 13:21:14 -03:00
7124d679fd UI: Friendly driver name reporting. (#6530)
* Implement friendly VkDriverID names for UI.

* Capitalise NVIDIA

* Prefer vendor name on macOS

* Typo fix

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2024-03-27 14:55:34 -03:00
b323a01738 Implement host tracked memory manager mode (#6356)
* Add host tracked memory manager mode

* Skipping flush is no longer needed

* Formatting + revert unrelated change

* LightningJit: Ensure that dest register is saved for load ops that do partial updates

* avoid allocations when doing address space lookup

Add missing improvement

* IsRmwMemory -> IsPartialRegisterUpdateMemory

* Ensure we iterate all private allocations in range

* PR feedback and potential fixes

* Simplified bridges a lot

* Skip calling SignalMappingChanged if Guest is true

* Late map bridge too

* Force address masking for prefetch instructions

* Reprotection for bridges

* Move partition list validation to separate debug method

* Move host tracked related classes to HostTracked folder

* New HostTracked namespace

* Move host tracked modes to the end of enum to avoid PPTC invalidation

---------

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2024-03-26 23:33:24 -03:00
jcm
f6d24449b6 Recreate swapchain correctly when toggling VSync (#6521)
Co-authored-by: jcm <butt@butts.com>
2024-03-26 22:54:13 -03:00
72bdc24db8 Disable push descriptors for Intel ARC GPUs on Windows (#6551)
* Move some init logic out of PrintGpuInformation, then delete it

* Disable push descriptors for Intel ARC on Windows

* Re-add PrintGpuInformation just to show it in the log
2024-03-26 22:27:48 -03:00
43514771bf New gamecard icons (#6557)
* Replaced all gamecard images and added a blank variant

* File optimization
2024-03-23 21:33:27 +01:00
dbfe859ed7 Add a few missing locale strings on Avalonia (#6556)
* Add a few missing locale strings on Avalonia

* Rename LDN MitM to ldn_mitm
2024-03-23 16:31:54 -03:00
c94a73ec60 Updates the default value for BufferedQuery (#6351)
AMD GPUs (possibly just RDNA 3) could hang with the previous value
until the MaxQueryRetries was hit.

Fix #6056

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2024-03-21 21:44:11 -03:00
20a280525f [UI] Fix Display Name Translations & Update some Chinese Translations (#6388)
* Remove incorrect additional back-slash

* Touch-up on TChinese translations

* Little touch-up on SChinese translations
2024-03-21 20:08:25 +01:00
75a4ea3370 New Crowdin updates (#6541)
* New translations en_us.json (French)

* New translations en_us.json (Spanish)

* New translations en_us.json (Arabic)

* New translations en_us.json (German)

* New translations en_us.json (Greek)

* New translations en_us.json (Hebrew)

* New translations en_us.json (Italian)

* New translations en_us.json (Japanese)

* New translations en_us.json (Korean)

* New translations en_us.json (Polish)

* New translations en_us.json (Russian)

* New translations en_us.json (Turkish)

* New translations en_us.json (Ukrainian)

* New translations en_us.json (Chinese Simplified)

* New translations en_us.json (Chinese Traditional)

* New translations en_us.json (Portuguese, Brazilian)

* New translations en_us.json (Thai)

* Add missing Thai language name

* Add new languages

* Enable RTL for Arabic

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2024-03-20 21:07:27 -03:00
d26ef2eec3 nuget: bump Microsoft.CodeAnalysis.CSharp from 4.8.0 to 4.9.2 (#6397)
Bumps [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn) from 4.8.0 to 4.9.2.
- [Release notes](https://github.com/dotnet/roslyn/releases)
- [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md)
- [Commits](https://github.com/dotnet/roslyn/commits)

---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.CSharp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 02:55:11 +01:00
a0552fd78b Ava UI: Fix locale crash (#6385)
* Fix locale crash

* Apply suggestions from code review

---------

Co-authored-by: Ac_K <Acoustik666@gmail.com>
2024-03-17 02:27:14 +01:00
bb8c5ebae1 Ava UI: Content Dialog Fixes (#6482)
* Don’t use ContentDialogHelper when not necessary

* Remove `ExtendClientAreaToDecorationsHint`
2024-03-16 20:34:26 +01:00
50bdda5baa nuget: bump Microsoft.IdentityModel.JsonWebTokens from 7.3.0 to 7.4.0 (#6366)
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/7.3.0...v7.4.0)

---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 20:13:30 +01:00
18df25f66f nuget: bump the avalonia group with 2 updates (#6505)
Bumps the avalonia group with 2 updates: [Avalonia.Svg](https://github.com/wieslawsoltes/Svg.Skia) and [Avalonia.Svg.Skia](https://github.com/wieslawsoltes/Svg.Skia).


Updates `Avalonia.Svg` from 11.0.0.14 to 11.0.0.16
- [Release notes](https://github.com/wieslawsoltes/Svg.Skia/releases)
- [Changelog](https://github.com/wieslawsoltes/Svg.Skia/blob/master/CHANGELOG.md)
- [Commits](https://github.com/wieslawsoltes/Svg.Skia/commits)

Updates `Avalonia.Svg.Skia` from 11.0.0.14 to 11.0.0.16
- [Release notes](https://github.com/wieslawsoltes/Svg.Skia/releases)
- [Changelog](https://github.com/wieslawsoltes/Svg.Skia/blob/master/CHANGELOG.md)
- [Commits](https://github.com/wieslawsoltes/Svg.Skia/commits)

---
updated-dependencies:
- dependency-name: Avalonia.Svg
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: avalonia
- dependency-name: Avalonia.Svg.Skia
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: avalonia
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-16 20:02:18 +01:00
e19e7622a3 chore: remove repetitive words (#6500)
Signed-off-by: standstaff <zhengxingru@yeah.net>
2024-03-16 19:49:54 +01:00
26026d1357 Fix Title Update Manager not refreshing app list (#6507) 2024-03-16 19:46:03 +01:00
24068b023c discord: Update ApplicationID (#6513) 2024-03-16 19:41:38 +01:00
1217a8e69b GPU: Rebind RTs if scale changes when binding textures (#6493)
This fixes a longstanding issue with resolution scale that could result in flickering graphics, typically the first frame something is drawn, or on camera cuts in cutscenes.

The root cause of the issue is that texture scale can be changed when binding textures or images. This typically happens because a texture becomes a view of a larger texture, such as a 400x225 texture becoming a view of a 800x450 texture with two levels. If the 400x225 texture is bound as a render target and has state [1x Undesired], but the storage texture is [2x Scaled], the render target texture's scale is changed to [2x Scaled] to match its new storage. This means the scale changed after the render target state was processed...

This can cause a number of issues. When render target state is processed, texture scales are examined and potentially changed so that they are all the same value. If one texture is scaled, all textures must be. If one texture is blacklisted from scaling, all of them must be. This results in a single resolution scale value being assigned to the TextureManager, which also scales the scissor and viewport values.

If the scale is chosen as 1x, and a later texture binding changes one of the textures to be 2x, the scale in TextureManager no longer matches all of the bound textures. What's worse, the scales in these textures could mismatch entirely. This typically results in the support buffer scale, viewport and scissor being wrong for at least one of the bound render targets.

This PR fixes the issue by re-evaluating render target state if any scale mismatches the expected scale after texture bindings happen. This can actually cause scale to change again, so it must loop back to perform texture bindings again. This can happen as many times as it needs to, but I don't expect it to happen more than once. Problematic bindings will just result in a blacklist, which will propagate to other bound targets.
2024-03-14 19:59:09 -03:00
732db7581f Consider Polygon as unsupported is triangle fans are unsupported on Vulkan (#6490) 2024-03-14 19:46:57 -03:00
fdd3263e31 Separate guest/host tracking + unaligned protection (#6486)
* WIP: Separate guest/host tracking + unaligned protection

Allow memory manager to define support for single byte guest tracking

* Formatting

* Improve docs

* Properly handle cases where the address space bits are too low

* Address feedback
2024-03-14 19:38:27 -03:00
ce607db944 Ava UI: Update Ava (#6430)
* Update Ava

* Newline
2024-03-14 02:29:13 +01:00
6b4ee82e5d infra: Fix updater for old Ava users (#6441)
* Add binaries with both names to release archives

* Add migration code for the new filename

* Add Ryujinx.Ava to all win/linux releases for a while
2024-03-13 23:26:35 +01:00
e2a655f1a4 Update AutoDeleteCache.cs (#6471)
Increase the texture cache limit from 512 MB to 1 GB.
2024-03-13 17:39:39 -03:00
d9a18919b0 Fix geometry shader passthrough issue (#6462)
* Fix geometry shader passthrough issue (Diagnosed by gdkchan)

* Fix whitespace formatting

* Fix whitespace formatting

* Bump shader cache version

* Don't apply PassthroughNV decorations to output geometry shader variables
2024-03-13 17:26:19 -03:00
8354434a37 Passthrough mouse for win32 (#6450)
* passthrough mouse for win32

* remove unused enums
2024-03-11 21:21:39 -03:00
5a900f38c5 Fix lost copy and swap problem on shader SSA deconstruction (#6455)
* Fix lost copy on shader SSA deconstruction

* Shader cache version bump
2024-03-10 21:16:40 -03:00
a3a63d4394 Refactor memory managers to a common base class, consolidate Read() method logic (#6360)
* - add new abstract class `VirtualMemoryManagerBase`
- rename `MemoryManagerBase` to `VirtualMemoryManagerRefCountedBase` and derive from `VirtualMemoryManagerBase`
- change `AddressSpaceManager`, `HvMemoryManager`, `MemoryManager`, and `MemoryManagerHostMapped` to implement abstract members and use the inherited `void VirtualMemoryManagerBase.Read(TVirtual va, Span<byte> data)` implementation.

* move property `AddressSpaceSize` up by the other properties
2024-03-09 21:01:51 -03:00
3924bd1a43 Update dependencies from SixLabors to the latest version before the license change (#6440)
* nuget: bump SixLabors.ImageSharp from 1.0.4 to 2.1.3 (#3976)

* nuget: bump SixLabors.ImageSharp from 1.0.4 to 2.1.3

Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 1.0.4 to 2.1.3.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v1.0.4...v2.1.3)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update for 2.x changes

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mary <mary@mary.zone>

* Update SixLabors.ImageSharp to 2.1.7

This is the latest version we can update to without the license change.

* Update SixLabors.ImageSharp.Drawing to v1.0.0

This is the latest version we can update to without the license change.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mary <mary@mary.zone>
2024-03-08 13:16:32 -03:00
50458b2472 LightningJit: Disable some cache ops and CTR_EL0 access on Windows Arm (#6326)
* LightningJit: Disable some cache ops and CTR_EL0 access on Windows Arm

* Format whitespace

* Delete unused code

* Fix typo

Co-authored-by: riperiperi <rhy3756547@hotmail.com>

---------

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2024-03-07 20:55:54 -03:00
dda0f26067 UI: Update minimum window size to 800x500 (#6425) 2024-03-07 20:38:56 -03:00
4e1a60328e Add title of game to screenshot text (#6266)
* Add sanitize method

* Add app name to screenshot text output

* Add app name to screenshot text
2024-03-07 22:49:57 +00:00
2505a1abcd misc: Remove myself from reviews
I have been mostly inactive on the project for the past year and a half
apart from handling CI and reviews because of a lack of motivation and
time.
2024-03-05 17:54:35 +01:00
bc4d99a078 ci: try to fix toctou on release creation
Signed-off-by: Mary Guillemard <mary@mary.zone>
2024-03-02 12:58:03 +01:00
ec6cb0abb4 infra: Make Avalonia the default UI (#6375)
* misc: Move Ryujinx project to Ryujinx.Gtk3

This breaks release CI for now but that's fine.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* misc: Move Ryujinx.Ava project to Ryujinx

This breaks CI for now, but it's fine.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* infra: Make Avalonia the default UI

Should fix CI after the previous changes.

GTK3 isn't build by the release job anymore, only by PR CI.

This also ensure that the test-ava update package is still generated to
allow update from the old testing channel.

Signed-off-by: Mary Guillemard <mary@mary.zone>

* Fix missing copy in create_app_bundle.sh

Signed-off-by: Mary Guillemard <mary@mary.zone>

* Fix syntax error

Signed-off-by: Mary Guillemard <mary@mary.zone>

---------

Signed-off-by: Mary Guillemard <mary@mary.zone>
2024-03-02 12:51:05 +01:00
53b5985da6 Avalonia: only enable gamescope workaround under it (#6374)
Signed-off-by: Mary Guillemard <mary@mary.zone>
2024-03-02 11:16:46 +01:00
87f238be60 Change packed aliasing formats to UInt (#6358) 2024-02-24 19:02:20 -03:00
4d0dbbfae2 nuget: bump System.Drawing.Common from 8.0.1 to 8.0.2 (#6308)
Bumps [System.Drawing.Common](https://github.com/dotnet/winforms) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/dotnet/winforms/releases)
- [Changelog](https://github.com/dotnet/winforms/blob/main/docs/release-activity.md)
- [Commits](https://github.com/dotnet/winforms/compare/v8.0.1...v8.0.2)

---
updated-dependencies:
- dependency-name: System.Drawing.Common
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-24 01:17:24 +01:00
08384ee5a8 IPC code gen improvements (#6352)
* HipcGenerator: skip do-nothing call to MemoryMarshal.Cast<byte, byte>() in generated code

* HipcGenerator: fix method name typos

* HipcGenerator: make generated methods use stackalloc for `isBufferMapAlias` bool array

* HipcGenerator: make generated GetCommandHandlers() method return a FrozenDictionary<int, CommandHandler>

* HipcGenerator: return `FrozenDictionary<,>.Empty` when there are no command implementations, otherwise create `FrozenDictionary` from a `IEnumerable<KeyValuePair<,>>`` instead of a `Dictionary<,>``
2024-02-23 21:12:24 -03:00
d4d0a48bfe Migrate Audio service to new IPC (#6285)
* Migrate audren to new IPC

* Migrate audout

* Migrate audin

* Migrate hwopus

* Bye bye old audio service

* Switch volume control to IHardwareDeviceDriver

* Somewhat unrelated changes

* Remove Concentus reference from HLE

* Implement OpenAudioRendererForManualExecution

* Remove SetVolume/GetVolume methods that are not necessary

* Remove SetVolume/GetVolume methods that are not necessary (2)

* Fix incorrect volume update

* PR feedback

* PR feedback

* Stub audrec

* Init outParameter

* Make FinalOutputRecorderParameter/Internal readonly

* Make FinalOutputRecorder IDisposable

* Fix HardwareOpusDecoderManager parameter buffers

* Opus work buffer size and error handling improvements

* Add AudioInProtocolName enum

* Fix potential divisions by zero
2024-02-22 16:58:33 -03:00
57d8afd0c9 OpenGL: Mask out all color outputs with no fragment shader (#6341)
* OpenGL: Mask out all color outputs with no fragment shader

This appears to match Vulkan's behaviour, which is needed for stencil shadows in Penny's Big Breakaway. It's far from the only issue, you can try the Full Bindless PR if you want to see it in a more intact state.

* Remove unused member
2024-02-22 18:43:19 +01:00
c43fb92bbf Ensure service init runs after Horizon constructor (#6342) 2024-02-22 13:55:29 -03:00
167f50bbcd Implement virtual buffer dependencies (#6190)
* Implement virtual buffer copies

* Introduce TranslateAndCreateMultiBuffersPhysicalOnly, use it for copy and clear

* Rename VirtualBufferCache to VirtualRangeCache

* Fix potential issue where virtual range could exist in the cache, without a physical buffer

* Fix bug that could cause copy with negative size on CopyToDependantVirtualBuffer

* Remove virtual copy back for SyncAction

* GetData XML docs

* Make field readonly

* Fix virtual buffer modification tracking

* Remove CopyFromDependantVirtualBuffers from ExternalFlush

* Move things around a little to avoid perf impact

- Inline null check for CopyFromDependantVirtualBuffers
- Remove extra method call for SynchronizeMemoryWithVirtualCopyBack, prefer calling CopyFromDependantVirtualBuffers separately

* Fix up XML doc

---------

Co-authored-by: riperiperi <rhy3756547@hotmail.com>
2024-02-22 11:03:07 -03:00
ba91f5d401 Vulkan: Properly reset barrier batch when splitting due to mismatching flags (#6345)
Forgot to set the end variable here. Should stop it from crashing when this path is taken.
2024-02-22 10:43:22 +01:00
79f6c18a9b Vulkan: Disable push descriptors on older NVIDIA GPUs (#6340)
Disables push descriptors on older NVIDIA GPUs (10xx and below), since it is clearly broken beyond comprehension. The existing workaround wasn't good enough and a more thorough one will probably cost more performance than the feature gains. The workaround has been removed.

Fixes #6331.
2024-02-21 23:52:13 -03:00
4f63782bac Vulkan: Fix barrier batching past limit (#6339)
If more than 16 barriers were queued at one time, the _queuedBarrierCount would no longer match the number of remaining barriers, because when breaking out of the loop consuming them it deleted all barriers, not just the 16 that were consumed.

Should fix freezes that started occurring with #6240. Fixes issue #6338.
2024-02-21 23:41:08 -03:00
6f5fcb7970 Avalonia UI: Update English tooltips (#6305)
* update English tooltips

* missed a dot
2024-02-19 23:27:15 +01:00
6ef8946169 Avalonia: Fix gamescope once and for all (#6301)
Enable input focus proxy, makes WM_TAKE_FOCUS capability to be exposed.

This fix menu on gamescope, for real this time....

Signed-off-by: Mary Guillemard <mary@mary.zone>
2024-02-19 21:18:46 +01:00
42340fc743 LightningJit: Add a limit on the number of instructions per function for Arm64 (#6328) 2024-02-17 17:30:54 -03:00
103e7cb021 hid: Stub SetTouchScreenResolution (#6322)
* hid: Implement SetTouchScreenResolution

* Fix Tomb Raider I-III Remastered from crashing without enabling Ignore Missing Services

* PR Feedback: Update Comments
2024-02-17 14:49:50 -03:00
31ed061bea Vulkan: Improve texture barrier usage, timing and batching (#6240)
* WIP barrier batch

* Add store op to image usage barrier

* Dispose the barrier batch

* Fix encoding?

* Handle read and write on the load op barrier.

Load op consumes read accesses but does not add one, as the only other operation that can read is another load.

* Simplify null check

* Insert barriers on program change in case stale bindings are reintroduced

* Not sure how I messed this one up

* Improve location of bindings barrier update

This is also important for emergency deferred clear

* Update src/Ryujinx.Graphics.Vulkan/BarrierBatch.cs

Co-authored-by: Mary Guillemard <thog@protonmail.com>

---------

Co-authored-by: Mary Guillemard <thog@protonmail.com>
2024-02-17 00:21:37 -03:00
4218311e6a Vulkan: Use push descriptors for uniform bindings when possible (#6154)
* Fix Push Descriptors

* Use push descriptor templates

* Use reserved bindings

* Formatting

* Disable when using MVK

("my heart will go on" starts playing as thousands of mac users shed a tear in unison)

* Introduce limit on push descriptor binding number

The bitmask used for updating push descriptors is ulong, so only 64 bindings can be tracked for now.

* Address feedback

* Fix logic for binding rejection

Should only offset limit when reserved bindings are less than the requested one.

* Workaround pascal and older nv bug

* Add GPU number detection for nvidia

* Only do workaround if it's valid to do so.
2024-02-16 21:41:30 -03:00
685 changed files with 26536 additions and 13188 deletions

View File

@ -259,12 +259,12 @@ dotnet_diagnostic.CA1861.severity = none
# Disable "Prefer using 'string.Equals(string, StringComparison)' to perform a case-insensitive comparison, but keep in mind that this might cause subtle changes in behavior, so make sure to conduct thorough testing after applying the suggestion, or if culturally sensitive comparison is not required, consider using 'StringComparison.OrdinalIgnoreCase'"
dotnet_diagnostic.CA1862.severity = none
[src/Ryujinx.HLE/HOS/Services/**.cs]
# Disable "mark members as static" rule for services
[src/Ryujinx/UI/ViewModels/**.cs]
# Disable "mark members as static" rule for ViewModels
dotnet_diagnostic.CA1822.severity = none
[src/Ryujinx.Ava/UI/ViewModels/**.cs]
# Disable "mark members as static" rule for ViewModels
[src/Ryujinx.HLE/HOS/Services/**.cs]
# Disable "mark members as static" rule for services
dotnet_diagnostic.CA1822.severity = none
[src/Ryujinx.Tests/Cpu/*.cs]

View File

@ -7,7 +7,7 @@ updates:
labels:
- "infra"
reviewers:
- marysaka
- TSRBerry
commit-message:
prefix: "ci"
@ -19,7 +19,7 @@ updates:
labels:
- "infra"
reviewers:
- marysaka
- TSRBerry
commit-message:
prefix: nuget
groups:

2
.github/labeler.yml vendored
View File

@ -20,7 +20,7 @@ gpu:
gui:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**', 'src/Ryujinx.Ava/**']
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**', 'src/Ryujinx.Gtk3/**']
horizon:
- changed-files:

View File

@ -1,31 +1,24 @@
audio:
- marysaka
cpu:
- gdkchan
- riperiperi
- marysaka
- LDj3SNuD
gpu:
- gdkchan
- riperiperi
- marysaka
gui:
- Ack77
- emmauss
- TSRBerry
- marysaka
horizon:
- gdkchan
- Ack77
- marysaka
- TSRBerry
infra:
- marysaka
- TSRBerry
default:

View File

@ -20,7 +20,7 @@ jobs:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
- { name: osx-x64, os: macOS-latest, zip_os_name: osx_x64 }
- { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
fail-fast: false
steps:
@ -41,12 +41,12 @@ jobs:
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Change config filename for macOS
run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os == 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13'
- name: Build
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER
@ -61,21 +61,21 @@ jobs:
- name: Publish Ryujinx
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Publish Ryujinx.Headless.SDL2
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Headless.SDL2 --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Publish Ryujinx.Ava
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_ava -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Ava --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
- name: Publish Ryujinx.Gtk3
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish_gtk -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx.Gtk3 --self-contained true
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Set executable bit
run: |
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
chmod +x ./publish_ava/Ryujinx.Ava ./publish_ava/Ryujinx.sh
chmod +x ./publish_gtk/Ryujinx.Gtk3 ./publish_gtk/Ryujinx.sh
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
- name: Upload Ryujinx artifact
@ -83,21 +83,21 @@ jobs:
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Upload Ryujinx.Headless.SDL2 artifact
uses: actions/upload-artifact@v4
with:
name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_sdl2_headless
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Upload Ryujinx.Ava artifact
- name: Upload Ryujinx.Gtk3 artifact
uses: actions/upload-artifact@v4
with:
name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_ava
if: github.event_name == 'pull_request' && matrix.platform.os != 'macOS-latest'
name: gtk-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish_gtk
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
build_macos:
name: macOS Universal (${{ matrix.configuration }})
@ -140,19 +140,19 @@ jobs:
shell: bash
if: github.event_name == 'pull_request'
- name: Publish macOS Ryujinx.Ava
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
- name: Publish macOS Ryujinx.Headless.SDL2
run: |
./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
- name: Upload Ryujinx.Ava artifact
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v4
with:
name: ava-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
path: "publish_ava/*.tar.gz"
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
path: "publish/*.tar.gz"
if: github.event_name == 'pull_request'
- name: Upload Ryujinx.Headless.SDL2 artifact

View File

@ -39,24 +39,24 @@ jobs:
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request:\n`;
let hidden_avalonia_artifacts = `\n\n <details><summary>Experimental GUI (Avalonia)</summary>\n`;
let hidden_gtk_artifacts = `\n\n <details><summary>Old GUI (GTK3)</summary>\n`;
let hidden_headless_artifacts = `\n\n <details><summary>GUI-less (SDL2)</summary>\n`;
let hidden_debug_artifacts = `\n\n <details><summary>Only for Developers</summary>\n`;
for (const art of artifacts) {
if(art.name.includes('Debug')) {
hidden_debug_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else if(art.name.includes('ava-ryujinx')) {
hidden_avalonia_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else if(art.name.includes('gtk-ryujinx')) {
hidden_gtk_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else if(art.name.includes('sdl2-ryujinx-headless')) {
hidden_headless_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
} else {
body += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`;
}
}
hidden_avalonia_artifacts += `\n</details>`;
hidden_gtk_artifacts += `\n</details>`;
hidden_headless_artifacts += `\n</details>`;
hidden_debug_artifacts += `\n</details>`;
body += hidden_avalonia_artifacts;
body += hidden_gtk_artifacts;
body += hidden_headless_artifacts;
body += hidden_debug_artifacts;

View File

@ -44,6 +44,17 @@ jobs:
sha: context.sha
})
- name: Create release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
tag: ${{ steps.version_info.outputs.build_version }}
body: "For more information about this release please check out the official [Changelog](https://github.com/Ryujinx/Ryujinx/wiki/Changelog)."
omitBodyDuringUpdate: true
owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}
repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}
token: ${{ secrets.RELEASE_TOKEN }}
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
@ -60,7 +71,7 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
@ -86,43 +97,37 @@ jobs:
- name: Publish
run: |
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_gtk/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained true
dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Ava --self-contained true
- name: Packing Windows builds
if: matrix.platform.os == 'windows-latest'
run: |
pushd publish_gtk
pushd publish_ava
cp publish/Ryujinx.exe publish/Ryujinx.Ava.exe
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
pushd publish_sdl2_headless
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
pushd publish_ava
7z a ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
popd
shell: bash
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
run: |
pushd publish_gtk
chmod +x publish/Ryujinx.sh publish/Ryujinx
pushd publish_ava
cp publish/Ryujinx publish/Ryujinx.Ava
chmod +x publish/Ryujinx.sh publish/Ryujinx.Ava publish/Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
popd
pushd publish_sdl2_headless
chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
popd
pushd publish_ava
chmod +x publish/Ryujinx.sh publish/Ryujinx.Ava
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
popd
shell: bash
- name: Pushing new release
@ -183,10 +188,10 @@ jobs:
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
- name: Publish macOS Ryujinx.Ava
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release
- name: Publish macOS Ryujinx.Headless.SDL2
run: |
./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release

View File

@ -3,24 +3,24 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.7" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.7" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.7" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.7" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.13" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.13" />
<PackageVersion Include="Avalonia" Version="11.0.10" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="Concentus" Version="1.1.7" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="8.3.27" />
<PackageVersion Include="DynamicData" Version="8.4.1" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@ -42,10 +42,9 @@
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.16.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.16.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.8" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="1.0.0" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.Drawing.Common" Version="8.0.1" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
<PackageVersion Include="System.Management" Version="8.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />

View File

@ -36,8 +36,8 @@
## Compatibility
As of October 2023, Ryujinx has been tested on approximately 4,200 titles;
over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable.
As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Gtk3", "src\Ryujinx.Gtk3\Ryujinx.Gtk3.csproj", "{074045D4-3ED2-4711-9169-E385F2BFB5A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests", "src\Ryujinx.Tests\Ryujinx.Tests.csproj", "{EBB55AEA-C7D7-4DEB-BF96-FA1789E225E9}"
EndProject
@ -69,7 +69,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Headless.SDL2", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec.FFmpeg", "src\Ryujinx.Graphics.Nvdec.FFmpeg\Ryujinx.Graphics.Nvdec.FFmpeg.csproj", "{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Ava", "src\Ryujinx.Ava\Ryujinx.Ava.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx", "src\Ryujinx\Ryujinx.csproj", "{7C1B2721-13DA-4B62-B046-C626605ECCE6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.UI.Common", "src\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj", "{BA161CA0-CD65-4E6E-B644-51C8D1E542DC}"
EndProject

View File

@ -4,6 +4,8 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ASET/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Astc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Luma/@EntryIndexedValue">True</s:Boolean>

View File

@ -6,10 +6,6 @@ if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL2" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL2"
fi
if [ -f "$SCRIPT_DIR/Ryujinx.Ava" ]; then
RYUJINX_BIN="Ryujinx.Ava"
fi
if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
RYUJINX_BIN="Ryujinx"
fi

View File

@ -14,8 +14,8 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
# Copy executables first
cp "$PUBLISH_DIRECTORY/Ryujinx.Ava" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
# Copy executable and nsure executable can be executed
cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
# Then all libraries

View File

@ -22,9 +22,9 @@ EXTRA_ARGS=$8
if [ "$VERSION" == "1.1.0" ];
then
RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
else
RELEASE_TAR_FILE_NAME=test-ava-ryujinx-$VERSION-macos_universal.app.tar
RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
fi
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
@ -38,9 +38,9 @@ mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
dotnet restore
dotnet build -c "$CONFIGURATION" src/Ryujinx.Ava
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx.Ava
dotnet build -c "$CONFIGURATION" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
@ -108,6 +108,13 @@ tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME"
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
rm "$RELEASE_TAR_FILE_NAME"
# Create legacy update package for Avalonia to not left behind old testers.
if [ "$VERSION" != "1.1.0" ];
then
cp $RELEASE_TAR_FILE_NAME.gz test-ava-ryujinx-$VERSION-macos_universal.app.tar.gz
fi
popd
echo "Done"

View File

@ -33,8 +33,3 @@ Project Docs
=================
To be added. Many project files will contain basic XML docs for key functions and classes in the meantime.
Other Information
=================
- N/A

View File

@ -19,6 +19,12 @@ namespace ARMeilleure.Instructions
Operand n = GetAluN(context);
Operand m = GetAluM(context, setCarry: false);
if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
{
// For ADR, PC is always 4 bytes aligned, even in Thumb mode.
n = context.BitwiseAnd(n, Const(~3u));
}
Operand res = context.Add(n, m);
if (ShouldSetFlags(context))
@ -467,6 +473,12 @@ namespace ARMeilleure.Instructions
Operand n = GetAluN(context);
Operand m = GetAluM(context, setCarry: false);
if (op.Rn == RegisterAlias.Aarch32Pc && op is OpCodeT32AluImm12)
{
// For ADR, PC is always 4 bytes aligned, even in Thumb mode.
n = context.BitwiseAnd(n, Const(~3u));
}
Operand res = context.Subtract(n, m);
if (ShouldSetFlags(context))

View File

@ -157,7 +157,7 @@ namespace ARMeilleure.Instructions
context.Copy(temp, value);
if (!context.Memory.Type.IsHostMapped())
if (!context.Memory.Type.IsHostMappedOrTracked())
{
context.Branch(lblEnd);
@ -198,7 +198,7 @@ namespace ARMeilleure.Instructions
SetInt(context, rt, value);
if (!context.Memory.Type.IsHostMapped())
if (!context.Memory.Type.IsHostMappedOrTracked())
{
context.Branch(lblEnd);
@ -265,7 +265,7 @@ namespace ARMeilleure.Instructions
context.Copy(GetVec(rt), value);
if (!context.Memory.Type.IsHostMapped())
if (!context.Memory.Type.IsHostMappedOrTracked())
{
context.Branch(lblEnd);
@ -312,7 +312,7 @@ namespace ARMeilleure.Instructions
break;
}
if (!context.Memory.Type.IsHostMapped())
if (!context.Memory.Type.IsHostMappedOrTracked())
{
context.Branch(lblEnd);
@ -385,7 +385,7 @@ namespace ARMeilleure.Instructions
break;
}
if (!context.Memory.Type.IsHostMapped())
if (!context.Memory.Type.IsHostMappedOrTracked())
{
context.Branch(lblEnd);
@ -403,6 +403,27 @@ namespace ARMeilleure.Instructions
{
return EmitHostMappedPointer(context, address);
}
else if (context.Memory.Type.IsHostTracked())
{
if (address.Type == OperandType.I32)
{
address = context.ZeroExtend32(OperandType.I64, address);
}
if (context.Memory.Type == MemoryManagerType.HostTracked)
{
Operand mask = Const(ulong.MaxValue >> (64 - context.Memory.AddressSpaceBits));
address = context.BitwiseAnd(address, mask);
}
Operand ptBase = !context.HasPtc
? Const(context.Memory.PageTablePointer.ToInt64())
: Const(context.Memory.PageTablePointer.ToInt64(), Ptc.PageTableSymbol);
Operand ptOffset = context.ShiftRightUI(address, Const(PageBits));
return context.Add(address, context.Load(OperandType.I64, context.Add(ptBase, context.ShiftLeft(ptOffset, Const(3)))));
}
int ptLevelBits = context.Memory.AddressSpaceBits - PageBits;
int ptLevelSize = 1 << ptLevelBits;

View File

@ -2426,7 +2426,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtss, GetVec(op.Rn)), scalar: true);
// RSQRTSS handles subnormals as zero, which differs from Arm, so we can't use it here.
Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtss, GetVec(op.Rn));
res = context.AddIntrinsic(Intrinsic.X86Rcpss, res);
res = EmitSse41Round32Exp8OpF(context, res, scalar: true);
context.Copy(GetVec(op.Rd), context.VectorZeroUpper96(res));
}
@ -2451,7 +2455,11 @@ namespace ARMeilleure.Instructions
}
else if (Optimizations.FastFP && Optimizations.UseSse41 && sizeF == 0)
{
Operand res = EmitSse41Round32Exp8OpF(context, context.AddIntrinsic(Intrinsic.X86Rsqrtps, GetVec(op.Rn)), scalar: false);
// RSQRTPS handles subnormals as zero, which differs from Arm, so we can't use it here.
Operand res = context.AddIntrinsic(Intrinsic.X86Sqrtps, GetVec(op.Rn));
res = context.AddIntrinsic(Intrinsic.X86Rcpps, res);
res = EmitSse41Round32Exp8OpF(context, res, scalar: false);
if (op.RegisterSize == RegisterSize.Simd64)
{

View File

@ -91,54 +91,54 @@ namespace ARMeilleure.Instructions
#region "Read"
public static byte ReadByte(ulong address)
{
return GetMemoryManager().ReadTracked<byte>(address);
return GetMemoryManager().ReadGuest<byte>(address);
}
public static ushort ReadUInt16(ulong address)
{
return GetMemoryManager().ReadTracked<ushort>(address);
return GetMemoryManager().ReadGuest<ushort>(address);
}
public static uint ReadUInt32(ulong address)
{
return GetMemoryManager().ReadTracked<uint>(address);
return GetMemoryManager().ReadGuest<uint>(address);
}
public static ulong ReadUInt64(ulong address)
{
return GetMemoryManager().ReadTracked<ulong>(address);
return GetMemoryManager().ReadGuest<ulong>(address);
}
public static V128 ReadVector128(ulong address)
{
return GetMemoryManager().ReadTracked<V128>(address);
return GetMemoryManager().ReadGuest<V128>(address);
}
#endregion
#region "Write"
public static void WriteByte(ulong address, byte value)
{
GetMemoryManager().Write(address, value);
GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt16(ulong address, ushort value)
{
GetMemoryManager().Write(address, value);
GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt32(ulong address, uint value)
{
GetMemoryManager().Write(address, value);
GetMemoryManager().WriteGuest(address, value);
}
public static void WriteUInt64(ulong address, ulong value)
{
GetMemoryManager().Write(address, value);
GetMemoryManager().WriteGuest(address, value);
}
public static void WriteVector128(ulong address, V128 value)
{
GetMemoryManager().Write(address, value);
GetMemoryManager().WriteGuest(address, value);
}
#endregion

View File

@ -28,6 +28,17 @@ namespace ARMeilleure.Memory
/// <returns>The data</returns>
T ReadTracked<T>(ulong va) where T : unmanaged;
/// <summary>
/// Reads data from CPU mapped memory, from guest code. (with read tracking)
/// </summary>
/// <typeparam name="T">Type of the data being read</typeparam>
/// <param name="va">Virtual address of the data in memory</param>
/// <returns>The data</returns>
T ReadGuest<T>(ulong va) where T : unmanaged
{
return ReadTracked<T>(va);
}
/// <summary>
/// Writes data to CPU mapped memory.
/// </summary>
@ -36,6 +47,17 @@ namespace ARMeilleure.Memory
/// <param name="value">Data to be written</param>
void Write<T>(ulong va, T value) where T : unmanaged;
/// <summary>
/// Writes data to CPU mapped memory, from guest code.
/// </summary>
/// <typeparam name="T">Type of the data being written</typeparam>
/// <param name="va">Virtual address to write the data into</param>
/// <param name="value">Data to be written</param>
void WriteGuest<T>(ulong va, T value) where T : unmanaged
{
Write(va, value);
}
/// <summary>
/// Gets a read-only span of data from CPU mapped memory.
/// </summary>

View File

@ -29,6 +29,18 @@ namespace ARMeilleure.Memory
/// Allows invalid access from JIT code to the rest of the program, but is faster.
/// </summary>
HostMappedUnsafe,
/// <summary>
/// High level implementation using a software flat page table for address translation
/// with no support for handling invalid or non-contiguous memory access.
/// </summary>
HostTracked,
/// <summary>
/// High level implementation using a software flat page table for address translation
/// without masking the address and no support for handling invalid or non-contiguous memory access.
/// </summary>
HostTrackedUnsafe,
}
public static class MemoryManagerTypeExtensions
@ -37,5 +49,15 @@ namespace ARMeilleure.Memory
{
return type == MemoryManagerType.HostMapped || type == MemoryManagerType.HostMappedUnsafe;
}
public static bool IsHostTracked(this MemoryManagerType type)
{
return type == MemoryManagerType.HostTracked || type == MemoryManagerType.HostTrackedUnsafe;
}
public static bool IsHostMappedOrTracked(this MemoryManagerType type)
{
return type.IsHostMapped() || type.IsHostTracked();
}
}
}

View File

@ -21,10 +21,8 @@ namespace ARMeilleure.Signal
private const uint EXCEPTION_ACCESS_VIOLATION = 0xc0000005;
private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize, ulong pageSize)
private static Operand EmitGenericRegionCheck(EmitterContext context, IntPtr signalStructPtr, Operand faultAddress, Operand isWrite, int rangeStructSize)
{
ulong pageMask = pageSize - 1;
Operand inRegionLocal = context.AllocateLocal(OperandType.I32);
context.Copy(inRegionLocal, Const(0));
@ -51,7 +49,7 @@ namespace ARMeilleure.Signal
// Only call tracking if in range.
context.BranchIfFalse(nextLabel, inRange, BasicBlockFrequency.Cold);
Operand offset = context.BitwiseAnd(context.Subtract(faultAddress, rangeAddress), Const(~pageMask));
Operand offset = context.Subtract(faultAddress, rangeAddress);
// Call the tracking action, with the pointer's relative offset to the base address.
Operand trackingActionPtr = context.Load(OperandType.I64, Const((ulong)signalStructPtr + rangeBaseOffset + 20));
@ -62,8 +60,10 @@ namespace ARMeilleure.Signal
// Tracking action should be non-null to call it, otherwise assume false return.
context.BranchIfFalse(skipActionLabel, trackingActionPtr);
Operand result = context.Call(trackingActionPtr, OperandType.I32, offset, Const(pageSize), isWrite);
context.Copy(inRegionLocal, result);
Operand result = context.Call(trackingActionPtr, OperandType.I64, offset, Const(1UL), isWrite);
context.Copy(inRegionLocal, context.ICompareNotEqual(result, Const(0UL)));
GenerateFaultAddressPatchCode(context, faultAddress, result);
context.MarkLabel(skipActionLabel);
@ -155,7 +155,7 @@ namespace ARMeilleure.Signal
throw new PlatformNotSupportedException();
}
public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize, ulong pageSize)
public static byte[] GenerateUnixSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@ -168,7 +168,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize, pageSize);
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@ -203,7 +203,7 @@ namespace ARMeilleure.Signal
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
}
public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize, ulong pageSize)
public static byte[] GenerateWindowsSignalHandler(IntPtr signalStructPtr, int rangeStructSize)
{
EmitterContext context = new();
@ -232,7 +232,7 @@ namespace ARMeilleure.Signal
Operand isWrite = context.ICompareNotEqual(writeFlag, Const(0L)); // Normalize to 0/1.
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize, pageSize);
Operand isInRegion = EmitGenericRegionCheck(context, signalStructPtr, faultAddress, isWrite, rangeStructSize);
Operand endLabel = Label();
@ -256,5 +256,86 @@ namespace ARMeilleure.Signal
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Code;
}
private static void GenerateFaultAddressPatchCode(EmitterContext context, Operand faultAddress, Operand newAddress)
{
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
if (SupportsFaultAddressPatchingForHostOs())
{
Operand lblSkip = Label();
context.BranchIf(lblSkip, faultAddress, newAddress, Comparison.Equal);
Operand ucontextPtr = context.LoadArgument(OperandType.I64, 2);
Operand pcCtxAddress = default;
ulong baseRegsOffset = 0;
if (OperatingSystem.IsLinux())
{
pcCtxAddress = context.Add(ucontextPtr, Const(440UL));
baseRegsOffset = 184UL;
}
else if (OperatingSystem.IsMacOS() || OperatingSystem.IsIOS())
{
ucontextPtr = context.Load(OperandType.I64, context.Add(ucontextPtr, Const(48UL)));
pcCtxAddress = context.Add(ucontextPtr, Const(272UL));
baseRegsOffset = 16UL;
}
Operand pc = context.Load(OperandType.I64, pcCtxAddress);
Operand reg = GetAddressRegisterFromArm64Instruction(context, pc);
Operand reg64 = context.ZeroExtend32(OperandType.I64, reg);
Operand regCtxAddress = context.Add(ucontextPtr, context.Add(context.ShiftLeft(reg64, Const(3)), Const(baseRegsOffset)));
Operand regAddress = context.Load(OperandType.I64, regCtxAddress);
Operand addressDelta = context.Subtract(regAddress, faultAddress);
context.Store(regCtxAddress, context.Add(newAddress, addressDelta));
context.MarkLabel(lblSkip);
}
}
}
private static Operand GetAddressRegisterFromArm64Instruction(EmitterContext context, Operand pc)
{
Operand inst = context.Load(OperandType.I32, pc);
Operand reg = context.AllocateLocal(OperandType.I32);
Operand isSysInst = context.ICompareEqual(context.BitwiseAnd(inst, Const(0xFFF80000)), Const(0xD5080000));
Operand lblSys = Label();
Operand lblEnd = Label();
context.BranchIfTrue(lblSys, isSysInst, BasicBlockFrequency.Cold);
context.Copy(reg, context.BitwiseAnd(context.ShiftRightUI(inst, Const(5)), Const(0x1F)));
context.Branch(lblEnd);
context.MarkLabel(lblSys);
context.Copy(reg, context.BitwiseAnd(inst, Const(0x1F)));
context.MarkLabel(lblEnd);
return reg;
}
public static bool SupportsFaultAddressPatchingForHost()
{
return SupportsFaultAddressPatchingForHostArch() && SupportsFaultAddressPatchingForHostOs();
}
private static bool SupportsFaultAddressPatchingForHostArch()
{
return RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
}
private static bool SupportsFaultAddressPatchingForHostOs()
{
return OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS();
}
}
}

View File

@ -29,7 +29,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 5518; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 6634; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@ -857,8 +857,14 @@ namespace ARMeilleure.Translation.PTC
Stopwatch sw = Stopwatch.StartNew();
threads.ForEach((thread) => thread.Start());
threads.ForEach((thread) => thread.Join());
foreach (var thread in threads)
{
thread.Start();
}
foreach (var thread in threads)
{
thread.Join();
}
threads.Clear();

View File

@ -20,6 +20,25 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _stillRunning;
private readonly Thread _updaterThread;
private float _volume;
public float Volume
{
get
{
return _volume;
}
set
{
_volume = value;
foreach (OpenALHardwareDeviceSession session in _sessions.Keys)
{
session.UpdateMasterVolume(value);
}
}
}
public OpenALHardwareDeviceDriver()
{
_device = ALC.OpenDevice("");
@ -34,6 +53,8 @@ namespace Ryujinx.Audio.Backends.OpenAL
Name = "HardwareDeviceDriver.OpenAL",
};
_volume = 1f;
_updaterThread.Start();
}
@ -52,7 +73,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
}
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@ -73,7 +94,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
throw new ArgumentException($"{channelCount}");
}
OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
OpenALHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);

View File

@ -16,10 +16,11 @@ namespace Ryujinx.Audio.Backends.OpenAL
private bool _isActive;
private readonly Queue<OpenALAudioBuffer> _queuedBuffers;
private ulong _playedSampleCount;
private float _volume;
private readonly object _lock = new();
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
public OpenALHardwareDeviceSession(OpenALHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_queuedBuffers = new Queue<OpenALAudioBuffer>();
@ -27,7 +28,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
_targetFormat = GetALFormat();
_isActive = false;
_playedSampleCount = 0;
SetVolume(requestedVolume);
SetVolume(1f);
}
private ALFormat GetALFormat()
@ -85,17 +86,22 @@ namespace Ryujinx.Audio.Backends.OpenAL
public override void SetVolume(float volume)
{
lock (_lock)
{
AL.Source(_sourceId, ALSourcef.Gain, volume);
}
_volume = volume;
UpdateMasterVolume(_driver.Volume);
}
public override float GetVolume()
{
AL.GetSource(_sourceId, ALSourcef.Gain, out float volume);
return _volume;
}
return volume;
public void UpdateMasterVolume(float newVolume)
{
lock (_lock)
{
AL.Source(_sourceId, ALSourcef.Gain, newVolume * _volume);
}
}
public override void Start()

View File

@ -20,6 +20,8 @@ namespace Ryujinx.Audio.Backends.SDL2
private readonly bool _supportSurroundConfiguration;
public float Volume { get; set; }
// TODO: Add this to SDL2-CS
// NOTE: We use a DllImport here because of marshaling issue for spec.
#pragma warning disable SYSLIB1054
@ -48,6 +50,8 @@ namespace Ryujinx.Audio.Backends.SDL2
{
_supportSurroundConfiguration = spec.channels >= 6;
}
Volume = 1f;
}
public static bool IsSupported => IsSupportedInternal();
@ -74,7 +78,7 @@ namespace Ryujinx.Audio.Backends.SDL2
return _pauseEvent;
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@ -91,7 +95,7 @@ namespace Ryujinx.Audio.Backends.SDL2
throw new NotImplementedException("Input direction is currently not implemented on SDL2 backend!");
}
SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
SDL2HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);

View File

@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Threading;
@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Backends.SDL2
private float _volume;
private readonly ushort _nativeSampleFormat;
public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
public SDL2HardwareDeviceSession(SDL2HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
@ -37,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SDL2
_nativeSampleFormat = SDL2HardwareDeviceDriver.GetSDL2Format(RequestedSampleFormat);
_sampleCount = uint.MaxValue;
_started = false;
_volume = requestedVolume;
_volume = 1f;
}
private void EnsureAudioStreamSetup(AudioBuffer buffer)
@ -87,7 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
byte[] samples = new byte[frameCount * _bytesPerFrame];
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame);
Span<byte> samples = samplesOwner.Memory.Span;
_ringBuffer.Read(samples, 0, samples.Length);
@ -99,7 +103,7 @@ namespace Ryujinx.Audio.Backends.SDL2
streamSpan.Clear();
// Apply volume to written data
SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_volume * SDL_MIX_MAXVOLUME));
SDL_MixAudioFormat(stream, pStreamSrc, _nativeSampleFormat, (uint)samples.Length, (int)(_driver.Volume * _volume * SDL_MIX_MAXVOLUME));
}
ulong sampleCount = GetSampleCount(samples.Length);

View File

@ -19,6 +19,25 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly ConcurrentDictionary<SoundIoHardwareDeviceSession, byte> _sessions;
private int _disposeState;
private float _volume = 1f;
public float Volume
{
get
{
return _volume;
}
set
{
_volume = value;
foreach (SoundIoHardwareDeviceSession session in _sessions.Keys)
{
session.UpdateMasterVolume(value);
}
}
}
public SoundIoHardwareDeviceDriver()
{
_audioContext = SoundIoContext.Create();
@ -122,7 +141,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
return _pauseEvent;
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@ -134,14 +153,12 @@ namespace Ryujinx.Audio.Backends.SoundIo
sampleRate = Constants.TargetSampleRate;
}
volume = Math.Clamp(volume, 0, 1);
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on SoundIO backend!");
}
SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
SoundIoHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);

View File

@ -1,8 +1,10 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Backends.SoundIo.Native;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
@ -18,16 +20,18 @@ namespace Ryujinx.Audio.Backends.SoundIo
private readonly DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount;
private readonly ManualResetEvent _updateRequiredEvent;
private float _volume;
private int _disposeState;
public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
public SoundIoHardwareDeviceSession(SoundIoHardwareDeviceDriver driver, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue<SoundIoAudioBuffer>();
_ringBuffer = new DynamicRingBuffer();
_volume = 1f;
SetupOutputStream(requestedVolume);
SetupOutputStream(driver.Volume);
}
private void SetupOutputStream(float requestedVolume)
@ -35,7 +39,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
_outputStream = _driver.OpenStream(RequestedSampleFormat, RequestedSampleRate, RequestedChannelCount);
_outputStream.WriteCallback += Update;
_outputStream.Volume = requestedVolume;
// TODO: Setup other callbacks (errors, ect).
// TODO: Setup other callbacks (errors, etc.)
_outputStream.Open();
}
@ -47,7 +51,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override float GetVolume()
{
return _outputStream.Volume;
return _volume;
}
public override void PrepareToClose() { }
@ -63,7 +67,14 @@ namespace Ryujinx.Audio.Backends.SoundIo
public override void SetVolume(float volume)
{
_outputStream.SetVolume(volume);
_volume = volume;
_outputStream.SetVolume(_driver.Volume * volume);
}
public void UpdateMasterVolume(float newVolume)
{
_outputStream.SetVolume(newVolume * _volume);
}
public override void Start()
@ -111,7 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
byte[] samples = new byte[frameCount * bytesPerFrame];
using IMemoryOwner<byte> samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame);
Span<byte> samples = samplesOwner.Memory.Span;
_ringBuffer.Read(samples, 0, samples.Length);

View File

@ -1,5 +1,7 @@
using Ryujinx.Common;
using Ryujinx.Common.Memory;
using System;
using System.Buffers;
namespace Ryujinx.Audio.Backends.Common
{
@ -12,7 +14,8 @@ namespace Ryujinx.Audio.Backends.Common
private readonly object _lock = new();
private byte[] _buffer;
private IMemoryOwner<byte> _bufferOwner;
private Memory<byte> _buffer;
private int _size;
private int _headOffset;
private int _tailOffset;
@ -21,7 +24,8 @@ namespace Ryujinx.Audio.Backends.Common
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
_buffer = new byte[initialCapacity];
_bufferOwner = ByteMemoryPool.RentCleared(initialCapacity);
_buffer = _bufferOwner.Memory;
}
public void Clear()
@ -33,6 +37,11 @@ namespace Ryujinx.Audio.Backends.Common
public void Clear(int size)
{
if (size == 0)
{
return;
}
lock (_lock)
{
if (size > _size)
@ -40,11 +49,6 @@ namespace Ryujinx.Audio.Backends.Common
size = _size;
}
if (size == 0)
{
return;
}
_headOffset = (_headOffset + size) % _buffer.Length;
_size -= size;
@ -58,28 +62,31 @@ namespace Ryujinx.Audio.Backends.Common
private void SetCapacityLocked(int capacity)
{
byte[] buffer = new byte[capacity];
IMemoryOwner<byte> newBufferOwner = ByteMemoryPool.RentCleared(capacity);
Memory<byte> newBuffer = newBufferOwner.Memory;
if (_size > 0)
{
if (_headOffset < _tailOffset)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _size);
_buffer.Slice(_headOffset, _size).CopyTo(newBuffer);
}
else
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, 0, _buffer.Length - _headOffset);
Buffer.BlockCopy(_buffer, 0, buffer, _buffer.Length - _headOffset, _tailOffset);
_buffer[_headOffset..].CopyTo(newBuffer);
_buffer[.._tailOffset].CopyTo(newBuffer[(_buffer.Length - _headOffset)..]);
}
}
_buffer = buffer;
_bufferOwner.Dispose();
_bufferOwner = newBufferOwner;
_buffer = newBuffer;
_headOffset = 0;
_tailOffset = _size;
}
public void Write<T>(T[] buffer, int index, int count)
public void Write(ReadOnlySpan<byte> buffer, int index, int count)
{
if (count == 0)
{
@ -99,17 +106,17 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
else
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, tailLength);
Buffer.BlockCopy(buffer, index + tailLength, _buffer, 0, count - tailLength);
buffer.Slice(index, tailLength).CopyTo(_buffer.Span[_tailOffset..]);
buffer.Slice(index + tailLength, count - tailLength).CopyTo(_buffer.Span);
}
}
else
{
Buffer.BlockCopy(buffer, index, _buffer, _tailOffset, count);
buffer.Slice(index, count).CopyTo(_buffer.Span[_tailOffset..]);
}
_size += count;
@ -117,8 +124,13 @@ namespace Ryujinx.Audio.Backends.Common
}
}
public int Read<T>(T[] buffer, int index, int count)
public int Read(Span<byte> buffer, int index, int count)
{
if (count == 0)
{
return 0;
}
lock (_lock)
{
if (count > _size)
@ -126,14 +138,9 @@ namespace Ryujinx.Audio.Backends.Common
count = _size;
}
if (count == 0)
{
return 0;
}
if (_headOffset < _tailOffset)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
_buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
@ -141,12 +148,12 @@ namespace Ryujinx.Audio.Backends.Common
if (tailLength >= count)
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, count);
_buffer.Span.Slice(_headOffset, count).CopyTo(buffer[index..]);
}
else
{
Buffer.BlockCopy(_buffer, _headOffset, buffer, index, tailLength);
Buffer.BlockCopy(_buffer, 0, buffer, index + tailLength, count - tailLength);
_buffer.Span.Slice(_headOffset, tailLength).CopyTo(buffer[index..]);
_buffer.Span[..(count - tailLength)].CopyTo(buffer[(index + tailLength)..]);
}
}

View File

@ -16,6 +16,12 @@ namespace Ryujinx.Audio.Backends.CompatLayer
public static bool IsSupported => true;
public float Volume
{
get => _realDriver.Volume;
set => _realDriver.Volume = value;
}
public CompatLayerHardwareDeviceDriver(IHardwareDeviceDriver realDevice)
{
_realDriver = realDevice;
@ -90,7 +96,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
throw new ArgumentException("No valid sample format configuration found!");
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
@ -102,8 +108,6 @@ namespace Ryujinx.Audio.Backends.CompatLayer
sampleRate = Constants.TargetSampleRate;
}
volume = Math.Clamp(volume, 0, 1);
if (!_realDriver.SupportsDirection(direction))
{
if (direction == Direction.Input)
@ -119,7 +123,7 @@ namespace Ryujinx.Audio.Backends.CompatLayer
SampleFormat hardwareSampleFormat = SelectHardwareSampleFormat(sampleFormat);
uint hardwareChannelCount = SelectHardwareChannelCount(channelCount);
IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount, volume);
IHardwareDeviceSession realSession = _realDriver.OpenDeviceSession(direction, memoryManager, hardwareSampleFormat, sampleRate, hardwareChannelCount);
if (hardwareChannelCount == channelCount && hardwareSampleFormat == sampleFormat)
{

View File

@ -14,13 +14,17 @@ namespace Ryujinx.Audio.Backends.Dummy
public static bool IsSupported => true;
public float Volume { get; set; }
public DummyHardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
Volume = 1f;
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume)
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (sampleRate == 0)
{
@ -34,7 +38,7 @@ namespace Ryujinx.Audio.Backends.Dummy
if (direction == Direction.Output)
{
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount, volume);
return new DummyHardwareDeviceSessionOutput(this, memoryManager, sampleFormat, sampleRate, channelCount);
}
return new DummyHardwareDeviceSessionInput(this, memoryManager);

View File

@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Backends.Dummy
private ulong _playedSampleCount;
public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount, float requestedVolume) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
public DummyHardwareDeviceSessionOutput(IHardwareDeviceDriver manager, IVirtualMemoryManager memoryManager, SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_volume = requestedVolume;
_volume = 1f;
_manager = manager;
}

View File

@ -166,7 +166,6 @@ namespace Ryujinx.Audio.Input
/// </summary>
/// <param name="filtered">If true, filter disconnected devices</param>
/// <returns>The list of all audio inputs name</returns>
#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioIns(bool filtered)
{
if (filtered)
@ -176,7 +175,6 @@ namespace Ryujinx.Audio.Input
return new[] { Constants.DefaultDeviceInputName };
}
#pragma warning restore CA1822
/// <summary>
/// Open a new <see cref="AudioInputSystem"/>.
@ -188,8 +186,6 @@ namespace Ryujinx.Audio.Input
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
/// <param name="processHandle">The process handle of the application</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode OpenAudioIn(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@ -197,9 +193,7 @@ namespace Ryujinx.Audio.Input
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
ref AudioInputConfiguration parameter,
ulong appletResourceUserId,
uint processHandle)
ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();

View File

@ -13,9 +13,9 @@ namespace Ryujinx.Audio.Integration
private readonly byte[] _buffer;
public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate, float volume)
public HardwareDeviceImpl(IHardwareDeviceDriver deviceDriver, uint channelCount, uint sampleRate)
{
_session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount, volume);
_session = deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, null, SampleFormat.PcmInt16, sampleRate, channelCount);
_channelCount = channelCount;
_sampleRate = sampleRate;
_currentBufferTag = 0;

View File

@ -16,7 +16,9 @@ namespace Ryujinx.Audio.Integration
Output,
}
IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount, float volume = 1f);
float Volume { get; set; }
IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager, SampleFormat sampleFormat, uint sampleRate, uint channelCount);
ManualResetEvent GetUpdateRequiredEvent();
ManualResetEvent GetPauseEvent();

View File

@ -165,12 +165,10 @@ namespace Ryujinx.Audio.Output
/// Get the list of all audio outputs name.
/// </summary>
/// <returns>The list of all audio outputs name</returns>
#pragma warning disable CA1822 // Mark member as static
public string[] ListAudioOuts()
{
return new[] { Constants.DefaultDeviceOutputName };
}
#pragma warning restore CA1822
/// <summary>
/// Open a new <see cref="AudioOutputSystem"/>.
@ -182,9 +180,6 @@ namespace Ryujinx.Audio.Output
/// <param name="inputDeviceName">The input device name wanted by the user</param>
/// <param name="sampleFormat">The sample format to use</param>
/// <param name="parameter">The user configuration</param>
/// <param name="appletResourceUserId">The applet resource user id of the application</param>
/// <param name="processHandle">The process handle of the application</param>
/// <param name="volume">The volume level to request</param>
/// <returns>A <see cref="ResultCode"/> reporting an error or a success</returns>
public ResultCode OpenAudioOut(out string outputDeviceName,
out AudioOutputConfiguration outputConfiguration,
@ -192,16 +187,13 @@ namespace Ryujinx.Audio.Output
IVirtualMemoryManager memoryManager,
string inputDeviceName,
SampleFormat sampleFormat,
ref AudioInputConfiguration parameter,
ulong appletResourceUserId,
uint processHandle,
float volume)
ref AudioInputConfiguration parameter)
{
int sessionId = AcquireSessionId();
_sessionsBufferEvents[sessionId].Clear();
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount, volume);
IHardwareDeviceSession deviceSession = _deviceDriver.OpenDeviceSession(IHardwareDeviceDriver.Direction.Output, memoryManager, sampleFormat, parameter.SampleRate, parameter.ChannelCount);
AudioOutputSystem audioOut = new(this, _lock, deviceSession, _sessionsBufferEvents[sessionId]);
@ -234,41 +226,6 @@ namespace Ryujinx.Audio.Output
return result;
}
/// <summary>
/// Sets the volume for all output devices.
/// </summary>
/// <param name="volume">The volume to set.</param>
public void SetVolume(float volume)
{
if (_sessions != null)
{
foreach (AudioOutputSystem session in _sessions)
{
session?.SetVolume(volume);
}
}
}
/// <summary>
/// Gets the volume for all output devices.
/// </summary>
/// <returns>A float indicating the volume level.</returns>
public float GetVolume()
{
if (_sessions != null)
{
foreach (AudioOutputSystem session in _sessions)
{
if (session != null)
{
return session.GetVolume();
}
}
}
return 0.0f;
}
public void Dispose()
{
GC.SuppressFinalize(this);

View File

@ -25,7 +25,7 @@ namespace Ryujinx.Audio.Renderer.Common
public ulong Flags;
/// <summary>
/// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
/// Represents an error during <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ErrorInfo

View File

@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Common
{
/// <summary>
/// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.ReadOnlyMemory{byte})"/>.
/// Update data header used for input and output of <see cref="Server.AudioRenderSystem.Update(System.Memory{byte}, System.Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/>.
/// </summary>
public struct UpdateDataHeader
{

View File

@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_event = new ManualResetEvent(false);
}
#pragma warning disable IDE0051 // Remove unused private member
private static uint GetHardwareChannelCount(IHardwareDeviceDriver deviceDriver)
{
// Get the real device driver (In case the compat layer is on top of it).
@ -59,9 +58,8 @@ namespace Ryujinx.Audio.Renderer.Dsp
// NOTE: We default to stereo as this will get downmixed to mono by the compat layer if it's not compatible.
return 2;
}
#pragma warning restore IDE0051
public void Start(IHardwareDeviceDriver deviceDriver, float volume)
public void Start(IHardwareDeviceDriver deviceDriver)
{
OutputDevices = new IHardwareDevice[Constants.AudioRendererSessionCountMax];
@ -70,7 +68,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < OutputDevices.Length; i++)
{
// TODO: Don't hardcode sample rate.
OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate, volume);
OutputDevices[i] = new HardwareDeviceImpl(deviceDriver, channelCount, Constants.TargetSampleRate);
}
_mailbox = new Mailbox<MailboxMessage>();
@ -231,33 +229,6 @@ namespace Ryujinx.Audio.Renderer.Dsp
_mailbox.SendResponse(MailboxMessage.Stop);
}
public float GetVolume()
{
if (OutputDevices != null)
{
foreach (IHardwareDevice outputDevice in OutputDevices)
{
if (outputDevice != null)
{
return outputDevice.GetVolume();
}
}
}
return 0f;
}
public void SetVolume(float volume)
{
if (OutputDevices != null)
{
foreach (IHardwareDevice outputDevice in OutputDevices)
{
outputDevice?.SetVolume(volume);
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
@ -269,6 +240,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
if (disposing)
{
_event.Dispose();
_mailbox?.Dispose();
}
}
}

View File

@ -8,7 +8,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// <summary>
/// Output information for behaviour.
/// </summary>
/// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, ReadOnlyMemory{byte})"/> processing.</remarks>
/// <remarks>This is used to report errors to the user during <see cref="Server.AudioRenderSystem.Update(Memory{byte}, Memory{byte}, System.Buffers.ReadOnlySequence{byte})"/> processing.</remarks>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BehaviourErrorInfoOutStatus
{

View File

@ -386,7 +386,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlyMemory<byte> input)
public ResultCode Update(Memory<byte> output, Memory<byte> performanceOutput, ReadOnlySequence<byte> input)
{
lock (_lock)
{
@ -419,14 +419,16 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
result = stateUpdater.UpdateVoices(_voiceContext, _memoryPools);
PoolMapper poolMapper = new PoolMapper(_processHandle, _memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
result = stateUpdater.UpdateVoices(_voiceContext, poolMapper);
if (result != ResultCode.Success)
{
return result;
}
result = stateUpdater.UpdateEffects(_effectContext, _isActive, _memoryPools);
result = stateUpdater.UpdateEffects(_effectContext, _isActive, poolMapper);
if (result != ResultCode.Success)
{
@ -450,7 +452,7 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
result = stateUpdater.UpdateSinks(_sinkContext, _memoryPools);
result = stateUpdater.UpdateSinks(_sinkContext, poolMapper);
if (result != ResultCode.Success)
{

View File

@ -177,12 +177,12 @@ namespace Ryujinx.Audio.Renderer.Server
/// <summary>
/// Start the <see cref="AudioProcessor"/> and worker thread.
/// </summary>
private void StartLocked(float volume)
private void StartLocked()
{
_isRunning = true;
// TODO: virtual device mapping (IAudioDevice)
Processor.Start(_deviceDriver, volume);
Processor.Start(_deviceDriver);
_workerThread = new Thread(SendCommands)
{
@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Register a new <see cref="AudioRenderSystem"/>.
/// </summary>
/// <param name="renderer">The <see cref="AudioRenderSystem"/> to register.</param>
private void Register(AudioRenderSystem renderer, float volume)
private void Register(AudioRenderSystem renderer)
{
lock (_sessionLock)
{
@ -265,7 +265,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (!_isRunning)
{
StartLocked(volume);
StartLocked();
}
}
}
@ -312,8 +312,7 @@ namespace Ryujinx.Audio.Renderer.Server
ulong appletResourceUserId,
ulong workBufferAddress,
ulong workBufferSize,
uint processHandle,
float volume)
uint processHandle)
{
int sessionId = AcquireSessionId();
@ -338,7 +337,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
renderer = audioRenderer;
Register(renderer, volume);
Register(renderer);
}
else
{
@ -350,21 +349,6 @@ namespace Ryujinx.Audio.Renderer.Server
return result;
}
public float GetVolume()
{
if (Processor != null)
{
return Processor.GetVolume();
}
return 0f;
}
public void SetVolume(float volume)
{
Processor?.SetVolume(volume);
}
public void Dispose()
{
GC.SuppressFinalize(this);

View File

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Diagnostics;
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
@ -211,7 +212,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// <summary>
/// Check if the audio renderer should fix the GC-ADPCM context not being provided to the DSP.
/// </summary>
/// <returns>True if if the audio renderer should fix it.</returns>
/// <returns>True if the audio renderer should fix it.</returns>
public bool IsAdpcmLoopContextBugFixed()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision2);
@ -273,7 +274,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
/// <summary>
/// Check if the audio renderer should trust the user destination count in <see cref="Splitter.SplitterState.Update(Splitter.SplitterContext, ref Parameter.SplitterInParameter, ReadOnlySpan{byte})"/>.
/// Check if the audio renderer should trust the user destination count in <see cref="Renderer.Server.Splitter.SplitterState.Update(Renderer.Server.Splitter.SplitterContext, Renderer.Parameter.SplitterInParameter, SequenceReader{byte})"/>.
/// </summary>
/// <returns>True if the audio renderer should trust the user destination count.</returns>
public bool IsSplitterBugFixed()

View File

@ -33,21 +33,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return WorkBuffers[index].GetReference(true);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;

View File

@ -81,7 +81,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// </summary>
/// <param name="parameter">The user parameter.</param>
/// <returns>Returns true if the <see cref="EffectType"/> sent by the user matches the internal <see cref="EffectType"/>.</returns>
public bool IsTypeValid<T>(ref T parameter) where T : unmanaged, IEffectInParameter
public bool IsTypeValid<T>(in T parameter) where T : unmanaged, IEffectInParameter
{
return parameter.Type == TargetEffectType;
}
@ -98,7 +98,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// Update the internal common parameters from a user parameter.
/// </summary>
/// <param name="parameter">The user parameter.</param>
protected void UpdateParameterBase<T>(ref T parameter) where T : unmanaged, IEffectInParameter
protected void UpdateParameterBase<T>(in T parameter) where T : unmanaged, IEffectInParameter
{
MixId = parameter.MixId;
ProcessingOrder = parameter.ProcessingOrder;
@ -139,7 +139,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// <summary>
/// Initialize the given <paramref name="state"/> result state.
/// </summary>
/// <param name="state">The state to initalize</param>
/// <param name="state">The state to initialize</param>
public virtual void InitializeResultState(ref EffectResultState state) { }
/// <summary>
@ -155,9 +155,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
/// <param name="parameter">The user parameter.</param>
/// <param name="mapper">The mapper to use.</param>
public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
updateErrorInfo = new ErrorInfo();
}
@ -168,9 +168,9 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
/// <param name="updateErrorInfo">The possible <see cref="ErrorInfo"/> that was generated.</param>
/// <param name="parameter">The user parameter.</param>
/// <param name="mapper">The mapper to use.</param>
public virtual void Update(out ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public virtual void Update(out ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
updateErrorInfo = new ErrorInfo();
}

View File

@ -35,21 +35,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
public override EffectType TargetEffectType => EffectType.BiquadFilter;
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast<byte, BiquadFilterEffectParameter>(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;

View File

@ -19,21 +19,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
public override EffectType TargetEffectType => EffectType.BufferMix;
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast<byte, BufferMixParameter>(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;

View File

@ -32,21 +32,21 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return WorkBuffers[index].GetReference(true);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast<byte, AuxiliaryBufferParameter>(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;

View File

@ -39,17 +39,17 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
// Nintendo doesn't do anything here but we still require updateErrorInfo to be initialised.
updateErrorInfo = new BehaviourParameter.ErrorInfo();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = MemoryMarshal.Cast<byte, CompressorParameter>(parameter.SpecificData)[0];
IsEnabled = parameter.IsEnabled;

View File

@ -37,19 +37,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref DelayParameter delayParameter = ref MemoryMarshal.Cast<byte, DelayParameter>(parameter.SpecificData)[0];
@ -57,7 +57,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (delayParameter.IsChannelCountMaxValid())
{
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.Status;

View File

@ -39,25 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref LimiterParameter limiterParameter = ref MemoryMarshal.Cast<byte, LimiterParameter>(parameter.SpecificData)[0];
updateErrorInfo = new BehaviourParameter.ErrorInfo();
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
Parameter = limiterParameter;

View File

@ -36,19 +36,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref Reverb3dParameter reverbParameter = ref MemoryMarshal.Cast<byte, Reverb3dParameter>(parameter.SpecificData)[0];
@ -56,7 +56,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (reverbParameter.IsChannelCountMaxValid())
{
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.ParameterStatus;

View File

@ -39,19 +39,19 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
return GetSingleBuffer();
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion1 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion1 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, ref EffectInParameterVersion2 parameter, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo updateErrorInfo, in EffectInParameterVersion2 parameter, PoolMapper mapper)
{
Update(out updateErrorInfo, ref parameter, mapper);
Update(out updateErrorInfo, in parameter, mapper);
}
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
public void Update<T>(out BehaviourParameter.ErrorInfo updateErrorInfo, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref ReverbParameter reverbParameter = ref MemoryMarshal.Cast<byte, ReverbParameter>(parameter.SpecificData)[0];
@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Effect
if (reverbParameter.IsChannelCountMaxValid())
{
UpdateParameterBase(ref parameter);
UpdateParameterBase(in parameter);
UsageState oldParameterStatus = Parameter.Status;

View File

@ -249,7 +249,7 @@ namespace Ryujinx.Audio.Renderer.Server.MemoryPool
/// <param name="inParameter">Input user parameter.</param>
/// <param name="outStatus">Output user parameter.</param>
/// <returns>Returns the <see cref="UpdateResult"/> of the operations performed.</returns>
public UpdateResult Update(ref MemoryPoolState memoryPool, ref MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
public UpdateResult Update(ref MemoryPoolState memoryPool, in MemoryPoolInParameter inParameter, ref MemoryPoolOutStatus outStatus)
{
MemoryPoolUserState inputState = inParameter.State;

View File

@ -195,7 +195,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
/// <param name="parameter">The input parameter of the mix.</param>
/// <param name="splitterContext">The splitter context.</param>
/// <returns>Return true, new connections were done on the adjacency matrix.</returns>
private bool UpdateConnection(EdgeMatrix edgeMatrix, ref MixParameter parameter, ref SplitterContext splitterContext)
private bool UpdateConnection(EdgeMatrix edgeMatrix, in MixParameter parameter, ref SplitterContext splitterContext)
{
bool hasNewConnections;
@ -259,7 +259,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
/// <param name="splitterContext">The splitter context.</param>
/// <param name="behaviourContext">The behaviour context.</param>
/// <returns>Return true if the mix was changed.</returns>
public bool Update(EdgeMatrix edgeMatrix, ref MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
public bool Update(EdgeMatrix edgeMatrix, in MixParameter parameter, EffectContext effectContext, SplitterContext splitterContext, BehaviourContext behaviourContext)
{
bool isDirty;
@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
if (behaviourContext.IsSplitterSupported())
{
isDirty = UpdateConnection(edgeMatrix, ref parameter, ref splitterContext);
isDirty = UpdateConnection(edgeMatrix, in parameter, ref splitterContext);
}
else
{

View File

@ -59,7 +59,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
/// </summary>
/// <param name="parameter">The user parameter.</param>
/// <returns>Return true, if the <see cref="SinkType"/> sent by the user match the internal <see cref="SinkType"/>.</returns>
public bool IsTypeValid(ref SinkInParameter parameter)
public bool IsTypeValid(in SinkInParameter parameter)
{
return parameter.Type == TargetSinkType;
}
@ -76,7 +76,7 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
/// Update the internal common parameters from user parameter.
/// </summary>
/// <param name="parameter">The user parameter.</param>
protected void UpdateStandardParameter(ref SinkInParameter parameter)
protected void UpdateStandardParameter(in SinkInParameter parameter)
{
if (IsUsed != parameter.IsUsed)
{
@ -92,9 +92,9 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
/// <param name="parameter">The user parameter.</param>
/// <param name="outStatus">The user output status.</param>
/// <param name="mapper">The mapper to use.</param>
public virtual void Update(out ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
public virtual void Update(out ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
errorInfo = new ErrorInfo();
}

View File

@ -44,18 +44,18 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
public override SinkType TargetSinkType => SinkType.CircularBuffer;
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
errorInfo = new BehaviourParameter.ErrorInfo();
outStatus = new SinkOutStatus();
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref CircularBufferParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, CircularBufferParameter>(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed || ShouldSkip)
{
UpdateStandardParameter(ref parameter);
UpdateStandardParameter(in parameter);
if (parameter.IsUsed)
{

View File

@ -49,15 +49,15 @@ namespace Ryujinx.Audio.Renderer.Server.Sink
public override SinkType TargetSinkType => SinkType.Device;
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, ref SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
public override void Update(out BehaviourParameter.ErrorInfo errorInfo, in SinkInParameter parameter, ref SinkOutStatus outStatus, PoolMapper mapper)
{
Debug.Assert(IsTypeValid(ref parameter));
Debug.Assert(IsTypeValid(in parameter));
ref DeviceParameter inputDeviceParameter = ref MemoryMarshal.Cast<byte, DeviceParameter>(parameter.SpecificData)[0];
if (parameter.IsUsed != IsUsed)
{
UpdateStandardParameter(ref parameter);
UpdateStandardParameter(in parameter);
Parameter = inputDeviceParameter;
}
else

View File

@ -2,10 +2,11 @@ using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common;
using Ryujinx.Common.Extensions;
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
@ -25,7 +26,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
private Memory<SplitterDestination> _splitterDestinations;
/// <summary>
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.
/// If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.
/// </summary>
public bool IsBugFixed { get; private set; }
@ -110,7 +111,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
/// <param name="splitters">The <see cref="SplitterState"/> storage.</param>
/// <param name="splitterDestinations">The <see cref="SplitterDestination"/> storage.</param>
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, ref SplitterInParameter, ReadOnlySpan{byte})"/>.</param>
/// <param name="isBugFixed">If set to true, trust the user destination count in <see cref="SplitterState.Update(SplitterContext, in SplitterInParameter, ref SequenceReader{byte})"/>.</param>
private void Setup(Memory<SplitterState> splitters, Memory<SplitterDestination> splitterDestinations, bool isBugFixed)
{
_splitters = splitters;
@ -148,11 +149,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
/// <param name="inputHeader">The splitter header.</param>
/// <param name="input">The raw data after the splitter header.</param>
private void UpdateState(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
private void UpdateState(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
{
for (int i = 0; i < inputHeader.SplitterCount; i++)
{
SplitterInParameter parameter = MemoryMarshal.Read<SplitterInParameter>(input);
ref readonly SplitterInParameter parameter = ref input.GetRefOrRefToCopy<SplitterInParameter>(out _);
Debug.Assert(parameter.IsMagicValid());
@ -162,10 +163,16 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
ref SplitterState splitter = ref GetState(parameter.Id);
splitter.Update(this, ref parameter, input[Unsafe.SizeOf<SplitterInParameter>()..]);
splitter.Update(this, in parameter, ref input);
}
input = input[(0x1C + parameter.DestinationCount * 4)..];
// NOTE: there are 12 bytes of unused/unknown data after the destination IDs array.
input.Advance(0xC);
}
else
{
input.Rewind(Unsafe.SizeOf<SplitterInParameter>());
break;
}
}
}
@ -175,11 +182,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// </summary>
/// <param name="inputHeader">The splitter header.</param>
/// <param name="input">The raw data after the splitter header.</param>
private void UpdateData(scoped ref SplitterInParameterHeader inputHeader, ref ReadOnlySpan<byte> input)
private void UpdateData(in SplitterInParameterHeader inputHeader, ref SequenceReader<byte> input)
{
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{
SplitterDestinationInParameter parameter = MemoryMarshal.Read<SplitterDestinationInParameter>(input);
ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy<SplitterDestinationInParameter>(out _);
Debug.Assert(parameter.IsMagicValid());
@ -191,8 +198,11 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
destination.Update(parameter);
}
input = input[Unsafe.SizeOf<SplitterDestinationInParameter>()..];
}
else
{
input.Rewind(Unsafe.SizeOf<SplitterDestinationInParameter>());
break;
}
}
}
@ -201,36 +211,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Update splitter from user parameters.
/// </summary>
/// <param name="input">The input raw user data.</param>
/// <param name="consumedSize">The total consumed size.</param>
/// <returns>Return true if the update was successful.</returns>
public bool Update(ReadOnlySpan<byte> input, out int consumedSize)
public bool Update(ref SequenceReader<byte> input)
{
if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
{
consumedSize = 0;
return true;
}
int originalSize = input.Length;
SplitterInParameterHeader header = SpanIOHelper.Read<SplitterInParameterHeader>(ref input);
ref readonly SplitterInParameterHeader header = ref input.GetRefOrRefToCopy<SplitterInParameterHeader>(out _);
if (header.IsMagicValid())
{
ClearAllNewConnectionFlag();
UpdateState(ref header, ref input);
UpdateData(ref header, ref input);
UpdateState(in header, ref input);
UpdateData(in header, ref input);
consumedSize = BitUtils.AlignUp(originalSize - input.Length, 0x10);
input.SetConsumed(BitUtils.AlignUp(input.Consumed, 0x10));
return true;
}
else
{
input.Rewind(Unsafe.SizeOf<SplitterInParameterHeader>());
consumedSize = 0;
return false;
return false;
}
}
/// <summary>

View File

@ -1,4 +1,5 @@
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Common.Extensions;
using System;
using System.Buffers;
using System.Diagnostics;
@ -122,7 +123,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// <param name="context">The splitter context.</param>
/// <param name="parameter">The user parameter.</param>
/// <param name="input">The raw input data after the <paramref name="parameter"/>.</param>
public void Update(SplitterContext context, ref SplitterInParameter parameter, ReadOnlySpan<byte> input)
public void Update(SplitterContext context, in SplitterInParameter parameter, ref SequenceReader<byte> input)
{
ClearLinks();
@ -139,9 +140,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (destinationCount > 0)
{
ReadOnlySpan<int> destinationIds = MemoryMarshal.Cast<byte, int>(input);
input.ReadLittleEndian(out int destinationId);
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationIds[0]);
Memory<SplitterDestination> destination = context.GetDestinationMemory(destinationId);
SetDestination(ref destination.Span[0]);
@ -149,13 +150,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
for (int i = 1; i < destinationCount; i++)
{
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationIds[i]);
input.ReadLittleEndian(out destinationId);
Memory<SplitterDestination> nextDestination = context.GetDestinationMemory(destinationId);
destination.Span[0].Link(ref nextDestination.Span[0]);
destination = nextDestination;
}
}
if (destinationCount < parameter.DestinationCount)
{
input.Advance((parameter.DestinationCount - destinationCount) * sizeof(int));
}
Debug.Assert(parameter.Id == Id);
if (parameter.Id == Id)

View File

@ -9,41 +9,40 @@ using Ryujinx.Audio.Renderer.Server.Sink;
using Ryujinx.Audio.Renderer.Server.Splitter;
using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common.Extensions;
using Ryujinx.Common.Logging;
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ryujinx.Audio.Renderer.Common.BehaviourParameter;
namespace Ryujinx.Audio.Renderer.Server
{
public class StateUpdater
public ref struct StateUpdater
{
private readonly ReadOnlyMemory<byte> _inputOrigin;
private SequenceReader<byte> _inputReader;
private readonly ReadOnlyMemory<byte> _outputOrigin;
private ReadOnlyMemory<byte> _input;
private Memory<byte> _output;
private readonly uint _processHandle;
private BehaviourContext _behaviourContext;
private UpdateDataHeader _inputHeader;
private readonly ref readonly UpdateDataHeader _inputHeader;
private readonly Memory<UpdateDataHeader> _outputHeader;
private ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
private readonly ref UpdateDataHeader OutputHeader => ref _outputHeader.Span[0];
public StateUpdater(ReadOnlyMemory<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
public StateUpdater(ReadOnlySequence<byte> input, Memory<byte> output, uint processHandle, BehaviourContext behaviourContext)
{
_input = input;
_inputOrigin = _input;
_inputReader = new SequenceReader<byte>(input);
_output = output;
_outputOrigin = _output;
_processHandle = processHandle;
_behaviourContext = behaviourContext;
_inputHeader = SpanIOHelper.Read<UpdateDataHeader>(ref _input);
_inputHeader = ref _inputReader.GetRefOrRefToCopy<UpdateDataHeader>(out _);
_outputHeader = SpanMemoryManager<UpdateDataHeader>.Cast(_output[..Unsafe.SizeOf<UpdateDataHeader>()]);
OutputHeader.Initialize(_behaviourContext.UserRevision);
@ -52,7 +51,7 @@ namespace Ryujinx.Audio.Renderer.Server
public ResultCode UpdateBehaviourContext()
{
BehaviourParameter parameter = SpanIOHelper.Read<BehaviourParameter>(ref _input);
ref readonly BehaviourParameter parameter = ref _inputReader.GetRefOrRefToCopy<BehaviourParameter>(out _);
if (!BehaviourContext.CheckValidRevision(parameter.UserRevision) || parameter.UserRevision != _behaviourContext.UserRevision)
{
@ -81,11 +80,11 @@ namespace Ryujinx.Audio.Renderer.Server
foreach (ref MemoryPoolState memoryPool in memoryPools)
{
MemoryPoolInParameter parameter = SpanIOHelper.Read<MemoryPoolInParameter>(ref _input);
ref readonly MemoryPoolInParameter parameter = ref _inputReader.GetRefOrRefToCopy<MemoryPoolInParameter>(out _);
ref MemoryPoolOutStatus outStatus = ref SpanIOHelper.GetWriteRef<MemoryPoolOutStatus>(ref _output)[0];
PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, ref parameter, ref outStatus);
PoolMapper.UpdateResult updateResult = mapper.Update(ref memoryPool, in parameter, ref outStatus);
if (updateResult != PoolMapper.UpdateResult.Success &&
updateResult != PoolMapper.UpdateResult.MapError &&
@ -115,7 +114,7 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < context.GetCount(); i++)
{
VoiceChannelResourceInParameter parameter = SpanIOHelper.Read<VoiceChannelResourceInParameter>(ref _input);
ref readonly VoiceChannelResourceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceChannelResourceInParameter>(out _);
ref VoiceChannelResource resource = ref context.GetChannelResource(i);
@ -127,7 +126,7 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.Success;
}
public ResultCode UpdateVoices(VoiceContext context, Memory<MemoryPoolState> memoryPools)
public ResultCode UpdateVoices(VoiceContext context, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf<VoiceInParameter>() != _inputHeader.VoicesSize)
{
@ -136,11 +135,7 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
ReadOnlySpan<VoiceInParameter> parameters = MemoryMarshal.Cast<byte, VoiceInParameter>(_input[..(int)_inputHeader.VoicesSize].Span);
_input = _input[(int)_inputHeader.VoicesSize..];
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
long initialInputConsumed = _inputReader.Consumed;
// First make everything not in use.
for (int i = 0; i < context.GetCount(); i++)
@ -157,7 +152,7 @@ namespace Ryujinx.Audio.Renderer.Server
// Start processing
for (int i = 0; i < context.GetCount(); i++)
{
VoiceInParameter parameter = parameters[i];
ref readonly VoiceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<VoiceInParameter>(out _);
voiceUpdateStates.Fill(Memory<VoiceUpdateState>.Empty);
@ -181,14 +176,14 @@ namespace Ryujinx.Audio.Renderer.Server
currentVoiceState.Initialize();
}
currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, ref parameter, ref mapper, ref _behaviourContext);
currentVoiceState.UpdateParameters(out ErrorInfo updateParameterError, in parameter, mapper, ref _behaviourContext);
if (updateParameterError.ErrorCode != ResultCode.Success)
{
_behaviourContext.AppendError(ref updateParameterError);
}
currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, ref parameter, voiceUpdateStates, ref mapper, ref _behaviourContext);
currentVoiceState.UpdateWaveBuffers(out ErrorInfo[] waveBufferUpdateErrorInfos, in parameter, voiceUpdateStates, mapper, ref _behaviourContext);
foreach (ref ErrorInfo errorInfo in waveBufferUpdateErrorInfos.AsSpan())
{
@ -198,7 +193,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
currentVoiceState.WriteOutStatus(ref outStatus, ref parameter, voiceUpdateStates);
currentVoiceState.WriteOutStatus(ref outStatus, in parameter, voiceUpdateStates);
}
}
@ -211,10 +206,12 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.VoicesSize);
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.VoicesSize);
return ResultCode.Success;
}
private static void ResetEffect<T>(ref BaseEffect effect, ref T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
private static void ResetEffect<T>(ref BaseEffect effect, in T parameter, PoolMapper mapper) where T : unmanaged, IEffectInParameter
{
effect.ForceUnmapBuffers(mapper);
@ -234,17 +231,17 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
public ResultCode UpdateEffects(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (_behaviourContext.IsEffectInfoVersion2Supported())
{
return UpdateEffectsVersion2(context, isAudioRendererActive, memoryPools);
return UpdateEffectsVersion2(context, isAudioRendererActive, mapper);
}
return UpdateEffectsVersion1(context, isAudioRendererActive, memoryPools);
return UpdateEffectsVersion1(context, isAudioRendererActive, mapper);
}
public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
public ResultCode UpdateEffectsVersion2(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion2>() != _inputHeader.EffectsSize)
{
@ -253,26 +250,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
ReadOnlySpan<EffectInParameterVersion2> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion2>(_input[..(int)_inputHeader.EffectsSize].Span);
_input = _input[(int)_inputHeader.EffectsSize..];
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
EffectInParameterVersion2 parameter = parameters[i];
ref readonly EffectInParameterVersion2 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion2>(out _);
ref EffectOutStatusVersion2 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion2>(ref _output)[0];
ref BaseEffect effect = ref context.GetEffect(i);
if (!effect.IsTypeValid(ref parameter))
if (!effect.IsTypeValid(in parameter))
{
ResetEffect(ref effect, ref parameter, mapper);
ResetEffect(ref effect, in parameter, mapper);
}
effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@ -297,10 +290,12 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
return ResultCode.Success;
}
public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, Memory<MemoryPoolState> memoryPools)
public ResultCode UpdateEffectsVersion1(EffectContext context, bool isAudioRendererActive, PoolMapper mapper)
{
if (context.GetCount() * Unsafe.SizeOf<EffectInParameterVersion1>() != _inputHeader.EffectsSize)
{
@ -309,26 +304,22 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
ReadOnlySpan<EffectInParameterVersion1> parameters = MemoryMarshal.Cast<byte, EffectInParameterVersion1>(_input[..(int)_inputHeader.EffectsSize].Span);
_input = _input[(int)_inputHeader.EffectsSize..];
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
EffectInParameterVersion1 parameter = parameters[i];
ref readonly EffectInParameterVersion1 parameter = ref _inputReader.GetRefOrRefToCopy<EffectInParameterVersion1>(out _);
ref EffectOutStatusVersion1 outStatus = ref SpanIOHelper.GetWriteRef<EffectOutStatusVersion1>(ref _output)[0];
ref BaseEffect effect = ref context.GetEffect(i);
if (!effect.IsTypeValid(ref parameter))
if (!effect.IsTypeValid(in parameter))
{
ResetEffect(ref effect, ref parameter, mapper);
ResetEffect(ref effect, in parameter, mapper);
}
effect.Update(out ErrorInfo updateErrorInfo, ref parameter, mapper);
effect.Update(out ErrorInfo updateErrorInfo, in parameter, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@ -345,38 +336,40 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.EffectsSize);
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.EffectsSize);
return ResultCode.Success;
}
public ResultCode UpdateSplitter(SplitterContext context)
{
if (context.Update(_input.Span, out int consumedSize))
if (context.Update(ref _inputReader))
{
_input = _input[consumedSize..];
return ResultCode.Success;
}
return ResultCode.InvalidUpdateInfo;
}
private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, ReadOnlySpan<MixParameter> parameters)
private static bool CheckMixParametersValidity(MixContext mixContext, uint mixBufferCount, uint inputMixCount, SequenceReader<byte> parameters)
{
uint maxMixStateCount = mixContext.GetCount();
uint totalRequiredMixBufferCount = 0;
for (int i = 0; i < inputMixCount; i++)
{
if (parameters[i].IsUsed)
ref readonly MixParameter parameter = ref parameters.GetRefOrRefToCopy<MixParameter>(out _);
if (parameter.IsUsed)
{
if (parameters[i].DestinationMixId != Constants.UnusedMixId &&
parameters[i].DestinationMixId > maxMixStateCount &&
parameters[i].MixId != Constants.FinalMixId)
if (parameter.DestinationMixId != Constants.UnusedMixId &&
parameter.DestinationMixId > maxMixStateCount &&
parameter.MixId != Constants.FinalMixId)
{
return true;
}
totalRequiredMixBufferCount += parameters[i].BufferCount;
totalRequiredMixBufferCount += parameter.BufferCount;
}
}
@ -391,7 +384,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
{
MixInParameterDirtyOnlyUpdate parameter = MemoryMarshal.Cast<byte, MixInParameterDirtyOnlyUpdate>(_input.Span)[0];
ref readonly MixInParameterDirtyOnlyUpdate parameter = ref _inputReader.GetRefOrRefToCopy<MixInParameterDirtyOnlyUpdate>(out _);
mixCount = parameter.MixCount;
@ -411,25 +404,20 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.InvalidUpdateInfo;
}
if (_behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported())
{
_input = _input[Unsafe.SizeOf<MixInParameterDirtyOnlyUpdate>()..];
}
long initialInputConsumed = _inputReader.Consumed;
ReadOnlySpan<MixParameter> parameters = MemoryMarshal.Cast<byte, MixParameter>(_input.Span[..(int)inputMixSize]);
int parameterCount = (int)inputMixSize / Unsafe.SizeOf<MixParameter>();
_input = _input[(int)inputMixSize..];
if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, parameters))
if (CheckMixParametersValidity(mixContext, mixBufferCount, mixCount, _inputReader))
{
return ResultCode.InvalidUpdateInfo;
}
bool isMixContextDirty = false;
for (int i = 0; i < parameters.Length; i++)
for (int i = 0; i < parameterCount; i++)
{
MixParameter parameter = parameters[i];
ref readonly MixParameter parameter = ref _inputReader.GetRefOrRefToCopy<MixParameter>(out _);
int mixId = i;
@ -454,7 +442,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (mix.IsUsed)
{
isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, ref parameter, effectContext, splitterContext, _behaviourContext);
isMixContextDirty |= mix.Update(mixContext.EdgeMatrix, in parameter, effectContext, splitterContext, _behaviourContext);
}
}
@ -473,10 +461,12 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
_inputReader.SetConsumed(initialInputConsumed + inputMixSize);
return ResultCode.Success;
}
private static void ResetSink(ref BaseSink sink, ref SinkInParameter parameter)
private static void ResetSink(ref BaseSink sink, in SinkInParameter parameter)
{
sink.CleanUp();
@ -489,10 +479,8 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
public ResultCode UpdateSinks(SinkContext context, Memory<MemoryPoolState> memoryPools)
public ResultCode UpdateSinks(SinkContext context, PoolMapper mapper)
{
PoolMapper mapper = new(_processHandle, memoryPools, _behaviourContext.IsMemoryPoolForceMappingEnabled());
if (context.GetCount() * Unsafe.SizeOf<SinkInParameter>() != _inputHeader.SinksSize)
{
return ResultCode.InvalidUpdateInfo;
@ -500,22 +488,20 @@ namespace Ryujinx.Audio.Renderer.Server
int initialOutputSize = _output.Length;
ReadOnlySpan<SinkInParameter> parameters = MemoryMarshal.Cast<byte, SinkInParameter>(_input[..(int)_inputHeader.SinksSize].Span);
_input = _input[(int)_inputHeader.SinksSize..];
long initialInputConsumed = _inputReader.Consumed;
for (int i = 0; i < context.GetCount(); i++)
{
SinkInParameter parameter = parameters[i];
ref readonly SinkInParameter parameter = ref _inputReader.GetRefOrRefToCopy<SinkInParameter>(out _);
ref SinkOutStatus outStatus = ref SpanIOHelper.GetWriteRef<SinkOutStatus>(ref _output)[0];
ref BaseSink sink = ref context.GetSink(i);
if (!sink.IsTypeValid(ref parameter))
if (!sink.IsTypeValid(in parameter))
{
ResetSink(ref sink, ref parameter);
ResetSink(ref sink, in parameter);
}
sink.Update(out ErrorInfo updateErrorInfo, ref parameter, ref outStatus, mapper);
sink.Update(out ErrorInfo updateErrorInfo, in parameter, ref outStatus, mapper);
if (updateErrorInfo.ErrorCode != ResultCode.Success)
{
@ -530,6 +516,8 @@ namespace Ryujinx.Audio.Renderer.Server
Debug.Assert((initialOutputSize - currentOutputSize) == OutputHeader.SinksSize);
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SinksSize);
return ResultCode.Success;
}
@ -540,7 +528,7 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.InvalidUpdateInfo;
}
PerformanceInParameter parameter = SpanIOHelper.Read<PerformanceInParameter>(ref _input);
ref readonly PerformanceInParameter parameter = ref _inputReader.GetRefOrRefToCopy<PerformanceInParameter>(out _);
ref PerformanceOutStatus outStatus = ref SpanIOHelper.GetWriteRef<PerformanceOutStatus>(ref _output)[0];
@ -585,9 +573,9 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.Success;
}
public ResultCode CheckConsumedSize()
public readonly ResultCode CheckConsumedSize()
{
int consumedInputSize = _inputOrigin.Length - _input.Length;
long consumedInputSize = _inputReader.Consumed;
int consumedOutputSize = _outputOrigin.Length - _output.Length;
if (consumedInputSize != _inputHeader.TotalSize)

View File

@ -254,7 +254,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// </summary>
/// <param name="parameter">The user parameter.</param>
/// <returns>Return true, if the server voice information needs to be updated.</returns>
private readonly bool ShouldUpdateParameters(ref VoiceInParameter parameter)
private readonly bool ShouldUpdateParameters(in VoiceInParameter parameter)
{
if (DataSourceStateAddressInfo.CpuAddress == parameter.DataSourceStateAddress)
{
@ -273,7 +273,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// <param name="parameter">The user parameter.</param>
/// <param name="poolMapper">The mapper to use.</param>
/// <param name="behaviourContext">The behaviour context.</param>
public void UpdateParameters(out ErrorInfo outErrorInfo, ref VoiceInParameter parameter, ref PoolMapper poolMapper, ref BehaviourContext behaviourContext)
public void UpdateParameters(out ErrorInfo outErrorInfo, in VoiceInParameter parameter, PoolMapper poolMapper, ref BehaviourContext behaviourContext)
{
InUse = parameter.InUse;
Id = parameter.Id;
@ -326,7 +326,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
VoiceDropFlag = false;
}
if (ShouldUpdateParameters(ref parameter))
if (ShouldUpdateParameters(in parameter))
{
DataSourceStateUnmapped = !poolMapper.TryAttachBuffer(out outErrorInfo, ref DataSourceStateAddressInfo, parameter.DataSourceStateAddress, parameter.DataSourceStateSize);
}
@ -380,7 +380,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// <param name="outStatus">The given user output.</param>
/// <param name="parameter">The user parameter.</param>
/// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
public void WriteOutStatus(ref VoiceOutStatus outStatus, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
public void WriteOutStatus(ref VoiceOutStatus outStatus, in VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates)
{
#if DEBUG
// Sanity check in debug mode of the internal state
@ -426,7 +426,12 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// <param name="voiceUpdateStates">The voice states associated to the <see cref="VoiceState"/>.</param>
/// <param name="mapper">The mapper to use.</param>
/// <param name="behaviourContext">The behaviour context.</param>
public void UpdateWaveBuffers(out ErrorInfo[] errorInfos, ref VoiceInParameter parameter, ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
public void UpdateWaveBuffers(
out ErrorInfo[] errorInfos,
in VoiceInParameter parameter,
ReadOnlySpan<Memory<VoiceUpdateState>> voiceUpdateStates,
PoolMapper mapper,
ref BehaviourContext behaviourContext)
{
errorInfos = new ErrorInfo[Constants.VoiceWaveBufferCount * 2];
@ -444,7 +449,7 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
for (int i = 0; i < Constants.VoiceWaveBufferCount; i++)
{
UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], ref mapper, ref behaviourContext);
UpdateWaveBuffer(errorInfos.AsSpan(i * 2, 2), ref WaveBuffers[i], ref parameter.WaveBuffers[i], parameter.SampleFormat, voiceUpdateState.IsWaveBufferValid[i], mapper, ref behaviourContext);
}
}
@ -458,7 +463,14 @@ namespace Ryujinx.Audio.Renderer.Server.Voice
/// <param name="isValid">If set to true, the server side wavebuffer is considered valid.</param>
/// <param name="mapper">The mapper to use.</param>
/// <param name="behaviourContext">The behaviour context.</param>
private void UpdateWaveBuffer(Span<ErrorInfo> errorInfos, ref WaveBuffer waveBuffer, ref WaveBufferInternal inputWaveBuffer, SampleFormat sampleFormat, bool isValid, ref PoolMapper mapper, ref BehaviourContext behaviourContext)
private void UpdateWaveBuffer(
Span<ErrorInfo> errorInfos,
ref WaveBuffer waveBuffer,
ref WaveBufferInternal inputWaveBuffer,
SampleFormat sampleFormat,
bool isValid,
PoolMapper mapper,
ref BehaviourContext behaviourContext)
{
if (!isValid && waveBuffer.IsSendToAudioProcessor && waveBuffer.BufferAddressInfo.CpuAddress != 0)
{

View File

@ -1,656 +0,0 @@
{
"Language": "简体中文",
"MenuBarFileOpenApplet": "打开小程序",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序",
"SettingsTabInputDirectMouseAccess": "直通鼠标操作",
"SettingsTabSystemMemoryManagerMode": "内存管理模式:",
"SettingsTabSystemMemoryManagerModeSoftware": "软件",
"SettingsTabSystemMemoryManagerModeHost": "本机 (快速)",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机 (最快)",
"SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化",
"MenuBarFile": "文件",
"MenuBarFileOpenFromFile": "加载文件",
"MenuBarFileOpenUnpacked": "加载解包后的游戏",
"MenuBarFileOpenEmuFolder": "打开 Ryujinx 文件夹",
"MenuBarFileOpenLogsFolder": "打开日志文件夹",
"MenuBarFileExit": "退出",
"MenuBarOptions": "选项",
"MenuBarOptionsToggleFullscreen": "切换全屏",
"MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
"MenuBarOptionsStopEmulation": "停止模拟",
"MenuBarOptionsSettings": "设置",
"MenuBarOptionsManageUserProfiles": "管理用户账户",
"MenuBarActions": "操作",
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
"MenuBarTools": "工具",
"MenuBarToolsInstallFirmware": "安装固件",
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 安装固件",
"MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹安装固件",
"MenuBarToolsManageFileTypes": "管理文件扩展名",
"MenuBarToolsInstallFileTypes": "关联文件扩展名",
"MenuBarToolsUninstallFileTypes": "取消关联扩展名",
"MenuBarHelp": "帮助",
"MenuBarHelpCheckForUpdates": "检查更新",
"MenuBarHelpAbout": "关于",
"MenuSearch": "搜索…",
"GameListHeaderFavorite": "收藏",
"GameListHeaderIcon": "图标",
"GameListHeaderApplication": "名称",
"GameListHeaderDeveloper": "制作商",
"GameListHeaderVersion": "版本",
"GameListHeaderTimePlayed": "游玩时长",
"GameListHeaderLastPlayed": "最近游玩",
"GameListHeaderFileExtension": "扩展名",
"GameListHeaderFileSize": "大小",
"GameListHeaderPath": "路径",
"GameListContextMenuOpenUserSaveDirectory": "打开用户存档目录",
"GameListContextMenuOpenUserSaveDirectoryToolTip": "打开储存游戏存档的目录",
"GameListContextMenuOpenDeviceSaveDirectory": "打开系统目录",
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "打开包含游戏系统设置的目录",
"GameListContextMenuOpenBcatSaveDirectory": "打开 BCAT 目录",
"GameListContextMenuOpenBcatSaveDirectoryToolTip": "打开包含游戏 BCAT 数据的目录",
"GameListContextMenuManageTitleUpdates": "管理游戏更新",
"GameListContextMenuManageTitleUpdatesToolTip": "打开更新管理器",
"GameListContextMenuManageDlc": "管理 DLC",
"GameListContextMenuManageDlcToolTip": "打开 DLC 管理窗口",
"GameListContextMenuOpenModsDirectory": "打开 MOD 目录",
"GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏 MOD 的目录",
"GameListContextMenuCacheManagement": "缓存管理",
"GameListContextMenuCacheManagementPurgePptc": "清除已编译的 PPTC 文件",
"GameListContextMenuCacheManagementPurgePptcToolTip": "仅删除 PPTC 转换后的文件,下次打开游戏时将根据 .info 文件重新生成 PPTC 文件。\n如想彻底清除缓存请进入目录把 .info 文件一并删除",
"GameListContextMenuCacheManagementPurgeShaderCache": "清除着色器缓存",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "删除游戏的着色器缓存",
"GameListContextMenuCacheManagementOpenPptcDirectory": "打开 PPTC 目录",
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "打开包含游戏 PPTC 缓存的目录",
"GameListContextMenuCacheManagementOpenShaderCacheDirectory": "打开着色器缓存目录",
"GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "打开包含游戏着色器缓存的目录",
"GameListContextMenuExtractData": "提取数据",
"GameListContextMenuExtractDataExeFS": "ExeFS",
"GameListContextMenuExtractDataExeFSToolTip": "从游戏的当前状态中提取 ExeFS 分区 (包括更新)",
"GameListContextMenuExtractDataRomFS": "RomFS",
"GameListContextMenuExtractDataRomFSToolTip": "从游戏的当前状态中提取 RomFS 分区 (包括更新)",
"GameListContextMenuExtractDataLogo": "图标",
"GameListContextMenuExtractDataLogoToolTip": "从游戏的当前状态中提取图标 (包括更新)",
"StatusBarGamesLoaded": "{0}/{1} 游戏加载完成",
"StatusBarSystemVersion": "系统版本:{0}",
"LinuxVmMaxMapCountDialogTitle": "检测到内存映射的限制过低",
"LinuxVmMaxMapCountDialogTextPrimary": "你想要将 vm.max_map_count 的值增加到 {0} 吗",
"LinuxVmMaxMapCountDialogTextSecondary": "有些游戏可能会试图创建超过当前允许的内存映射数量。当超过此限制时Ryujinx会立即崩溃。",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "确定,关闭后重置",
"LinuxVmMaxMapCountDialogButtonPersistent": "确定,永久保存",
"LinuxVmMaxMapCountWarningTextPrimary": "内存映射的最大数量低于推荐值。",
"LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count ({0}) 的当前值小于 {1}。 有些游戏可能会试图创建超过当前允许的内存映射量。当大于此限制时Ryujinx 会立即崩溃。\n\n你可以手动增加内存映射限制或者安装 pkexec它可以辅助Ryujinx解决该问题。",
"Settings": "设置",
"SettingsTabGeneral": "用户界面",
"SettingsTabGeneralGeneral": "常规",
"SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示",
"SettingsTabGeneralCheckUpdatesOnLaunch": "自动检查更新",
"SettingsTabGeneralShowConfirmExitDialog": "显示 \"确认退出\" 对话框",
"SettingsTabGeneralHideCursor": "隐藏鼠标指针:",
"SettingsTabGeneralHideCursorNever": "从不",
"SettingsTabGeneralHideCursorOnIdle": "自动隐藏",
"SettingsTabGeneralHideCursorAlways": "始终",
"SettingsTabGeneralGameDirectories": "游戏目录",
"SettingsTabGeneralAdd": "添加",
"SettingsTabGeneralRemove": "删除",
"SettingsTabSystem": "系统",
"SettingsTabSystemCore": "核心",
"SettingsTabSystemSystemRegion": "系统区域:",
"SettingsTabSystemSystemRegionJapan": "日本",
"SettingsTabSystemSystemRegionUSA": "美国",
"SettingsTabSystemSystemRegionEurope": "欧洲",
"SettingsTabSystemSystemRegionAustralia": "澳大利亚",
"SettingsTabSystemSystemRegionChina": "中国",
"SettingsTabSystemSystemRegionKorea": "韩国",
"SettingsTabSystemSystemRegionTaiwan": "台湾地区",
"SettingsTabSystemSystemLanguage": "系统语言:",
"SettingsTabSystemSystemLanguageJapanese": "日语",
"SettingsTabSystemSystemLanguageAmericanEnglish": "美式英语",
"SettingsTabSystemSystemLanguageFrench": "法语",
"SettingsTabSystemSystemLanguageGerman": "德语",
"SettingsTabSystemSystemLanguageItalian": "意大利语",
"SettingsTabSystemSystemLanguageSpanish": "西班牙语",
"SettingsTabSystemSystemLanguageChinese": "简体中文",
"SettingsTabSystemSystemLanguageKorean": "韩语",
"SettingsTabSystemSystemLanguageDutch": "荷兰语",
"SettingsTabSystemSystemLanguagePortuguese": "葡萄牙语",
"SettingsTabSystemSystemLanguageRussian": "俄语",
"SettingsTabSystemSystemLanguageTaiwanese": "繁体中文(台湾)",
"SettingsTabSystemSystemLanguageBritishEnglish": "英式英语",
"SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法语",
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉美西班牙语",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "简体中文(推荐)",
"SettingsTabSystemSystemLanguageTraditionalChinese": "繁体中文(推荐)",
"SettingsTabSystemSystemTimeZone": "系统时区:",
"SettingsTabSystemSystemTime": "系统时钟:",
"SettingsTabSystemEnableVsync": "启用垂直同步",
"SettingsTabSystemEnablePptc": "开启 PPTC 缓存",
"SettingsTabSystemEnableFsIntegrityChecks": "文件系统完整性检查",
"SettingsTabSystemAudioBackend": "音频后端:",
"SettingsTabSystemAudioBackendDummy": "无",
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "音频输入/输出",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemHacks": "修正",
"SettingsTabSystemHacksNote": "(会引起模拟器不稳定)",
"SettingsTabSystemExpandDramSize": "使用开发机的内存布局",
"SettingsTabSystemIgnoreMissingServices": "忽略缺失的服务",
"SettingsTabGraphics": "图形",
"SettingsTabGraphicsAPI": "图形 API",
"SettingsTabGraphicsEnableShaderCache": "启用着色器缓存",
"SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "自动",
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
"SettingsTabGraphicsAnisotropicFiltering4x": "4x",
"SettingsTabGraphicsAnisotropicFiltering8x": "8x",
"SettingsTabGraphicsAnisotropicFiltering16x": "16x",
"SettingsTabGraphicsResolutionScale": "分辨率缩放:",
"SettingsTabGraphicsResolutionScaleCustom": "自定义(不推荐)",
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
"SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
"SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p)",
"SettingsTabGraphicsAspectRatio": "宽高比:",
"SettingsTabGraphicsAspectRatio4x3": "4:3",
"SettingsTabGraphicsAspectRatio16x9": "16:9",
"SettingsTabGraphicsAspectRatio16x10": "16:10",
"SettingsTabGraphicsAspectRatio21x9": "21:9",
"SettingsTabGraphicsAspectRatio32x9": "32:9",
"SettingsTabGraphicsAspectRatioStretch": "拉伸以适应窗口",
"SettingsTabGraphicsDeveloperOptions": "开发者选项",
"SettingsTabGraphicsShaderDumpPath": "图形着色器转储路径:",
"SettingsTabLogging": "日志",
"SettingsTabLoggingLogging": "日志",
"SettingsTabLoggingEnableLoggingToFile": "保存日志为文件",
"SettingsTabLoggingEnableStubLogs": "启用 Stub 日志",
"SettingsTabLoggingEnableInfoLogs": "启用信息日志",
"SettingsTabLoggingEnableWarningLogs": "启用警告日志",
"SettingsTabLoggingEnableErrorLogs": "启用错误日志",
"SettingsTabLoggingEnableTraceLogs": "启用跟踪日志",
"SettingsTabLoggingEnableGuestLogs": "启用来宾日志",
"SettingsTabLoggingEnableFsAccessLogs": "启用访问日志",
"SettingsTabLoggingFsGlobalAccessLogMode": "全局访问日志模式:",
"SettingsTabLoggingDeveloperOptions": "开发者选项",
"SettingsTabLoggingDeveloperOptionsNote": "警告:会降低性能",
"SettingsTabLoggingGraphicsBackendLogLevel": "图形后端日志级别:",
"SettingsTabLoggingGraphicsBackendLogLevelNone": "无",
"SettingsTabLoggingGraphicsBackendLogLevelError": "错误",
"SettingsTabLoggingGraphicsBackendLogLevelPerformance": "减速",
"SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
"SettingsTabLoggingEnableDebugLogs": "启用调试日志",
"SettingsTabInput": "输入",
"SettingsTabInputEnableDockedMode": "主机模式",
"SettingsTabInputDirectKeyboardAccess": "直通键盘控制",
"SettingsButtonSave": "保存",
"SettingsButtonClose": "取消",
"SettingsButtonOk": "确定",
"SettingsButtonCancel": "取消",
"SettingsButtonApply": "应用",
"ControllerSettingsPlayer": "玩家",
"ControllerSettingsPlayer1": "玩家 1",
"ControllerSettingsPlayer2": "玩家 2",
"ControllerSettingsPlayer3": "玩家 3",
"ControllerSettingsPlayer4": "玩家 4",
"ControllerSettingsPlayer5": "玩家 5",
"ControllerSettingsPlayer6": "玩家 6",
"ControllerSettingsPlayer7": "玩家 7",
"ControllerSettingsPlayer8": "玩家 8",
"ControllerSettingsHandheld": "掌机模式",
"ControllerSettingsInputDevice": "输入设备",
"ControllerSettingsRefresh": "刷新",
"ControllerSettingsDeviceDisabled": "关闭",
"ControllerSettingsControllerType": "手柄类型",
"ControllerSettingsControllerTypeHandheld": "掌机",
"ControllerSettingsControllerTypeProController": "Pro 手柄",
"ControllerSettingsControllerTypeJoyConPair": "JoyCon 组合",
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
"ControllerSettingsProfile": "预设",
"ControllerSettingsProfileDefault": "默认布局",
"ControllerSettingsLoad": "加载",
"ControllerSettingsAdd": "新建",
"ControllerSettingsRemove": "删除",
"ControllerSettingsButtons": "按键",
"ControllerSettingsButtonA": "A",
"ControllerSettingsButtonB": "B",
"ControllerSettingsButtonX": "X",
"ControllerSettingsButtonY": "Y",
"ControllerSettingsButtonPlus": "+",
"ControllerSettingsButtonMinus": "-",
"ControllerSettingsDPad": "方向键",
"ControllerSettingsDPadUp": "上",
"ControllerSettingsDPadDown": "下",
"ControllerSettingsDPadLeft": "左",
"ControllerSettingsDPadRight": "右",
"ControllerSettingsStickButton": "按下摇杆",
"ControllerSettingsStickUp": "上",
"ControllerSettingsStickDown": "下",
"ControllerSettingsStickLeft": "左",
"ControllerSettingsStickRight": "右",
"ControllerSettingsStickStick": "摇杆",
"ControllerSettingsStickInvertXAxis": "反转 X 轴方向",
"ControllerSettingsStickInvertYAxis": "反转 Y 轴方向",
"ControllerSettingsStickDeadzone": "死区:",
"ControllerSettingsLStick": "左摇杆",
"ControllerSettingsRStick": "右摇杆",
"ControllerSettingsTriggersLeft": "左扳机",
"ControllerSettingsTriggersRight": "右扳机",
"ControllerSettingsTriggersButtonsLeft": "左扳机键",
"ControllerSettingsTriggersButtonsRight": "右扳机键",
"ControllerSettingsTriggers": "扳机",
"ControllerSettingsTriggerL": "L",
"ControllerSettingsTriggerR": "R",
"ControllerSettingsTriggerZL": "ZL",
"ControllerSettingsTriggerZR": "ZR",
"ControllerSettingsLeftSL": "SL",
"ControllerSettingsLeftSR": "SR",
"ControllerSettingsRightSL": "SL",
"ControllerSettingsRightSR": "SR",
"ControllerSettingsExtraButtonsLeft": "左背键",
"ControllerSettingsExtraButtonsRight": "右背键",
"ControllerSettingsMisc": "其他",
"ControllerSettingsTriggerThreshold": "扳机阈值:",
"ControllerSettingsMotion": "体感",
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 体感协议",
"ControllerSettingsMotionControllerSlot": "手柄:",
"ControllerSettingsMotionMirrorInput": "镜像操作",
"ControllerSettingsMotionRightJoyConSlot": "右JoyCon",
"ControllerSettingsMotionServerHost": "服务器Host",
"ControllerSettingsMotionGyroSensitivity": "陀螺仪敏感度:",
"ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:",
"ControllerSettingsSave": "保存",
"ControllerSettingsClose": "关闭",
"UserProfilesSelectedUserProfile": "选择的用户账户:",
"UserProfilesSaveProfileName": "保存名称",
"UserProfilesChangeProfileImage": "更换头像",
"UserProfilesAvailableUserProfiles": "现有账户:",
"UserProfilesAddNewProfile": "新建账户",
"UserProfilesDelete": "删除",
"UserProfilesClose": "关闭",
"ProfileNameSelectionWatermark": "选择昵称",
"ProfileImageSelectionTitle": "选择头像",
"ProfileImageSelectionHeader": "选择合适的头像图片",
"ProfileImageSelectionNote": "您可以导入自定义头像,或从系统中选择头像",
"ProfileImageSelectionImportImage": "导入图像文件",
"ProfileImageSelectionSelectAvatar": "选择系统头像",
"InputDialogTitle": "输入对话框",
"InputDialogOk": "完成",
"InputDialogCancel": "取消",
"InputDialogAddNewProfileTitle": "选择用户名称",
"InputDialogAddNewProfileHeader": "请输入账户名称",
"InputDialogAddNewProfileSubtext": "(最大长度:{0})",
"AvatarChoose": "选择头像",
"AvatarSetBackgroundColor": "设置背景色",
"AvatarClose": "关闭",
"ControllerSettingsLoadProfileToolTip": "加载预设",
"ControllerSettingsAddProfileToolTip": "新增预设",
"ControllerSettingsRemoveProfileToolTip": "删除预设",
"ControllerSettingsSaveProfileToolTip": "保存预设",
"MenuBarFileToolsTakeScreenshot": "保存截图",
"MenuBarFileToolsHideUi": "隐藏界面",
"GameListContextMenuRunApplication": "运行应用",
"GameListContextMenuToggleFavorite": "收藏",
"GameListContextMenuToggleFavoriteToolTip": "标记喜爱的游戏",
"SettingsTabGeneralTheme": "主题",
"SettingsTabGeneralThemeCustomTheme": "自选主题路径",
"SettingsTabGeneralThemeBaseStyle": "主题色调",
"SettingsTabGeneralThemeBaseStyleDark": "暗黑",
"SettingsTabGeneralThemeBaseStyleLight": "浅色",
"SettingsTabGeneralThemeEnableCustomTheme": "使用自选主题界面",
"ButtonBrowse": "浏览",
"ControllerSettingsConfigureGeneral": "配置",
"ControllerSettingsRumble": "震动",
"ControllerSettingsRumbleStrongMultiplier": "强震动幅度",
"ControllerSettingsRumbleWeakMultiplier": "弱震动幅度",
"DialogMessageSaveNotAvailableMessage": "没有{0} [{1:x16}]的游戏存档",
"DialogMessageSaveNotAvailableCreateSaveMessage": "是否创建该游戏的存档文件夹?",
"DialogConfirmationTitle": "Ryujinx - 设置",
"DialogUpdaterTitle": "Ryujinx - 更新",
"DialogErrorTitle": "Ryujinx - 错误",
"DialogWarningTitle": "Ryujinx - 警告",
"DialogExitTitle": "Ryujinx - 关闭",
"DialogErrorMessage": "Ryujinx 发生错误",
"DialogExitMessage": "是否关闭 Ryujinx",
"DialogExitSubMessage": "未保存的进度将会丢失!",
"DialogMessageCreateSaveErrorMessage": "创建特定的存档时出错:{0}",
"DialogMessageFindSaveErrorMessage": "查找特定的存档时出错:{0}",
"FolderDialogExtractTitle": "选择要解压到的文件夹",
"DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...",
"DialogNcaExtractionTitle": "Ryujinx - NCA分区提取",
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败。所选文件中不含主NCA文件",
"DialogNcaExtractionCheckLogErrorMessage": "提取失败。请查看日志文件获取详情。",
"DialogNcaExtractionSuccessMessage": "提取成功。",
"DialogUpdaterConvertFailedMessage": "无法转换当前 Ryujinx 版本。",
"DialogUpdaterCancelUpdateMessage": "更新取消!",
"DialogUpdaterAlreadyOnLatestVersionMessage": "您使用的 Ryujinx 是最新版本。",
"DialogUpdaterFailedToGetVersionMessage": "尝试从 Github 获取版本信息时无效。\n可能由于 GitHub Actions 正在编译新版本。请过一会再试。",
"DialogUpdaterConvertFailedGithubMessage": "无法转换从 Github 接收到的 Ryujinx 版本。",
"DialogUpdaterDownloadingMessage": "下载新版本中...",
"DialogUpdaterExtractionMessage": "正在提取更新...",
"DialogUpdaterRenamingMessage": "正在删除旧文件...",
"DialogUpdaterAddingFilesMessage": "安装更新中...",
"DialogUpdaterCompleteMessage": "更新成功!",
"DialogUpdaterRestartMessage": "立即重启 Ryujinx 完成更新?",
"DialogUpdaterArchNotSupportedMessage": "您运行的系统架构不受支持!",
"DialogUpdaterArchNotSupportedSubMessage": "(仅支持 x64 系统)",
"DialogUpdaterNoInternetMessage": "没有连接到互联网",
"DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。",
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx",
"DialogUpdaterDirtyBuildSubMessage": "如果希望使用受支持的版本,请您在 https://ryujinx.org/ 下载。",
"DialogRestartRequiredMessage": "需要重启模拟器",
"DialogThemeRestartMessage": "主题设置已保存。需要重新启动才能生效。",
"DialogThemeRestartSubMessage": "您是否要重启?",
"DialogFirmwareInstallEmbeddedMessage": "要安装游戏内置的固件吗?(固件 {0})",
"DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安装的固件,但 Ryujinx 可以从现有的游戏安装固件{0}.\n模拟器现在可以运行。",
"DialogFirmwareNoFirmwareInstalledMessage": "未安装固件",
"DialogFirmwareInstalledMessage": "已安装固件 {0}",
"DialogInstallFileTypesSuccessMessage": "关联文件类型成功!",
"DialogInstallFileTypesErrorMessage": "关联文件类型失败。",
"DialogUninstallFileTypesSuccessMessage": "成功解除文件类型关联!",
"DialogUninstallFileTypesErrorMessage": "解除文件类型关联失败。",
"DialogOpenSettingsWindowLabel": "打开设置窗口",
"DialogControllerAppletTitle": "控制器小窗口",
"DialogMessageDialogErrorExceptionMessage": "显示消息对话框时出错:{0}",
"DialogSoftwareKeyboardErrorExceptionMessage": "显示软件键盘时出错:{0}",
"DialogErrorAppletErrorExceptionMessage": "显示错误对话框时出错:{0}",
"DialogUserErrorDialogMessage": "{0}: {1}",
"DialogUserErrorDialogInfoMessage": "\n有关修复此错误的更多信息可以遵循我们的设置指南。",
"DialogUserErrorDialogTitle": "Ryujinx 错误 ({0})",
"DialogAmiiboApiTitle": "Amiibo API",
"DialogAmiiboApiFailFetchMessage": "从 API 获取信息时出错。",
"DialogAmiiboApiConnectErrorMessage": "无法连接到 Amiibo API 服务器。服务器可能已关闭,或者您没有连接网络。",
"DialogProfileInvalidProfileErrorMessage": "预设 {0} 与当前输入配置系统不兼容。",
"DialogProfileDefaultProfileOverwriteErrorMessage": "默认预设不能被覆盖",
"DialogProfileDeleteProfileTitle": "删除预设",
"DialogProfileDeleteProfileMessage": "删除后不可恢复,确认删除吗?",
"DialogWarning": "警告",
"DialogPPTCDeletionMessage": "您即将删除:\n\n{0} 的 PPTC 缓存\n\n确定吗",
"DialogPPTCDeletionErrorMessage": "清除位于 {0} 的 PPTC 缓存时出错:{1}",
"DialogShaderDeletionMessage": "您即将删除:\n\n{0} 的着色器缓存\n\n确定吗",
"DialogShaderDeletionErrorMessage": "清除位于 {0} 的着色器缓存时出错:{1}",
"DialogRyujinxErrorMessage": "Ryujinx 遇到错误",
"DialogInvalidTitleIdErrorMessage": "UI错误所选游戏没有有效的标题ID",
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路径 {0} 找不到有效的系统固件。",
"DialogFirmwareInstallerFirmwareInstallTitle": "固件 {0}",
"DialogFirmwareInstallerFirmwareInstallMessage": "即将安装系统版本 {0} 。",
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n会替换当前系统版本 {0} 。",
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否确认继续",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装固件中...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统版本 {0} 。",
"DialogUserProfileDeletionWarningMessage": "删除后将没有可选择的用户账户",
"DialogUserProfileDeletionConfirmMessage": "是否删除选择的账户",
"DialogUserProfileUnsavedChangesTitle": "警告 - 未保存的更改",
"DialogUserProfileUnsavedChangesMessage": "您为该用户做出的部分改动尚未保存。",
"DialogUserProfileUnsavedChangesSubMessage": "是否舍弃这些改动?",
"DialogControllerSettingsModifiedConfirmMessage": "目前的输入预设已更新",
"DialogControllerSettingsModifiedConfirmSubMessage": "要保存吗?",
"DialogLoadNcaErrorMessage": "{0}. 错误的文件:{1}",
"DialogDlcNoDlcErrorMessage": "选择的文件不包含所选游戏的 DLC",
"DialogPerformanceCheckLoggingEnabledMessage": "您启用了跟踪日志,仅供开发人员使用。",
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "为了获得最佳性能,建议禁用跟踪日志记录。您是否要立即禁用?",
"DialogPerformanceCheckShaderDumpEnabledMessage": "您启用了着色器转储,仅供开发人员使用。",
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "为了获得最佳性能,建议禁用着色器转储。您是否要立即禁用?",
"DialogLoadAppGameAlreadyLoadedMessage": "已有游戏正在运行",
"DialogLoadAppGameAlreadyLoadedSubMessage": "请停止模拟或关闭程序,再启动另一个游戏。",
"DialogUpdateAddUpdateErrorMessage": "选择的文件不包含所选游戏的更新!",
"DialogSettingsBackendThreadingWarningTitle": "警告 - 后端多线程",
"DialogSettingsBackendThreadingWarningMessage": "改变此选项后必须重启 Ryujinx 才能生效。\n\n取决于您的硬件可能需要手动禁用驱动面板中的线程优化。",
"SettingsTabGraphicsFeaturesOptions": "功能",
"SettingsTabGraphicsBackendMultithreading": "多线程图形后端:",
"CommonAuto": "自动(推荐)",
"CommonOff": "关闭",
"CommonOn": "打开",
"InputDialogYes": "是",
"InputDialogNo": "否",
"DialogProfileInvalidProfileNameErrorMessage": "文件名包含无效字符,请重试。",
"MenuBarOptionsPauseEmulation": "暂停",
"MenuBarOptionsResumeEmulation": "继续",
"AboutUrlTooltipMessage": "在浏览器中打开 Ryujinx 官网。",
"AboutDisclaimerMessage": "Ryujinx 以任何方式与 Nintendo™ 及其任何商业伙伴都没有关联",
"AboutAmiiboDisclaimerMessage": "我们的 Amiibo 模拟使用了\nAmiiboAPI (www.amiiboapi.com) ",
"AboutPatreonUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Patreon 赞助页。",
"AboutGithubUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 GitHub 代码库。",
"AboutDiscordUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Discord 邀请链接。",
"AboutTwitterUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Twitter 主页。",
"AboutRyujinxAboutTitle": "关于:",
"AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模拟器。\n您可以在 Patreon 上赞助 Ryujinx。\n关注 Twitter 或 Discord 可以获取模拟器最新动态。\n如果您对开发感兴趣欢迎来 GitHub 或 Discord 加入我们!",
"AboutRyujinxMaintainersTitle": "由以下作者维护:",
"AboutRyujinxMaintainersContentTooltipMessage": "在浏览器中打开贡献者页面",
"AboutRyujinxSupprtersTitle": "感谢 Patreon 的赞助者:",
"AmiiboSeriesLabel": "Amiibo 系列",
"AmiiboCharacterLabel": "角色",
"AmiiboScanButtonLabel": "扫描",
"AmiiboOptionsShowAllLabel": "显示所有 Amiibo 系列",
"AmiiboOptionsUsRandomTagLabel": "修复:使用随机标记的 UUID",
"DlcManagerTableHeadingEnabledLabel": "启用",
"DlcManagerTableHeadingTitleIdLabel": "游戏ID",
"DlcManagerTableHeadingContainerPathLabel": "文件夹路径",
"DlcManagerTableHeadingFullPathLabel": "完整路径",
"DlcManagerRemoveAllButton": "全部删除",
"DlcManagerEnableAllButton": "全部启用",
"DlcManagerDisableAllButton": "全部禁用",
"MenuBarOptionsChangeLanguage": "更改语言",
"MenuBarShowFileTypes": "主页显示的文件类型",
"CommonSort": "排序",
"CommonShowNames": "显示名称",
"CommonFavorite": "收藏",
"OrderAscending": "从小到大",
"OrderDescending": "从大到小",
"SettingsTabGraphicsFeatures": "功能与增强",
"ErrorWindowTitle": "错误窗口",
"ToggleDiscordTooltip": "控制是否在 Discord 中显示您的游玩状态",
"AddGameDirBoxTooltip": "输入要添加的游戏目录",
"AddGameDirTooltip": "添加游戏目录到列表中",
"RemoveGameDirTooltip": "移除选中的目录",
"CustomThemeCheckTooltip": "使用自定义UI主题来更改模拟器的外观样式",
"CustomThemePathTooltip": "自定义主题的目录",
"CustomThemeBrowseTooltip": "查找自定义主题",
"DockModeToggleTooltip": "启用 Switch 的主机模式。\n绝大多数游戏画质会提高略微增加性能消耗。\n在掌机和主机模式切换的过程中您可能需要重新设置手柄类型。",
"DirectKeyboardTooltip": "开启 \"直连键盘访问(HID)支持\"\n部分游戏可以使用您的键盘输入文字",
"DirectMouseTooltip": "开启 \"直连鼠标访问(HID)支持\"\n部分游戏可以使用您的鼠标导航",
"RegionTooltip": "更改系统区域",
"LanguageTooltip": "更改系统语言",
"TimezoneTooltip": "更改系统时区",
"TimeTooltip": "更改系统时钟",
"VSyncToggleTooltip": "关闭后小部分游戏可以超过60FPS帧率以获得高帧率体验。\n但是可能出现软锁或读盘时间增加。\n如不确定就请保持开启状态。",
"PptcToggleTooltip": "缓存编译完成的游戏CPU指令。减少启动时间和卡顿提高游戏响应速度。\n如不确定就请保持开启状态。",
"FsIntegrityToggleTooltip": "检查游戏文件内容的完整性。\n遇到损坏的文件则记录到日志文件有助于排查错误。\n对性能没有影响。\n如不确定就请保持开启状态。",
"AudioBackendTooltip": "默认推荐SDL2但每种音频后端对各类游戏兼容性不同遇到音频问题可以尝试切换后端。",
"MemoryManagerTooltip": "改变 Switch 内存映射到电脑内存的方式会影响CPU性能消耗。",
"MemoryManagerSoftwareTooltip": "使用软件内存页管理,最精确但是速度最慢。",
"MemoryManagerHostTooltip": "直接映射内存页到电脑内存,使得即时编译效率更高。",
"MemoryManagerUnsafeTooltip": "直接映射内存页,但不检查内存溢出,使得即时编译效率更高。\nRyujinx 可以访问任何位置的内存,因而相对不安全。\n此模式下只应运行您信任的游戏或软件(即官方游戏)。",
"UseHypervisorTooltip": "使用 Hypervisor 虚拟机代替即时编译。在可用的情况下能大幅提高性能。但目前可能不稳定。",
"DRamTooltip": "使用Switch开发机的内存布局。\n不会提高任何性能某些高清纹理包或 4k 分辨率 MOD 可能需要此选项。\n如果不确定请始终关闭该选项。",
"IgnoreMissingServicesTooltip": "开启后,游戏会忽略未实现的系统服务,从而继续运行。\n少部分新发布的游戏由于使用新的未知系统服务可能需要此选项来避免闪退。\n模拟器更新完善系统服务之后则无需开启选项。\n如您的游戏已经正常运行请保持此选项关闭。",
"GraphicsBackendThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译减少卡顿提高 GPU 的性能。\n\n如果不确定请设置为自动。",
"GalThreadingTooltip": "在第二个线程上执行图形后端命令。\n\n加速着色器编译减少卡顿提高 GPU 的性能。\n\n如果不确定请设置为自动。",
"ShaderCacheToggleTooltip": "开启后,模拟器会保存编译完成的着色器到磁盘,减少游戏渲染新特效和场景时的卡顿。",
"ResolutionScaleTooltip": "缩放渲染的分辨率",
"ResolutionScaleEntryTooltip": "尽可能使用例如1.5的浮点倍数。非整数的倍率易引起 BUG。",
"AnisotropyTooltip": "各向异性过滤等级。提高倾斜视角纹理的清晰度\n('自动'使用游戏默认的等级)",
"AspectRatioTooltip": "渲染窗口的宽高比。",
"ShaderDumpPathTooltip": "转储图形着色器的路径",
"FileLogTooltip": "保存日志文件到硬盘。不会影响性能。",
"StubLogTooltip": "在控制台中打印 stub 日志消息。不影响性能。",
"InfoLogTooltip": "在控制台中打印信息日志消息。不影响性能。",
"WarnLogTooltip": "在控制台中打印警告日志消息。不影响性能。",
"ErrorLogTooltip": "打印控制台中的错误日志消息。不影响性能。",
"TraceLogTooltip": "在控制台中打印跟踪日志消息。不影响性能。",
"GuestLogTooltip": "在控制台中打印访客日志消息。不影响性能。",
"FileAccessLogTooltip": "在控制台中打印文件访问日志信息。",
"FSAccessLogModeTooltip": "启用访问日志输出到控制台。可能的模式为 0-3",
"DeveloperOptionTooltip": "谨慎使用",
"OpenGlLogLevel": "需要打开适当的日志等级",
"DebugLogTooltip": "记录Debug消息",
"LoadApplicationFileTooltip": "选择 Switch 支持的游戏格式并加载",
"LoadApplicationFolderTooltip": "选择解包后的 Switch 游戏并加载",
"OpenRyujinxFolderTooltip": "打开 Ryujinx 系统目录",
"OpenRyujinxLogsTooltip": "打开日志存放的目录",
"ExitTooltip": "关闭 Ryujinx",
"OpenSettingsTooltip": "打开设置窗口",
"OpenProfileManagerTooltip": "打开用户账户管理界面",
"StopEmulationTooltip": "停止运行当前游戏并回到主界面",
"CheckUpdatesTooltip": "检查 Ryujinx 新版本",
"OpenAboutTooltip": "打开“关于”窗口",
"GridSize": "网格尺寸",
"GridSizeTooltip": "调整网格模式的大小",
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙语",
"AboutRyujinxContributorsButtonHeader": "查看所有参与者",
"SettingsTabSystemAudioVolume": "音量:",
"AudioVolumeTooltip": "调节音量",
"SettingsTabSystemEnableInternetAccess": "允许网络访问/局域网模式",
"EnableInternetAccessTooltip": "允许模拟的游戏进程访问互联网。\n当多个模拟器/真实的 Switch 连接到同一个局域网时,带有 LAN 模式的游戏可以相互通信。\n即使开启选项也无法访问 Nintendo 服务器。此外可能导致某些尝试联网的游戏崩溃。\n如果您不确定请关闭该选项。",
"GameListContextMenuManageCheatToolTip": "管理金手指",
"GameListContextMenuManageCheat": "管理金手指",
"ControllerSettingsStickRange": "范围:",
"DialogStopEmulationTitle": "Ryujinx - 停止模拟",
"DialogStopEmulationMessage": "是否确定停止模拟?",
"SettingsTabCpu": "CPU",
"SettingsTabAudio": "音频",
"SettingsTabNetwork": "网络",
"SettingsTabNetworkConnection": "网络连接",
"SettingsTabCpuCache": "CPU 缓存",
"SettingsTabCpuMemory": "CPU 内存",
"DialogUpdaterFlatpakNotSupportedMessage": "请通过 FlatHub 更新 Ryujinx。",
"UpdaterDisabledWarningTitle": "更新已禁用!",
"GameListContextMenuOpenSdModsDirectory": "打开 Atmosphere MOD 目录",
"GameListContextMenuOpenSdModsDirectoryToolTip": "打开适用于 Atmosphere 自制系统的 MOD 目录",
"ControllerSettingsRotate90": "顺时针旋转 90°",
"IconSize": "图标尺寸",
"IconSizeTooltip": "更改游戏图标大小",
"MenuBarOptionsShowConsole": "显示控制台",
"ShaderCachePurgeError": "清除着色器缓存时出错:{0}: {1}",
"UserErrorNoKeys": "找不到密钥",
"UserErrorNoFirmware": "找不到固件",
"UserErrorFirmwareParsingFailed": "固件解析错误",
"UserErrorApplicationNotFound": "找不到应用程序",
"UserErrorUnknown": "未知错误",
"UserErrorUndefined": "未定义错误",
"UserErrorNoKeysDescription": "Ryujinx 找不到 'prod.keys' 文件",
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安装的固件",
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 无法解密选择的固件。这通常是由于使用了过旧的密钥。",
"UserErrorApplicationNotFoundDescription": "Ryujinx 在选中路径找不到有效的应用程序。",
"UserErrorUnknownDescription": "发生未知错误!",
"UserErrorUndefinedDescription": "发生了未定义错误!此类错误不应出现,请联系开发人员!",
"OpenSetupGuideMessage": "打开设置教程",
"NoUpdate": "无更新",
"TitleUpdateVersionLabel": "版本 {0}",
"RyujinxInfo": "Ryujinx - 信息",
"RyujinxConfirm": "Ryujinx - 确认",
"FileDialogAllTypes": "全部类型",
"Never": "从不",
"SwkbdMinCharacters": "至少应为 {0} 个字长",
"SwkbdMinRangeCharacters": "必须为 {0}-{1} 个字长",
"SoftwareKeyboard": "软件键盘",
"SoftwareKeyboardModeNumbersOnly": "只接受数字",
"SoftwareKeyboardModeAlphabet": "只接受非中日韩文字",
"SoftwareKeyboardModeASCII": "只接受 ASCII 符号",
"DialogControllerAppletMessagePlayerRange": "游戏需要 {0} 个玩家并满足以下要求:\n\n手柄类型{1}\n\n玩家类型{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
"DialogControllerAppletMessage": "游戏需要刚好 {0} 个玩家并满足以下要求:\n\n手柄类型{1}\n\n玩家类型{2}\n\n{3} 请打开设置窗口,重新配置手柄输入;或者关闭返回。",
"DialogControllerAppletDockModeSet": "目前处于主机模式,无法使用掌机操作方式",
"UpdaterRenaming": "正在删除旧文件...",
"UpdaterRenameFailed": "更新过程中无法重命名文件:{0}",
"UpdaterAddingFiles": "安装更新中...",
"UpdaterExtracting": "正在提取更新...",
"UpdaterDownloading": "下载新版本中...",
"Game": "游戏",
"Docked": "主机模式",
"Handheld": "掌机模式",
"ConnectionError": "连接错误。",
"AboutPageDeveloperListMore": "{0} 等开发者...",
"ApiError": "API错误。",
"LoadingHeading": "正在启动 {0}",
"CompilingPPTC": "编译PPTC缓存中",
"CompilingShaders": "编译着色器中",
"AllKeyboards": "所有键盘",
"OpenFileDialogTitle": "选择一个支持的文件以打开",
"OpenFolderDialogTitle": "选择一个包含解包游戏的文件夹",
"AllSupportedFormats": "所有支持的格式",
"RyujinxUpdater": "Ryujinx 更新程序",
"SettingsTabHotkeys": "快捷键",
"SettingsTabHotkeysHotkeys": "键盘快捷键",
"SettingsTabHotkeysToggleVsyncHotkey": "切换垂直同步:",
"SettingsTabHotkeysScreenshotHotkey": "截屏:",
"SettingsTabHotkeysShowUiHotkey": "隐藏 界面:",
"SettingsTabHotkeysPauseHotkey": "暂停:",
"SettingsTabHotkeysToggleMuteHotkey": "静音:",
"ControllerMotionTitle": "体感操作设置",
"ControllerRumbleTitle": "震动设置",
"SettingsSelectThemeFileDialogTitle": "选择主题文件",
"SettingsXamlThemeFile": "Xaml 主题文件",
"AvatarWindowTitle": "管理账户 - 头像",
"Amiibo": "Amiibo",
"Unknown": "未知",
"Usage": "扫描可获得",
"Writable": "可写入",
"SelectDlcDialogTitle": "选择 DLC 文件",
"SelectUpdateDialogTitle": "选择更新文件",
"UserProfileWindowTitle": "管理用户账户",
"CheatWindowTitle": "金手指管理器",
"DlcWindowTitle": "管理 {0} ({1}) 的 DLC",
"UpdateWindowTitle": "游戏更新管理器",
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
"BuildId": "游戏版本ID:",
"DlcWindowHeading": "{0} 个适用于 {1} ({2}) 的 DLC",
"UserProfilesEditProfile": "编辑选中账户",
"Cancel": "取消",
"Save": "保存",
"Discard": "返回",
"UserProfilesSetProfileImage": "选择头像",
"UserProfileEmptyNameError": "必须输入名称",
"UserProfileNoImageError": "请选择您的头像",
"GameUpdateWindowHeading": "管理 {0} ({1}) 的更新",
"SettingsTabHotkeysResScaleUpHotkey": "提高分辨率:",
"SettingsTabHotkeysResScaleDownHotkey": "降低分辨率:",
"UserProfilesName": "名称:",
"UserProfilesUserId": "用户ID",
"SettingsTabGraphicsBackend": "图形后端",
"SettingsTabGraphicsBackendTooltip": "显卡使用的图形后端",
"SettingsEnableTextureRecompression": "启用纹理重压缩",
"SettingsEnableTextureRecompressionTooltip": "压缩某些纹理以减少显存的使用。\n适合显存小于 4GiB 的 GPU 开启。\n如果您不确定请保持此项关闭。",
"SettingsTabGraphicsPreferredGpu": "首选 GPU",
"SettingsTabGraphicsPreferredGpuTooltip": "选择 Vulkan API 使用的显卡。\n此选项不会影响 OpenGL API。\n如果您不确定建议选择\"dGPU(独立显卡)\"。如果没有独立显卡,则无需改动此选项。",
"SettingsAppRequiredRestartMessage": "Ryujinx 需要重启",
"SettingsGpuBackendRestartMessage": "您修改了图形 API 或显卡设置。需要重新启动才能生效",
"SettingsGpuBackendRestartSubMessage": "是否重启模拟器?",
"RyujinxUpdaterMessage": "是否更新 Ryujinx 到最新的版本?",
"SettingsTabHotkeysVolumeUpHotkey": "音量加:",
"SettingsTabHotkeysVolumeDownHotkey": "音量减:",
"SettingsEnableMacroHLE": "启用 HLE 宏",
"SettingsEnableMacroHLETooltip": "GPU 宏代码的高级模拟。\n提高性能表现但可能在某些游戏中引起图形错误。\n如果您不确定请保持此项开启。",
"SettingsEnableColorSpacePassthrough": "颜色空间穿透",
"SettingsEnableColorSpacePassthroughTooltip": "指示 Vulkan 后端在不指定颜色空间的情况下传递颜色信息。对于具有宽色域显示器的用户来说,这可能会以颜色正确性为代价,产生更鲜艳的颜色。",
"VolumeShort": "音量",
"UserProfilesManageSaves": "管理存档",
"DeleteUserSave": "确定删除这个游戏的存档吗?",
"IrreversibleActionNote": "删除后不可恢复。",
"SaveManagerHeading": "管理 {0} ({1}) 的存档",
"SaveManagerTitle": "存档管理器",
"Name": "名称",
"Size": "大小",
"Search": "搜索",
"UserProfilesRecoverLostAccounts": "恢复丢失的账户",
"Recover": "恢复",
"UserProfilesRecoverHeading": "找到了这些用户的存档数据",
"UserProfilesRecoverEmptyList": "没有可以恢复的配置文件",
"GraphicsAATooltip": "将抗锯齿使用到游戏渲染中",
"GraphicsAALabel": "抗锯齿:",
"GraphicsScalingFilterLabel": "缩放过滤:",
"GraphicsScalingFilterTooltip": "对帧缓冲区进行缩放",
"GraphicsScalingFilterLevelLabel": "等级",
"GraphicsScalingFilterLevelTooltip": "设置缩放过滤级别",
"SmaaLow": "SMAA 低质量",
"SmaaMedium": "SMAA 中质量",
"SmaaHigh": "SMAA 高质量",
"SmaaUltra": "SMAA 极致质量",
"UserEditorTitle": "编辑用户",
"UserEditorTitleCreate": "创建用户",
"SettingsTabNetworkInterface": "网络接口:",
"NetworkInterfaceTooltip": "用于局域网功能的网络接口",
"NetworkInterfaceDefault": "默认",
"PackagingShaders": "整合着色器中",
"AboutChangelogButton": "在Github上查看更新日志",
"AboutChangelogButtonTooltipMessage": "点击这里在您的默认浏览器中打开此版本的更新日志。"
}

View File

@ -1,656 +0,0 @@
{
"Language": "英文 (美國)",
"MenuBarFileOpenApplet": "開啟 Applet 應用程序",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "開啟獨立的Mii修改器應用程序",
"SettingsTabInputDirectMouseAccess": "滑鼠直接操作",
"SettingsTabSystemMemoryManagerMode": "記憶體管理模式:",
"SettingsTabSystemMemoryManagerModeSoftware": "軟體",
"SettingsTabSystemMemoryManagerModeHost": "主機模式 (快速)",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "主機略過檢查模式 (最快, 但不安全)",
"SettingsTabSystemUseHypervisor": "使用 Hypervisor",
"MenuBarFile": "_檔案",
"MenuBarFileOpenFromFile": "_載入檔案",
"MenuBarFileOpenUnpacked": "載入_已解開封裝的遊戲",
"MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾",
"MenuBarFileOpenLogsFolder": "開啟日誌資料夾",
"MenuBarFileExit": "_退出",
"MenuBarOptions": "選項",
"MenuBarOptionsToggleFullscreen": "切換全螢幕模式",
"MenuBarOptionsStartGamesInFullscreen": "使用全螢幕模式啟動遊戲",
"MenuBarOptionsStopEmulation": "停止模擬",
"MenuBarOptionsSettings": "_設定",
"MenuBarOptionsManageUserProfiles": "_管理使用者帳戶",
"MenuBarActions": "_動作",
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
"MenuBarTools": "_工具",
"MenuBarToolsInstallFirmware": "安裝韌體",
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
"MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體",
"MenuBarToolsManageFileTypes": "管理檔案類型",
"MenuBarToolsInstallFileTypes": "註冊檔案類型",
"MenuBarToolsUninstallFileTypes": "取消註冊檔案類型",
"MenuBarHelp": "幫助",
"MenuBarHelpCheckForUpdates": "檢查更新",
"MenuBarHelpAbout": "關於",
"MenuSearch": "搜尋...",
"GameListHeaderFavorite": "收藏",
"GameListHeaderIcon": "圖示",
"GameListHeaderApplication": "名稱",
"GameListHeaderDeveloper": "開發人員",
"GameListHeaderVersion": "版本",
"GameListHeaderTimePlayed": "遊玩時數",
"GameListHeaderLastPlayed": "最近遊玩",
"GameListHeaderFileExtension": "副檔名",
"GameListHeaderFileSize": "檔案大小",
"GameListHeaderPath": "路徑",
"GameListContextMenuOpenUserSaveDirectory": "開啟使用者存檔資料夾",
"GameListContextMenuOpenUserSaveDirectoryToolTip": "開啟此遊戲的存檔資料夾",
"GameListContextMenuOpenDeviceSaveDirectory": "開啟系統資料夾",
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "開啟此遊戲的系統設定資料夾",
"GameListContextMenuOpenBcatSaveDirectory": "開啟 BCAT 資料夾",
"GameListContextMenuOpenBcatSaveDirectoryToolTip": "開啟此遊戲的 BCAT 資料夾\n",
"GameListContextMenuManageTitleUpdates": "管理遊戲更新",
"GameListContextMenuManageTitleUpdatesToolTip": "開啟遊戲更新管理視窗",
"GameListContextMenuManageDlc": "管理 DLC",
"GameListContextMenuManageDlcToolTip": "開啟 DLC 管理視窗",
"GameListContextMenuOpenModsDirectory": "開啟模組資料夾",
"GameListContextMenuOpenModsDirectoryToolTip": "開啟此遊戲的模組資料夾",
"GameListContextMenuCacheManagement": "快取管理",
"GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 快取",
"GameListContextMenuCacheManagementPurgePptcToolTip": "刪除遊戲的 PPTC 快取",
"GameListContextMenuCacheManagementPurgeShaderCache": "清除著色器快取",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除遊戲的著色器快取",
"GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾",
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "開啟此遊戲的 PPTC 快取資料夾",
"GameListContextMenuCacheManagementOpenShaderCacheDirectory": "開啟著色器快取資料夾",
"GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "開啟此遊戲的著色器快取資料夾",
"GameListContextMenuExtractData": "提取資料",
"GameListContextMenuExtractDataExeFS": "ExeFS",
"GameListContextMenuExtractDataExeFSToolTip": "從遊戲的目前狀態中提取 ExeFS 分區(包含更新)",
"GameListContextMenuExtractDataRomFS": "RomFS",
"GameListContextMenuExtractDataRomFSToolTip": "從遊戲的目前狀態中提取 RomFS 分區(包含更新)",
"GameListContextMenuExtractDataLogo": "圖示",
"GameListContextMenuExtractDataLogoToolTip": "從遊戲的目前狀態中提取圖示(包含更新)",
"StatusBarGamesLoaded": "{0}/{1} 遊戲載入完成",
"StatusBarSystemVersion": "系統版本: {0}",
"LinuxVmMaxMapCountDialogTitle": "檢測到映射的記憶體上限過低",
"LinuxVmMaxMapCountDialogTextPrimary": "你願意增加 vm.max_map_count to {0} 的數值嗎?",
"LinuxVmMaxMapCountDialogTextSecondary": "遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "碓定 (直至下一次重新啟動)",
"LinuxVmMaxMapCountDialogButtonPersistent": "碓定 (永遠設定)",
"LinuxVmMaxMapCountWarningTextPrimary": "映射記憶體的最大值少於目前建議的下限.",
"LinuxVmMaxMapCountWarningTextSecondary": "目前 vm.max_map_count ({0}) 的數值少於 {1}. 遊戲佔用的記憶體超出了映射的上限. Ryujinx的處理程序即將面臨崩潰.\n\n你可能需要手動增加上限或安裝 pkexec, 這些都能協助Ryujinx完成此操作.",
"Settings": "設定",
"SettingsTabGeneral": "使用者介面",
"SettingsTabGeneralGeneral": "一般",
"SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示",
"SettingsTabGeneralCheckUpdatesOnLaunch": "自動檢查更新",
"SettingsTabGeneralShowConfirmExitDialog": "顯示「確認離開」對話框",
"SettingsTabGeneralHideCursor": "隱藏滑鼠遊標:",
"SettingsTabGeneralHideCursorNever": "永不",
"SettingsTabGeneralHideCursorOnIdle": "自動隱藏滑鼠",
"SettingsTabGeneralHideCursorAlways": "總是",
"SettingsTabGeneralGameDirectories": "遊戲資料夾",
"SettingsTabGeneralAdd": "新增",
"SettingsTabGeneralRemove": "刪除",
"SettingsTabSystem": "系統",
"SettingsTabSystemCore": "核心",
"SettingsTabSystemSystemRegion": "系統區域:",
"SettingsTabSystemSystemRegionJapan": "日本",
"SettingsTabSystemSystemRegionUSA": "美國",
"SettingsTabSystemSystemRegionEurope": "歐洲",
"SettingsTabSystemSystemRegionAustralia": "澳洲",
"SettingsTabSystemSystemRegionChina": "中國",
"SettingsTabSystemSystemRegionKorea": "韓國",
"SettingsTabSystemSystemRegionTaiwan": "台灣",
"SettingsTabSystemSystemLanguage": "系統語言:",
"SettingsTabSystemSystemLanguageJapanese": "日文",
"SettingsTabSystemSystemLanguageAmericanEnglish": "英文 (美國)",
"SettingsTabSystemSystemLanguageFrench": "法文",
"SettingsTabSystemSystemLanguageGerman": "德文",
"SettingsTabSystemSystemLanguageItalian": "義大利文",
"SettingsTabSystemSystemLanguageSpanish": "西班牙文",
"SettingsTabSystemSystemLanguageChinese": "中文 (中國)",
"SettingsTabSystemSystemLanguageKorean": "韓文",
"SettingsTabSystemSystemLanguageDutch": "荷蘭文",
"SettingsTabSystemSystemLanguagePortuguese": "葡萄牙文",
"SettingsTabSystemSystemLanguageRussian": "俄文",
"SettingsTabSystemSystemLanguageTaiwanese": "中文 (台灣)",
"SettingsTabSystemSystemLanguageBritishEnglish": "英文 (英國)",
"SettingsTabSystemSystemLanguageCanadianFrench": "加拿大法語",
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "拉丁美洲西班牙文",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "簡體中文",
"SettingsTabSystemSystemLanguageTraditionalChinese": "繁體中文",
"SettingsTabSystemSystemTimeZone": "系統時區:",
"SettingsTabSystemSystemTime": "系統時鐘:",
"SettingsTabSystemEnableVsync": "垂直同步",
"SettingsTabSystemEnablePptc": "啟用 PPTC 快取",
"SettingsTabSystemEnableFsIntegrityChecks": "開啟檔案系統完整性檢查",
"SettingsTabSystemAudioBackend": "音效處理後台架構:",
"SettingsTabSystemAudioBackendDummy": "模擬",
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemHacks": "修正",
"SettingsTabSystemHacksNote": " (會引起模擬器不穩定)",
"SettingsTabSystemExpandDramSize": "使用額外的記憶體佈局 (開發人員)",
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服務",
"SettingsTabGraphics": "圖像",
"SettingsTabGraphicsAPI": "圖像處理應用程式介面",
"SettingsTabGraphicsEnableShaderCache": "啟用著色器快取",
"SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "自動",
"SettingsTabGraphicsAnisotropicFiltering2x": "2 倍",
"SettingsTabGraphicsAnisotropicFiltering4x": "4 倍",
"SettingsTabGraphicsAnisotropicFiltering8x": "8 倍",
"SettingsTabGraphicsAnisotropicFiltering16x": "16倍",
"SettingsTabGraphicsResolutionScale": "解析度比例:",
"SettingsTabGraphicsResolutionScaleCustom": "自訂 (不建議使用)",
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "2 倍 (1440p/2160p)",
"SettingsTabGraphicsResolutionScale3x": "3 倍 (2160p/3240p)",
"SettingsTabGraphicsResolutionScale4x": "4 倍 (2880p/4320p)",
"SettingsTabGraphicsAspectRatio": "螢幕長寬比例:",
"SettingsTabGraphicsAspectRatio4x3": "43",
"SettingsTabGraphicsAspectRatio16x9": "169",
"SettingsTabGraphicsAspectRatio16x10": "1610",
"SettingsTabGraphicsAspectRatio21x9": "219",
"SettingsTabGraphicsAspectRatio32x9": "329",
"SettingsTabGraphicsAspectRatioStretch": "伸展至螢幕大小",
"SettingsTabGraphicsDeveloperOptions": "開發者選項",
"SettingsTabGraphicsShaderDumpPath": "圖形著色器轉存路徑:",
"SettingsTabLogging": "日誌",
"SettingsTabLoggingLogging": "日誌",
"SettingsTabLoggingEnableLoggingToFile": "儲存記錄日誌為檔案",
"SettingsTabLoggingEnableStubLogs": "啟用 Stub 記錄",
"SettingsTabLoggingEnableInfoLogs": "啟用資訊記錄",
"SettingsTabLoggingEnableWarningLogs": "啟用警告記錄",
"SettingsTabLoggingEnableErrorLogs": "啟用錯誤記錄",
"SettingsTabLoggingEnableTraceLogs": "啟用追蹤記錄",
"SettingsTabLoggingEnableGuestLogs": "啟用賓客記錄",
"SettingsTabLoggingEnableFsAccessLogs": "啟用檔案存取記錄",
"SettingsTabLoggingFsGlobalAccessLogMode": "記錄全域檔案存取模式:",
"SettingsTabLoggingDeveloperOptions": "開發者選項",
"SettingsTabLoggingDeveloperOptionsNote": "警告:此操作會降低效能",
"SettingsTabLoggingGraphicsBackendLogLevel": "圖像處理後台記錄等級:",
"SettingsTabLoggingGraphicsBackendLogLevelNone": "無",
"SettingsTabLoggingGraphicsBackendLogLevelError": "錯誤",
"SettingsTabLoggingGraphicsBackendLogLevelPerformance": "減速",
"SettingsTabLoggingGraphicsBackendLogLevelAll": "全部",
"SettingsTabLoggingEnableDebugLogs": "啟用除錯記錄",
"SettingsTabInput": "輸入",
"SettingsTabInputEnableDockedMode": "Docked 模式",
"SettingsTabInputDirectKeyboardAccess": "鍵盤直接操作",
"SettingsButtonSave": "儲存",
"SettingsButtonClose": "關閉",
"SettingsButtonOk": "確定",
"SettingsButtonCancel": "取消",
"SettingsButtonApply": "套用",
"ControllerSettingsPlayer": "玩家",
"ControllerSettingsPlayer1": "玩家 1",
"ControllerSettingsPlayer2": "玩家 2",
"ControllerSettingsPlayer3": "玩家 3",
"ControllerSettingsPlayer4": "玩家 4",
"ControllerSettingsPlayer5": "玩家 5",
"ControllerSettingsPlayer6": "玩家 6",
"ControllerSettingsPlayer7": "玩家 7",
"ControllerSettingsPlayer8": "玩家 8",
"ControllerSettingsHandheld": "掌機模式",
"ControllerSettingsInputDevice": "輸入裝置",
"ControllerSettingsRefresh": "更新",
"ControllerSettingsDeviceDisabled": "關閉",
"ControllerSettingsControllerType": "控制器類型",
"ControllerSettingsControllerTypeHandheld": "掌機",
"ControllerSettingsControllerTypeProController": "Nintendo Switch Pro控制器",
"ControllerSettingsControllerTypeJoyConPair": "JoyCon",
"ControllerSettingsControllerTypeJoyConLeft": "左 JoyCon",
"ControllerSettingsControllerTypeJoyConRight": "右 JoyCon",
"ControllerSettingsProfile": "配置檔案",
"ControllerSettingsProfileDefault": "預設",
"ControllerSettingsLoad": "載入",
"ControllerSettingsAdd": "建立",
"ControllerSettingsRemove": "刪除",
"ControllerSettingsButtons": "按鍵",
"ControllerSettingsButtonA": "A",
"ControllerSettingsButtonB": "B",
"ControllerSettingsButtonX": "X",
"ControllerSettingsButtonY": "Y",
"ControllerSettingsButtonPlus": "+",
"ControllerSettingsButtonMinus": "-",
"ControllerSettingsDPad": "方向鍵",
"ControllerSettingsDPadUp": "上",
"ControllerSettingsDPadDown": "下",
"ControllerSettingsDPadLeft": "左",
"ControllerSettingsDPadRight": "右",
"ControllerSettingsStickButton": "按鍵",
"ControllerSettingsStickUp": "上",
"ControllerSettingsStickDown": "下",
"ControllerSettingsStickLeft": "左",
"ControllerSettingsStickRight": "右",
"ControllerSettingsStickStick": "搖桿",
"ControllerSettingsStickInvertXAxis": "搖桿左右反向",
"ControllerSettingsStickInvertYAxis": "搖桿上下反向",
"ControllerSettingsStickDeadzone": "盲區:",
"ControllerSettingsLStick": "左搖桿",
"ControllerSettingsRStick": "右搖桿",
"ControllerSettingsTriggersLeft": "左 Triggers",
"ControllerSettingsTriggersRight": "右 Triggers",
"ControllerSettingsTriggersButtonsLeft": "左 Triggers 鍵",
"ControllerSettingsTriggersButtonsRight": "右 Triggers 鍵",
"ControllerSettingsTriggers": "板機",
"ControllerSettingsTriggerL": "L",
"ControllerSettingsTriggerR": "R",
"ControllerSettingsTriggerZL": "ZL",
"ControllerSettingsTriggerZR": "ZR",
"ControllerSettingsLeftSL": "SL",
"ControllerSettingsLeftSR": "SR",
"ControllerSettingsRightSL": "SL",
"ControllerSettingsRightSR": "SR",
"ControllerSettingsExtraButtonsLeft": "左按鍵",
"ControllerSettingsExtraButtonsRight": "右按鍵",
"ControllerSettingsMisc": "其他",
"ControllerSettingsTriggerThreshold": "Triggers 閾值:",
"ControllerSettingsMotion": "傳感器",
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "使用 CemuHook 相容性傳感協定",
"ControllerSettingsMotionControllerSlot": "控制器插槽:",
"ControllerSettingsMotionMirrorInput": "鏡像輸入",
"ControllerSettingsMotionRightJoyConSlot": "右 JoyCon",
"ControllerSettingsMotionServerHost": "伺服器IP地址",
"ControllerSettingsMotionGyroSensitivity": "陀螺儀敏感度:",
"ControllerSettingsMotionGyroDeadzone": "陀螺儀盲區:",
"ControllerSettingsSave": "儲存",
"ControllerSettingsClose": "關閉",
"UserProfilesSelectedUserProfile": "選擇使用者帳戶:",
"UserProfilesSaveProfileName": "儲存帳戶名稱",
"UserProfilesChangeProfileImage": "更換帳戶頭像",
"UserProfilesAvailableUserProfiles": "現有的使用者帳戶:",
"UserProfilesAddNewProfile": "建立帳戶",
"UserProfilesDelete": "刪除",
"UserProfilesClose": "關閉",
"ProfileNameSelectionWatermark": "選擇一個暱稱",
"ProfileImageSelectionTitle": "帳戶頭像選擇",
"ProfileImageSelectionHeader": "選擇帳戶頭像",
"ProfileImageSelectionNote": "你可以導入自訂頭像,或從系統中選擇頭像",
"ProfileImageSelectionImportImage": "導入圖片檔案",
"ProfileImageSelectionSelectAvatar": "選擇系統頭像",
"InputDialogTitle": "輸入對話框",
"InputDialogOk": "完成",
"InputDialogCancel": "取消",
"InputDialogAddNewProfileTitle": "選擇帳戶名稱",
"InputDialogAddNewProfileHeader": "請輸入帳戶名稱",
"InputDialogAddNewProfileSubtext": "(最大長度:{0})",
"AvatarChoose": "選擇",
"AvatarSetBackgroundColor": "設定背景顏色",
"AvatarClose": "關閉",
"ControllerSettingsLoadProfileToolTip": "載入配置檔案",
"ControllerSettingsAddProfileToolTip": "新增配置檔案",
"ControllerSettingsRemoveProfileToolTip": "刪除配置檔案",
"ControllerSettingsSaveProfileToolTip": "儲存配置檔案",
"MenuBarFileToolsTakeScreenshot": "儲存截圖",
"MenuBarFileToolsHideUi": "隱藏使用者介面",
"GameListContextMenuRunApplication": "執行程式",
"GameListContextMenuToggleFavorite": "標記為收藏",
"GameListContextMenuToggleFavoriteToolTip": "啟用或取消收藏標記",
"SettingsTabGeneralTheme": "佈景主題",
"SettingsTabGeneralThemeCustomTheme": "自訂佈景主題路徑",
"SettingsTabGeneralThemeBaseStyle": "基本佈景主題式樣",
"SettingsTabGeneralThemeBaseStyleDark": "深色模式",
"SettingsTabGeneralThemeBaseStyleLight": "淺色模式",
"SettingsTabGeneralThemeEnableCustomTheme": "使用自訂佈景主題",
"ButtonBrowse": "瀏覽",
"ControllerSettingsConfigureGeneral": "配置",
"ControllerSettingsRumble": "震動",
"ControllerSettingsRumbleStrongMultiplier": "強震動調節",
"ControllerSettingsRumbleWeakMultiplier": "弱震動調節",
"DialogMessageSaveNotAvailableMessage": "沒有{0} [{1:x16}]的遊戲存檔",
"DialogMessageSaveNotAvailableCreateSaveMessage": "是否建立該遊戲的存檔資料夾?",
"DialogConfirmationTitle": "Ryujinx - 設定",
"DialogUpdaterTitle": "Ryujinx - 更新",
"DialogErrorTitle": "Ryujinx - 錯誤",
"DialogWarningTitle": "Ryujinx - 警告",
"DialogExitTitle": "Ryujinx - 關閉",
"DialogErrorMessage": "Ryujinx 遇到了錯誤",
"DialogExitMessage": "你確定要關閉 Ryujinx 嗎?",
"DialogExitSubMessage": "所有未儲存的資料將會遺失!",
"DialogMessageCreateSaveErrorMessage": "建立特定的存檔時出現錯誤: {0}",
"DialogMessageFindSaveErrorMessage": "查找特定的存檔時出現錯誤: {0}",
"FolderDialogExtractTitle": "選擇要解壓到的資料夾",
"DialogNcaExtractionMessage": "提取{1}的{0}分區...",
"DialogNcaExtractionTitle": "Ryujinx - NCA分區提取",
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不含主NCA檔案",
"DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請查看日誌檔案取得詳情。",
"DialogNcaExtractionSuccessMessage": "提取成功。",
"DialogUpdaterConvertFailedMessage": "無法轉換目前 Ryujinx 版本。",
"DialogUpdaterCancelUpdateMessage": "更新取消!",
"DialogUpdaterAlreadyOnLatestVersionMessage": "你使用的 Ryujinx 是最新版本。",
"DialogUpdaterFailedToGetVersionMessage": "嘗試從 Github 取得版本訊息時失敗。可能是因為 GitHub Actions 正在編譯新版本。請於數分數後重試。",
"DialogUpdaterConvertFailedGithubMessage": "無法轉換從 Github 接收到的 Ryujinx 版本。",
"DialogUpdaterDownloadingMessage": "下載最新版本中...",
"DialogUpdaterExtractionMessage": "正在提取更新...",
"DialogUpdaterRenamingMessage": "正在刪除舊檔案...",
"DialogUpdaterAddingFilesMessage": "安裝更新中...",
"DialogUpdaterCompleteMessage": "更新成功!",
"DialogUpdaterRestartMessage": "你確定要立即重新啟動 Ryujinx 嗎?",
"DialogUpdaterArchNotSupportedMessage": "你執行的系統架構不被支援!",
"DialogUpdaterArchNotSupportedSubMessage": "(僅支援 x64 系統)",
"DialogUpdaterNoInternetMessage": "你沒有連接到網際網絡!",
"DialogUpdaterNoInternetSubMessage": "請確保網際網絡連接正常!",
"DialogUpdaterDirtyBuildMessage": "不能更新非官方版本的 Ryujinx",
"DialogUpdaterDirtyBuildSubMessage": "如果你希望使用被受支援的Ryujinx版本請你在官方網址 https://ryujinx.org/ 下載.",
"DialogRestartRequiredMessage": "模擬器必須重新啟動",
"DialogThemeRestartMessage": "佈景主題設定已儲存。需要重新啟動才能生效。",
"DialogThemeRestartSubMessage": "你確定要現在重新啟動嗎?",
"DialogFirmwareInstallEmbeddedMessage": "要安裝遊戲內建的韌體嗎?(韌體 {0})",
"DialogFirmwareInstallEmbeddedSuccessMessage": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}.\\n模擬器現在可以執行。",
"DialogFirmwareNoFirmwareInstalledMessage": "未安裝韌體",
"DialogFirmwareInstalledMessage": "已安裝韌體{0}",
"DialogInstallFileTypesSuccessMessage": "成功註冊檔案類型!",
"DialogInstallFileTypesErrorMessage": "註冊檔案類型失敗。",
"DialogUninstallFileTypesSuccessMessage": "成功取消註冊檔案類型!",
"DialogUninstallFileTypesErrorMessage": "取消註冊檔案類型失敗。",
"DialogOpenSettingsWindowLabel": "開啟設定視窗",
"DialogControllerAppletTitle": "控制器小視窗",
"DialogMessageDialogErrorExceptionMessage": "顯示訊息對話框時出現錯誤: {0}",
"DialogSoftwareKeyboardErrorExceptionMessage": "顯示軟體鍵盤時出現錯誤: {0}",
"DialogErrorAppletErrorExceptionMessage": "顯示錯誤對話框時出現錯誤: {0}",
"DialogUserErrorDialogMessage": "{0} {1}",
"DialogUserErrorDialogInfoMessage": "\n有關修復此錯誤的更多訊息可以遵循我們的設定指南。",
"DialogUserErrorDialogTitle": "Ryujinx 錯誤 ({0})",
"DialogAmiiboApiTitle": "Amiibo 應用程式介面",
"DialogAmiiboApiFailFetchMessage": "從 API 取得訊息時出錯。",
"DialogAmiiboApiConnectErrorMessage": "無法連接到 Amiibo API 伺服器。伺服器可能已關閉,或你沒有連接到網際網路。",
"DialogProfileInvalidProfileErrorMessage": "配置檔案 {0} 與目前輸入系統不相容。",
"DialogProfileDefaultProfileOverwriteErrorMessage": "無法覆蓋預設的配置檔案",
"DialogProfileDeleteProfileTitle": "刪除帳戶",
"DialogProfileDeleteProfileMessage": "此操作不可撤銷, 您確定要繼續嗎?",
"DialogWarning": "警告",
"DialogPPTCDeletionMessage": "下一次重啟時將會重新建立以下遊戲的 PPTC 快取\n\n{0}\n\n你確定要繼續嗎",
"DialogPPTCDeletionErrorMessage": "清除位於{0}的 PPTC 快取時出錯: {1}",
"DialogShaderDeletionMessage": "即將刪除以下遊戲的著色器快取:\n\n{0}\n\n你確定要繼續嗎",
"DialogShaderDeletionErrorMessage": "清除{0}的著色器快取時出現錯誤: {1}",
"DialogRyujinxErrorMessage": "Ryujinx 遇到錯誤",
"DialogInvalidTitleIdErrorMessage": "UI 錯誤:所選遊戲沒有有效的標題ID",
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "路徑{0}找不到有效的系統韌體。",
"DialogFirmwareInstallerFirmwareInstallTitle": "安裝韌體{0}",
"DialogFirmwareInstallerFirmwareInstallMessage": "將安裝{0}版本的系統。",
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n這將替換目前系統版本{0}。",
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "你確定要繼續嗎?",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "安裝韌體中...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本{0}。",
"DialogUserProfileDeletionWarningMessage": "刪除後將沒有可選擇的使用者帳戶",
"DialogUserProfileDeletionConfirmMessage": "你確定要刪除選擇中的帳戶嗎?",
"DialogUserProfileUnsavedChangesTitle": "警告 - 有未儲存的更改",
"DialogUserProfileUnsavedChangesMessage": "你對此帳戶所做的更改尚未儲存.",
"DialogUserProfileUnsavedChangesSubMessage": "你確定要捨棄更改嗎?",
"DialogControllerSettingsModifiedConfirmMessage": "目前的輸入配置檔案已更新",
"DialogControllerSettingsModifiedConfirmSubMessage": "你確定要儲存嗎?",
"DialogLoadNcaErrorMessage": "{0}. 錯誤的檔案: {1}",
"DialogDlcNoDlcErrorMessage": "選擇的檔案不包含所選遊戲的 DLC",
"DialogPerformanceCheckLoggingEnabledMessage": "你啟用了跟蹤記錄,它的設計僅限開發人員使用。",
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "為了獲得最佳效能,建議停用追蹤記錄。你是否要立即停用?",
"DialogPerformanceCheckShaderDumpEnabledMessage": "你啟用了著色器轉存,它的設計僅限開發人員使用。",
"DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "為了獲得最佳效能,建議停用著色器轉存。你是否要立即停用?",
"DialogLoadAppGameAlreadyLoadedMessage": "目前已載入此遊戲",
"DialogLoadAppGameAlreadyLoadedSubMessage": "請停止模擬或關閉程式,再啟動另一個遊戲。",
"DialogUpdateAddUpdateErrorMessage": "選擇的檔案不包含所選遊戲的更新!",
"DialogSettingsBackendThreadingWarningTitle": "警告 - 後台多工執行中",
"DialogSettingsBackendThreadingWarningMessage": "更改此選項後必須重啟 Ryujinx 才能生效。根據你的硬體您開啟該選項時可能需要手動停用驅動程式本身的GPU多執行緒。",
"SettingsTabGraphicsFeaturesOptions": "功能",
"SettingsTabGraphicsBackendMultithreading": "圖像處理後台多線程支援:",
"CommonAuto": "自動(推薦)",
"CommonOff": "關閉",
"CommonOn": "打開",
"InputDialogYes": "是",
"InputDialogNo": "否",
"DialogProfileInvalidProfileNameErrorMessage": "檔案名包含無效字元,請重試。",
"MenuBarOptionsPauseEmulation": "暫停",
"MenuBarOptionsResumeEmulation": "繼續",
"AboutUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的官方網站。",
"AboutDisclaimerMessage": "Ryujinx 與 Nintendo™ 並沒有任何關聯, 包括其合作伙伴, 及任何形式上。",
"AboutAmiiboDisclaimerMessage": "我們的 Amiibo 模擬使用了\nAmiiboAPI (www.amiiboapi.com) ",
"AboutPatreonUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Patreon 贊助網頁。",
"AboutGithubUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 GitHub 儲存庫。",
"AboutDiscordUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Discord 伺服器邀請連結。",
"AboutTwitterUrlTooltipMessage": "在瀏覽器中打開 Ryujinx 的 Twitter 首頁。",
"AboutRyujinxAboutTitle": "關於:",
"AboutRyujinxAboutContent": "Ryujinx 是 Nintendo Switch™ 的一款模擬器。\n懇請您在 Patreon 上贊助我們。\n關注 Twitter 或 Discord 可以取得我們的最新動態。\n如果您對開發本軟體感興趣歡迎來 GitHub 和 Discord 加入我們!",
"AboutRyujinxMaintainersTitle": "開發及維護名單:",
"AboutRyujinxMaintainersContentTooltipMessage": "在瀏覽器中打開貢獻者的網頁",
"AboutRyujinxSupprtersTitle": "Patreon 的贊助人:",
"AmiiboSeriesLabel": "Amiibo 系列",
"AmiiboCharacterLabel": "角色",
"AmiiboScanButtonLabel": "掃描",
"AmiiboOptionsShowAllLabel": "顯示所有 Amiibo",
"AmiiboOptionsUsRandomTagLabel": "侵略:使用隨機標記的 Uuid 編碼",
"DlcManagerTableHeadingEnabledLabel": "已啟用",
"DlcManagerTableHeadingTitleIdLabel": "遊戲ID",
"DlcManagerTableHeadingContainerPathLabel": "資料夾路徑",
"DlcManagerTableHeadingFullPathLabel": "完整路徑",
"DlcManagerRemoveAllButton": "全部刪除",
"DlcManagerEnableAllButton": "啟用全部",
"DlcManagerDisableAllButton": "停用全部",
"MenuBarOptionsChangeLanguage": "更改語言",
"MenuBarShowFileTypes": "顯示檔案類型",
"CommonSort": "排序",
"CommonShowNames": "顯示名稱",
"CommonFavorite": "收藏",
"OrderAscending": "從小到大",
"OrderDescending": "從大到小",
"SettingsTabGraphicsFeatures": "功能及優化",
"ErrorWindowTitle": "錯誤視窗",
"ToggleDiscordTooltip": "啟用或關閉 Discord 動態狀態展示",
"AddGameDirBoxTooltip": "輸入要添加的遊戲資料夾",
"AddGameDirTooltip": "添加遊戲資料夾到列表中",
"RemoveGameDirTooltip": "移除選擇中的遊戲資料夾",
"CustomThemeCheckTooltip": "啟用或關閉自訂佈景主題",
"CustomThemePathTooltip": "自訂佈景主題的資料夾",
"CustomThemeBrowseTooltip": "查找自訂佈景主題",
"DockModeToggleTooltip": "是否開啟 Switch 的 Docked 模式",
"DirectKeyboardTooltip": "支援鍵盤直接存取 (HID協定) . 可供給遊戲使用你的鍵盤作為輸入文字裝置.",
"DirectMouseTooltip": "支援滑鼠直接存取 (HID協定) . 可供給遊戲使用你的滑鼠作為瞄準裝置.",
"RegionTooltip": "更改系統區域",
"LanguageTooltip": "更改系統語言",
"TimezoneTooltip": "更改系統時區",
"TimeTooltip": "更改系統時鐘",
"VSyncToggleTooltip": "模擬遊戲主機垂直同步更新頻率. 重要地反映著遊戲本身的速度; 關閉它可能會令後使用動態更新率的遊戲速度過高, 或會引致載入錯誤等等.\n\n可在遊戲中利用自訂快速鍵開關此功能. 我們也建議使用快速鍵, 如果你計劃關上它.\n\n如果不確定請設定為\"開啟\".",
"PptcToggleTooltip": "開啟以後減少遊戲啟動時間和卡頓",
"FsIntegrityToggleTooltip": "是否檢查遊戲檔案內容的完整性",
"AudioBackendTooltip": "更改音效處理後台架構.\n\n推薦使用SDL2架構, 而OpenAL及SoundIO架構用作後備. Dummy是沒有音效的.\n\n如果不確定請設定為\"SDL2\".",
"MemoryManagerTooltip": "更改模擬器記憶體至電腦記憶體的映射和存取方式極其影響CPU效能.\n\n如果不確定, 請設定為\"主機略過檢查模式\".",
"MemoryManagerSoftwareTooltip": "使用軟體虛擬分頁表換算, 最精確但是速度最慢.",
"MemoryManagerHostTooltip": "直接地映射模擬記憶體到電腦記憶體. 對 JIT 編譯和執行效率有顯著提升. ",
"MemoryManagerUnsafeTooltip": "直接地映射模擬記憶體到電腦記憶體, 但是不檢查記憶體溢出. 由於 Ryujinx 的子程式可以存取任何位置的記憶體, 因而相對不安全. 故在此模式下只應執行你信任的遊戲或軟體.",
"UseHypervisorTooltip": "使用 Hypervisor 代替 JIT。在本功能可用時就可以大幅增大效能但目前狀態還不穩定。",
"DRamTooltip": "利用可選擇性的記憶體模式來模擬Switch發展中型號.\n\n此選項只會對高畫質材質包或4K模組有用. 而這並不會提升效能. \n\n如果不確定請關閉本功能.",
"IgnoreMissingServicesTooltip": "忽略某些未被實施的系統服務. 此功能有助於繞過當啟動遊戲時帶來的故障.\n\n如果不確定請關閉本功能。",
"GraphicsBackendThreadingTooltip": "執行雙線程後台繪圖指令, 能夠減少著色器編譯斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
"GalThreadingTooltip": "執行雙線程後台繪圖指令.\n\n能夠加速著色器編譯及減少斷續, 並提高GPU驅動效能, 即將它不支持多線程處理. 而對於多線程處理也有少量提升.\n\n如果你不確定請設定為\"自動\"",
"ShaderCacheToggleTooltip": "儲存著色器快取到硬碟,減少存取斷續。\n\n如果不確定請設定為\"開啟\"。",
"ResolutionScaleTooltip": "解析度繪圖倍率",
"ResolutionScaleEntryTooltip": "盡量使用如1.5的浮點倍數。非整數的倍率易引起錯誤",
"AnisotropyTooltip": "各向異性過濾等級。提高傾斜視角材質的清晰度\n(選擇「自動」將使用遊戲預設指定的等級)",
"AspectRatioTooltip": "模擬器視窗解析度的長寬比",
"ShaderDumpPathTooltip": "圖形著色器轉存路徑",
"FileLogTooltip": "是否儲存日誌檔案到硬碟",
"StubLogTooltip": "在控制台顯示及記錄 Stub 訊息",
"InfoLogTooltip": "在控制台顯示及記錄資訊訊息",
"WarnLogTooltip": "在控制台顯示及記錄警告訊息\n",
"ErrorLogTooltip": "在控制台顯示及記錄錯誤訊息",
"TraceLogTooltip": "在控制台顯示及記錄追蹤訊息",
"GuestLogTooltip": "在控制台顯示及記錄賓客訊息",
"FileAccessLogTooltip": "在控制台顯示及記錄檔案存取訊息",
"FSAccessLogModeTooltip": "在控制台顯示及記錄FS 存取訊息. 可選的模式是 0-3",
"DeveloperOptionTooltip": "使用請謹慎",
"OpenGlLogLevel": "需要打開適當的日誌等級",
"DebugLogTooltip": "在控制台顯示及記錄除錯訊息.\n\n僅限於受訓的成員使用, 因為它很難理解而且令模擬的效能非常地差.\n",
"LoadApplicationFileTooltip": "選擇 Switch 支援的遊戲格式並載入",
"LoadApplicationFolderTooltip": "選擇解包後的 Switch 遊戲並載入",
"OpenRyujinxFolderTooltip": "開啟 Ryujinx 系統資料夾",
"OpenRyujinxLogsTooltip": "開啟存放日誌的資料夾",
"ExitTooltip": "關閉 Ryujinx",
"OpenSettingsTooltip": "開啟設定視窗",
"OpenProfileManagerTooltip": "開啟使用者帳戶管理視窗",
"StopEmulationTooltip": "停止執行目前遊戲並回到選擇界面",
"CheckUpdatesTooltip": "檢查 Ryujinx 新版本",
"OpenAboutTooltip": "開啟關於視窗",
"GridSize": "網格尺寸",
"GridSizeTooltip": "調整網格模式的大小",
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "巴西葡萄牙語",
"AboutRyujinxContributorsButtonHeader": "查看所有參與者",
"SettingsTabSystemAudioVolume": "音量:",
"AudioVolumeTooltip": "調節音量",
"SettingsTabSystemEnableInternetAccess": "啟用網路連接",
"EnableInternetAccessTooltip": "開啟網路存取。此選項打開後,效果類似於 Switch 連接到網路的狀態。注意即使此選項關閉,應用程式偶爾也有可能連接到網路",
"GameListContextMenuManageCheatToolTip": "管理金手指",
"GameListContextMenuManageCheat": "管理金手指",
"ControllerSettingsStickRange": "範圍:",
"DialogStopEmulationTitle": "Ryujinx - 停止模擬",
"DialogStopEmulationMessage": "你確定要停止模擬嗎?",
"SettingsTabCpu": "處理器",
"SettingsTabAudio": "音訊",
"SettingsTabNetwork": "網路",
"SettingsTabNetworkConnection": "網路連接",
"SettingsTabCpuCache": "CPU 快取",
"SettingsTabCpuMemory": "CPU 模式",
"DialogUpdaterFlatpakNotSupportedMessage": "請透過 Flathub 更新 Ryujinx。",
"UpdaterDisabledWarningTitle": "更新已停用!",
"GameListContextMenuOpenSdModsDirectory": "開啟 Atmosphere 模組資料夾",
"GameListContextMenuOpenSdModsDirectoryToolTip": "開啟此遊戲額外的SD記憶卡Atmosphere模組資料夾. 有用於包裝在真實主機上的模組.\n",
"ControllerSettingsRotate90": "順時針旋轉 90°",
"IconSize": "圖示尺寸",
"IconSizeTooltip": "更改遊戲圖示大小",
"MenuBarOptionsShowConsole": "顯示控制台",
"ShaderCachePurgeError": "清除 {0} 著色器快取時出現錯誤: {1}",
"UserErrorNoKeys": "找不到金鑰",
"UserErrorNoFirmware": "找不到韌體",
"UserErrorFirmwareParsingFailed": "韌體解析錯誤",
"UserErrorApplicationNotFound": "找不到應用程式",
"UserErrorUnknown": "未知錯誤",
"UserErrorUndefined": "未定義錯誤",
"UserErrorNoKeysDescription": "Ryujinx 找不到 『prod.keys』 檔案",
"UserErrorNoFirmwareDescription": "Ryujinx 找不到任何已安裝的韌體",
"UserErrorFirmwareParsingFailedDescription": "Ryujinx 無法解密選擇的韌體。這通常是由於金鑰過舊。",
"UserErrorApplicationNotFoundDescription": "Ryujinx 在選中路徑找不到有效的應用程式。",
"UserErrorUnknownDescription": "發生未知錯誤!",
"UserErrorUndefinedDescription": "發生了未定義錯誤!此類錯誤不應出現,請聯絡開發人員!",
"OpenSetupGuideMessage": "開啟設定教學",
"NoUpdate": "沒有新版本",
"TitleUpdateVersionLabel": "版本 {0} - {1}",
"RyujinxInfo": "Ryujinx - 訊息",
"RyujinxConfirm": "Ryujinx - 確認",
"FileDialogAllTypes": "全部類型",
"Never": "從不",
"SwkbdMinCharacters": "至少應為 {0} 個字長",
"SwkbdMinRangeCharacters": "必須為 {0}-{1} 個字長",
"SoftwareKeyboard": "軟體鍵盤",
"SoftwareKeyboardModeNumbersOnly": "只接受數字",
"SoftwareKeyboardModeAlphabet": "不支援中日韓統一表意文字字元",
"SoftwareKeyboardModeASCII": "只接受 ASCII 符號",
"DialogControllerAppletMessagePlayerRange": "本遊戲需要 {0} 個玩家持有:\n\n類型{1}\n\n玩家{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
"DialogControllerAppletMessage": "本遊戲需要剛好 {0} 個玩家持有:\n\n類型{1}\n\n玩家{2}\n\n{3}請打開設定畫面並配置控制器,或者關閉本視窗。",
"DialogControllerAppletDockModeSet": "現在處於主機模式,無法使用掌機操作方式\n\n",
"UpdaterRenaming": "正在重新命名舊檔案...",
"UpdaterRenameFailed": "更新過程中無法重新命名檔案: {0}",
"UpdaterAddingFiles": "安裝更新中...",
"UpdaterExtracting": "正在提取更新...",
"UpdaterDownloading": "下載新版本中...",
"Game": "遊戲",
"Docked": "主機模式",
"Handheld": "掌機模式",
"ConnectionError": "連接錯誤。",
"AboutPageDeveloperListMore": "{0} 等開發者...",
"ApiError": "API 錯誤",
"LoadingHeading": "正在啟動 {0}",
"CompilingPPTC": "編譯 PPTC 快取中",
"CompilingShaders": "編譯著色器中",
"AllKeyboards": "所有鍵盤",
"OpenFileDialogTitle": "選擇支援的檔案格式",
"OpenFolderDialogTitle": "選擇一個包含已解開封裝遊戲的資料夾\n",
"AllSupportedFormats": "全部支援的格式",
"RyujinxUpdater": "Ryujinx 更新程式",
"SettingsTabHotkeys": "快捷鍵",
"SettingsTabHotkeysHotkeys": "鍵盤快捷鍵",
"SettingsTabHotkeysToggleVsyncHotkey": "切換垂直同步:",
"SettingsTabHotkeysScreenshotHotkey": "截圖:",
"SettingsTabHotkeysShowUiHotkey": "隱藏使用者介面:",
"SettingsTabHotkeysPauseHotkey": "暫停:",
"SettingsTabHotkeysToggleMuteHotkey": "靜音:",
"ControllerMotionTitle": "體感操作設定",
"ControllerRumbleTitle": "震動設定",
"SettingsSelectThemeFileDialogTitle": "選擇主題檔案",
"SettingsXamlThemeFile": "Xaml 主題檔案",
"AvatarWindowTitle": "管理帳號 - 頭貼",
"Amiibo": "Amiibo",
"Unknown": "未知",
"Usage": "用途",
"Writable": "可寫入",
"SelectDlcDialogTitle": "選擇 DLC 檔案",
"SelectUpdateDialogTitle": "選擇更新檔",
"UserProfileWindowTitle": "管理使用者帳戶",
"CheatWindowTitle": "管理遊戲金手指",
"DlcWindowTitle": "管理遊戲 DLC",
"UpdateWindowTitle": "管理遊戲更新",
"CheatWindowHeading": "金手指可用於 {0} [{1}]",
"BuildId": "版本編號:",
"DlcWindowHeading": "DLC 可用於 {0} [{1}]",
"UserProfilesEditProfile": "編輯所選",
"Cancel": "取消",
"Save": "儲存",
"Discard": "放棄更改",
"UserProfilesSetProfileImage": "設定帳戶頭像",
"UserProfileEmptyNameError": "使用者名稱為必填",
"UserProfileNoImageError": "必須設定帳戶頭像",
"GameUpdateWindowHeading": "更新可用於 {0} [{1}]",
"SettingsTabHotkeysResScaleUpHotkey": "提高解析度:",
"SettingsTabHotkeysResScaleDownHotkey": "降低解析度:",
"UserProfilesName": "使用者名稱:",
"UserProfilesUserId": "使用者 ID",
"SettingsTabGraphicsBackend": "圖像處理後台架構",
"SettingsTabGraphicsBackendTooltip": "用來圖像處理的後台架構",
"SettingsEnableTextureRecompression": "開啟材質重新壓縮",
"SettingsEnableTextureRecompressionTooltip": "壓縮某些材質以減少 VRAM 使用。\n\n推薦用於小於 4GiB VRAM 的 GPU。\n\n如果不確定請關閉本功能。",
"SettingsTabGraphicsPreferredGpu": "優先選取的 GPU",
"SettingsTabGraphicsPreferredGpuTooltip": "選擇支持運行Vulkan圖像處理架構的GPU.\n\n此設定不會影響GPU運行OpenGL.\n\n如果不確定, 請設定標籤為\"dGPU\"的GPU. 如果沒有, 請保留原始設定.",
"SettingsAppRequiredRestartMessage": "必須重啟 Ryujinx",
"SettingsGpuBackendRestartMessage": "圖像處理後台架構或GPU相關設定已被修改。需要重新啟動才能套用。",
"SettingsGpuBackendRestartSubMessage": "你確定要現在重新啟動嗎?",
"RyujinxUpdaterMessage": "你確定要將 Ryujinx 更新到最新版本嗎?",
"SettingsTabHotkeysVolumeUpHotkey": "增加音量:",
"SettingsTabHotkeysVolumeDownHotkey": "降低音量:",
"SettingsEnableMacroHLE": "啟用 Macro HLE",
"SettingsEnableMacroHLETooltip": "GPU 微代碼的高階模擬。\n\n可以提升效能但可能會導致某些遊戲出現圖形顯示故障。\n\n如果不確定請設定為\"開啟\"。",
"SettingsEnableColorSpacePassthrough": "色彩空間直通",
"SettingsEnableColorSpacePassthroughTooltip": "指揮Vulkan後端直通色彩資訊而不需指定色彩空間對於廣色域顯示的使用者這會造成較多抖色犧牲色彩正確性。",
"VolumeShort": "音量",
"UserProfilesManageSaves": "管理遊戲存檔",
"DeleteUserSave": "你確定要刪除此遊戲的存檔嗎?",
"IrreversibleActionNote": "本動作將無法挽回。",
"SaveManagerHeading": "管理 {0} 的遊戲存檔",
"SaveManagerTitle": "遊戲存檔管理器",
"Name": "名稱",
"Size": "大小",
"Search": "搜尋",
"UserProfilesRecoverLostAccounts": "恢復遺失的帳戶",
"Recover": "恢復",
"UserProfilesRecoverHeading": "在以下帳戶找到了一些遊戲存檔",
"UserProfilesRecoverEmptyList": "沒有可恢復的使用者帳戶",
"GraphicsAATooltip": "在遊戲繪圖上套用抗鋸齒",
"GraphicsAALabel": "抗鋸齒:",
"GraphicsScalingFilterLabel": "縮放過濾器:",
"GraphicsScalingFilterTooltip": "啟用畫幀緩衝區縮放",
"GraphicsScalingFilterLevelLabel": "記錄檔等級",
"GraphicsScalingFilterLevelTooltip": "設定縮放過濾器的強度",
"SmaaLow": "低階 SMAA",
"SmaaMedium": "中階 SMAA",
"SmaaHigh": "高階 SMAA",
"SmaaUltra": "超高階 SMAA",
"UserEditorTitle": "編輯使用者",
"UserEditorTitleCreate": "建立使用者",
"SettingsTabNetworkInterface": "網路介面:",
"NetworkInterfaceTooltip": "用於具有 LAN 功能的網路介面",
"NetworkInterfaceDefault": "預設",
"PackagingShaders": "著色器封裝",
"AboutChangelogButton": "在 GitHub 查看更新日誌",
"AboutChangelogButtonTooltipMessage": "在瀏覽器中打開此Ryujinx版本的更新日誌。"
}

View File

@ -1,236 +0,0 @@
using Avalonia;
using Avalonia.Threading;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using Ryujinx.UI.Common.SystemInfo;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Ryujinx.Ava
{
internal partial class Program
{
public static double WindowScaleFactor { get; set; }
public static double DesktopScaleFactor { get; set; } = 1.0;
public static string Version { get; private set; }
public static string ConfigurationPath { get; private set; }
public static bool PreviewerDetached { get; private set; }
[LibraryImport("user32.dll", SetLastError = true)]
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
private const uint MbIconwarning = 0x30;
public static void Main(string[] args)
{
Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
_ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
}
PreviewerDetached = true;
Initialize(args);
LoggerAdapter.Register();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
EnableIme = true,
RenderingMode = new[] { X11RenderingMode.Glx, X11RenderingMode.Software },
})
.With(new Win32PlatformOptions
{
WinUICompositionBackdropCornerRadius = 8.0f,
RenderingMode = new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software },
})
.UseSkia();
}
private static void Initialize(string[] args)
{
// Parse arguments
CommandLineState.ParseArguments(args);
// Delete backup files after updating.
Task.Run(Updater.CleanupUpdate);
Console.Title = $"Ryujinx Console {Version}";
// Hook unhandled exception and process exit events.
AppDomain.CurrentDomain.UnhandledException += (sender, e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exit();
// Setup base data directory.
AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
// Initialize the configuration.
ConfigurationState.Initialize();
// Initialize the logger system.
LoggerModule.Initialize();
// Initialize Discord integration.
DiscordIntegrationModule.Initialize();
// Initialize SDL2 driver
SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
ReloadConfig();
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
// Logging system information.
PrintSystemInfo();
// Enable OGL multithreading on the driver, when available.
DriverUtilities.ToggleOGLThreading(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
// Check if keys exists.
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
{
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
{
MainWindow.ShowKeyErrorOnLoad = true;
}
}
if (CommandLineState.LaunchPathArg != null)
{
MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
}
}
public static void ReloadConfig()
{
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
// Now load the configuration as the other subsystems are now registered
if (File.Exists(localConfigurationPath))
{
ConfigurationPath = localConfigurationPath;
}
else if (File.Exists(appDataConfigurationPath))
{
ConfigurationPath = appDataConfigurationPath;
}
if (ConfigurationPath == null)
{
// No configuration, we load the default values and save it to disk
ConfigurationPath = appDataConfigurationPath;
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
}
else
{
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
}
else
{
ConfigurationState.Instance.LoadDefault();
Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
}
}
// Check if graphics backend was overridden
if (CommandLineState.OverrideGraphicsBackend != null)
{
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
}
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
{
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
}
}
// Check if docked mode was overriden.
if (CommandLineState.OverrideDockedMode.HasValue)
{
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
}
// Check if HideCursor was overridden.
if (CommandLineState.OverrideHideCursor is not null)
{
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
{
"never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor.Value,
};
}
}
private static void PrintSystemInfo()
{
Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
SystemInfo.Gather().Print();
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(Logger.GetEnabledLevels().Count == 0 ? "<None>" : string.Join(", ", Logger.GetEnabledLevels()))}");
if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
{
Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
}
else
{
Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
}
}
private static void ProcessUnhandledException(Exception ex, bool isTerminating)
{
string message = $"Unhandled exception caught: {ex}";
Logger.Error?.PrintMsg(LogClass.Application, message);
if (Logger.Error == null)
{
Logger.Notice.PrintMsg(LogClass.Application, message);
}
if (isTerminating)
{
Exit();
}
}
public static void Exit()
{
DiscordIntegrationModule.Exit();
Logger.Shutdown();
}
}
}

View File

@ -1,167 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0-dirty</Version>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
<RootNamespace>Ryujinx.Ava</RootNamespace>
<ApplicationIcon>Ryujinx.ico</ApplicationIcon>
<TieredPGO>true</TieredPGO>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
</Target>
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
<PublishSingleFile>true</PublishSingleFile>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
<!--
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
See:
https://github.com/amwx/FluentAvalonia/issues/481
https://devblogs.microsoft.com/dotnet/system-text-json-in-dotnet-8/
-->
<PropertyGroup>
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)'=='Debug'" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" />
<PackageReference Include="Avalonia.Svg" />
<PackageReference Include="Avalonia.Svg.Skia" />
<PackageReference Include="DynamicData" />
<PackageReference Include="FluentAvaloniaUI" />
<PackageReference Include="OpenTK.Core" />
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
<PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SPB" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="SixLabors.ImageSharp" />
<!--NOTE: DO NOT REMOVE, THIS IS REQUIRED AS A RESULT OF A TRIMMING ISSUE IN AVALONIA -->
<PackageReference Include="System.Drawing.Common" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
<ProjectReference Include="..\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj" />
<ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\distribution\windows\alsoft.ini" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>alsoft.ini</TargetPath>
</Content>
<Content Include="..\..\distribution\legal\THIRDPARTY.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>THIRDPARTY.md</TargetPath>
</Content>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>LICENSE.txt</TargetPath>
</Content>
</ItemGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64'">
<Content Include="..\..\distribution\linux\Ryujinx.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>mime\Ryujinx.xml</TargetPath>
</Content>
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="UI\**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\Fonts\SegoeFluentIcons.ttf" />
<AvaloniaResource Include="Assets\Styles\Themes.xaml">
<Generator>MSBuild:Compile</Generator>
</AvaloniaResource>
<AvaloniaResource Include="Assets\Styles\Styles.xaml" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Locales\el_GR.json" />
<None Remove="Assets\Locales\en_US.json" />
<None Remove="Assets\Locales\es_ES.json" />
<None Remove="Assets\Locales\fr_FR.json" />
<None Remove="Assets\Locales\he_IL.json" />
<None Remove="Assets\Locales\de_DE.json" />
<None Remove="Assets\Locales\it_IT.json" />
<None Remove="Assets\Locales\ja_JP.json" />
<None Remove="Assets\Locales\ko_KR.json" />
<None Remove="Assets\Locales\pl_PL.json" />
<None Remove="Assets\Locales\pt_BR.json" />
<None Remove="Assets\Locales\ru_RU.json" />
<None Remove="Assets\Locales\tr_TR.json" />
<None Remove="Assets\Locales\uk_UA.json" />
<None Remove="Assets\Locales\zh_CN.json" />
<None Remove="Assets\Locales\zh_TW.json" />
<None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
<None Remove="Assets\Icons\Controller_JoyConPair.svg" />
<None Remove="Assets\Icons\Controller_JoyConRight.svg" />
<None Remove="Assets\Icons\Controller_ProCon.svg" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Locales\el_GR.json" />
<EmbeddedResource Include="Assets\Locales\en_US.json" />
<EmbeddedResource Include="Assets\Locales\es_ES.json" />
<EmbeddedResource Include="Assets\Locales\fr_FR.json" />
<EmbeddedResource Include="Assets\Locales\he_IL.json" />
<EmbeddedResource Include="Assets\Locales\de_DE.json" />
<EmbeddedResource Include="Assets\Locales\it_IT.json" />
<EmbeddedResource Include="Assets\Locales\ja_JP.json" />
<EmbeddedResource Include="Assets\Locales\ko_KR.json" />
<EmbeddedResource Include="Assets\Locales\pl_PL.json" />
<EmbeddedResource Include="Assets\Locales\pt_BR.json" />
<EmbeddedResource Include="Assets\Locales\ru_RU.json" />
<EmbeddedResource Include="Assets\Locales\tr_TR.json" />
<EmbeddedResource Include="Assets\Locales\uk_UA.json" />
<EmbeddedResource Include="Assets\Locales\zh_CN.json" />
<EmbeddedResource Include="Assets\Locales\zh_TW.json" />
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\Locales\en_US.json" />
</ItemGroup>
</Project>

View File

@ -1,46 +0,0 @@
using Avalonia.Data.Converters;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Helpers
{
internal class KeyValueConverter : IValueConverter
{
public static KeyValueConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return null;
}
return value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
object key = null;
if (value != null)
{
if (targetType == typeof(Key))
{
key = Enum.Parse<Key>(value.ToString());
}
else if (targetType == typeof(GamepadInputId))
{
key = Enum.Parse<GamepadInputId>(value.ToString());
}
else if (targetType == typeof(StickInputId))
{
key = Enum.Parse<StickInputId>(value.ToString());
}
}
return key;
}
}
}

View File

@ -1,456 +0,0 @@
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System;
namespace Ryujinx.Ava.UI.Models
{
internal class InputConfiguration<TKey, TStick> : BaseModel
{
private float _deadzoneRight;
private float _triggerThreshold;
private float _deadzoneLeft;
private double _gyroDeadzone;
private int _sensitivity;
private bool _enableMotion;
private float _weakRumble;
private float _strongRumble;
private float _rangeLeft;
private float _rangeRight;
public InputBackendType Backend { get; set; }
/// <summary>
/// Controller id
/// </summary>
public string Id { get; set; }
/// <summary>
/// Controller's Type
/// </summary>
public ControllerType ControllerType { get; set; }
/// <summary>
/// Player's Index for the controller
/// </summary>
public PlayerIndex PlayerIndex { get; set; }
public TStick LeftJoystick { get; set; }
public bool LeftInvertStickX { get; set; }
public bool LeftInvertStickY { get; set; }
public bool RightRotate90 { get; set; }
public TKey LeftControllerStickButton { get; set; }
public TStick RightJoystick { get; set; }
public bool RightInvertStickX { get; set; }
public bool RightInvertStickY { get; set; }
public bool LeftRotate90 { get; set; }
public TKey RightControllerStickButton { get; set; }
public float DeadzoneLeft
{
get => _deadzoneLeft;
set
{
_deadzoneLeft = MathF.Round(value, 3);
OnPropertyChanged();
}
}
public float RangeLeft
{
get => _rangeLeft;
set
{
_rangeLeft = MathF.Round(value, 3);
OnPropertyChanged();
}
}
public float DeadzoneRight
{
get => _deadzoneRight;
set
{
_deadzoneRight = MathF.Round(value, 3);
OnPropertyChanged();
}
}
public float RangeRight
{
get => _rangeRight;
set
{
_rangeRight = MathF.Round(value, 3);
OnPropertyChanged();
}
}
public float TriggerThreshold
{
get => _triggerThreshold;
set
{
_triggerThreshold = MathF.Round(value, 3);
OnPropertyChanged();
}
}
public MotionInputBackendType MotionBackend { get; set; }
public TKey ButtonMinus { get; set; }
public TKey ButtonL { get; set; }
public TKey ButtonZl { get; set; }
public TKey LeftButtonSl { get; set; }
public TKey LeftButtonSr { get; set; }
public TKey DpadUp { get; set; }
public TKey DpadDown { get; set; }
public TKey DpadLeft { get; set; }
public TKey DpadRight { get; set; }
public TKey ButtonPlus { get; set; }
public TKey ButtonR { get; set; }
public TKey ButtonZr { get; set; }
public TKey RightButtonSl { get; set; }
public TKey RightButtonSr { get; set; }
public TKey ButtonX { get; set; }
public TKey ButtonB { get; set; }
public TKey ButtonY { get; set; }
public TKey ButtonA { get; set; }
public TKey LeftStickUp { get; set; }
public TKey LeftStickDown { get; set; }
public TKey LeftStickLeft { get; set; }
public TKey LeftStickRight { get; set; }
public TKey LeftKeyboardStickButton { get; set; }
public TKey RightStickUp { get; set; }
public TKey RightStickDown { get; set; }
public TKey RightStickLeft { get; set; }
public TKey RightStickRight { get; set; }
public TKey RightKeyboardStickButton { get; set; }
public int Sensitivity
{
get => _sensitivity;
set
{
_sensitivity = value;
OnPropertyChanged();
}
}
public double GyroDeadzone
{
get => _gyroDeadzone;
set
{
_gyroDeadzone = Math.Round(value, 3);
OnPropertyChanged();
}
}
public bool EnableMotion
{
get => _enableMotion; set
{
_enableMotion = value;
OnPropertyChanged();
}
}
public bool EnableCemuHookMotion { get; set; }
public int Slot { get; set; }
public int AltSlot { get; set; }
public bool MirrorInput { get; set; }
public string DsuServerHost { get; set; }
public int DsuServerPort { get; set; }
public bool EnableRumble { get; set; }
public float WeakRumble
{
get => _weakRumble; set
{
_weakRumble = value;
OnPropertyChanged();
}
}
public float StrongRumble
{
get => _strongRumble; set
{
_strongRumble = value;
OnPropertyChanged();
}
}
public InputConfiguration(InputConfig config)
{
if (config != null)
{
Backend = config.Backend;
Id = config.Id;
ControllerType = config.ControllerType;
PlayerIndex = config.PlayerIndex;
if (config is StandardKeyboardInputConfig keyboardConfig)
{
LeftStickUp = (TKey)(object)keyboardConfig.LeftJoyconStick.StickUp;
LeftStickDown = (TKey)(object)keyboardConfig.LeftJoyconStick.StickDown;
LeftStickLeft = (TKey)(object)keyboardConfig.LeftJoyconStick.StickLeft;
LeftStickRight = (TKey)(object)keyboardConfig.LeftJoyconStick.StickRight;
LeftKeyboardStickButton = (TKey)(object)keyboardConfig.LeftJoyconStick.StickButton;
RightStickUp = (TKey)(object)keyboardConfig.RightJoyconStick.StickUp;
RightStickDown = (TKey)(object)keyboardConfig.RightJoyconStick.StickDown;
RightStickLeft = (TKey)(object)keyboardConfig.RightJoyconStick.StickLeft;
RightStickRight = (TKey)(object)keyboardConfig.RightJoyconStick.StickRight;
RightKeyboardStickButton = (TKey)(object)keyboardConfig.RightJoyconStick.StickButton;
ButtonA = (TKey)(object)keyboardConfig.RightJoycon.ButtonA;
ButtonB = (TKey)(object)keyboardConfig.RightJoycon.ButtonB;
ButtonX = (TKey)(object)keyboardConfig.RightJoycon.ButtonX;
ButtonY = (TKey)(object)keyboardConfig.RightJoycon.ButtonY;
ButtonR = (TKey)(object)keyboardConfig.RightJoycon.ButtonR;
RightButtonSl = (TKey)(object)keyboardConfig.RightJoycon.ButtonSl;
RightButtonSr = (TKey)(object)keyboardConfig.RightJoycon.ButtonSr;
ButtonZr = (TKey)(object)keyboardConfig.RightJoycon.ButtonZr;
ButtonPlus = (TKey)(object)keyboardConfig.RightJoycon.ButtonPlus;
DpadUp = (TKey)(object)keyboardConfig.LeftJoycon.DpadUp;
DpadDown = (TKey)(object)keyboardConfig.LeftJoycon.DpadDown;
DpadLeft = (TKey)(object)keyboardConfig.LeftJoycon.DpadLeft;
DpadRight = (TKey)(object)keyboardConfig.LeftJoycon.DpadRight;
ButtonMinus = (TKey)(object)keyboardConfig.LeftJoycon.ButtonMinus;
LeftButtonSl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSl;
LeftButtonSr = (TKey)(object)keyboardConfig.LeftJoycon.ButtonSr;
ButtonZl = (TKey)(object)keyboardConfig.LeftJoycon.ButtonZl;
ButtonL = (TKey)(object)keyboardConfig.LeftJoycon.ButtonL;
}
else if (config is StandardControllerInputConfig controllerConfig)
{
LeftJoystick = (TStick)(object)controllerConfig.LeftJoyconStick.Joystick;
LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX;
LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY;
LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW;
LeftControllerStickButton = (TKey)(object)controllerConfig.LeftJoyconStick.StickButton;
RightJoystick = (TStick)(object)controllerConfig.RightJoyconStick.Joystick;
RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX;
RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY;
RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW;
RightControllerStickButton = (TKey)(object)controllerConfig.RightJoyconStick.StickButton;
ButtonA = (TKey)(object)controllerConfig.RightJoycon.ButtonA;
ButtonB = (TKey)(object)controllerConfig.RightJoycon.ButtonB;
ButtonX = (TKey)(object)controllerConfig.RightJoycon.ButtonX;
ButtonY = (TKey)(object)controllerConfig.RightJoycon.ButtonY;
ButtonR = (TKey)(object)controllerConfig.RightJoycon.ButtonR;
RightButtonSl = (TKey)(object)controllerConfig.RightJoycon.ButtonSl;
RightButtonSr = (TKey)(object)controllerConfig.RightJoycon.ButtonSr;
ButtonZr = (TKey)(object)controllerConfig.RightJoycon.ButtonZr;
ButtonPlus = (TKey)(object)controllerConfig.RightJoycon.ButtonPlus;
DpadUp = (TKey)(object)controllerConfig.LeftJoycon.DpadUp;
DpadDown = (TKey)(object)controllerConfig.LeftJoycon.DpadDown;
DpadLeft = (TKey)(object)controllerConfig.LeftJoycon.DpadLeft;
DpadRight = (TKey)(object)controllerConfig.LeftJoycon.DpadRight;
ButtonMinus = (TKey)(object)controllerConfig.LeftJoycon.ButtonMinus;
LeftButtonSl = (TKey)(object)controllerConfig.LeftJoycon.ButtonSl;
LeftButtonSr = (TKey)(object)controllerConfig.LeftJoycon.ButtonSr;
ButtonZl = (TKey)(object)controllerConfig.LeftJoycon.ButtonZl;
ButtonL = (TKey)(object)controllerConfig.LeftJoycon.ButtonL;
DeadzoneLeft = controllerConfig.DeadzoneLeft;
DeadzoneRight = controllerConfig.DeadzoneRight;
RangeLeft = controllerConfig.RangeLeft;
RangeRight = controllerConfig.RangeRight;
TriggerThreshold = controllerConfig.TriggerThreshold;
if (controllerConfig.Motion != null)
{
EnableMotion = controllerConfig.Motion.EnableMotion;
MotionBackend = controllerConfig.Motion.MotionBackend;
GyroDeadzone = controllerConfig.Motion.GyroDeadzone;
Sensitivity = controllerConfig.Motion.Sensitivity;
if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook)
{
EnableCemuHookMotion = true;
DsuServerHost = cemuHook.DsuServerHost;
DsuServerPort = cemuHook.DsuServerPort;
Slot = cemuHook.Slot;
AltSlot = cemuHook.AltSlot;
MirrorInput = cemuHook.MirrorInput;
}
if (controllerConfig.Rumble != null)
{
EnableRumble = controllerConfig.Rumble.EnableRumble;
WeakRumble = controllerConfig.Rumble.WeakRumble;
StrongRumble = controllerConfig.Rumble.StrongRumble;
}
}
}
}
}
public InputConfiguration()
{
}
public InputConfig GetConfig()
{
if (Backend == InputBackendType.WindowKeyboard)
{
return new StandardKeyboardInputConfig
{
Id = Id,
Backend = Backend,
PlayerIndex = PlayerIndex,
ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
DpadUp = (Key)(object)DpadUp,
DpadDown = (Key)(object)DpadDown,
DpadLeft = (Key)(object)DpadLeft,
DpadRight = (Key)(object)DpadRight,
ButtonL = (Key)(object)ButtonL,
ButtonZl = (Key)(object)ButtonZl,
ButtonSl = (Key)(object)LeftButtonSl,
ButtonSr = (Key)(object)LeftButtonSr,
ButtonMinus = (Key)(object)ButtonMinus,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
ButtonA = (Key)(object)ButtonA,
ButtonB = (Key)(object)ButtonB,
ButtonX = (Key)(object)ButtonX,
ButtonY = (Key)(object)ButtonY,
ButtonPlus = (Key)(object)ButtonPlus,
ButtonSl = (Key)(object)RightButtonSl,
ButtonSr = (Key)(object)RightButtonSr,
ButtonR = (Key)(object)ButtonR,
ButtonZr = (Key)(object)ButtonZr,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = (Key)(object)LeftStickUp,
StickDown = (Key)(object)LeftStickDown,
StickRight = (Key)(object)LeftStickRight,
StickLeft = (Key)(object)LeftStickLeft,
StickButton = (Key)(object)LeftKeyboardStickButton,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = (Key)(object)RightStickUp,
StickDown = (Key)(object)RightStickDown,
StickLeft = (Key)(object)RightStickLeft,
StickRight = (Key)(object)RightStickRight,
StickButton = (Key)(object)RightKeyboardStickButton,
},
Version = InputConfig.CurrentVersion,
};
}
if (Backend == InputBackendType.GamepadSDL2)
{
var config = new StandardControllerInputConfig
{
Id = Id,
Backend = Backend,
PlayerIndex = PlayerIndex,
ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<GamepadInputId>
{
DpadUp = (GamepadInputId)(object)DpadUp,
DpadDown = (GamepadInputId)(object)DpadDown,
DpadLeft = (GamepadInputId)(object)DpadLeft,
DpadRight = (GamepadInputId)(object)DpadRight,
ButtonL = (GamepadInputId)(object)ButtonL,
ButtonZl = (GamepadInputId)(object)ButtonZl,
ButtonSl = (GamepadInputId)(object)LeftButtonSl,
ButtonSr = (GamepadInputId)(object)LeftButtonSr,
ButtonMinus = (GamepadInputId)(object)ButtonMinus,
},
RightJoycon = new RightJoyconCommonConfig<GamepadInputId>
{
ButtonA = (GamepadInputId)(object)ButtonA,
ButtonB = (GamepadInputId)(object)ButtonB,
ButtonX = (GamepadInputId)(object)ButtonX,
ButtonY = (GamepadInputId)(object)ButtonY,
ButtonPlus = (GamepadInputId)(object)ButtonPlus,
ButtonSl = (GamepadInputId)(object)RightButtonSl,
ButtonSr = (GamepadInputId)(object)RightButtonSr,
ButtonR = (GamepadInputId)(object)ButtonR,
ButtonZr = (GamepadInputId)(object)ButtonZr,
},
LeftJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>
{
Joystick = (StickInputId)(object)LeftJoystick,
InvertStickX = LeftInvertStickX,
InvertStickY = LeftInvertStickY,
Rotate90CW = LeftRotate90,
StickButton = (GamepadInputId)(object)LeftControllerStickButton,
},
RightJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>
{
Joystick = (StickInputId)(object)RightJoystick,
InvertStickX = RightInvertStickX,
InvertStickY = RightInvertStickY,
Rotate90CW = RightRotate90,
StickButton = (GamepadInputId)(object)RightControllerStickButton,
},
Rumble = new RumbleConfigController
{
EnableRumble = EnableRumble,
WeakRumble = WeakRumble,
StrongRumble = StrongRumble,
},
Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft,
DeadzoneRight = DeadzoneRight,
RangeLeft = RangeLeft,
RangeRight = RangeRight,
TriggerThreshold = TriggerThreshold,
Motion = EnableCemuHookMotion
? new CemuHookMotionConfigController
{
DsuServerHost = DsuServerHost,
DsuServerPort = DsuServerPort,
Slot = Slot,
AltSlot = AltSlot,
MirrorInput = MirrorInput,
MotionBackend = MotionInputBackendType.CemuHook,
}
: new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
},
};
config.Motion.Sensitivity = Sensitivity;
config.Motion.EnableMotion = EnableMotion;
config.Motion.GyroDeadzone = GyroDeadzone;
return config;
}
return null;
}
}
}

View File

@ -1,181 +0,0 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
namespace Ryujinx.Ava.UI.Views.Input
{
public partial class ControllerInputView : UserControl
{
private bool _dialogOpen;
private ButtonKeyAssigner _currentAssigner;
internal ControllerInputViewModel ViewModel { get; set; }
public ControllerInputView()
{
DataContext = ViewModel = new ControllerInputViewModel(this);
InitializeComponent();
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{
if (visual is ToggleButton button && visual is not CheckBox)
{
button.IsCheckedChanged += Button_IsCheckedChanged;
}
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver)
{
_currentAssigner.Cancel();
}
}
private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
{
if ((bool)button.IsChecked)
{
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
bool isStick = button.Tag != null && button.Tag.ToString() == "stick";
if (_currentAssigner == null)
{
_currentAssigner = new ButtonKeyAssigner(button);
this.Focus(NavigationMethod.Pointer);
PointerPressed += MouseClick;
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
IButtonAssigner assigner = CreateButtonAssigner(isStick);
_currentAssigner.ButtonAssigned += (sender, e) =>
{
if (e.IsAssigned)
{
ViewModel.IsModified = true;
}
};
_currentAssigner.GetInputAndAssign(assigner, keyboard);
}
else
{
if (_currentAssigner != null)
{
ToggleButton oldButton = _currentAssigner.ToggledButton;
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
else
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
}
}
public void SaveCurrentProfile()
{
ViewModel.Save();
}
private IButtonAssigner CreateButtonAssigner(bool forStick)
{
IButtonAssigner assigner;
var device = ViewModel.Devices[ViewModel.Device];
if (device.Type == DeviceType.Keyboard)
{
assigner = new KeyboardKeyAssigner((IKeyboard)ViewModel.SelectedGamepad);
}
else if (device.Type == DeviceType.Controller)
{
assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick);
}
else
{
throw new Exception("Controller not supported");
}
return assigner;
}
private void MouseClick(object sender, PointerPressedEventArgs e)
{
bool shouldUnbind = false;
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
{
shouldUnbind = true;
}
_currentAssigner?.Cancel(shouldUnbind);
PointerPressed -= MouseClick;
}
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ViewModel.IsModified && !_dialogOpen)
{
_dialogOpen = true;
var result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage],
LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage],
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
ViewModel.Save();
}
_dialogOpen = false;
ViewModel.IsModified = false;
if (e.AddedItems.Count > 0)
{
var player = (PlayerModel)e.AddedItems[0];
ViewModel.PlayerId = player.Id;
}
}
}
public void Dispose()
{
_currentAssigner?.Cancel();
_currentAssigner = null;
ViewModel.Dispose();
}
}
}

View File

@ -1,103 +0,0 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel"
Focusable="True">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:KeyValueConverter x:Key="Key" />
</UserControl.Resources>
<ScrollViewer
Name="HotkeysPage"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="10" Orientation="Vertical" Spacing="10">
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ShowUI, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
<TextBlock
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
TextAlignment="Center" />
</ToggleButton>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -1,81 +0,0 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsHotkeysView : UserControl
{
private ButtonKeyAssigner _currentAssigner;
private readonly IGamepadDriver _avaloniaKeyboardDriver;
public SettingsHotkeysView()
{
InitializeComponent();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
private void MouseClick(object sender, PointerPressedEventArgs e)
{
bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
_currentAssigner?.Cancel(shouldUnbind);
PointerPressed -= MouseClick;
}
private void Button_Checked(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
{
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked)
{
_currentAssigner = new ButtonKeyAssigner(button);
this.Focus(NavigationMethod.Pointer);
PointerPressed += MouseClick;
var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad(_avaloniaKeyboardDriver.GamepadsIds[0]);
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
_currentAssigner.GetInputAndAssign(assigner);
}
else
{
if (_currentAssigner != null)
{
ToggleButton oldButton = _currentAssigner.ToggledButton;
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
}
private void Button_Unchecked(object sender, RoutedEventArgs e)
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
public void Dispose()
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
}
}

View File

@ -5,10 +5,10 @@ namespace Ryujinx.Common.Collections
/// </summary>
public class IntrusiveRedBlackTreeNode<T> where T : IntrusiveRedBlackTreeNode<T>
{
internal bool Color = true;
internal T Left;
internal T Right;
internal T Parent;
public bool Color = true;
public T Left;
public T Right;
public T Parent;
public T Predecessor => IntrusiveRedBlackTreeImpl<T>.PredecessorOf((T)this);
public T Successor => IntrusiveRedBlackTreeImpl<T>.SuccessorOf((T)this);

View File

@ -1,7 +1,5 @@
namespace Ryujinx.Common.Configuration.Hid
{
// NOTE: Please don't change this to struct.
// This breaks Avalonia's TwoWay binding, which makes us unable to save new KeyboardHotkeys.
public class KeyboardHotkeys
{
public Key ToggleVsync { get; set; }

View File

@ -0,0 +1,181 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.Extensions
{
public static class SequenceReaderExtensions
{
/// <summary>
/// Dumps the entire <see cref="SequenceReader{byte}"/> to a file, restoring its previous location afterward.
/// Useful for debugging purposes.
/// </summary>
/// <param name="reader">The <see cref="SequenceReader{Byte}"/> to write to a file</param>
/// <param name="fileFullName">The path and name of the file to create and dump to</param>
public static void DumpToFile(this ref SequenceReader<byte> reader, string fileFullName)
{
var initialConsumed = reader.Consumed;
reader.Rewind(initialConsumed);
using (var fileStream = System.IO.File.Create(fileFullName, 4096, System.IO.FileOptions.None))
{
while (reader.End == false)
{
var span = reader.CurrentSpan;
fileStream.Write(span);
reader.Advance(span.Length);
}
}
reader.SetConsumed(initialConsumed);
}
/// <summary>
/// Returns a reference to the desired value. This ref should always be used. The argument passed in <paramref name="copyDestinationIfRequiredDoNotUse"/> should never be used, as this is only used for storage if the value
/// must be copied from multiple <see cref="ReadOnlyMemory{Byte}"/> segments held by the <see cref="SequenceReader{Byte}"/>.
/// </summary>
/// <typeparam name="T">Type to get</typeparam>
/// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
/// <param name="copyDestinationIfRequiredDoNotUse">A location used as storage if (and only if) the value to be read spans multiple <see cref="ReadOnlyMemory{Byte}"/> segments</param>
/// <returns>A reference to the desired value, either directly to memory in the <see cref="SequenceReader{Byte}"/>, or to <paramref name="copyDestinationIfRequiredDoNotUse"/> if it has been used for copying the value in to</returns>
/// <remarks>
/// DO NOT use <paramref name="copyDestinationIfRequiredDoNotUse"/> after calling this method, as it will only
/// contain a value if the value couldn't be referenced directly because it spans multiple <see cref="ReadOnlyMemory{Byte}"/> segments.
/// To discourage use, it is recommended to call this method like the following:
/// <c>
/// ref readonly MyStruct value = ref sequenceReader.GetRefOrRefToCopy{MyStruct}(out _);
/// </c>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">The <see cref="SequenceReader{Byte}"/> does not contain enough data to read a value of type <typeparamref name="T"/></exception>
public static ref readonly T GetRefOrRefToCopy<T>(this scoped ref SequenceReader<byte> reader, out T copyDestinationIfRequiredDoNotUse) where T : unmanaged
{
int lengthRequired = Unsafe.SizeOf<T>();
ReadOnlySpan<byte> span = reader.UnreadSpan;
if (lengthRequired <= span.Length)
{
reader.Advance(lengthRequired);
copyDestinationIfRequiredDoNotUse = default;
ReadOnlySpan<T> spanOfT = MemoryMarshal.Cast<byte, T>(span);
return ref spanOfT[0];
}
else
{
copyDestinationIfRequiredDoNotUse = default;
Span<T> valueSpan = MemoryMarshal.CreateSpan(ref copyDestinationIfRequiredDoNotUse, 1);
Span<byte> valueBytesSpan = MemoryMarshal.AsBytes(valueSpan);
if (!reader.TryCopyTo(valueBytesSpan))
{
throw new ArgumentOutOfRangeException(nameof(reader), "The sequence is not long enough to read the desired value.");
}
reader.Advance(lengthRequired);
return ref valueSpan[0];
}
}
/// <summary>
/// Reads an <see cref="int"/> as little endian.
/// </summary>
/// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
/// <param name="value">A location to receive the read value</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if there wasn't enough data for an <see cref="int"/></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadLittleEndian(this ref SequenceReader<byte> reader, out int value)
{
if (!reader.TryReadLittleEndian(out value))
{
throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
}
}
/// <summary>
/// Reads the desired unmanaged value by copying it to the specified <paramref name="value"/>.
/// </summary>
/// <typeparam name="T">Type to read</typeparam>
/// <param name="reader">The <see cref="SequenceReader{Byte}"/> to read from</param>
/// <param name="value">The target that will receive the read value</param>
/// <exception cref="ArgumentOutOfRangeException">The <see cref="SequenceReader{Byte}"/> does not contain enough data to read a value of type <typeparamref name="T"/></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReadUnmanaged<T>(this ref SequenceReader<byte> reader, out T value) where T : unmanaged
{
if (!reader.TryReadUnmanaged(out value))
{
throw new ArgumentOutOfRangeException(nameof(value), "The sequence is not long enough to read the desired value.");
}
}
/// <summary>
/// Sets the reader's position as bytes consumed.
/// </summary>
/// <param name="reader">The <see cref="SequenceReader{Byte}"/> to set the position</param>
/// <param name="consumed">The number of bytes consumed</param>
public static void SetConsumed(ref this SequenceReader<byte> reader, long consumed)
{
reader.Rewind(reader.Consumed);
reader.Advance(consumed);
}
/// <summary>
/// Try to read the given type out of the buffer if possible. Warning: this is dangerous to use with arbitrary
/// structs - see remarks for full details.
/// </summary>
/// <typeparam name="T">Type to read</typeparam>
/// <remarks>
/// IMPORTANT: The read is a straight copy of bits. If a struct depends on specific state of it's members to
/// behave correctly this can lead to exceptions, etc. If reading endian specific integers, use the explicit
/// overloads such as <see cref="SequenceReader{T}.TryReadLittleEndian"/>
/// </remarks>
/// <returns>
/// True if successful. <paramref name="value"/> will be default if failed (due to lack of space).
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool TryReadUnmanaged<T>(ref this SequenceReader<byte> reader, out T value) where T : unmanaged
{
ReadOnlySpan<byte> span = reader.UnreadSpan;
if (span.Length < sizeof(T))
{
return TryReadUnmanagedMultiSegment(ref reader, out value);
}
value = Unsafe.ReadUnaligned<T>(ref MemoryMarshal.GetReference(span));
reader.Advance(sizeof(T));
return true;
}
private static unsafe bool TryReadUnmanagedMultiSegment<T>(ref SequenceReader<byte> reader, out T value) where T : unmanaged
{
Debug.Assert(reader.UnreadSpan.Length < sizeof(T));
// Not enough data in the current segment, try to peek for the data we need.
T buffer = default;
Span<byte> tempSpan = new Span<byte>(&buffer, sizeof(T));
if (!reader.TryCopyTo(tempSpan))
{
value = default;
return false;
}
value = Unsafe.ReadUnaligned<T>(ref MemoryMarshal.GetReference(tempSpan));
reader.Advance(sizeof(T));
return true;
}
}
}

View File

@ -4,7 +4,7 @@ using System.Threading;
namespace Ryujinx.Common.Memory
{
public sealed partial class ByteMemoryPool
public partial class ByteMemoryPool
{
/// <summary>
/// Represents a <see cref="IMemoryOwner{Byte}"/> that wraps an array rented from

View File

@ -6,24 +6,8 @@ namespace Ryujinx.Common.Memory
/// <summary>
/// Provides a pool of re-usable byte array instances.
/// </summary>
public sealed partial class ByteMemoryPool
public static partial class ByteMemoryPool
{
private static readonly ByteMemoryPool _shared = new();
/// <summary>
/// Constructs a <see cref="ByteMemoryPool"/> instance. Private to force access through
/// the <see cref="ByteMemoryPool.Shared"/> instance.
/// </summary>
private ByteMemoryPool()
{
// No implementation
}
/// <summary>
/// Retrieves a shared <see cref="ByteMemoryPool"/> instance.
/// </summary>
public static ByteMemoryPool Shared => _shared;
/// <summary>
/// Returns the maximum buffer size supported by this pool.
/// </summary>
@ -95,6 +79,20 @@ namespace Ryujinx.Common.Memory
return buffer;
}
/// <summary>
/// Copies <paramref name="buffer"/> into a newly rented byte memory buffer.
/// </summary>
/// <param name="buffer">The byte buffer to copy</param>
/// <returns>A <see cref="IMemoryOwner{Byte}"/> wrapping the rented memory with <paramref name="buffer"/> copied to it</returns>
public static IMemoryOwner<byte> RentCopy(ReadOnlySpan<byte> buffer)
{
var copy = RentImpl(buffer.Length);
buffer.CopyTo(copy.Memory.Span);
return copy;
}
private static ByteMemoryPoolBuffer RentImpl(int length)
{
if ((uint)length > Array.MaxLength)

View File

@ -1,89 +0,0 @@
using System;
namespace Ryujinx.Common.Memory
{
/// <summary>
/// A struct that can represent both a Span and Array.
/// This is useful to keep the Array representation when possible to avoid copies.
/// </summary>
/// <typeparam name="T">Element Type</typeparam>
public readonly ref struct SpanOrArray<T> where T : unmanaged
{
public readonly T[] Array;
public readonly ReadOnlySpan<T> Span;
/// <summary>
/// Create a new SpanOrArray from an array.
/// </summary>
/// <param name="array">Array to store</param>
public SpanOrArray(T[] array)
{
Array = array;
Span = ReadOnlySpan<T>.Empty;
}
/// <summary>
/// Create a new SpanOrArray from a readonly span.
/// </summary>
/// <param name="array">Span to store</param>
public SpanOrArray(ReadOnlySpan<T> span)
{
Array = null;
Span = span;
}
/// <summary>
/// Return the contained array, or convert the span if necessary.
/// </summary>
/// <returns>An array containing the data</returns>
public T[] ToArray()
{
return Array ?? Span.ToArray();
}
/// <summary>
/// Return a ReadOnlySpan from either the array or ReadOnlySpan.
/// </summary>
/// <returns>A ReadOnlySpan containing the data</returns>
public ReadOnlySpan<T> AsSpan()
{
return Array ?? Span;
}
/// <summary>
/// Cast an array to a SpanOrArray.
/// </summary>
/// <param name="array">Source array</param>
public static implicit operator SpanOrArray<T>(T[] array)
{
return new SpanOrArray<T>(array);
}
/// <summary>
/// Cast a ReadOnlySpan to a SpanOrArray.
/// </summary>
/// <param name="span">Source ReadOnlySpan</param>
public static implicit operator SpanOrArray<T>(ReadOnlySpan<T> span)
{
return new SpanOrArray<T>(span);
}
/// <summary>
/// Cast a Span to a SpanOrArray.
/// </summary>
/// <param name="span">Source Span</param>
public static implicit operator SpanOrArray<T>(Span<T> span)
{
return new SpanOrArray<T>(span);
}
/// <summary>
/// Cast a SpanOrArray to a ReadOnlySpan
/// </summary>
/// <param name="spanOrArray">Source SpanOrArray</param>
public static implicit operator ReadOnlySpan<T>(SpanOrArray<T> spanOrArray)
{
return spanOrArray.AsSpan();
}
}
}

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Utilities;
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using System.Reflection;
@ -41,6 +42,22 @@ namespace Ryujinx.Common
return StreamUtils.StreamToBytes(stream);
}
public static IMemoryOwner<byte> ReadFileToRentedMemory(string filename)
{
var (assembly, path) = ResolveManifestPath(filename);
return ReadFileToRentedMemory(assembly, path);
}
public static IMemoryOwner<byte> ReadFileToRentedMemory(Assembly assembly, string filename)
{
using var stream = GetStream(assembly, filename);
return stream is null
? null
: StreamUtils.StreamToRentedMemory(stream);
}
public async static Task<byte[]> ReadAsync(Assembly assembly, string filename)
{
using var stream = GetStream(assembly, filename);

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Ryujinx.Common.Utilities
{
@ -44,5 +46,11 @@ namespace Ryujinx.Common.Utilities
CopyDirectory(sourceDir, destinationDir, true);
Directory.Delete(sourceDir, true);
}
public static string SanitizeFileName(string fileName)
{
var reservedChars = new HashSet<char>(Path.GetInvalidFileNameChars());
return string.Concat(fileName.Select(c => reservedChars.Contains(c) ? '_' : c));
}
}
}

View File

@ -1,4 +1,6 @@
using Microsoft.IO;
using Ryujinx.Common.Memory;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -9,12 +11,50 @@ namespace Ryujinx.Common.Utilities
{
public static byte[] StreamToBytes(Stream input)
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input);
return output.ToArray();
}
input.CopyTo(stream);
public static IMemoryOwner<byte> StreamToRentedMemory(Stream input)
{
if (input is MemoryStream inputMemoryStream)
{
return MemoryStreamToRentedMemory(inputMemoryStream);
}
else if (input.CanSeek)
{
long bytesExpected = input.Length;
return stream.ToArray();
IMemoryOwner<byte> ownedMemory = ByteMemoryPool.Rent(bytesExpected);
var destSpan = ownedMemory.Memory.Span;
int totalBytesRead = 0;
while (totalBytesRead < bytesExpected)
{
int bytesRead = input.Read(destSpan[totalBytesRead..]);
if (bytesRead == 0)
{
ownedMemory.Dispose();
throw new IOException($"Tried reading {bytesExpected} but the stream closed after reading {totalBytesRead}.");
}
totalBytesRead += bytesRead;
}
return ownedMemory;
}
else
{
// If input is (non-seekable) then copy twice: first into a RecyclableMemoryStream, then to a rented IMemoryOwner<byte>.
using RecyclableMemoryStream output = StreamToRecyclableMemoryStream(input);
return MemoryStreamToRentedMemory(output);
}
}
public static async Task<byte[]> StreamToBytesAsync(Stream input, CancellationToken cancellationToken = default)
@ -25,5 +65,26 @@ namespace Ryujinx.Common.Utilities
return stream.ToArray();
}
private static IMemoryOwner<byte> MemoryStreamToRentedMemory(MemoryStream input)
{
input.Position = 0;
IMemoryOwner<byte> ownedMemory = ByteMemoryPool.Rent(input.Length);
// Discard the return value because we assume reading a MemoryStream always succeeds completely.
_ = input.Read(ownedMemory.Memory.Span);
return ownedMemory;
}
private static RecyclableMemoryStream StreamToRecyclableMemoryStream(Stream input)
{
RecyclableMemoryStream stream = MemoryStreamManager.Shared.GetStream();
input.CopyTo(stream);
return stream;
}
}
}

View File

@ -1,5 +1,3 @@
using Ryujinx.Common;
using Ryujinx.Common.Collections;
using Ryujinx.Memory;
using System;
@ -7,175 +5,23 @@ namespace Ryujinx.Cpu
{
public class AddressSpace : IDisposable
{
private const int DefaultBlockAlignment = 1 << 20;
private enum MappingType : byte
{
None,
Private,
Shared,
}
private class Mapping : IntrusiveRedBlackTreeNode<Mapping>, IComparable<Mapping>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public MappingType Type { get; private set; }
public Mapping(ulong address, ulong size, MappingType type)
{
Address = address;
Size = size;
Type = type;
}
public Mapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
Mapping left = new(Address, leftSize, Type);
Address = splitAddress;
Size = rightSize;
return left;
}
public void UpdateState(MappingType newType)
{
Type = newType;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(Mapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private class PrivateMapping : IntrusiveRedBlackTreeNode<PrivateMapping>, IComparable<PrivateMapping>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public PrivateMemoryAllocation PrivateAllocation { get; private set; }
public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation)
{
Address = address;
Size = size;
PrivateAllocation = privateAllocation;
}
public PrivateMapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
(var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize);
PrivateMapping left = new(Address, leftSize, leftAllocation);
Address = splitAddress;
Size = rightSize;
return left;
}
public void Map(MemoryBlock baseBlock, MemoryBlock mirrorBlock, PrivateMemoryAllocation newAllocation)
{
baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
mirrorBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address, Size);
PrivateAllocation = newAllocation;
}
public void Unmap(MemoryBlock baseBlock, MemoryBlock mirrorBlock)
{
if (PrivateAllocation.IsValid)
{
baseBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
mirrorBlock.UnmapView(PrivateAllocation.Memory, Address, Size);
PrivateAllocation.Dispose();
}
PrivateAllocation = default;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(PrivateMapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private readonly MemoryBlock _backingMemory;
private readonly PrivateMemoryAllocator _privateMemoryAllocator;
private readonly IntrusiveRedBlackTree<Mapping> _mappingTree;
private readonly IntrusiveRedBlackTree<PrivateMapping> _privateTree;
private readonly object _treeLock;
private readonly bool _supports4KBPages;
public MemoryBlock Base { get; }
public MemoryBlock Mirror { get; }
public ulong AddressSpaceSize { get; }
public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize, bool supports4KBPages)
public AddressSpace(MemoryBlock backingMemory, MemoryBlock baseMemory, MemoryBlock mirrorMemory, ulong addressSpaceSize)
{
if (!supports4KBPages)
{
_privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable | MemoryAllocationFlags.NoMap);
_mappingTree = new IntrusiveRedBlackTree<Mapping>();
_privateTree = new IntrusiveRedBlackTree<PrivateMapping>();
_treeLock = new object();
_mappingTree.Add(new Mapping(0UL, addressSpaceSize, MappingType.None));
_privateTree.Add(new PrivateMapping(0UL, addressSpaceSize, default));
}
_backingMemory = backingMemory;
_supports4KBPages = supports4KBPages;
Base = baseMemory;
Mirror = mirrorMemory;
AddressSpaceSize = addressSpaceSize;
}
public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, bool supports4KBPages, out AddressSpace addressSpace)
public static bool TryCreate(MemoryBlock backingMemory, ulong asSize, out AddressSpace addressSpace)
{
addressSpace = null;
@ -193,7 +39,7 @@ namespace Ryujinx.Cpu
{
baseMemory = new MemoryBlock(addressSpaceSize, AsFlags);
mirrorMemory = new MemoryBlock(addressSpaceSize, AsFlags);
addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize, supports4KBPages);
addressSpace = new AddressSpace(backingMemory, baseMemory, mirrorMemory, addressSpaceSize);
break;
}
@ -209,289 +55,20 @@ namespace Ryujinx.Cpu
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
if (_supports4KBPages)
{
Base.MapView(_backingMemory, pa, va, size);
Mirror.MapView(_backingMemory, pa, va, size);
return;
}
lock (_treeLock)
{
ulong alignment = MemoryBlock.GetPageSize();
bool isAligned = ((va | pa | size) & (alignment - 1)) == 0;
if (flags.HasFlag(MemoryMapFlags.Private) && !isAligned)
{
Update(va, pa, size, MappingType.Private);
}
else
{
// The update method assumes that shared mappings are already aligned.
if (!flags.HasFlag(MemoryMapFlags.Private))
{
if ((va & (alignment - 1)) != (pa & (alignment - 1)))
{
throw new InvalidMemoryRegionException($"Virtual address 0x{va:X} and physical address 0x{pa:X} are misaligned and can't be aligned.");
}
ulong endAddress = va + size;
va = BitUtils.AlignDown(va, alignment);
pa = BitUtils.AlignDown(pa, alignment);
size = BitUtils.AlignUp(endAddress, alignment) - va;
}
Update(va, pa, size, MappingType.Shared);
}
}
Base.MapView(_backingMemory, pa, va, size);
Mirror.MapView(_backingMemory, pa, va, size);
}
public void Unmap(ulong va, ulong size)
{
if (_supports4KBPages)
{
Base.UnmapView(_backingMemory, va, size);
Mirror.UnmapView(_backingMemory, va, size);
return;
}
lock (_treeLock)
{
Update(va, 0UL, size, MappingType.None);
}
}
private void Update(ulong va, ulong pa, ulong size, MappingType type)
{
Mapping map = _mappingTree.GetNode(new Mapping(va, 1UL, MappingType.None));
Update(map, va, pa, size, type);
}
private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type)
{
ulong endAddress = va + size;
for (; map != null; map = map.Successor)
{
if (map.Address < va)
{
_mappingTree.Add(map.Split(va));
}
if (map.EndAddress > endAddress)
{
Mapping newMap = map.Split(endAddress);
_mappingTree.Add(newMap);
map = newMap;
}
switch (type)
{
case MappingType.None:
if (map.Type == MappingType.Shared)
{
ulong startOffset = map.Address - va;
ulong mapVa = va + startOffset;
ulong mapSize = Math.Min(size - startOffset, map.Size);
ulong mapEndAddress = mapVa + mapSize;
ulong alignment = MemoryBlock.GetPageSize();
mapVa = BitUtils.AlignDown(mapVa, alignment);
mapEndAddress = BitUtils.AlignUp(mapEndAddress, alignment);
mapSize = mapEndAddress - mapVa;
Base.UnmapView(_backingMemory, mapVa, mapSize);
Mirror.UnmapView(_backingMemory, mapVa, mapSize);
}
else
{
UnmapPrivate(va, size);
}
break;
case MappingType.Private:
if (map.Type == MappingType.Shared)
{
throw new InvalidMemoryRegionException($"Private mapping request at 0x{va:X} with size 0x{size:X} overlaps shared mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
}
else
{
MapPrivate(va, size);
}
break;
case MappingType.Shared:
if (map.Type != MappingType.None)
{
throw new InvalidMemoryRegionException($"Shared mapping request at 0x{va:X} with size 0x{size:X} overlaps mapping at 0x{map.Address:X} with size 0x{map.Size:X}.");
}
else
{
ulong startOffset = map.Address - va;
ulong mapPa = pa + startOffset;
ulong mapVa = va + startOffset;
ulong mapSize = Math.Min(size - startOffset, map.Size);
Base.MapView(_backingMemory, mapPa, mapVa, mapSize);
Mirror.MapView(_backingMemory, mapPa, mapVa, mapSize);
}
break;
}
map.UpdateState(type);
map = TryCoalesce(map);
if (map.EndAddress >= endAddress)
{
break;
}
}
return map;
}
private Mapping TryCoalesce(Mapping map)
{
Mapping previousMap = map.Predecessor;
Mapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_mappingTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_mappingTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(Mapping left, Mapping right)
{
return left.Type == right.Type;
}
private void MapPrivate(ulong va, ulong size)
{
ulong endAddress = va + size;
ulong alignment = MemoryBlock.GetPageSize();
// Expand the range outwards based on page size to ensure that at least the requested region is mapped.
ulong vaAligned = BitUtils.AlignDown(va, alignment);
ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment);
PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
for (; map != null; map = map.Successor)
{
if (!map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Map(Base, Mirror, _privateMemoryAllocator.Allocate(map.Size, MemoryBlock.GetPageSize()));
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private void UnmapPrivate(ulong va, ulong size)
{
ulong endAddress = va + size;
ulong alignment = MemoryBlock.GetPageSize();
// Shrink the range inwards based on page size to ensure we won't unmap memory that might be still in use.
ulong vaAligned = BitUtils.AlignUp(va, alignment);
ulong endAddressAligned = BitUtils.AlignDown(endAddress, alignment);
if (endAddressAligned <= vaAligned)
{
return;
}
PrivateMapping map = _privateTree.GetNode(new PrivateMapping(va, 1UL, default));
for (; map != null; map = map.Successor)
{
if (map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Unmap(Base, Mirror);
map = TryCoalesce(map);
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private PrivateMapping TryCoalesce(PrivateMapping map)
{
PrivateMapping previousMap = map.Predecessor;
PrivateMapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_privateTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_privateTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(PrivateMapping left, PrivateMapping right)
{
return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid;
Base.UnmapView(_backingMemory, va, size);
Mirror.UnmapView(_backingMemory, va, size);
}
public void Dispose()
{
GC.SuppressFinalize(this);
_privateMemoryAllocator?.Dispose();
Base.Dispose();
Mirror.Dispose();
}

View File

@ -38,7 +38,7 @@ namespace Ryujinx.Cpu.AppleHv
private readonly HvIpaAllocator _ipaAllocator;
public HvMemoryBlockAllocator(HvIpaAllocator ipaAllocator, int blockAlignment) : base(blockAlignment, MemoryAllocationFlags.None)
public HvMemoryBlockAllocator(HvIpaAllocator ipaAllocator, ulong blockAlignment) : base(blockAlignment, MemoryAllocationFlags.None)
{
_ipaAllocator = ipaAllocator;
}

View File

@ -3,12 +3,11 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
namespace Ryujinx.Cpu.AppleHv
{
@ -16,31 +15,10 @@ namespace Ryujinx.Cpu.AppleHv
/// Represents a CPU memory manager which maps guest virtual memory directly onto the Hypervisor page table.
/// </summary>
[SupportedOSPlatform("macos")]
public class HvMemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
public sealed class HvMemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
public const int PageBits = 12;
public const int PageSize = 1 << PageBits;
public const int PageMask = PageSize - 1;
public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
private enum HostMappedPtBits : ulong
{
Unmapped = 0,
Mapped,
WriteTracked,
ReadWriteTracked,
MappedReplicated = 0x5555555555555555,
WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
ReadWriteTrackedReplicated = ulong.MaxValue,
}
private readonly InvalidAccessHandler _invalidAccessHandler;
private readonly ulong _addressSpaceSize;
private readonly HvAddressSpace _addressSpace;
internal HvAddressSpace AddressSpace => _addressSpace;
@ -48,9 +26,9 @@ namespace Ryujinx.Cpu.AppleHv
private readonly MemoryBlock _backingMemory;
private readonly PageTable<ulong> _pageTable;
private readonly ulong[] _pageBitmap;
private readonly ManagedPageFlags _pages;
public bool Supports4KBPages => true;
public bool UsesPrivateAllocations => false;
public int AddressSpaceBits { get; }
@ -62,6 +40,8 @@ namespace Ryujinx.Cpu.AppleHv
public event Action<ulong, ulong> UnmapEvent;
protected override ulong AddressSpaceSize { get; }
/// <summary>
/// Creates a new instance of the Hypervisor memory manager.
/// </summary>
@ -73,7 +53,7 @@ namespace Ryujinx.Cpu.AppleHv
_backingMemory = backingMemory;
_pageTable = new PageTable<ulong>();
_invalidAccessHandler = invalidAccessHandler;
_addressSpaceSize = addressSpaceSize;
AddressSpaceSize = addressSpaceSize;
ulong asSize = PageSize;
int asBits = PageBits;
@ -88,46 +68,10 @@ namespace Ryujinx.Cpu.AppleHv
AddressSpaceBits = asBits;
_pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))];
_pages = new ManagedPageFlags(AddressSpaceBits);
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler);
}
/// <summary>
/// Checks if the virtual address is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address</param>
/// <returns>True if the virtual address is part of the addressable space</returns>
private bool ValidateAddress(ulong va)
{
return va < _addressSpaceSize;
}
/// <summary>
/// Checks if the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <returns>True if the combination of virtual address and size is part of the addressable space</returns>
private bool ValidateAddressAndSize(ulong va, ulong size)
{
ulong endVa = va + size;
return endVa >= va && endVa >= size && endVa <= _addressSpaceSize;
}
/// <summary>
/// Ensures the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified outside the addressable space</exception>
private void AssertValidAddressAndSize(ulong va, ulong size)
{
if (!ValidateAddressAndSize(va, size))
{
throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
}
}
/// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
@ -135,7 +79,7 @@ namespace Ryujinx.Cpu.AppleHv
PtMap(va, pa, size);
_addressSpace.MapUser(va, pa, size, MemoryPermission.ReadWriteExecute);
AddMapping(va, size);
_pages.AddMapping(va, size);
Tracking.Map(va, size);
}
@ -152,12 +96,6 @@ namespace Ryujinx.Cpu.AppleHv
}
}
/// <inheritdoc/>
public void MapForeign(ulong va, nuint hostPointer, ulong size)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public void Unmap(ulong va, ulong size)
{
@ -166,7 +104,7 @@ namespace Ryujinx.Cpu.AppleHv
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
RemoveMapping(va, size);
_pages.RemoveMapping(va, size);
_addressSpace.UnmapUser(va, size);
PtUnmap(va, size);
}
@ -182,20 +120,11 @@ namespace Ryujinx.Cpu.AppleHv
}
}
/// <inheritdoc/>
public T Read<T>(ulong va) where T : unmanaged
{
return MemoryMarshal.Cast<byte, T>(GetSpan(va, Unsafe.SizeOf<T>()))[0];
}
/// <inheritdoc/>
public T ReadTracked<T>(ulong va) where T : unmanaged
public override T ReadTracked<T>(ulong va)
{
try
{
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), false);
return Read<T>(va);
return base.ReadTracked<T>(va);
}
catch (InvalidMemoryRegionException)
{
@ -208,107 +137,11 @@ namespace Ryujinx.Cpu.AppleHv
}
}
/// <inheritdoc/>
public void Read(ulong va, Span<byte> data)
{
ReadImpl(va, data);
}
/// <inheritdoc/>
public void Write<T>(ulong va, T value) where T : unmanaged
{
Write(va, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
}
/// <inheritdoc/>
public void Write(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return;
}
SignalMemoryTracking(va, (ulong)data.Length, true);
WriteImpl(va, data);
}
/// <inheritdoc/>
public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return;
}
WriteImpl(va, data);
}
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return false;
}
SignalMemoryTracking(va, (ulong)data.Length, false);
if (IsContiguousAndMapped(va, data.Length))
{
var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
bool changed = !data.SequenceEqual(target);
if (changed)
{
data.CopyTo(target);
}
return changed;
}
else
{
WriteImpl(va, data);
return true;
}
}
private void WriteImpl(ulong va, ReadOnlySpan<byte> data)
public override void Read(ulong va, Span<byte> data)
{
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
if (IsContiguousAndMapped(va, data.Length))
{
data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));
}
else
{
int offset = 0, size;
if ((va & PageMask) != 0)
{
ulong pa = GetPhysicalAddressChecked(va);
size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
data[..size].CopyTo(_backingMemory.GetSpan(pa, size));
offset += size;
}
for (; offset < data.Length; offset += size)
{
ulong pa = GetPhysicalAddressChecked(va + (ulong)offset);
size = Math.Min(data.Length - offset, PageSize);
data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size));
}
}
base.Read(va, data);
}
catch (InvalidMemoryRegionException)
{
@ -319,61 +152,53 @@ namespace Ryujinx.Cpu.AppleHv
}
}
/// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
public override void Write(ulong va, ReadOnlySpan<byte> data)
{
if (size == 0)
try
{
return ReadOnlySpan<byte>.Empty;
base.Write(va, data);
}
if (tracked)
catch (InvalidMemoryRegionException)
{
SignalMemoryTracking(va, (ulong)size, false);
}
if (IsContiguousAndMapped(va, size))
{
return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size);
}
else
{
Span<byte> data = new byte[size];
ReadImpl(va, data);
return data;
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
/// <inheritdoc/>
public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
public override void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
if (size == 0)
try
{
return new WritableRegion(null, va, Memory<byte>.Empty);
base.WriteUntracked(va, data);
}
if (tracked)
catch (InvalidMemoryRegionException)
{
SignalMemoryTracking(va, (ulong)size, true);
}
if (IsContiguousAndMapped(va, size))
{
return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size));
}
else
{
Memory<byte> memory = new byte[size];
ReadImpl(va, memory.Span);
return new WritableRegion(this, va, memory);
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
public override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
try
{
return base.GetReadOnlySequence(va, size, tracked);
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return ReadOnlySequence<byte>.Empty;
}
}
/// <inheritdoc/>
public ref T GetRef<T>(ulong va) where T : unmanaged
{
if (!IsContiguous(va, Unsafe.SizeOf<T>()))
@ -386,26 +211,10 @@ namespace Ryujinx.Cpu.AppleHv
return ref _backingMemory.GetRef<T>(GetPhysicalAddressChecked(va));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsMapped(ulong va)
public override bool IsMapped(ulong va)
{
return ValidateAddress(va) && IsMappedImpl(va);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsMappedImpl(ulong va)
{
ulong page = va >> PageBits;
int bit = (int)((page & 31) << 1);
int pageIndex = (int)(page >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte = Volatile.Read(ref pageRef);
return ((pte >> bit) & 3) != 0;
return ValidateAddress(va) && _pages.IsMapped(va);
}
/// <inheritdoc/>
@ -413,91 +222,7 @@ namespace Ryujinx.Cpu.AppleHv
{
AssertValidAddressAndSize(va, size);
return IsRangeMappedImpl(va, size);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
{
startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
pageIndex = (int)(pageStart >> PageToPteShift);
pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
}
private bool IsRangeMappedImpl(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
if (pages == 1)
{
return IsMappedImpl(va);
}
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
// Check if either bit in each 2 bit page entry is set.
// OR the block with itself shifted down by 1, and check the first bit of each entry.
ulong mask = BlockMappedMask & startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte = Volatile.Read(ref pageRef);
pte |= pte >> 1;
if ((pte & mask) != mask)
{
return false;
}
mask = BlockMappedMask;
}
return true;
}
private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsContiguous(ulong va, int size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
{
return false;
}
int pages = GetPagesCount(va, (uint)size, out va);
for (int page = 0; page < pages - 1; page++)
{
if (!ValidateAddress(va + PageSize))
{
return false;
}
if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
{
return false;
}
va += PageSize;
}
return true;
return _pages.IsRangeMapped(va, size);
}
/// <inheritdoc/>
@ -576,53 +301,10 @@ namespace Ryujinx.Cpu.AppleHv
return regions;
}
private void ReadImpl(ulong va, Span<byte> data)
{
if (data.Length == 0)
{
return;
}
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
int offset = 0, size;
if ((va & PageMask) != 0)
{
ulong pa = GetPhysicalAddressChecked(va);
size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
_backingMemory.GetSpan(pa, size).CopyTo(data[..size]);
offset += size;
}
for (; offset < data.Length; offset += size)
{
ulong pa = GetPhysicalAddressChecked(va + (ulong)offset);
size = Math.Min(data.Length - offset, PageSize);
_backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size));
}
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
/// <inheritdoc/>
/// <remarks>
/// This function also validates that the given range is both valid and mapped, and will throw if it is not.
/// </remarks>
public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@ -632,199 +314,37 @@ namespace Ryujinx.Cpu.AppleHv
return;
}
// Software table, used for managed memory tracking.
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
if (pages == 1)
{
ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked);
int bit = (int)((pageStart & 31) << 1);
int pageIndex = (int)(pageStart >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte = Volatile.Read(ref pageRef);
ulong state = ((pte >> bit) & 3);
if (state >= tag)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
return;
}
else if (state == 0)
{
ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
}
else
{
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte = Volatile.Read(ref pageRef);
ulong mappedMask = mask & BlockMappedMask;
ulong mappedPte = pte | (pte >> 1);
if ((mappedPte & mappedMask) != mappedMask)
{
ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
pte &= mask;
if ((pte & anyTrackingTag) != 0) // Search for any tracking.
{
// Writes trigger any tracking.
// Only trigger tracking from reads if both bits are set on any page.
if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
break;
}
}
mask = ulong.MaxValue;
}
}
_pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
/// <summary>
/// Computes the number of pages in a virtual address range.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range</param>
/// <param name="startVa">The virtual address of the beginning of the first page</param>
/// <remarks>This function does not differentiate between allocated and unallocated pages.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
{
// WARNING: Always check if ulong does not overflow during the operations.
startVa = va & ~(ulong)PageMask;
ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
return (int)(vaSpan / PageSize);
}
/// <inheritdoc/>
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
// TODO
}
/// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
// Protection is inverted on software pages, since the default value is 0.
protection = (~protection) & MemoryPermission.ReadAndWrite;
int pages = GetPagesCount(va, size, out va);
ulong pageStart = va >> PageBits;
if (pages == 1)
if (guest)
{
ulong protTag = protection switch
{
MemoryPermission.None => (ulong)HostMappedPtBits.Mapped,
MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked,
_ => (ulong)HostMappedPtBits.ReadWriteTracked,
};
int bit = (int)((pageStart & 31) << 1);
ulong tagMask = 3UL << bit;
ulong invTagMask = ~tagMask;
ulong tag = protTag << bit;
int pageIndex = (int)(pageStart >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte;
do
{
pte = Volatile.Read(ref pageRef);
}
while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
_addressSpace.ReprotectUser(va, size, protection);
}
else
{
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
ulong protTag = protection switch
{
MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated,
MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated,
_ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated,
};
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
ulong mappedMask;
// Change the protection of all 2 bit entries that are mapped.
do
{
pte = Volatile.Read(ref pageRef);
mappedMask = pte | (pte >> 1);
mappedMask |= (mappedMask & BlockMappedMask) << 1;
mappedMask &= mask; // Only update mapped pages within the given range.
}
while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
mask = ulong.MaxValue;
}
_pages.TrackingReprotect(va, size, protection);
}
protection = protection switch
{
MemoryPermission.None => MemoryPermission.ReadAndWrite,
MemoryPermission.Write => MemoryPermission.Read,
_ => MemoryPermission.None,
};
_addressSpace.ReprotectUser(va, size, protection);
}
/// <inheritdoc/>
public RegionHandle BeginTracking(ulong address, ulong size, int id)
public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginTracking(address, size, id);
return Tracking.BeginTracking(address, size, id, flags);
}
/// <inheritdoc/>
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id)
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
/// <inheritdoc/>
@ -833,87 +353,7 @@ namespace Ryujinx.Cpu.AppleHv
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
/// <summary>
/// Adds the given address mapping to the page table.
/// </summary>
/// <param name="va">Virtual memory address</param>
/// <param name="size">Size to be mapped</param>
private void AddMapping(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
ulong mappedMask;
// Map all 2-bit entries that are unmapped.
do
{
pte = Volatile.Read(ref pageRef);
mappedMask = pte | (pte >> 1);
mappedMask |= (mappedMask & BlockMappedMask) << 1;
mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
}
while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
mask = ulong.MaxValue;
}
}
/// <summary>
/// Removes the given address mapping from the page table.
/// </summary>
/// <param name="va">Virtual memory address</param>
/// <param name="size">Size to be unmapped</param>
private void RemoveMapping(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
startMask = ~startMask;
endMask = ~endMask;
ulong mask = startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask |= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
do
{
pte = Volatile.Read(ref pageRef);
}
while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
mask = 0;
}
}
private ulong GetPhysicalAddressChecked(ulong va)
private nuint GetPhysicalAddressChecked(ulong va)
{
if (!IsMapped(va))
{
@ -923,9 +363,9 @@ namespace Ryujinx.Cpu.AppleHv
return GetPhysicalAddressInternal(va);
}
private ulong GetPhysicalAddressInternal(ulong va)
private nuint GetPhysicalAddressInternal(ulong va)
{
return _pageTable.Read(va) + (va & PageMask);
return (nuint)(_pageTable.Read(va) + (va & PageMask));
}
/// <summary>
@ -936,6 +376,17 @@ namespace Ryujinx.Cpu.AppleHv
_addressSpace.Dispose();
}
private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
protected override Memory<byte> GetPhysicalAddressMemory(nuint pa, int size)
=> _backingMemory.GetMemory(pa, size);
protected override Span<byte> GetPhysicalAddressSpan(nuint pa, int size)
=> _backingMemory.GetSpan(pa, size);
protected override nuint TranslateVirtualAddressChecked(ulong va)
=> GetPhysicalAddressChecked(va);
protected override nuint TranslateVirtualAddressUnchecked(ulong va)
=> GetPhysicalAddressInternal(va);
}
}

View File

@ -28,8 +28,9 @@ namespace Ryujinx.Cpu
/// <param name="address">CPU virtual address of the region</param>
/// <param name="size">Size of the region</param>
/// <param name="id">Handle ID</param>
/// <param name="flags">Region flags</param>
/// <returns>The memory tracking handle</returns>
RegionHandle BeginTracking(ulong address, ulong size, int id);
RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None);
/// <summary>
/// Obtains a memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with.
@ -39,8 +40,9 @@ namespace Ryujinx.Cpu
/// <param name="handles">Handles to inherit state from or reuse. When none are present, provide null</param>
/// <param name="granularity">Desired granularity of write tracking</param>
/// <param name="id">Handle ID</param>
/// <param name="flags">Region flags</param>
/// <returns>The memory tracking handle</returns>
MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id);
MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None);
/// <summary>
/// Obtains a smart memory tracking handle for the given virtual region, with a specified granularity. This should be disposed when finished with.

View File

@ -0,0 +1,35 @@
using Ryujinx.Common.Collections;
using System;
namespace Ryujinx.Cpu.Jit.HostTracked
{
internal class AddressIntrusiveRedBlackTree<T> : IntrusiveRedBlackTree<T> where T : IntrusiveRedBlackTreeNode<T>, IComparable<T>, IComparable<ulong>
{
/// <summary>
/// Retrieve the node that is considered equal to the specified address by the comparator.
/// </summary>
/// <param name="address">Address to compare with</param>
/// <returns>Node that is equal to <paramref name="address"/></returns>
public T GetNode(ulong address)
{
T node = Root;
while (node != null)
{
int cmp = node.CompareTo(address);
if (cmp < 0)
{
node = node.Left;
}
else if (cmp > 0)
{
node = node.Right;
}
else
{
return node;
}
}
return null;
}
}
}

View File

@ -0,0 +1,708 @@
using Ryujinx.Common;
using Ryujinx.Common.Collections;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
using System.Threading;
namespace Ryujinx.Cpu.Jit.HostTracked
{
readonly struct PrivateRange
{
public readonly MemoryBlock Memory;
public readonly ulong Offset;
public readonly ulong Size;
public static PrivateRange Empty => new(null, 0, 0);
public PrivateRange(MemoryBlock memory, ulong offset, ulong size)
{
Memory = memory;
Offset = offset;
Size = size;
}
}
class AddressSpacePartition : IDisposable
{
public const ulong GuestPageSize = 0x1000;
private const int DefaultBlockAlignment = 1 << 20;
private enum MappingType : byte
{
None,
Private,
}
private class Mapping : IntrusiveRedBlackTreeNode<Mapping>, IComparable<Mapping>, IComparable<ulong>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public MappingType Type { get; private set; }
public Mapping(ulong address, ulong size, MappingType type)
{
Address = address;
Size = size;
Type = type;
}
public Mapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
Mapping left = new(Address, leftSize, Type);
Address = splitAddress;
Size = rightSize;
return left;
}
public void UpdateState(MappingType newType)
{
Type = newType;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(Mapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
public int CompareTo(ulong address)
{
if (address < Address)
{
return -1;
}
else if (address <= EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private class PrivateMapping : IntrusiveRedBlackTreeNode<PrivateMapping>, IComparable<PrivateMapping>, IComparable<ulong>
{
public ulong Address { get; private set; }
public ulong Size { get; private set; }
public ulong EndAddress => Address + Size;
public PrivateMemoryAllocation PrivateAllocation { get; private set; }
public PrivateMapping(ulong address, ulong size, PrivateMemoryAllocation privateAllocation)
{
Address = address;
Size = size;
PrivateAllocation = privateAllocation;
}
public PrivateMapping Split(ulong splitAddress)
{
ulong leftSize = splitAddress - Address;
ulong rightSize = EndAddress - splitAddress;
Debug.Assert(leftSize > 0);
Debug.Assert(rightSize > 0);
(var leftAllocation, PrivateAllocation) = PrivateAllocation.Split(leftSize);
PrivateMapping left = new(Address, leftSize, leftAllocation);
Address = splitAddress;
Size = rightSize;
return left;
}
public void Map(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress, PrivateMemoryAllocation newAllocation)
{
baseBlock.MapView(newAllocation.Memory, newAllocation.Offset, Address - baseAddress, Size);
PrivateAllocation = newAllocation;
}
public void Unmap(AddressSpacePartitionMultiAllocation baseBlock, ulong baseAddress)
{
if (PrivateAllocation.IsValid)
{
baseBlock.UnmapView(PrivateAllocation.Memory, Address - baseAddress, Size);
PrivateAllocation.Dispose();
}
PrivateAllocation = default;
}
public void Extend(ulong sizeDelta)
{
Size += sizeDelta;
}
public int CompareTo(PrivateMapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
public int CompareTo(ulong address)
{
if (address < Address)
{
return -1;
}
else if (address <= EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private readonly MemoryBlock _backingMemory;
private readonly AddressSpacePartitionMultiAllocation _baseMemory;
private readonly PrivateMemoryAllocator _privateMemoryAllocator;
private readonly AddressIntrusiveRedBlackTree<Mapping> _mappingTree;
private readonly AddressIntrusiveRedBlackTree<PrivateMapping> _privateTree;
private readonly ReaderWriterLockSlim _treeLock;
private readonly ulong _hostPageSize;
private ulong? _firstPagePa;
private ulong? _lastPagePa;
private ulong _cachedFirstPagePa;
private ulong _cachedLastPagePa;
private MemoryBlock _firstPageMemoryForUnmap;
private ulong _firstPageOffsetForLateMap;
private MemoryPermission _firstPageMemoryProtection;
public ulong Address { get; }
public ulong Size { get; }
public ulong EndAddress => Address + Size;
public AddressSpacePartition(AddressSpacePartitionAllocation baseMemory, MemoryBlock backingMemory, ulong address, ulong size)
{
_privateMemoryAllocator = new PrivateMemoryAllocator(DefaultBlockAlignment, MemoryAllocationFlags.Mirrorable);
_mappingTree = new AddressIntrusiveRedBlackTree<Mapping>();
_privateTree = new AddressIntrusiveRedBlackTree<PrivateMapping>();
_treeLock = new ReaderWriterLockSlim();
_mappingTree.Add(new Mapping(address, size, MappingType.None));
_privateTree.Add(new PrivateMapping(address, size, default));
_hostPageSize = MemoryBlock.GetPageSize();
_backingMemory = backingMemory;
_baseMemory = new(baseMemory);
_cachedFirstPagePa = ulong.MaxValue;
_cachedLastPagePa = ulong.MaxValue;
Address = address;
Size = size;
}
public bool IsEmpty()
{
_treeLock.EnterReadLock();
try
{
Mapping map = _mappingTree.GetNode(Address);
return map != null && map.Address == Address && map.Size == Size && map.Type == MappingType.None;
}
finally
{
_treeLock.ExitReadLock();
}
}
public void Map(ulong va, ulong pa, ulong size)
{
Debug.Assert(va >= Address);
Debug.Assert(va + size <= EndAddress);
if (va == Address)
{
_firstPagePa = pa;
}
if (va <= EndAddress - GuestPageSize && va + size > EndAddress - GuestPageSize)
{
_lastPagePa = pa + ((EndAddress - GuestPageSize) - va);
}
Update(va, pa, size, MappingType.Private);
}
public void Unmap(ulong va, ulong size)
{
Debug.Assert(va >= Address);
Debug.Assert(va + size <= EndAddress);
if (va == Address)
{
_firstPagePa = null;
}
if (va <= EndAddress - GuestPageSize && va + size > EndAddress - GuestPageSize)
{
_lastPagePa = null;
}
Update(va, 0UL, size, MappingType.None);
}
public void ReprotectAligned(ulong va, ulong size, MemoryPermission protection)
{
Debug.Assert(va >= Address);
Debug.Assert(va + size <= EndAddress);
_baseMemory.Reprotect(va - Address, size, protection, false);
if (va == Address)
{
_firstPageMemoryProtection = protection;
}
}
public void Reprotect(
ulong va,
ulong size,
MemoryPermission protection,
AddressSpacePartitioned addressSpace,
Action<ulong, IntPtr, ulong> updatePtCallback)
{
if (_baseMemory.LazyInitMirrorForProtection(addressSpace, Address, Size, protection))
{
LateMap();
}
updatePtCallback(va, _baseMemory.GetPointerForProtection(va - Address, size, protection), size);
}
public IntPtr GetPointer(ulong va, ulong size)
{
Debug.Assert(va >= Address);
Debug.Assert(va + size <= EndAddress);
return _baseMemory.GetPointer(va - Address, size);
}
public void InsertBridgeAtEnd(AddressSpacePartition partitionAfter, bool useProtectionMirrors)
{
ulong firstPagePa = partitionAfter?._firstPagePa ?? ulong.MaxValue;
ulong lastPagePa = _lastPagePa ?? ulong.MaxValue;
if (firstPagePa != _cachedFirstPagePa || lastPagePa != _cachedLastPagePa)
{
if (partitionAfter != null && partitionAfter._firstPagePa.HasValue)
{
(MemoryBlock firstPageMemory, ulong firstPageOffset) = partitionAfter.GetFirstPageMemoryAndOffset();
_baseMemory.MapView(firstPageMemory, firstPageOffset, Size, _hostPageSize);
if (!useProtectionMirrors)
{
_baseMemory.Reprotect(Size, _hostPageSize, partitionAfter._firstPageMemoryProtection, throwOnFail: false);
}
_firstPageMemoryForUnmap = firstPageMemory;
_firstPageOffsetForLateMap = firstPageOffset;
}
else
{
MemoryBlock firstPageMemoryForUnmap = _firstPageMemoryForUnmap;
if (firstPageMemoryForUnmap != null)
{
_baseMemory.UnmapView(firstPageMemoryForUnmap, Size, _hostPageSize);
_firstPageMemoryForUnmap = null;
}
}
_cachedFirstPagePa = firstPagePa;
_cachedLastPagePa = lastPagePa;
}
}
public void ReprotectBridge(MemoryPermission protection)
{
if (_firstPageMemoryForUnmap != null)
{
_baseMemory.Reprotect(Size, _hostPageSize, protection, throwOnFail: false);
}
}
private (MemoryBlock, ulong) GetFirstPageMemoryAndOffset()
{
_treeLock.EnterReadLock();
try
{
PrivateMapping map = _privateTree.GetNode(Address);
if (map != null && map.PrivateAllocation.IsValid)
{
return (map.PrivateAllocation.Memory, map.PrivateAllocation.Offset + (Address - map.Address));
}
}
finally
{
_treeLock.ExitReadLock();
}
return (_backingMemory, _firstPagePa.Value);
}
public PrivateRange GetPrivateAllocation(ulong va)
{
_treeLock.EnterReadLock();
try
{
PrivateMapping map = _privateTree.GetNode(va);
if (map != null && map.PrivateAllocation.IsValid)
{
return new(map.PrivateAllocation.Memory, map.PrivateAllocation.Offset + (va - map.Address), map.Size - (va - map.Address));
}
}
finally
{
_treeLock.ExitReadLock();
}
return PrivateRange.Empty;
}
private void Update(ulong va, ulong pa, ulong size, MappingType type)
{
_treeLock.EnterWriteLock();
try
{
Mapping map = _mappingTree.GetNode(va);
Update(map, va, pa, size, type);
}
finally
{
_treeLock.ExitWriteLock();
}
}
private Mapping Update(Mapping map, ulong va, ulong pa, ulong size, MappingType type)
{
ulong endAddress = va + size;
for (; map != null; map = map.Successor)
{
if (map.Address < va)
{
_mappingTree.Add(map.Split(va));
}
if (map.EndAddress > endAddress)
{
Mapping newMap = map.Split(endAddress);
_mappingTree.Add(newMap);
map = newMap;
}
switch (type)
{
case MappingType.None:
ulong alignment = _hostPageSize;
bool unmappedBefore = map.Predecessor == null ||
(map.Predecessor.Type == MappingType.None && map.Predecessor.Address <= BitUtils.AlignDown(va, alignment));
bool unmappedAfter = map.Successor == null ||
(map.Successor.Type == MappingType.None && map.Successor.EndAddress >= BitUtils.AlignUp(endAddress, alignment));
UnmapPrivate(va, size, unmappedBefore, unmappedAfter);
break;
case MappingType.Private:
MapPrivate(va, size);
break;
}
map.UpdateState(type);
map = TryCoalesce(map);
if (map.EndAddress >= endAddress)
{
break;
}
}
return map;
}
private Mapping TryCoalesce(Mapping map)
{
Mapping previousMap = map.Predecessor;
Mapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_mappingTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_mappingTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(Mapping left, Mapping right)
{
return left.Type == right.Type;
}
private void MapPrivate(ulong va, ulong size)
{
ulong endAddress = va + size;
ulong alignment = _hostPageSize;
// Expand the range outwards based on page size to ensure that at least the requested region is mapped.
ulong vaAligned = BitUtils.AlignDown(va, alignment);
ulong endAddressAligned = BitUtils.AlignUp(endAddress, alignment);
PrivateMapping map = _privateTree.GetNode(va);
for (; map != null; map = map.Successor)
{
if (!map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Map(_baseMemory, Address, _privateMemoryAllocator.Allocate(map.Size, _hostPageSize));
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private void UnmapPrivate(ulong va, ulong size, bool unmappedBefore, bool unmappedAfter)
{
ulong endAddress = va + size;
ulong alignment = _hostPageSize;
// If the adjacent mappings are unmapped, expand the range outwards,
// otherwise shrink it inwards. We must ensure we won't unmap pages that might still be in use.
ulong vaAligned = unmappedBefore ? BitUtils.AlignDown(va, alignment) : BitUtils.AlignUp(va, alignment);
ulong endAddressAligned = unmappedAfter ? BitUtils.AlignUp(endAddress, alignment) : BitUtils.AlignDown(endAddress, alignment);
if (endAddressAligned <= vaAligned)
{
return;
}
PrivateMapping map = _privateTree.GetNode(vaAligned);
for (; map != null; map = map.Successor)
{
if (map.PrivateAllocation.IsValid)
{
if (map.Address < vaAligned)
{
_privateTree.Add(map.Split(vaAligned));
}
if (map.EndAddress > endAddressAligned)
{
PrivateMapping newMap = map.Split(endAddressAligned);
_privateTree.Add(newMap);
map = newMap;
}
map.Unmap(_baseMemory, Address);
map = TryCoalesce(map);
}
if (map.EndAddress >= endAddressAligned)
{
break;
}
}
}
private PrivateMapping TryCoalesce(PrivateMapping map)
{
PrivateMapping previousMap = map.Predecessor;
PrivateMapping nextMap = map.Successor;
if (previousMap != null && CanCoalesce(previousMap, map))
{
previousMap.Extend(map.Size);
_privateTree.Remove(map);
map = previousMap;
}
if (nextMap != null && CanCoalesce(map, nextMap))
{
map.Extend(nextMap.Size);
_privateTree.Remove(nextMap);
}
return map;
}
private static bool CanCoalesce(PrivateMapping left, PrivateMapping right)
{
return !left.PrivateAllocation.IsValid && !right.PrivateAllocation.IsValid;
}
private void LateMap()
{
// Map all existing private allocations.
// This is necessary to ensure mirrors that are lazily created have the same mappings as the main one.
PrivateMapping map = _privateTree.GetNode(Address);
for (; map != null; map = map.Successor)
{
if (map.PrivateAllocation.IsValid)
{
_baseMemory.LateMapView(map.PrivateAllocation.Memory, map.PrivateAllocation.Offset, map.Address - Address, map.Size);
}
}
MemoryBlock firstPageMemory = _firstPageMemoryForUnmap;
ulong firstPageOffset = _firstPageOffsetForLateMap;
if (firstPageMemory != null)
{
_baseMemory.LateMapView(firstPageMemory, firstPageOffset, Size, _hostPageSize);
}
}
public PrivateRange GetFirstPrivateAllocation(ulong va, ulong size, out ulong nextVa)
{
_treeLock.EnterReadLock();
try
{
PrivateMapping map = _privateTree.GetNode(va);
nextVa = map.EndAddress;
if (map != null && map.PrivateAllocation.IsValid)
{
ulong startOffset = va - map.Address;
return new(
map.PrivateAllocation.Memory,
map.PrivateAllocation.Offset + startOffset,
Math.Min(map.PrivateAllocation.Size - startOffset, size));
}
}
finally
{
_treeLock.ExitReadLock();
}
return PrivateRange.Empty;
}
public bool HasPrivateAllocation(ulong va, ulong size, ulong startVa, ulong startSize, ref PrivateRange range)
{
ulong endVa = va + size;
_treeLock.EnterReadLock();
try
{
for (PrivateMapping map = _privateTree.GetNode(va); map != null && map.Address < endVa; map = map.Successor)
{
if (map.PrivateAllocation.IsValid)
{
if (map.Address <= startVa && map.EndAddress >= startVa + startSize)
{
ulong startOffset = startVa - map.Address;
range = new(
map.PrivateAllocation.Memory,
map.PrivateAllocation.Offset + startOffset,
Math.Min(map.PrivateAllocation.Size - startOffset, startSize));
}
return true;
}
}
}
finally
{
_treeLock.ExitReadLock();
}
return false;
}
public void Dispose()
{
GC.SuppressFinalize(this);
_privateMemoryAllocator.Dispose();
_baseMemory.Dispose();
}
}
}

View File

@ -0,0 +1,202 @@
using Ryujinx.Common;
using Ryujinx.Common.Collections;
using Ryujinx.Memory;
using Ryujinx.Memory.Tracking;
using System;
namespace Ryujinx.Cpu.Jit.HostTracked
{
readonly struct AddressSpacePartitionAllocation : IDisposable
{
private readonly AddressSpacePartitionAllocator _owner;
private readonly PrivateMemoryAllocatorImpl<AddressSpacePartitionAllocator.Block>.Allocation _allocation;
public IntPtr Pointer => (IntPtr)((ulong)_allocation.Block.Memory.Pointer + _allocation.Offset);
public bool IsValid => _owner != null;
public AddressSpacePartitionAllocation(
AddressSpacePartitionAllocator owner,
PrivateMemoryAllocatorImpl<AddressSpacePartitionAllocator.Block>.Allocation allocation)
{
_owner = owner;
_allocation = allocation;
}
public void RegisterMapping(ulong va, ulong endVa)
{
_allocation.Block.AddMapping(_allocation.Offset, _allocation.Size, va, endVa);
}
public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
{
_allocation.Block.Memory.MapView(srcBlock, srcOffset, _allocation.Offset + dstOffset, size);
}
public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size)
{
_allocation.Block.Memory.UnmapView(srcBlock, _allocation.Offset + offset, size);
}
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail)
{
_allocation.Block.Memory.Reprotect(_allocation.Offset + offset, size, permission, throwOnFail);
}
public IntPtr GetPointer(ulong offset, ulong size)
{
return _allocation.Block.Memory.GetPointer(_allocation.Offset + offset, size);
}
public void Dispose()
{
_allocation.Block.RemoveMapping(_allocation.Offset, _allocation.Size);
_owner.Free(_allocation.Block, _allocation.Offset, _allocation.Size);
}
}
class AddressSpacePartitionAllocator : PrivateMemoryAllocatorImpl<AddressSpacePartitionAllocator.Block>
{
private const ulong DefaultBlockAlignment = 1UL << 32; // 4GB
public class Block : PrivateMemoryAllocator.Block
{
private readonly MemoryTracking _tracking;
private readonly Func<ulong, ulong> _readPtCallback;
private readonly MemoryEhMeilleure _memoryEh;
private class Mapping : IntrusiveRedBlackTreeNode<Mapping>, IComparable<Mapping>, IComparable<ulong>
{
public ulong Address { get; }
public ulong Size { get; }
public ulong EndAddress => Address + Size;
public ulong Va { get; }
public ulong EndVa { get; }
public Mapping(ulong address, ulong size, ulong va, ulong endVa)
{
Address = address;
Size = size;
Va = va;
EndVa = endVa;
}
public int CompareTo(Mapping other)
{
if (Address < other.Address)
{
return -1;
}
else if (Address <= other.EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
public int CompareTo(ulong address)
{
if (address < Address)
{
return -1;
}
else if (address <= EndAddress - 1UL)
{
return 0;
}
else
{
return 1;
}
}
}
private readonly AddressIntrusiveRedBlackTree<Mapping> _mappingTree;
private readonly object _lock;
public Block(MemoryTracking tracking, Func<ulong, ulong> readPtCallback, MemoryBlock memory, ulong size, object locker) : base(memory, size)
{
_tracking = tracking;
_readPtCallback = readPtCallback;
_memoryEh = new(memory, null, tracking, VirtualMemoryEvent);
_mappingTree = new();
_lock = locker;
}
public void AddMapping(ulong offset, ulong size, ulong va, ulong endVa)
{
_mappingTree.Add(new(offset, size, va, endVa));
}
public void RemoveMapping(ulong offset, ulong size)
{
_mappingTree.Remove(_mappingTree.GetNode(offset));
}
private ulong VirtualMemoryEvent(ulong address, ulong size, bool write)
{
Mapping map;
lock (_lock)
{
map = _mappingTree.GetNode(address);
}
if (map == null)
{
return 0;
}
address -= map.Address;
ulong addressAligned = BitUtils.AlignDown(address, AddressSpacePartition.GuestPageSize);
ulong endAddressAligned = BitUtils.AlignUp(address + size, AddressSpacePartition.GuestPageSize);
ulong sizeAligned = endAddressAligned - addressAligned;
if (!_tracking.VirtualMemoryEvent(map.Va + addressAligned, sizeAligned, write))
{
return 0;
}
return _readPtCallback(map.Va + address);
}
public override void Destroy()
{
_memoryEh.Dispose();
base.Destroy();
}
}
private readonly MemoryTracking _tracking;
private readonly Func<ulong, ulong> _readPtCallback;
private readonly object _lock;
public AddressSpacePartitionAllocator(
MemoryTracking tracking,
Func<ulong, ulong> readPtCallback,
object locker) : base(DefaultBlockAlignment, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible)
{
_tracking = tracking;
_readPtCallback = readPtCallback;
_lock = locker;
}
public AddressSpacePartitionAllocation Allocate(ulong va, ulong size)
{
AddressSpacePartitionAllocation allocation = new(this, Allocate(size, MemoryBlock.GetPageSize(), CreateBlock));
allocation.RegisterMapping(va, va + size);
return allocation;
}
private Block CreateBlock(MemoryBlock memory, ulong size)
{
return new Block(_tracking, _readPtCallback, memory, size, _lock);
}
}
}

View File

@ -0,0 +1,101 @@
using Ryujinx.Memory;
using System;
using System.Diagnostics;
namespace Ryujinx.Cpu.Jit.HostTracked
{
class AddressSpacePartitionMultiAllocation : IDisposable
{
private readonly AddressSpacePartitionAllocation _baseMemory;
private AddressSpacePartitionAllocation _baseMemoryRo;
private AddressSpacePartitionAllocation _baseMemoryNone;
public AddressSpacePartitionMultiAllocation(AddressSpacePartitionAllocation baseMemory)
{
_baseMemory = baseMemory;
}
public void MapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
{
_baseMemory.MapView(srcBlock, srcOffset, dstOffset, size);
if (_baseMemoryRo.IsValid)
{
_baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size);
_baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false);
}
}
public void LateMapView(MemoryBlock srcBlock, ulong srcOffset, ulong dstOffset, ulong size)
{
_baseMemoryRo.MapView(srcBlock, srcOffset, dstOffset, size);
_baseMemoryRo.Reprotect(dstOffset, size, MemoryPermission.Read, false);
}
public void UnmapView(MemoryBlock srcBlock, ulong offset, ulong size)
{
_baseMemory.UnmapView(srcBlock, offset, size);
if (_baseMemoryRo.IsValid)
{
_baseMemoryRo.UnmapView(srcBlock, offset, size);
}
}
public void Reprotect(ulong offset, ulong size, MemoryPermission permission, bool throwOnFail)
{
_baseMemory.Reprotect(offset, size, permission, throwOnFail);
}
public IntPtr GetPointer(ulong offset, ulong size)
{
return _baseMemory.GetPointer(offset, size);
}
public bool LazyInitMirrorForProtection(AddressSpacePartitioned addressSpace, ulong blockAddress, ulong blockSize, MemoryPermission permission)
{
if (permission == MemoryPermission.None && !_baseMemoryNone.IsValid)
{
_baseMemoryNone = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize);
}
else if (permission == MemoryPermission.Read && !_baseMemoryRo.IsValid)
{
_baseMemoryRo = addressSpace.CreateAsPartitionAllocation(blockAddress, blockSize);
return true;
}
return false;
}
public IntPtr GetPointerForProtection(ulong offset, ulong size, MemoryPermission permission)
{
AddressSpacePartitionAllocation allocation = permission switch
{
MemoryPermission.ReadAndWrite => _baseMemory,
MemoryPermission.Read => _baseMemoryRo,
MemoryPermission.None => _baseMemoryNone,
_ => throw new ArgumentException($"Invalid protection \"{permission}\"."),
};
Debug.Assert(allocation.IsValid);
return allocation.GetPointer(offset, size);
}
public void Dispose()
{
_baseMemory.Dispose();
if (_baseMemoryRo.IsValid)
{
_baseMemoryRo.Dispose();
}
if (_baseMemoryNone.IsValid)
{
_baseMemoryNone.Dispose();
}
}
}
}

View File

@ -0,0 +1,407 @@
using Ryujinx.Common;
using Ryujinx.Memory;
using Ryujinx.Memory.Tracking;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ryujinx.Cpu.Jit.HostTracked
{
class AddressSpacePartitioned : IDisposable
{
private const int PartitionBits = 25;
private const ulong PartitionSize = 1UL << PartitionBits;
private readonly MemoryBlock _backingMemory;
private readonly List<AddressSpacePartition> _partitions;
private readonly AddressSpacePartitionAllocator _asAllocator;
private readonly Action<ulong, IntPtr, ulong> _updatePtCallback;
private readonly bool _useProtectionMirrors;
public AddressSpacePartitioned(MemoryTracking tracking, MemoryBlock backingMemory, NativePageTable nativePageTable, bool useProtectionMirrors)
{
_backingMemory = backingMemory;
_partitions = new();
_asAllocator = new(tracking, nativePageTable.Read, _partitions);
_updatePtCallback = nativePageTable.Update;
_useProtectionMirrors = useProtectionMirrors;
}
public void Map(ulong va, ulong pa, ulong size)
{
ulong endVa = va + size;
lock (_partitions)
{
EnsurePartitionsLocked(va, size);
while (va < endVa)
{
int partitionIndex = FindPartitionIndexLocked(va);
AddressSpacePartition partition = _partitions[partitionIndex];
(ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
partition.Map(clampedVa, pa, clampedEndVa - clampedVa);
ulong currentSize = clampedEndVa - clampedVa;
va += currentSize;
pa += currentSize;
InsertOrRemoveBridgeIfNeeded(partitionIndex);
}
}
}
public void Unmap(ulong va, ulong size)
{
ulong endVa = va + size;
while (va < endVa)
{
AddressSpacePartition partition;
lock (_partitions)
{
int partitionIndex = FindPartitionIndexLocked(va);
if (partitionIndex < 0)
{
va += PartitionSize - (va & (PartitionSize - 1));
continue;
}
partition = _partitions[partitionIndex];
(ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
partition.Unmap(clampedVa, clampedEndVa - clampedVa);
va += clampedEndVa - clampedVa;
InsertOrRemoveBridgeIfNeeded(partitionIndex);
if (partition.IsEmpty())
{
_partitions.Remove(partition);
partition.Dispose();
}
}
}
}
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
ulong endVa = va + size;
lock (_partitions)
{
while (va < endVa)
{
AddressSpacePartition partition = FindPartitionWithIndex(va, out int partitionIndex);
if (partition == null)
{
va += PartitionSize - (va & (PartitionSize - 1));
continue;
}
(ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
if (_useProtectionMirrors)
{
partition.Reprotect(clampedVa, clampedEndVa - clampedVa, protection, this, _updatePtCallback);
}
else
{
partition.ReprotectAligned(clampedVa, clampedEndVa - clampedVa, protection);
if (clampedVa == partition.Address &&
partitionIndex > 0 &&
_partitions[partitionIndex - 1].EndAddress == partition.Address)
{
_partitions[partitionIndex - 1].ReprotectBridge(protection);
}
}
va += clampedEndVa - clampedVa;
}
}
}
public PrivateRange GetPrivateAllocation(ulong va)
{
AddressSpacePartition partition = FindPartition(va);
if (partition == null)
{
return PrivateRange.Empty;
}
return partition.GetPrivateAllocation(va);
}
public PrivateRange GetFirstPrivateAllocation(ulong va, ulong size, out ulong nextVa)
{
AddressSpacePartition partition = FindPartition(va);
if (partition == null)
{
nextVa = (va & ~(PartitionSize - 1)) + PartitionSize;
return PrivateRange.Empty;
}
return partition.GetFirstPrivateAllocation(va, size, out nextVa);
}
public bool HasAnyPrivateAllocation(ulong va, ulong size, out PrivateRange range)
{
range = PrivateRange.Empty;
ulong startVa = va;
ulong endVa = va + size;
while (va < endVa)
{
AddressSpacePartition partition = FindPartition(va);
if (partition == null)
{
va += PartitionSize - (va & (PartitionSize - 1));
continue;
}
(ulong clampedVa, ulong clampedEndVa) = ClampRange(partition, va, endVa);
if (partition.HasPrivateAllocation(clampedVa, clampedEndVa - clampedVa, startVa, size, ref range))
{
return true;
}
va += clampedEndVa - clampedVa;
}
return false;
}
private void InsertOrRemoveBridgeIfNeeded(int partitionIndex)
{
if (partitionIndex > 0)
{
if (_partitions[partitionIndex - 1].EndAddress == _partitions[partitionIndex].Address)
{
_partitions[partitionIndex - 1].InsertBridgeAtEnd(_partitions[partitionIndex], _useProtectionMirrors);
}
else
{
_partitions[partitionIndex - 1].InsertBridgeAtEnd(null, _useProtectionMirrors);
}
}
if (partitionIndex + 1 < _partitions.Count && _partitions[partitionIndex].EndAddress == _partitions[partitionIndex + 1].Address)
{
_partitions[partitionIndex].InsertBridgeAtEnd(_partitions[partitionIndex + 1], _useProtectionMirrors);
}
else
{
_partitions[partitionIndex].InsertBridgeAtEnd(null, _useProtectionMirrors);
}
}
public IntPtr GetPointer(ulong va, ulong size)
{
AddressSpacePartition partition = FindPartition(va);
return partition.GetPointer(va, size);
}
private static (ulong, ulong) ClampRange(AddressSpacePartition partition, ulong va, ulong endVa)
{
if (va < partition.Address)
{
va = partition.Address;
}
if (endVa > partition.EndAddress)
{
endVa = partition.EndAddress;
}
return (va, endVa);
}
private AddressSpacePartition FindPartition(ulong va)
{
lock (_partitions)
{
int index = FindPartitionIndexLocked(va);
if (index >= 0)
{
return _partitions[index];
}
}
return null;
}
private AddressSpacePartition FindPartitionWithIndex(ulong va, out int index)
{
lock (_partitions)
{
index = FindPartitionIndexLocked(va);
if (index >= 0)
{
return _partitions[index];
}
}
return null;
}
private int FindPartitionIndexLocked(ulong va)
{
int left = 0;
int middle;
int right = _partitions.Count - 1;
while (left <= right)
{
middle = left + ((right - left) >> 1);
AddressSpacePartition partition = _partitions[middle];
if (partition.Address <= va && partition.EndAddress > va)
{
return middle;
}
if (partition.Address >= va)
{
right = middle - 1;
}
else
{
left = middle + 1;
}
}
return -1;
}
private void EnsurePartitionsLocked(ulong va, ulong size)
{
ulong endVa = BitUtils.AlignUp(va + size, PartitionSize);
va = BitUtils.AlignDown(va, PartitionSize);
for (int i = 0; i < _partitions.Count && va < endVa; i++)
{
AddressSpacePartition partition = _partitions[i];
if (partition.Address <= va && partition.EndAddress > va)
{
if (partition.EndAddress >= endVa)
{
// Fully mapped already.
va = endVa;
break;
}
ulong gapSize;
if (i + 1 < _partitions.Count)
{
AddressSpacePartition nextPartition = _partitions[i + 1];
if (partition.EndAddress == nextPartition.Address)
{
va = partition.EndAddress;
continue;
}
gapSize = Math.Min(endVa, nextPartition.Address) - partition.EndAddress;
}
else
{
gapSize = endVa - partition.EndAddress;
}
_partitions.Insert(i + 1, CreateAsPartition(partition.EndAddress, gapSize));
va = partition.EndAddress + gapSize;
i++;
}
else if (partition.EndAddress > va)
{
Debug.Assert(partition.Address > va);
ulong gapSize;
if (partition.Address < endVa)
{
gapSize = partition.Address - va;
}
else
{
gapSize = endVa - va;
}
_partitions.Insert(i, CreateAsPartition(va, gapSize));
va = Math.Min(partition.EndAddress, endVa);
i++;
}
}
if (va < endVa)
{
_partitions.Add(CreateAsPartition(va, endVa - va));
}
ValidatePartitionList();
}
[Conditional("DEBUG")]
private void ValidatePartitionList()
{
for (int i = 1; i < _partitions.Count; i++)
{
Debug.Assert(_partitions[i].Address > _partitions[i - 1].Address);
Debug.Assert(_partitions[i].EndAddress > _partitions[i - 1].EndAddress);
}
}
private AddressSpacePartition CreateAsPartition(ulong va, ulong size)
{
return new(CreateAsPartitionAllocation(va, size), _backingMemory, va, size);
}
public AddressSpacePartitionAllocation CreateAsPartitionAllocation(ulong va, ulong size)
{
return _asAllocator.Allocate(va, size + MemoryBlock.GetPageSize());
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
foreach (AddressSpacePartition partition in _partitions)
{
partition.Dispose();
}
_partitions.Clear();
_asAllocator.Dispose();
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,223 @@
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Ryujinx.Cpu.Jit.HostTracked
{
sealed class NativePageTable : IDisposable
{
private delegate ulong TrackingEventDelegate(ulong address, ulong size, bool write);
private const int PageBits = 12;
private const int PageSize = 1 << PageBits;
private const int PageMask = PageSize - 1;
private const int PteSize = 8;
private readonly int _bitsPerPtPage;
private readonly int _entriesPerPtPage;
private readonly int _pageCommitmentBits;
private readonly PageTable<ulong> _pageTable;
private readonly MemoryBlock _nativePageTable;
private readonly ulong[] _pageCommitmentBitmap;
private readonly ulong _hostPageSize;
private readonly TrackingEventDelegate _trackingEvent;
private bool _disposed;
public IntPtr PageTablePointer => _nativePageTable.Pointer;
public NativePageTable(ulong asSize)
{
ulong hostPageSize = MemoryBlock.GetPageSize();
_entriesPerPtPage = (int)(hostPageSize / sizeof(ulong));
_bitsPerPtPage = BitOperations.Log2((uint)_entriesPerPtPage);
_pageCommitmentBits = PageBits + _bitsPerPtPage;
_hostPageSize = hostPageSize;
_pageTable = new PageTable<ulong>();
_nativePageTable = new MemoryBlock((asSize / PageSize) * PteSize + _hostPageSize, MemoryAllocationFlags.Reserve);
_pageCommitmentBitmap = new ulong[(asSize >> _pageCommitmentBits) / (sizeof(ulong) * 8)];
ulong ptStart = (ulong)_nativePageTable.Pointer;
ulong ptEnd = ptStart + _nativePageTable.Size;
_trackingEvent = VirtualMemoryEvent;
bool added = NativeSignalHandler.AddTrackedRegion((nuint)ptStart, (nuint)ptEnd, Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added)
{
throw new InvalidOperationException("Number of allowed tracked regions exceeded.");
}
}
public void Map(ulong va, ulong pa, ulong size, AddressSpacePartitioned addressSpace, MemoryBlock backingMemory, bool privateMap)
{
while (size != 0)
{
_pageTable.Map(va, pa);
EnsureCommitment(va);
if (privateMap)
{
_nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, addressSpace.GetPointer(va, PageSize)));
}
else
{
_nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, backingMemory.GetPointer(pa, PageSize)));
}
va += PageSize;
pa += PageSize;
size -= PageSize;
}
}
public void Unmap(ulong va, ulong size)
{
IntPtr guardPagePtr = GetGuardPagePointer();
while (size != 0)
{
_pageTable.Unmap(va);
_nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, guardPagePtr));
va += PageSize;
size -= PageSize;
}
}
public ulong Read(ulong va)
{
ulong pte = _nativePageTable.Read<ulong>((va / PageSize) * PteSize);
pte += va & ~(ulong)PageMask;
return pte + (va & PageMask);
}
public void Update(ulong va, IntPtr ptr, ulong size)
{
ulong remainingSize = size;
while (remainingSize != 0)
{
EnsureCommitment(va);
_nativePageTable.Write((va / PageSize) * PteSize, GetPte(va, ptr));
va += PageSize;
ptr += PageSize;
remainingSize -= PageSize;
}
}
private void EnsureCommitment(ulong va)
{
ulong bit = va >> _pageCommitmentBits;
int index = (int)(bit / (sizeof(ulong) * 8));
int shift = (int)(bit % (sizeof(ulong) * 8));
ulong mask = 1UL << shift;
ulong oldMask = _pageCommitmentBitmap[index];
if ((oldMask & mask) == 0)
{
lock (_pageCommitmentBitmap)
{
oldMask = _pageCommitmentBitmap[index];
if ((oldMask & mask) != 0)
{
return;
}
_nativePageTable.Commit(bit * _hostPageSize, _hostPageSize);
Span<ulong> pageSpan = MemoryMarshal.Cast<byte, ulong>(_nativePageTable.GetSpan(bit * _hostPageSize, (int)_hostPageSize));
Debug.Assert(pageSpan.Length == _entriesPerPtPage);
IntPtr guardPagePtr = GetGuardPagePointer();
for (int i = 0; i < pageSpan.Length; i++)
{
pageSpan[i] = GetPte((bit << _pageCommitmentBits) | ((ulong)i * PageSize), guardPagePtr);
}
_pageCommitmentBitmap[index] = oldMask | mask;
}
}
}
private IntPtr GetGuardPagePointer()
{
return _nativePageTable.GetPointer(_nativePageTable.Size - _hostPageSize, _hostPageSize);
}
private static ulong GetPte(ulong va, IntPtr ptr)
{
Debug.Assert((va & PageMask) == 0);
return (ulong)ptr - va;
}
public ulong GetPhysicalAddress(ulong va)
{
return _pageTable.Read(va) + (va & PageMask);
}
private ulong VirtualMemoryEvent(ulong address, ulong size, bool write)
{
if (address < _nativePageTable.Size - _hostPageSize)
{
// Some prefetch instructions do not cause faults with invalid addresses.
// Retry if we are hitting a case where the page table is unmapped, the next
// run will execute the actual instruction.
// The address loaded from the page table will be invalid, and it should hit the else case
// if the instruction faults on unmapped or protected memory.
ulong va = address * (PageSize / sizeof(ulong));
EnsureCommitment(va);
return (ulong)_nativePageTable.Pointer + address;
}
else
{
throw new InvalidMemoryRegionException();
}
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
NativeSignalHandler.RemoveTrackedRegion((nuint)_nativePageTable.Pointer);
_nativePageTable.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -15,9 +15,9 @@ namespace Ryujinx.Cpu.Jit
_tickSource = tickSource;
_translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit);
if (memory.Type.IsHostMapped())
if (memory.Type.IsHostMappedOrTracked())
{
NativeSignalHandler.InitializeSignalHandler(MemoryBlock.GetPageSize());
NativeSignalHandler.InitializeSignalHandler();
}
memory.UnmapEvent += UnmapHandler;

View File

@ -3,6 +3,7 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@ -14,12 +15,8 @@ namespace Ryujinx.Cpu.Jit
/// <summary>
/// Represents a CPU memory manager.
/// </summary>
public sealed class MemoryManager : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
public sealed class MemoryManager : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
public const int PageBits = 12;
public const int PageSize = 1 << PageBits;
public const int PageMask = PageSize - 1;
private const int PteSize = 8;
private const int PointerTagBit = 62;
@ -28,17 +25,17 @@ namespace Ryujinx.Cpu.Jit
private readonly InvalidAccessHandler _invalidAccessHandler;
/// <inheritdoc/>
public bool Supports4KBPages => true;
public bool UsesPrivateAllocations => false;
/// <summary>
/// Address space width in bits.
/// </summary>
public int AddressSpaceBits { get; }
private readonly ulong _addressSpaceSize;
private readonly MemoryBlock _pageTable;
private readonly ManagedPageFlags _pages;
/// <summary>
/// Page table base pointer.
/// </summary>
@ -50,6 +47,8 @@ namespace Ryujinx.Cpu.Jit
public event Action<ulong, ulong> UnmapEvent;
protected override ulong AddressSpaceSize { get; }
/// <summary>
/// Creates a new instance of the memory manager.
/// </summary>
@ -71,9 +70,11 @@ namespace Ryujinx.Cpu.Jit
}
AddressSpaceBits = asBits;
_addressSpaceSize = asSize;
AddressSpaceSize = asSize;
_pageTable = new MemoryBlock((asSize / PageSize) * PteSize);
_pages = new ManagedPageFlags(AddressSpaceBits);
Tracking = new MemoryTracking(this, PageSize);
}
@ -93,15 +94,10 @@ namespace Ryujinx.Cpu.Jit
remainingSize -= PageSize;
}
_pages.AddMapping(oVa, size);
Tracking.Map(oVa, size);
}
/// <inheritdoc/>
public void MapForeign(ulong va, nuint hostPointer, ulong size)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public void Unmap(ulong va, ulong size)
{
@ -115,6 +111,7 @@ namespace Ryujinx.Cpu.Jit
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
_pages.RemoveMapping(va, size);
ulong remainingSize = size;
while (remainingSize != 0)
@ -126,18 +123,29 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public T Read<T>(ulong va) where T : unmanaged
{
return MemoryMarshal.Cast<byte, T>(GetSpan(va, Unsafe.SizeOf<T>()))[0];
}
/// <inheritdoc/>
public T ReadTracked<T>(ulong va) where T : unmanaged
public override T ReadTracked<T>(ulong va)
{
try
{
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), false);
return base.ReadTracked<T>(va);
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return default;
}
}
/// <inheritdoc/>
public T ReadGuest<T>(ulong va) where T : unmanaged
{
try
{
SignalMemoryTrackingImpl(va, (ulong)Unsafe.SizeOf<T>(), false, true);
return Read<T>(va);
}
@ -153,112 +161,26 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public void Read(ulong va, Span<byte> data)
{
ReadImpl(va, data);
}
/// <inheritdoc/>
public void Write<T>(ulong va, T value) where T : unmanaged
{
Write(va, MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1)));
}
/// <inheritdoc/>
public void Write(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return;
}
SignalMemoryTracking(va, (ulong)data.Length, true);
WriteImpl(va, data);
}
/// <inheritdoc/>
public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return;
}
WriteImpl(va, data);
}
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return false;
}
SignalMemoryTracking(va, (ulong)data.Length, false);
if (IsContiguousAndMapped(va, data.Length))
{
var target = _backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length);
bool changed = !data.SequenceEqual(target);
if (changed)
{
data.CopyTo(target);
}
return changed;
}
else
{
WriteImpl(va, data);
return true;
}
}
/// <summary>
/// Writes data to CPU mapped memory.
/// </summary>
/// <param name="va">Virtual address to write the data into</param>
/// <param name="data">Data to be written</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteImpl(ulong va, ReadOnlySpan<byte> data)
public override void Read(ulong va, Span<byte> data)
{
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
if (IsContiguousAndMapped(va, data.Length))
base.Read(va, data);
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
data.CopyTo(_backingMemory.GetSpan(GetPhysicalAddressInternal(va), data.Length));
throw;
}
else
{
int offset = 0, size;
}
}
if ((va & PageMask) != 0)
{
ulong pa = GetPhysicalAddressInternal(va);
size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
data[..size].CopyTo(_backingMemory.GetSpan(pa, size));
offset += size;
}
for (; offset < data.Length; offset += size)
{
ulong pa = GetPhysicalAddressInternal(va + (ulong)offset);
size = Math.Min(data.Length - offset, PageSize);
data.Slice(offset, size).CopyTo(_backingMemory.GetSpan(pa, size));
}
}
public override void Write(ulong va, ReadOnlySpan<byte> data)
{
try
{
base.Write(va, data);
}
catch (InvalidMemoryRegionException)
{
@ -270,60 +192,47 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
public void WriteGuest<T>(ulong va, T value) where T : unmanaged
{
if (size == 0)
Span<byte> data = MemoryMarshal.Cast<T, byte>(MemoryMarshal.CreateSpan(ref value, 1));
SignalMemoryTrackingImpl(va, (ulong)data.Length, true, true);
Write(va, data);
}
public override void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
try
{
return ReadOnlySpan<byte>.Empty;
base.WriteUntracked(va, data);
}
if (tracked)
catch (InvalidMemoryRegionException)
{
SignalMemoryTracking(va, (ulong)size, false);
}
if (IsContiguousAndMapped(va, size))
{
return _backingMemory.GetSpan(GetPhysicalAddressInternal(va), size);
}
else
{
Span<byte> data = new byte[size];
ReadImpl(va, data);
return data;
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
/// <inheritdoc/>
public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
public override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
if (size == 0)
try
{
return new WritableRegion(null, va, Memory<byte>.Empty);
return base.GetReadOnlySequence(va, size, tracked);
}
if (IsContiguousAndMapped(va, size))
catch (InvalidMemoryRegionException)
{
if (tracked)
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
SignalMemoryTracking(va, (ulong)size, true);
throw;
}
return new WritableRegion(null, va, _backingMemory.GetMemory(GetPhysicalAddressInternal(va), size));
}
else
{
Memory<byte> memory = new byte[size];
GetSpan(va, size).CopyTo(memory.Span);
return new WritableRegion(this, va, memory, tracked);
return ReadOnlySequence<byte>.Empty;
}
}
/// <inheritdoc/>
public ref T GetRef<T>(ulong va) where T : unmanaged
{
if (!IsContiguous(va, Unsafe.SizeOf<T>()))
@ -336,56 +245,6 @@ namespace Ryujinx.Cpu.Jit
return ref _backingMemory.GetRef<T>(GetPhysicalAddressInternal(va));
}
/// <summary>
/// Computes the number of pages in a virtual address range.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range</param>
/// <param name="startVa">The virtual address of the beginning of the first page</param>
/// <remarks>This function does not differentiate between allocated and unallocated pages.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetPagesCount(ulong va, uint size, out ulong startVa)
{
// WARNING: Always check if ulong does not overflow during the operations.
startVa = va & ~(ulong)PageMask;
ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
return (int)(vaSpan / PageSize);
}
private static void ThrowMemoryNotContiguous() => throw new MemoryNotContiguousException();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsContiguousAndMapped(ulong va, int size) => IsContiguous(va, size) && IsMapped(va);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsContiguous(ulong va, int size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
{
return false;
}
int pages = GetPagesCount(va, (uint)size, out va);
for (int page = 0; page < pages - 1; page++)
{
if (!ValidateAddress(va + PageSize))
{
return false;
}
if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
{
return false;
}
va += PageSize;
}
return true;
}
/// <inheritdoc/>
public IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size)
{
@ -462,48 +321,6 @@ namespace Ryujinx.Cpu.Jit
return regions;
}
private void ReadImpl(ulong va, Span<byte> data)
{
if (data.Length == 0)
{
return;
}
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
int offset = 0, size;
if ((va & PageMask) != 0)
{
ulong pa = GetPhysicalAddressInternal(va);
size = Math.Min(data.Length, PageSize - (int)(va & PageMask));
_backingMemory.GetSpan(pa, size).CopyTo(data[..size]);
offset += size;
}
for (; offset < data.Length; offset += size)
{
ulong pa = GetPhysicalAddressInternal(va + (ulong)offset);
size = Math.Min(data.Length - offset, PageSize);
_backingMemory.GetSpan(pa, size).CopyTo(data.Slice(offset, size));
}
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
/// <inheritdoc/>
public bool IsRangeMapped(ulong va, ulong size)
{
@ -532,9 +349,8 @@ namespace Ryujinx.Cpu.Jit
return true;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsMapped(ulong va)
public override bool IsMapped(ulong va)
{
if (!ValidateAddress(va))
{
@ -544,40 +360,9 @@ namespace Ryujinx.Cpu.Jit
return _pageTable.Read<ulong>((va / PageSize) * PteSize) != 0;
}
private bool ValidateAddress(ulong va)
private nuint GetPhysicalAddressInternal(ulong va)
{
return va < _addressSpaceSize;
}
/// <summary>
/// Checks if the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <returns>True if the combination of virtual address and size is part of the addressable space</returns>
private bool ValidateAddressAndSize(ulong va, ulong size)
{
ulong endVa = va + size;
return endVa >= va && endVa >= size && endVa <= _addressSpaceSize;
}
/// <summary>
/// Ensures the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified outside the addressable space</exception>
private void AssertValidAddressAndSize(ulong va, ulong size)
{
if (!ValidateAddressAndSize(va, size))
{
throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
}
}
private ulong GetPhysicalAddressInternal(ulong va)
{
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
return (nuint)(PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask));
}
/// <inheritdoc/>
@ -587,50 +372,57 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
AssertValidAddressAndSize(va, size);
// Protection is inverted on software pages, since the default value is 0.
protection = (~protection) & MemoryPermission.ReadAndWrite;
long tag = protection switch
if (guest)
{
MemoryPermission.None => 0L,
MemoryPermission.Write => 2L << PointerTagBit,
_ => 3L << PointerTagBit,
};
// Protection is inverted on software pages, since the default value is 0.
protection = (~protection) & MemoryPermission.ReadAndWrite;
int pages = GetPagesCount(va, (uint)size, out va);
ulong pageStart = va >> PageBits;
long invTagMask = ~(0xffffL << 48);
for (int page = 0; page < pages; page++)
{
ref long pageRef = ref _pageTable.GetRef<long>(pageStart * PteSize);
long pte;
do
long tag = protection switch
{
pte = Volatile.Read(ref pageRef);
}
while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
MemoryPermission.None => 0L,
MemoryPermission.Write => 2L << PointerTagBit,
_ => 3L << PointerTagBit,
};
pageStart++;
int pages = GetPagesCount(va, (uint)size, out va);
ulong pageStart = va >> PageBits;
long invTagMask = ~(0xffffL << 48);
for (int page = 0; page < pages; page++)
{
ref long pageRef = ref _pageTable.GetRef<long>(pageStart * PteSize);
long pte;
do
{
pte = Volatile.Read(ref pageRef);
}
while (pte != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
pageStart++;
}
}
else
{
_pages.TrackingReprotect(va, size, protection);
}
}
/// <inheritdoc/>
public RegionHandle BeginTracking(ulong address, ulong size, int id)
public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginTracking(address, size, id);
return Tracking.BeginTracking(address, size, id, flags);
}
/// <inheritdoc/>
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id)
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
/// <inheritdoc/>
@ -639,8 +431,7 @@ namespace Ryujinx.Cpu.Jit
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
/// <inheritdoc/>
public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
private void SignalMemoryTrackingImpl(ulong va, ulong size, bool write, bool guest, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@ -650,31 +441,45 @@ namespace Ryujinx.Cpu.Jit
return;
}
// We emulate guard pages for software memory access. This makes for an easy transition to
// tracking using host guard pages in future, but also supporting platforms where this is not possible.
// If the memory tracking is coming from the guest, use the tag bits in the page table entry.
// Otherwise, use the managed page flags.
// Write tag includes read protection, since we don't have any read actions that aren't performed before write too.
long tag = (write ? 3L : 1L) << PointerTagBit;
int pages = GetPagesCount(va, (uint)size, out _);
ulong pageStart = va >> PageBits;
for (int page = 0; page < pages; page++)
if (guest)
{
ref long pageRef = ref _pageTable.GetRef<long>(pageStart * PteSize);
// We emulate guard pages for software memory access. This makes for an easy transition to
// tracking using host guard pages in future, but also supporting platforms where this is not possible.
long pte;
// Write tag includes read protection, since we don't have any read actions that aren't performed before write too.
long tag = (write ? 3L : 1L) << PointerTagBit;
pte = Volatile.Read(ref pageRef);
int pages = GetPagesCount(va, (uint)size, out _);
ulong pageStart = va >> PageBits;
if ((pte & tag) != 0)
for (int page = 0; page < pages; page++)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
break;
}
ref long pageRef = ref _pageTable.GetRef<long>(pageStart * PteSize);
pageStart++;
long pte = Volatile.Read(ref pageRef);
if ((pte & tag) != 0)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId, true);
break;
}
pageStart++;
}
}
else
{
_pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
}
/// <inheritdoc/>
public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
SignalMemoryTrackingImpl(va, size, write, false, precise, exemptId);
}
private ulong PaToPte(ulong pa)
@ -691,5 +496,17 @@ namespace Ryujinx.Cpu.Jit
/// Disposes of resources used by the memory manager.
/// </summary>
protected override void Destroy() => _pageTable.Dispose();
protected override Memory<byte> GetPhysicalAddressMemory(nuint pa, int size)
=> _backingMemory.GetMemory(pa, size);
protected override Span<byte> GetPhysicalAddressSpan(nuint pa, int size)
=> _backingMemory.GetSpan(pa, size);
protected override nuint TranslateVirtualAddressChecked(ulong va)
=> GetPhysicalAddressInternal(va);
protected override nuint TranslateVirtualAddressUnchecked(ulong va)
=> GetPhysicalAddressInternal(va);
}
}

View File

@ -3,52 +3,31 @@ using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Ryujinx.Cpu.Jit
{
/// <summary>
/// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region.
/// </summary>
public sealed class MemoryManagerHostMapped : MemoryManagerBase, IMemoryManager, IVirtualMemoryManagerTracked, IWritableBlock
public sealed class MemoryManagerHostMapped : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
public const int PageBits = 12;
public const int PageSize = 1 << PageBits;
public const int PageMask = PageSize - 1;
public const int PageToPteShift = 5; // 32 pages (2 bits each) in one ulong page table entry.
public const ulong BlockMappedMask = 0x5555555555555555; // First bit of each table entry set.
private enum HostMappedPtBits : ulong
{
Unmapped = 0,
Mapped,
WriteTracked,
ReadWriteTracked,
MappedReplicated = 0x5555555555555555,
WriteTrackedReplicated = 0xaaaaaaaaaaaaaaaa,
ReadWriteTrackedReplicated = ulong.MaxValue,
}
private readonly InvalidAccessHandler _invalidAccessHandler;
private readonly bool _unsafeMode;
private readonly AddressSpace _addressSpace;
public ulong AddressSpaceSize { get; }
private readonly PageTable<ulong> _pageTable;
private readonly MemoryEhMeilleure _memoryEh;
private readonly ulong[] _pageBitmap;
private readonly ManagedPageFlags _pages;
/// <inheritdoc/>
public bool Supports4KBPages => MemoryBlock.GetPageSize() == PageSize;
public bool UsesPrivateAllocations => false;
public int AddressSpaceBits { get; }
@ -60,6 +39,8 @@ namespace Ryujinx.Cpu.Jit
public event Action<ulong, ulong> UnmapEvent;
protected override ulong AddressSpaceSize { get; }
/// <summary>
/// Creates a new instance of the host mapped memory manager.
/// </summary>
@ -85,48 +66,12 @@ namespace Ryujinx.Cpu.Jit
AddressSpaceBits = asBits;
_pageBitmap = new ulong[1 << (AddressSpaceBits - (PageBits + PageToPteShift))];
_pages = new ManagedPageFlags(AddressSpaceBits);
Tracking = new MemoryTracking(this, (int)MemoryBlock.GetPageSize(), invalidAccessHandler);
_memoryEh = new MemoryEhMeilleure(_addressSpace.Base, _addressSpace.Mirror, Tracking);
}
/// <summary>
/// Checks if the virtual address is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address</param>
/// <returns>True if the virtual address is part of the addressable space</returns>
private bool ValidateAddress(ulong va)
{
return va < AddressSpaceSize;
}
/// <summary>
/// Checks if the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <returns>True if the combination of virtual address and size is part of the addressable space</returns>
private bool ValidateAddressAndSize(ulong va, ulong size)
{
ulong endVa = va + size;
return endVa >= va && endVa >= size && endVa <= AddressSpaceSize;
}
/// <summary>
/// Ensures the combination of virtual address and size is part of the addressable space.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range in bytes</param>
/// <exception cref="InvalidMemoryRegionException">Throw when the memory region specified outside the addressable space</exception>
private void AssertValidAddressAndSize(ulong va, ulong size)
{
if (!ValidateAddressAndSize(va, size))
{
throw new InvalidMemoryRegionException($"va=0x{va:X16}, size=0x{size:X16}");
}
}
/// <summary>
/// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
/// </summary>
@ -134,7 +79,7 @@ namespace Ryujinx.Cpu.Jit
/// <param name="size">Size of the range in bytes</param>
private void AssertMapped(ulong va, ulong size)
{
if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
if (!ValidateAddressAndSize(va, size) || !_pages.IsRangeMapped(va, size))
{
throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
@ -146,18 +91,12 @@ namespace Ryujinx.Cpu.Jit
AssertValidAddressAndSize(va, size);
_addressSpace.Map(va, pa, size, flags);
AddMapping(va, size);
_pages.AddMapping(va, size);
PtMap(va, pa, size);
Tracking.Map(va, size);
}
/// <inheritdoc/>
public void MapForeign(ulong va, nuint hostPointer, ulong size)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public void Unmap(ulong va, ulong size)
{
@ -166,7 +105,7 @@ namespace Ryujinx.Cpu.Jit
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
RemoveMapping(va, size);
_pages.RemoveMapping(va, size);
PtUnmap(va, size);
_addressSpace.Unmap(va, size);
}
@ -194,8 +133,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public T Read<T>(ulong va) where T : unmanaged
public override T Read<T>(ulong va)
{
try
{
@ -214,14 +152,11 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public T ReadTracked<T>(ulong va) where T : unmanaged
public override T ReadTracked<T>(ulong va)
{
try
{
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), false);
return Read<T>(va);
return base.ReadTracked<T>(va);
}
catch (InvalidMemoryRegionException)
{
@ -234,8 +169,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public void Read(ulong va, Span<byte> data)
public override void Read(ulong va, Span<byte> data)
{
try
{
@ -252,9 +186,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public void Write<T>(ulong va, T value) where T : unmanaged
public override void Write<T>(ulong va, T value)
{
try
{
@ -271,8 +203,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public void Write(ulong va, ReadOnlySpan<byte> data)
public override void Write(ulong va, ReadOnlySpan<byte> data)
{
try
{
@ -289,8 +220,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
public override void WriteUntracked(ulong va, ReadOnlySpan<byte> data)
{
try
{
@ -307,8 +237,7 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
try
{
@ -335,8 +264,21 @@ namespace Ryujinx.Cpu.Jit
}
}
/// <inheritdoc/>
public ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
public override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
if (tracked)
{
SignalMemoryTracking(va, (ulong)size, write: false);
}
else
{
AssertMapped(va, (ulong)size);
}
return new ReadOnlySequence<byte>(_addressSpace.Mirror.GetMemory(va, size));
}
public override ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
{
if (tracked)
{
@ -350,8 +292,7 @@ namespace Ryujinx.Cpu.Jit
return _addressSpace.Mirror.GetSpan(va, size);
}
/// <inheritdoc/>
public WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
{
if (tracked)
{
@ -365,7 +306,6 @@ namespace Ryujinx.Cpu.Jit
return _addressSpace.Mirror.GetWritableRegion(va, size);
}
/// <inheritdoc/>
public ref T GetRef<T>(ulong va) where T : unmanaged
{
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), true);
@ -373,26 +313,10 @@ namespace Ryujinx.Cpu.Jit
return ref _addressSpace.Mirror.GetRef<T>(va);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsMapped(ulong va)
public override bool IsMapped(ulong va)
{
return ValidateAddress(va) && IsMappedImpl(va);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsMappedImpl(ulong va)
{
ulong page = va >> PageBits;
int bit = (int)((page & 31) << 1);
int pageIndex = (int)(page >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte = Volatile.Read(ref pageRef);
return ((pte >> bit) & 3) != 0;
return ValidateAddress(va) && _pages.IsMapped(va);
}
/// <inheritdoc/>
@ -400,58 +324,7 @@ namespace Ryujinx.Cpu.Jit
{
AssertValidAddressAndSize(va, size);
return IsRangeMappedImpl(va, size);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void GetPageBlockRange(ulong pageStart, ulong pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex)
{
startMask = ulong.MaxValue << ((int)(pageStart & 31) << 1);
endMask = ulong.MaxValue >> (64 - ((int)(pageEnd & 31) << 1));
pageIndex = (int)(pageStart >> PageToPteShift);
pageEndIndex = (int)((pageEnd - 1) >> PageToPteShift);
}
private bool IsRangeMappedImpl(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
if (pages == 1)
{
return IsMappedImpl(va);
}
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
// Check if either bit in each 2 bit page entry is set.
// OR the block with itself shifted down by 1, and check the first bit of each entry.
ulong mask = BlockMappedMask & startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte = Volatile.Read(ref pageRef);
pte |= pte >> 1;
if ((pte & mask) != mask)
{
return false;
}
mask = BlockMappedMask;
}
return true;
return _pages.IsRangeMapped(va, size);
}
/// <inheritdoc/>
@ -512,11 +385,10 @@ namespace Ryujinx.Cpu.Jit
return _pageTable.Read(va) + (va & PageMask);
}
/// <inheritdoc/>
/// <remarks>
/// This function also validates that the given range is both valid and mapped, and will throw if it is not.
/// </remarks>
public void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
@ -526,93 +398,7 @@ namespace Ryujinx.Cpu.Jit
return;
}
// Software table, used for managed memory tracking.
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
if (pages == 1)
{
ulong tag = (ulong)(write ? HostMappedPtBits.WriteTracked : HostMappedPtBits.ReadWriteTracked);
int bit = (int)((pageStart & 31) << 1);
int pageIndex = (int)(pageStart >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte = Volatile.Read(ref pageRef);
ulong state = ((pte >> bit) & 3);
if (state >= tag)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
return;
}
else if (state == 0)
{
ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
}
else
{
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
ulong anyTrackingTag = (ulong)HostMappedPtBits.WriteTrackedReplicated;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte = Volatile.Read(ref pageRef);
ulong mappedMask = mask & BlockMappedMask;
ulong mappedPte = pte | (pte >> 1);
if ((mappedPte & mappedMask) != mappedMask)
{
ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
}
pte &= mask;
if ((pte & anyTrackingTag) != 0) // Search for any tracking.
{
// Writes trigger any tracking.
// Only trigger tracking from reads if both bits are set on any page.
if (write || (pte & (pte >> 1) & BlockMappedMask) != 0)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: false, exemptId);
break;
}
}
mask = ulong.MaxValue;
}
}
}
/// <summary>
/// Computes the number of pages in a virtual address range.
/// </summary>
/// <param name="va">Virtual address of the range</param>
/// <param name="size">Size of the range</param>
/// <param name="startVa">The virtual address of the beginning of the first page</param>
/// <remarks>This function does not differentiate between allocated and unallocated pages.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetPagesCount(ulong va, ulong size, out ulong startVa)
{
// WARNING: Always check if ulong does not overflow during the operations.
startVa = va & ~(ulong)PageMask;
ulong vaSpan = (va - startVa + size + PageMask) & ~(ulong)PageMask;
return (int)(vaSpan / PageSize);
_pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
/// <inheritdoc/>
@ -622,103 +408,28 @@ namespace Ryujinx.Cpu.Jit
}
/// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
// Protection is inverted on software pages, since the default value is 0.
protection = (~protection) & MemoryPermission.ReadAndWrite;
int pages = GetPagesCount(va, size, out va);
ulong pageStart = va >> PageBits;
if (pages == 1)
if (guest)
{
ulong protTag = protection switch
{
MemoryPermission.None => (ulong)HostMappedPtBits.Mapped,
MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTracked,
_ => (ulong)HostMappedPtBits.ReadWriteTracked,
};
int bit = (int)((pageStart & 31) << 1);
ulong tagMask = 3UL << bit;
ulong invTagMask = ~tagMask;
ulong tag = protTag << bit;
int pageIndex = (int)(pageStart >> PageToPteShift);
ref ulong pageRef = ref _pageBitmap[pageIndex];
ulong pte;
do
{
pte = Volatile.Read(ref pageRef);
}
while ((pte & tagMask) != 0 && Interlocked.CompareExchange(ref pageRef, (pte & invTagMask) | tag, pte) != pte);
_addressSpace.Base.Reprotect(va, size, protection, false);
}
else
{
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
ulong protTag = protection switch
{
MemoryPermission.None => (ulong)HostMappedPtBits.MappedReplicated,
MemoryPermission.Write => (ulong)HostMappedPtBits.WriteTrackedReplicated,
_ => (ulong)HostMappedPtBits.ReadWriteTrackedReplicated,
};
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
ulong mappedMask;
// Change the protection of all 2 bit entries that are mapped.
do
{
pte = Volatile.Read(ref pageRef);
mappedMask = pte | (pte >> 1);
mappedMask |= (mappedMask & BlockMappedMask) << 1;
mappedMask &= mask; // Only update mapped pages within the given range.
}
while (Interlocked.CompareExchange(ref pageRef, (pte & (~mappedMask)) | (protTag & mappedMask), pte) != pte);
mask = ulong.MaxValue;
}
_pages.TrackingReprotect(va, size, protection);
}
protection = protection switch
{
MemoryPermission.None => MemoryPermission.ReadAndWrite,
MemoryPermission.Write => MemoryPermission.Read,
_ => MemoryPermission.None,
};
_addressSpace.Base.Reprotect(va, size, protection, false);
}
/// <inheritdoc/>
public RegionHandle BeginTracking(ulong address, ulong size, int id)
public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginTracking(address, size, id);
return Tracking.BeginTracking(address, size, id, flags);
}
/// <inheritdoc/>
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id)
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginGranularTracking(address, size, handles, granularity, id);
return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
/// <inheritdoc/>
@ -727,86 +438,6 @@ namespace Ryujinx.Cpu.Jit
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
/// <summary>
/// Adds the given address mapping to the page table.
/// </summary>
/// <param name="va">Virtual memory address</param>
/// <param name="size">Size to be mapped</param>
private void AddMapping(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
ulong mask = startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask &= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
ulong mappedMask;
// Map all 2-bit entries that are unmapped.
do
{
pte = Volatile.Read(ref pageRef);
mappedMask = pte | (pte >> 1);
mappedMask |= (mappedMask & BlockMappedMask) << 1;
mappedMask |= ~mask; // Treat everything outside the range as mapped, thus unchanged.
}
while (Interlocked.CompareExchange(ref pageRef, (pte & mappedMask) | (BlockMappedMask & (~mappedMask)), pte) != pte);
mask = ulong.MaxValue;
}
}
/// <summary>
/// Removes the given address mapping from the page table.
/// </summary>
/// <param name="va">Virtual memory address</param>
/// <param name="size">Size to be unmapped</param>
private void RemoveMapping(ulong va, ulong size)
{
int pages = GetPagesCount(va, size, out _);
ulong pageStart = va >> PageBits;
ulong pageEnd = pageStart + (ulong)pages;
GetPageBlockRange(pageStart, pageEnd, out ulong startMask, out ulong endMask, out int pageIndex, out int pageEndIndex);
startMask = ~startMask;
endMask = ~endMask;
ulong mask = startMask;
while (pageIndex <= pageEndIndex)
{
if (pageIndex == pageEndIndex)
{
mask |= endMask;
}
ref ulong pageRef = ref _pageBitmap[pageIndex++];
ulong pte;
do
{
pte = Volatile.Read(ref pageRef);
}
while (Interlocked.CompareExchange(ref pageRef, pte & mask, pte) != pte);
mask = 0;
}
}
/// <summary>
/// Disposes of resources used by the memory manager.
/// </summary>
@ -816,6 +447,16 @@ namespace Ryujinx.Cpu.Jit
_memoryEh.Dispose();
}
private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
protected override Memory<byte> GetPhysicalAddressMemory(nuint pa, int size)
=> _addressSpace.Mirror.GetMemory(pa, size);
protected override Span<byte> GetPhysicalAddressSpan(nuint pa, int size)
=> _addressSpace.Mirror.GetSpan(pa, size);
protected override nuint TranslateVirtualAddressChecked(ulong va)
=> (nuint)GetPhysicalAddressChecked(va);
protected override nuint TranslateVirtualAddressUnchecked(ulong va)
=> (nuint)GetPhysicalAddressInternal(va);
}
}

View File

@ -0,0 +1,634 @@
using ARMeilleure.Memory;
using Ryujinx.Common.Memory;
using Ryujinx.Cpu.Jit.HostTracked;
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using Ryujinx.Memory.Tracking;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Ryujinx.Cpu.Jit
{
/// <summary>
/// Represents a CPU memory manager which maps guest virtual memory directly onto a host virtual region.
/// </summary>
public sealed class MemoryManagerHostTracked : VirtualMemoryManagerRefCountedBase, IMemoryManager, IVirtualMemoryManagerTracked
{
private readonly InvalidAccessHandler _invalidAccessHandler;
private readonly bool _unsafeMode;
private readonly MemoryBlock _backingMemory;
public int AddressSpaceBits { get; }
public MemoryTracking Tracking { get; }
private readonly NativePageTable _nativePageTable;
private readonly AddressSpacePartitioned _addressSpace;
private readonly ManagedPageFlags _pages;
protected override ulong AddressSpaceSize { get; }
/// <inheritdoc/>
public bool UsesPrivateAllocations => true;
public IntPtr PageTablePointer => _nativePageTable.PageTablePointer;
public MemoryManagerType Type => _unsafeMode ? MemoryManagerType.HostTrackedUnsafe : MemoryManagerType.HostTracked;
public event Action<ulong, ulong> UnmapEvent;
/// <summary>
/// Creates a new instance of the host tracked memory manager.
/// </summary>
/// <param name="backingMemory">Physical backing memory where virtual memory will be mapped to</param>
/// <param name="addressSpaceSize">Size of the address space</param>
/// <param name="unsafeMode">True if unmanaged access should not be masked (unsafe), false otherwise.</param>
/// <param name="invalidAccessHandler">Optional function to handle invalid memory accesses</param>
public MemoryManagerHostTracked(MemoryBlock backingMemory, ulong addressSpaceSize, bool unsafeMode, InvalidAccessHandler invalidAccessHandler)
{
bool useProtectionMirrors = MemoryBlock.GetPageSize() > PageSize;
Tracking = new MemoryTracking(this, PageSize, invalidAccessHandler, useProtectionMirrors);
_backingMemory = backingMemory;
_invalidAccessHandler = invalidAccessHandler;
_unsafeMode = unsafeMode;
AddressSpaceSize = addressSpaceSize;
ulong asSize = PageSize;
int asBits = PageBits;
while (asSize < AddressSpaceSize)
{
asSize <<= 1;
asBits++;
}
AddressSpaceBits = asBits;
if (useProtectionMirrors && !NativeSignalHandler.SupportsFaultAddressPatching())
{
// Currently we require being able to change the fault address to something else
// in order to "emulate" 4KB granularity protection on systems with larger page size.
throw new PlatformNotSupportedException();
}
_pages = new ManagedPageFlags(asBits);
_nativePageTable = new(asSize);
_addressSpace = new(Tracking, backingMemory, _nativePageTable, useProtectionMirrors);
}
public override ReadOnlySequence<byte> GetReadOnlySequence(ulong va, int size, bool tracked = false)
{
if (size == 0)
{
return ReadOnlySequence<byte>.Empty;
}
try
{
if (tracked)
{
SignalMemoryTracking(va, (ulong)size, false);
}
else
{
AssertValidAddressAndSize(va, (ulong)size);
}
ulong endVa = va + (ulong)size;
int offset = 0;
BytesReadOnlySequenceSegment first = null, last = null;
while (va < endVa)
{
(MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(size - offset));
Memory<byte> physicalMemory = memory.GetMemory(rangeOffset, (int)copySize);
if (first is null)
{
first = last = new BytesReadOnlySequenceSegment(physicalMemory);
}
else
{
if (last.IsContiguousWith(physicalMemory, out nuint contiguousStart, out int contiguousSize))
{
Memory<byte> contiguousPhysicalMemory = new NativeMemoryManager<byte>(contiguousStart, contiguousSize).Memory;
last.Replace(contiguousPhysicalMemory);
}
else
{
last = last.Append(physicalMemory);
}
}
va += copySize;
offset += (int)copySize;
}
return new ReadOnlySequence<byte>(first, 0, last, (int)(size - last.RunningIndex));
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return ReadOnlySequence<byte>.Empty;
}
}
/// <inheritdoc/>
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
{
AssertValidAddressAndSize(va, size);
if (flags.HasFlag(MemoryMapFlags.Private))
{
_addressSpace.Map(va, pa, size);
}
_pages.AddMapping(va, size);
_nativePageTable.Map(va, pa, size, _addressSpace, _backingMemory, flags.HasFlag(MemoryMapFlags.Private));
Tracking.Map(va, size);
}
/// <inheritdoc/>
public void Unmap(ulong va, ulong size)
{
AssertValidAddressAndSize(va, size);
_addressSpace.Unmap(va, size);
UnmapEvent?.Invoke(va, size);
Tracking.Unmap(va, size);
_pages.RemoveMapping(va, size);
_nativePageTable.Unmap(va, size);
}
public override T ReadTracked<T>(ulong va)
{
try
{
return base.ReadTracked<T>(va);
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
return default;
}
}
public override void Read(ulong va, Span<byte> data)
{
if (data.Length == 0)
{
return;
}
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
ulong endVa = va + (ulong)data.Length;
int offset = 0;
while (va < endVa)
{
(MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset));
memory.GetSpan(rangeOffset, (int)copySize).CopyTo(data.Slice(offset, (int)copySize));
va += copySize;
offset += (int)copySize;
}
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
public override bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
return false;
}
SignalMemoryTracking(va, (ulong)data.Length, false);
if (TryGetVirtualContiguous(va, data.Length, out MemoryBlock memoryBlock, out ulong offset))
{
var target = memoryBlock.GetSpan(offset, data.Length);
bool changed = !data.SequenceEqual(target);
if (changed)
{
data.CopyTo(target);
}
return changed;
}
else
{
WriteImpl(va, data);
return true;
}
}
public override ReadOnlySpan<byte> GetSpan(ulong va, int size, bool tracked = false)
{
if (size == 0)
{
return ReadOnlySpan<byte>.Empty;
}
if (tracked)
{
SignalMemoryTracking(va, (ulong)size, false);
}
if (TryGetVirtualContiguous(va, size, out MemoryBlock memoryBlock, out ulong offset))
{
return memoryBlock.GetSpan(offset, size);
}
else
{
Span<byte> data = new byte[size];
Read(va, data);
return data;
}
}
public override WritableRegion GetWritableRegion(ulong va, int size, bool tracked = false)
{
if (size == 0)
{
return new WritableRegion(null, va, Memory<byte>.Empty);
}
if (tracked)
{
SignalMemoryTracking(va, (ulong)size, true);
}
if (TryGetVirtualContiguous(va, size, out MemoryBlock memoryBlock, out ulong offset))
{
return new WritableRegion(null, va, memoryBlock.GetMemory(offset, size));
}
else
{
IMemoryOwner<byte> memoryOwner = ByteMemoryPool.Rent(size);
Read(va, memoryOwner.Memory.Span);
return new WritableRegion(this, va, memoryOwner);
}
}
public ref T GetRef<T>(ulong va) where T : unmanaged
{
if (!TryGetVirtualContiguous(va, Unsafe.SizeOf<T>(), out MemoryBlock memory, out ulong offset))
{
ThrowMemoryNotContiguous();
}
SignalMemoryTracking(va, (ulong)Unsafe.SizeOf<T>(), true);
return ref memory.GetRef<T>(offset);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsMapped(ulong va)
{
return ValidateAddress(va) && _pages.IsMapped(va);
}
public bool IsRangeMapped(ulong va, ulong size)
{
AssertValidAddressAndSize(va, size);
return _pages.IsRangeMapped(va, size);
}
private bool TryGetVirtualContiguous(ulong va, int size, out MemoryBlock memory, out ulong offset)
{
if (_addressSpace.HasAnyPrivateAllocation(va, (ulong)size, out PrivateRange range))
{
// If we have a private allocation overlapping the range,
// then the access is only considered contiguous if it covers the entire range.
if (range.Memory != null)
{
memory = range.Memory;
offset = range.Offset;
return true;
}
memory = null;
offset = 0;
return false;
}
memory = _backingMemory;
offset = GetPhysicalAddressInternal(va);
return IsPhysicalContiguous(va, size);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsPhysicalContiguous(ulong va, int size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, (ulong)size))
{
return false;
}
int pages = GetPagesCount(va, (uint)size, out va);
for (int page = 0; page < pages - 1; page++)
{
if (!ValidateAddress(va + PageSize))
{
return false;
}
if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
{
return false;
}
va += PageSize;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ulong GetContiguousSize(ulong va, ulong size)
{
ulong contiguousSize = PageSize - (va & PageMask);
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{
return contiguousSize;
}
int pages = GetPagesCount(va, size, out va);
for (int page = 0; page < pages - 1; page++)
{
if (!ValidateAddress(va + PageSize))
{
return contiguousSize;
}
if (GetPhysicalAddressInternal(va) + PageSize != GetPhysicalAddressInternal(va + PageSize))
{
return contiguousSize;
}
va += PageSize;
contiguousSize += PageSize;
}
return Math.Min(contiguousSize, size);
}
private (MemoryBlock, ulong, ulong) GetMemoryOffsetAndSize(ulong va, ulong size)
{
PrivateRange privateRange = _addressSpace.GetFirstPrivateAllocation(va, size, out ulong nextVa);
if (privateRange.Memory != null)
{
return (privateRange.Memory, privateRange.Offset, privateRange.Size);
}
ulong physSize = GetContiguousSize(va, Math.Min(size, nextVa - va));
return (_backingMemory, GetPhysicalAddressChecked(va), physSize);
}
public IEnumerable<HostMemoryRange> GetHostRegions(ulong va, ulong size)
{
if (!ValidateAddressAndSize(va, size))
{
return null;
}
var regions = new List<HostMemoryRange>();
ulong endVa = va + size;
try
{
while (va < endVa)
{
(MemoryBlock memory, ulong rangeOffset, ulong rangeSize) = GetMemoryOffsetAndSize(va, endVa - va);
regions.Add(new((UIntPtr)memory.GetPointer(rangeOffset, rangeSize), rangeSize));
va += rangeSize;
}
}
catch (InvalidMemoryRegionException)
{
return null;
}
return regions;
}
public IEnumerable<MemoryRange> GetPhysicalRegions(ulong va, ulong size)
{
if (size == 0)
{
return Enumerable.Empty<MemoryRange>();
}
return GetPhysicalRegionsImpl(va, size);
}
private List<MemoryRange> GetPhysicalRegionsImpl(ulong va, ulong size)
{
if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size))
{
return null;
}
int pages = GetPagesCount(va, (uint)size, out va);
var regions = new List<MemoryRange>();
ulong regionStart = GetPhysicalAddressInternal(va);
ulong regionSize = PageSize;
for (int page = 0; page < pages - 1; page++)
{
if (!ValidateAddress(va + PageSize))
{
return null;
}
ulong newPa = GetPhysicalAddressInternal(va + PageSize);
if (GetPhysicalAddressInternal(va) + PageSize != newPa)
{
regions.Add(new MemoryRange(regionStart, regionSize));
regionStart = newPa;
regionSize = 0;
}
va += PageSize;
regionSize += PageSize;
}
regions.Add(new MemoryRange(regionStart, regionSize));
return regions;
}
/// <inheritdoc/>
/// <remarks>
/// This function also validates that the given range is both valid and mapped, and will throw if it is not.
/// </remarks>
public override void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null)
{
AssertValidAddressAndSize(va, size);
if (precise)
{
Tracking.VirtualMemoryEvent(va, size, write, precise: true, exemptId);
return;
}
// Software table, used for managed memory tracking.
_pages.SignalMemoryTracking(Tracking, va, size, write, exemptId);
}
public RegionHandle BeginTracking(ulong address, ulong size, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginTracking(address, size, id, flags);
}
public MultiRegionHandle BeginGranularTracking(ulong address, ulong size, IEnumerable<IRegionHandle> handles, ulong granularity, int id, RegionFlags flags = RegionFlags.None)
{
return Tracking.BeginGranularTracking(address, size, handles, granularity, id, flags);
}
public SmartMultiRegionHandle BeginSmartGranularTracking(ulong address, ulong size, ulong granularity, int id)
{
return Tracking.BeginSmartGranularTracking(address, size, granularity, id);
}
private ulong GetPhysicalAddressChecked(ulong va)
{
if (!IsMapped(va))
{
ThrowInvalidMemoryRegionException($"Not mapped: va=0x{va:X16}");
}
return GetPhysicalAddressInternal(va);
}
private ulong GetPhysicalAddressInternal(ulong va)
{
return _nativePageTable.GetPhysicalAddress(va);
}
/// <inheritdoc/>
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
{
// TODO
}
/// <inheritdoc/>
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection, bool guest)
{
if (guest)
{
_addressSpace.Reprotect(va, size, protection);
}
else
{
_pages.TrackingReprotect(va, size, protection);
}
}
/// <summary>
/// Disposes of resources used by the memory manager.
/// </summary>
protected override void Destroy()
{
_addressSpace.Dispose();
_nativePageTable.Dispose();
}
protected override Memory<byte> GetPhysicalAddressMemory(nuint pa, int size)
=> _backingMemory.GetMemory(pa, size);
protected override Span<byte> GetPhysicalAddressSpan(nuint pa, int size)
=> _backingMemory.GetSpan(pa, size);
protected override void WriteImpl(ulong va, ReadOnlySpan<byte> data)
{
try
{
AssertValidAddressAndSize(va, (ulong)data.Length);
ulong endVa = va + (ulong)data.Length;
int offset = 0;
while (va < endVa)
{
(MemoryBlock memory, ulong rangeOffset, ulong copySize) = GetMemoryOffsetAndSize(va, (ulong)(data.Length - offset));
data.Slice(offset, (int)copySize).CopyTo(memory.GetSpan(rangeOffset, (int)copySize));
va += copySize;
offset += (int)copySize;
}
}
catch (InvalidMemoryRegionException)
{
if (_invalidAccessHandler == null || !_invalidAccessHandler(va))
{
throw;
}
}
}
protected override nuint TranslateVirtualAddressChecked(ulong va)
=> (nuint)GetPhysicalAddressChecked(va);
protected override nuint TranslateVirtualAddressUnchecked(ulong va)
=> (nuint)GetPhysicalAddressInternal(va);
}
}

View File

@ -1126,11 +1126,23 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
Operand destination64 = new(destination.Kind, OperandType.I64, destination.Value);
Operand basePointer = new(regAlloc.FixedPageTableRegister, RegisterType.Integer, OperandType.I64);
if (mmType == MemoryManagerType.HostMapped || mmType == MemoryManagerType.HostMappedUnsafe)
{
// We don't need to mask the address for the safe mode, since it is already naturally limited to 32-bit
// and can never reach out of the guest address space.
// We don't need to mask the address for the safe mode, since it is already naturally limited to 32-bit
// and can never reach out of the guest address space.
if (mmType.IsHostTracked())
{
int tempRegister = regAlloc.AllocateTempGprRegister();
Operand pte = new(tempRegister, RegisterType.Integer, OperandType.I64);
asm.Lsr(pte, guestAddress, new Operand(OperandKind.Constant, OperandType.I32, 12));
asm.LdrRr(pte, basePointer, pte, ArmExtensionType.Uxtx, true);
asm.Add(destination64, pte, guestAddress);
regAlloc.FreeTempGprRegister(tempRegister);
}
else if (mmType.IsHostMapped())
{
asm.Add(destination64, basePointer, guestAddress);
}
else

View File

@ -1106,6 +1106,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
case InstName.Mrs:
case InstName.MsrImm:
case InstName.MsrReg:
case InstName.Sysl:
return true;
}
@ -1130,5 +1131,37 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
return false;
}
public static bool IsPartialRegisterUpdateMemory(this InstName name)
{
switch (name)
{
case InstName.Ld1AdvsimdSnglAsNoPostIndex:
case InstName.Ld1AdvsimdSnglAsPostIndex:
case InstName.Ld2AdvsimdSnglAsNoPostIndex:
case InstName.Ld2AdvsimdSnglAsPostIndex:
case InstName.Ld3AdvsimdSnglAsNoPostIndex:
case InstName.Ld3AdvsimdSnglAsPostIndex:
case InstName.Ld4AdvsimdSnglAsNoPostIndex:
case InstName.Ld4AdvsimdSnglAsPostIndex:
return true;
}
return false;
}
public static bool IsPrefetchMemory(this InstName name)
{
switch (name)
{
case InstName.PrfmImm:
case InstName.PrfmLit:
case InstName.PrfmReg:
case InstName.Prfum:
return true;
}
return false;
}
}
}

View File

@ -1,15 +1,12 @@
using ARMeilleure.Memory;
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
using System;
using System.Diagnostics;
using System.Numerics;
namespace Ryujinx.Cpu.LightningJit.Arm64
{
class RegisterAllocator
{
public const int MaxTemps = 1;
public const int MaxTempsInclFixed = MaxTemps + 2;
private uint _gprMask;
private readonly uint _fpSimdMask;
private readonly uint _pStateMask;
@ -25,7 +22,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
public uint AllFpSimdMask => _fpSimdMask;
public uint AllPStateMask => _pStateMask;
public RegisterAllocator(uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall)
public RegisterAllocator(MemoryManagerType mmType, uint gprMask, uint fpSimdMask, uint pStateMask, bool hasHostCall)
{
_gprMask = gprMask;
_fpSimdMask = fpSimdMask;
@ -56,7 +53,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
BuildRegisterMap(_registerMap);
Span<int> tempRegisters = stackalloc int[MaxTemps];
Span<int> tempRegisters = stackalloc int[CalculateMaxTemps(mmType)];
for (int index = 0; index < tempRegisters.Length; index++)
{
@ -150,5 +147,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
{
mask &= ~(1u << index);
}
public static int CalculateMaxTemps(MemoryManagerType mmType)
{
return mmType.IsHostMapped() ? 1 : 2;
}
public static int CalculateMaxTempsInclFixed(MemoryManagerType mmType)
{
return CalculateMaxTemps(mmType) + 2;
}
}
}

View File

@ -247,7 +247,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
}
}
if (!flags.HasFlag(InstFlags.ReadRt))
if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory())
{
if (flags.HasFlag(InstFlags.Rt))
{
@ -281,7 +281,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64
gprMask |= MaskFromIndex(ExtractRd(flags, encoding));
}
if (!flags.HasFlag(InstFlags.ReadRt))
if (!flags.HasFlag(InstFlags.ReadRt) || name.IsPartialRegisterUpdateMemory())
{
if (flags.HasFlag(InstFlags.Rt))
{

View File

@ -0,0 +1,48 @@
using System.Diagnostics;
namespace Ryujinx.Cpu.LightningJit.Arm64
{
static class SysUtils
{
public static (uint, uint, uint, uint) UnpackOp1CRnCRmOp2(uint encoding)
{
uint op1 = (encoding >> 16) & 7;
uint crn = (encoding >> 12) & 0xf;
uint crm = (encoding >> 8) & 0xf;
uint op2 = (encoding >> 5) & 7;
return (op1, crn, crm, op2);
}
public static bool IsCacheInstEl0(uint encoding)
{
(uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding);
return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch
{
0b011_0111_0100_001 => true, // DC ZVA
0b011_0111_1010_001 => true, // DC CVAC
0b011_0111_1100_001 => true, // DC CVAP
0b011_0111_1011_001 => true, // DC CVAU
0b011_0111_1110_001 => true, // DC CIVAC
0b011_0111_0101_001 => true, // IC IVAU
_ => false,
};
}
public static bool IsCacheInstUciTrapped(uint encoding)
{
(uint op1, uint crn, uint crm, uint op2) = UnpackOp1CRnCRmOp2(encoding);
return ((op1 << 11) | (crn << 7) | (crm << 3) | op2) switch
{
0b011_0111_1010_001 => true, // DC CVAC
0b011_0111_1100_001 => true, // DC CVAP
0b011_0111_1011_001 => true, // DC CVAU
0b011_0111_1110_001 => true, // DC CIVAC
0b011_0111_0101_001 => true, // IC IVAU
_ => false,
};
}
}
}

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